From 4b4dee3c32d437ed2f34ec162e6014adf66e0538 Mon Sep 17 00:00:00 2001 From: sirpy Date: Wed, 12 Aug 2026 19:47:20 +0300 Subject: [PATCH 1/4] Add viem fallback client helper --- packages/core/README.md | 96 +++++++ packages/core/package.json | 8 +- packages/core/src/index.ts | 10 + packages/core/src/viemFallbackClient.ts | 311 ++++++++++++++++++++++ packages/core/tsup.config.ts | 14 +- pnpm-lock.yaml | 340 +++++------------------- 6 files changed, 504 insertions(+), 275 deletions(-) create mode 100644 packages/core/README.md create mode 100644 packages/core/src/viemFallbackClient.ts diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..e7ed2fdd --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,96 @@ +# @goodwidget/core + +Runtime helpers for GoodWidget providers, host detection, wallet context, and viem client setup. + +## viem fallback clients + +`createViemFallbackClient` wraps viem `createPublicClient` and `createWalletClient` so callers get a fallback HTTP transport built from cached Chainlist RPC URLs for the requested chain. + +The helper accepts a small key-value storage adapter. On initialization it reads cached RPC metadata from storage. If the cache is missing or older than 1 day, it starts a background refresh from Chainlist. Client creation waits for that first refresh only when no cached RPCs are available for the requested chain. + +```ts +import { createViemFallbackClient } from '@goodwidget/core/viemFallbackClient' +import { celo } from 'viem/chains' + +const viemClient = createViemFallbackClient(localStorage, { + onError(error) { + console.warn('RPC refresh failed', error) + }, +}) + +const publicClient = await viemClient.createPublicClient({ + chain: celo, + fallbackRpcs: ['https://forno.celo.org'], +}) +``` + +The same helper can create wallet clients. Any viem wallet client option, such as `account`, can be passed through. + +```ts +import { privateKeyToAccount } from 'viem/accounts' +import { celo } from 'viem/chains' + +const account = privateKeyToAccount('0x...') + +const walletClient = await viemClient.createWalletClient({ + account, + chain: celo, + fallbackRpcs: ['https://forno.celo.org'], +}) +``` + +If you pass `transport`, the wrapper leaves it unchanged and does not create a fallback transport for that client. + +```ts +import { http } from 'viem' +import { celo } from 'viem/chains' + +const client = await viemClient.createPublicClient({ + chain: celo, + transport: http('https://forno.celo.org'), +}) +``` + +## Storage adapters + +The adapter supports browser-style storage and Worker-style KV storage. + +```ts +createViemFallbackClient(localStorage) +``` + +```ts +createViemFallbackClient({ + get: (key) => env.KV.get(key, 'json'), + put: (key, value) => env.KV.put(key, value), +}) +``` + +Cached values use this shape: + +```ts +type ViemRpcCacheEntry = { + fetchedAt: string + rpcs: Array<{ + chainId: number + rpcs: string[] + }> +} +``` + +Only HTTPS RPC URLs are used. URLs containing Chainlist template placeholders are ignored. + +## Options + +```ts +createViemFallbackClient(storage, { + cacheKey: 'goodwidget:viem-rpcs', + chainlistRpcsUrl: 'https://chainlist.org/rpcs.json', + refreshIntervalMs: 24 * 60 * 60 * 1000, + fetchTimeoutMs: 10_000, + fetch: globalThis.fetch, + onError: console.warn, +}) +``` + +`chainlistRpcsUrl` is intended for Chainlist's HTTPS RPC JSON endpoint. The helper blocks non-HTTPS URLs and hosts outside the expected Chainlist domain. \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 7d540585..c93e930d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,6 +15,11 @@ "types": "./dist/wagmi.d.ts", "import": "./dist/wagmi.js", "require": "./dist/wagmi.cjs" + }, + "./viemFallbackClient": { + "types": "./dist/viemFallbackClient.d.ts", + "import": "./dist/viemFallbackClient.js", + "require": "./dist/viemFallbackClient.cjs" } }, "scripts": { @@ -35,7 +40,8 @@ }, "dependencies": { "@goodwidget/ui": "workspace:*", - "tamagui": "1.121.0" + "tamagui": "1.121.0", + "viem": "^2.0.0" }, "devDependencies": { "react": "^18.3.0", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index db0331d3..b9f0fa47 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,6 +2,16 @@ export { GoodWidgetProvider } from './provider' export type { WalletContextValue, HostContextValue, GoodWidgetContextValue } from './provider' export { useWallet, useHost, useGoodWidget } from './hooks' export { detectHost } from './detect' +export { createViemFallbackClient } from './viemFallbackClient' +export type { + CachedChainRpcs, + ViemFallbackClient, + ViemFallbackClientOptions, + ViemFallbackPublicClientParameters, + ViemFallbackStorage, + ViemFallbackWalletClientParameters, + ViemRpcCacheEntry, +} from './viemFallbackClient' export type { EIP1193Provider, diff --git a/packages/core/src/viemFallbackClient.ts b/packages/core/src/viemFallbackClient.ts new file mode 100644 index 00000000..9606b2f7 --- /dev/null +++ b/packages/core/src/viemFallbackClient.ts @@ -0,0 +1,311 @@ +import { + createPublicClient as createViemPublicClient, + createWalletClient as createViemWalletClient, + fallback, + http, + type Account, + type Address, + type Chain, + type PublicClient, + type PublicClientConfig, + type RpcSchema, + type Transport, + type WalletClient, + type WalletClientConfig, +} from 'viem' + +const DEFAULT_CHAINLIST_RPCS_URL = 'https://chainlist.org/rpcs.json' +const DEFAULT_CACHE_KEY = 'goodwidget:viem-rpcs' +const DEFAULT_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 +const DEFAULT_FETCH_TIMEOUT_MS = 10_000 + +type MaybePromise = T | Promise + +export interface ViemFallbackStorage { + get?: (key: string) => MaybePromise + put?: (key: string, value: string) => MaybePromise + getItem?: (key: string) => MaybePromise + setItem?: (key: string, value: string) => MaybePromise +} + +export interface ViemFallbackClientOptions { + cacheKey?: string + chainlistRpcsUrl?: string + refreshIntervalMs?: number + fetchTimeoutMs?: number + fetch?: typeof fetch + onError?: (error: unknown) => void +} + +export interface CachedChainRpcs { + chainId: number + rpcs: string[] +} + +export interface ViemRpcCacheEntry { + rpcs: CachedChainRpcs[] + fetchedAt: string +} + +export type ViemFallbackPublicClientParameters = Omit< + PublicClientConfig, + 'chain' | 'transport' +> & { + chain: Chain + transport?: Transport + fallbackRpcs?: string[] +} + +export type ViemFallbackWalletClientParameters = Omit< + WalletClientConfig, + 'chain' | 'transport' +> & { + chain: Chain + transport?: Transport + fallbackRpcs?: string[] +} + +export interface ViemFallbackClient { + ready: Promise + refreshRpcs: () => Promise + getRpcUrls: (chain: Chain, fallbackRpcs?: string[]) => Promise + createPublicClient: (parameters: ViemFallbackPublicClientParameters) => Promise + createWalletClient: (parameters: ViemFallbackWalletClientParameters) => Promise +} + +/** + * Creates viem client wrappers backed by a cached Chainlist RPC list. + */ +export function createViemFallbackClient( + storage: ViemFallbackStorage, + options: ViemFallbackClientOptions = {}, +): ViemFallbackClient { + const cacheKey = options.cacheKey ?? DEFAULT_CACHE_KEY + const chainlistRpcsUrl = options.chainlistRpcsUrl ?? DEFAULT_CHAINLIST_RPCS_URL + const refreshIntervalMs = options.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS + const fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS + const fetchImpl = options.fetch ?? globalThis.fetch + + let cache: ViemRpcCacheEntry | null = null + let refreshPromise: Promise | null = null + + const refreshRpcs = async (): Promise => { + if (!fetchImpl) throw new Error('fetch is not available') + + const chainlistUrl = new URL(chainlistRpcsUrl) + + const abortController = new AbortController() + const timeout = setTimeout(() => abortController.abort(), fetchTimeoutMs) + + try { + const response = await fetchImpl(chainlistUrl.href, { + redirect: 'error', + signal: abortController.signal, + }) + if (!response.ok) { + throw new Error(`Chainlist HTTP ${response.status}`) + } + + const payload = await response.json() + const nextCache: ViemRpcCacheEntry = { + rpcs: normalizeChainlistPayload(payload), + fetchedAt: new Date().toISOString(), + } + + if (nextCache.rpcs.length === 0) return + + cache = nextCache + await writeCache(storage, cacheKey, nextCache) + } finally { + clearTimeout(timeout) + } + } + + const startRefresh = (): Promise => { + if (!refreshPromise) { + refreshPromise = refreshRpcs() + .catch((error) => { + options.onError?.(error) + }) + .finally(() => { + refreshPromise = null + }) + } + + return refreshPromise + } + + const ready = readCache(storage, cacheKey) + .then((cached) => { + cache = cached + if (!cached || isStale(cached, refreshIntervalMs)) { + void startRefresh() + } + }) + .catch((error) => { + options.onError?.(error) + void startRefresh() + }) + + const getRpcUrls = async (chain: Chain, fallbackRpcs: string[] = []): Promise => { + await ready + + const cachedUrls = getCachedRpcUrls(cache, chain.id) + if (cachedUrls.length === 0 && refreshPromise) { + await refreshPromise + } + + return sanitizeRpcUrls([ + ...getCachedRpcUrls(cache, chain.id), + ...fallbackRpcs, + ...(chain.rpcUrls.default.http ?? []), + ]) + } + + const createFallbackTransport = async ( + chain: Chain, + fallbackRpcs?: string[], + ): Promise => { + const rpcUrls = await getRpcUrls(chain, fallbackRpcs) + const transports = rpcUrls.length > 0 ? rpcUrls.map((rpcUrl) => http(rpcUrl)) : [http()] + return fallback(transports) + } + + return { + ready, + refreshRpcs, + getRpcUrls, + async createPublicClient(parameters) { + const { fallbackRpcs, ...clientParameters } = parameters + const transport = + clientParameters.transport ?? + (await createFallbackTransport(clientParameters.chain, fallbackRpcs)) + + return createViemPublicClient({ + ...clientParameters, + transport, + } as PublicClientConfig< + Transport, + Chain, + Account | Address | undefined, + RpcSchema | undefined + >) + }, + async createWalletClient(parameters) { + const { fallbackRpcs, ...clientParameters } = parameters + const transport = + clientParameters.transport ?? + (await createFallbackTransport(clientParameters.chain, fallbackRpcs)) + + return createViemWalletClient({ + ...clientParameters, + transport, + } as WalletClientConfig< + Transport, + Chain, + Account | Address | undefined, + RpcSchema | undefined + >) + }, + } +} + +async function readCache( + storage: ViemFallbackStorage, + key: string, +): Promise { + const value = storage.get ? await storage.get(key) : await storage.getItem?.(key) + return parseCacheEntry(value) +} + +async function writeCache( + storage: ViemFallbackStorage, + key: string, + value: ViemRpcCacheEntry, +): Promise { + const serialized = JSON.stringify(value) + if (storage.put) { + await storage.put(key, serialized) + return + } + + await storage.setItem?.(key, serialized) +} + +function parseCacheEntry(value: unknown): ViemRpcCacheEntry | null { + if (!value) return null + + if (typeof value === 'string') { + try { + return parseCacheEntry(JSON.parse(value)) + } catch { + return null + } + } + + if (typeof value !== 'object') return null + + const candidate = value as Partial + if (typeof candidate.fetchedAt !== 'string' || !Array.isArray(candidate.rpcs)) return null + + const rpcs = candidate.rpcs + .map((entry) => ({ + chainId: typeof entry.chainId === 'number' ? entry.chainId : Number.NaN, + rpcs: Array.isArray(entry.rpcs) + ? sanitizeRpcUrls( + entry.rpcs.filter((rpcUrl): rpcUrl is string => typeof rpcUrl === 'string'), + ) + : [], + })) + .filter((entry) => Number.isInteger(entry.chainId) && entry.rpcs.length > 0) + + return { + fetchedAt: candidate.fetchedAt, + rpcs, + } +} + +function normalizeChainlistPayload(payload: unknown): CachedChainRpcs[] { + if (!Array.isArray(payload)) return [] + + return payload + .map((entry) => { + const candidate = entry as { chainId?: unknown; rpc?: unknown } + const rpcEntries = Array.isArray(candidate.rpc) ? candidate.rpc : [] + const urls = rpcEntries + .map((rpcEntry) => { + if (typeof rpcEntry === 'string') return rpcEntry + if (typeof rpcEntry === 'object' && rpcEntry && 'url' in rpcEntry) { + const url = (rpcEntry as { url?: unknown }).url + return typeof url === 'string' ? url : '' + } + return '' + }) + .filter((url) => url.length > 0) + + return { + chainId: typeof candidate.chainId === 'number' ? candidate.chainId : Number.NaN, + rpcs: sanitizeRpcUrls(urls), + } + }) + .filter((entry) => Number.isInteger(entry.chainId) && entry.rpcs.length > 0) +} + +function getCachedRpcUrls(cache: ViemRpcCacheEntry | null, chainId: number): string[] { + return cache?.rpcs.find((entry) => entry.chainId === chainId)?.rpcs ?? [] +} + +function isStale(cache: ViemRpcCacheEntry, refreshIntervalMs: number): boolean { + const fetchedAtMs = Date.parse(cache.fetchedAt) + return Number.isNaN(fetchedAtMs) || Date.now() - fetchedAtMs >= refreshIntervalMs +} + +function sanitizeRpcUrls(urls: string[]): string[] { + const deduped = new Set() + for (const url of urls) { + if (!url.startsWith('https://')) continue + if (url.includes('${')) continue + deduped.add(url) + } + return [...deduped] +} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index b0c20dc1..60d7407b 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -4,11 +4,23 @@ export default defineConfig({ entry: { index: 'src/index.ts', wagmi: 'src/wagmi.ts', + viemFallbackClient: 'src/viemFallbackClient.ts', }, format: ['esm', 'cjs'], dts: true, sourcemap: true, clean: true, tsconfig: 'tsconfig.build.json', - external: ['react', 'react-dom', 'react-native', 'react-native-web', 'wagmi', 'viem', '@wagmi/core', 'tamagui', '@tamagui/core', '@goodwidget/ui'], + external: [ + 'react', + 'react-dom', + 'react-native', + 'react-native-web', + 'wagmi', + 'viem', + '@wagmi/core', + 'tamagui', + '@tamagui/core', + '@goodwidget/ui', + ], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3111a2bf..9331f862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: version: link:../../packages/ui '@reown/appkit': specifier: ^1.8.22 - version: 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76) + version: 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@4.1.11) '@tamagui/core': specifier: 1.121.0 version: 1.121.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -461,6 +461,9 @@ importers: tamagui: specifier: 1.121.0 version: 1.121.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + viem: + specifier: ^2.0.0 + version: 2.48.4(typescript@5.9.3)(zod@4.1.11) devDependencies: '@types/react': specifier: ^18.3.0 @@ -491,19 +494,19 @@ importers: version: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@reown/appkit': specifier: ^1.8.22 - version: 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) + version: 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: ^1.8.22 - version: 1.8.22(v6adbyiuvbtrgqm65ozlnb246m) + version: 1.8.22(isknxadknpp2cdfgti7v5vosu4) '@tanstack/react-query': specifier: ^5.101.2 version: 5.101.2(react@18.3.1) viem: specifier: ^2.0.0 - version: 2.48.4(typescript@5.9.3)(zod@4.1.11) + version: 2.48.4(typescript@5.9.3)(zod@3.25.76) wagmi: specifier: ^3.7.1 - version: 3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)) + version: 3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76)) devDependencies: '@types/react': specifier: ^18.3.0 @@ -2051,7 +2054,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.28': resolution: {integrity: sha512-lvt72KNitGuixYD2l3SZmRKVu2G4zJpmg5V7WfUBNpmUU5oODBw/6qmiJ6kSLAlfDozscUk+BBGknBBzxUrwrA==} @@ -10731,31 +10734,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-org/account@2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11)': - dependencies: - '@coinbase/cdp-sdk': 1.52.0(typescript@5.9.3) - '@noble/hashes': 1.4.0 - clsx: 1.2.1 - eventemitter3: 5.0.1 - idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@4.1.11) - preact: 10.24.2 - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) - zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) - transitivePeerDependencies: - - '@types/react' - - bufferutil - - debug - - fastestsmallesttextencoderdecoder - - immer - - react - - typescript - - use-sync-external-store - - utf-8-validate - - zod - optional: true - - '@base-org/account@2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.52.0(typescript@5.9.3) '@noble/hashes': 1.4.0 @@ -10765,7 +10744,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) - zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -10829,28 +10808,7 @@ snapshots: - utf-8-validate optional: true - '@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11)': - dependencies: - '@noble/hashes': 1.4.0 - clsx: 1.2.1 - eventemitter3: 5.0.1 - idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@4.1.11) - preact: 10.24.2 - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) - zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) - transitivePeerDependencies: - - '@types/react' - - bufferutil - - immer - - react - - typescript - - use-sync-external-store - - utf-8-validate - - zod - optional: true - - '@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -10859,7 +10817,7 @@ snapshots: ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) preact: 10.24.2 viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) - zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + zustand: 5.0.3(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -12407,22 +12365,22 @@ snapshots: '@renovatebot/pep440@4.2.1': {} - '@reown/appkit-adapter-wagmi@1.8.22(v6adbyiuvbtrgqm65ozlnb246m)': + '@reown/appkit-adapter-wagmi@1.8.22(isknxadknpp2cdfgti7v5vosu4)': dependencies: - '@reown/appkit': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) + '@reown/appkit': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) + '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@3.25.76) + '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-polyfills': 1.8.22 - '@reown/appkit-scaffold-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) + '@reown/appkit-scaffold-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) - '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)) - '@walletconnect/universal-provider': 2.23.7(@vercel/blob@2.4.0)(typescript@5.9.3)(zod@4.1.11) + '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)) + '@walletconnect/universal-provider': 2.23.7(@vercel/blob@2.4.0)(typescript@5.9.3)(zod@3.25.76) valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) - wagmi: 3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)) + viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) + wagmi: 3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76)) optionalDependencies: - '@wagmi/connectors': 8.0.8(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)) + '@wagmi/connectors': 8.0.8(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76)) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12566,52 +12524,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11)': - dependencies: - '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) - lit: 3.3.0 - valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react - - typescript - - uploadthing - - use-sync-external-store - - utf-8-validate - - zod - - '@reown/appkit-pay@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76)': + '@reown/appkit-pay@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) lit: 3.3.0 valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) transitivePeerDependencies: @@ -12690,55 +12608,13 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11)': - dependencies: - '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) - '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) - lit: 3.3.0 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react - - typescript - - uploadthing - - use-sync-external-store - - utf-8-validate - - valtio - - zod - - '@reown/appkit-scaffold-ui@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) - '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76) + '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) lit: 3.3.0 transitivePeerDependencies: @@ -12888,55 +12764,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11)': - dependencies: - '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-polyfills': 1.8.22 - '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) - '@wallet-standard/wallet': 1.1.0 - '@walletconnect/logger': 3.0.2 - '@walletconnect/universal-provider': 2.23.7(@vercel/blob@2.4.0)(typescript@5.9.3)(zod@4.1.11) - valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) - optionalDependencies: - '@base-org/account': 2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@4.1.11) - '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@4.1.11) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react - - typescript - - uploadthing - - use-sync-external-store - - utf-8-validate - - zod - - '@reown/appkit-utils@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76)': + '@reown/appkit-utils@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) @@ -12948,8 +12776,8 @@ snapshots: valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) optionalDependencies: - '@base-org/account': 2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@3.25.76) '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@3.25.76) transitivePeerDependencies: @@ -13043,64 +12871,15 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11)': - dependencies: - '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@reown/appkit-polyfills': 1.8.22 - '@reown/appkit-scaffold-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) - '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@4.1.11) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@4.1.11) - '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) - '@walletconnect/universal-provider': 2.23.7(@vercel/blob@2.4.0)(typescript@5.9.3)(zod@4.1.11) - bs58: 6.0.0 - semver: 7.7.2 - valtio: 2.1.7(@types/react@18.3.28)(react@18.3.1) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) - optionalDependencies: - '@lit/react': 1.0.8(@types/react@18.3.28) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@types/react' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - debug - - encoding - - fastestsmallesttextencoderdecoder - - immer - - ioredis - - react - - typescript - - uploadthing - - use-sync-external-store - - utf-8-validate - - zod - - '@reown/appkit@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76)': + '@reown/appkit@1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76)': dependencies: '@reown/appkit-common': 1.8.22(typescript@5.9.3)(zod@3.25.76) '@reown/appkit-controllers': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) - '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@3.25.76) + '@reown/appkit-pay': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) '@reown/appkit-polyfills': 1.8.22 - '@reown/appkit-scaffold-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-scaffold-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) '@reown/appkit-ui': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(zod@3.25.76) - '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-utils': 1.8.22(@types/react@18.3.28)(@vercel/blob@2.4.0)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(valtio@2.1.7(@types/react@18.3.28)(react@18.3.1))(zod@3.25.76) '@reown/appkit-wallet': 1.8.22(typescript@5.9.3) '@walletconnect/universal-provider': 2.23.7(@vercel/blob@2.4.0)(typescript@5.9.3)(zod@3.25.76) bs58: 6.0.0 @@ -16220,14 +15999,14 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 - '@wagmi/connectors@8.0.22(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11))': + '@wagmi/connectors@8.0.22(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76))': dependencies: - '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) + '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)) + viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) optionalDependencies: - '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@4.1.11) - '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@4.1.11) + '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@3.25.76) typescript: 5.9.3 '@wagmi/connectors@8.0.22(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11))': @@ -16240,17 +16019,32 @@ snapshots: '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@4.1.11) typescript: 5.9.3 - '@wagmi/connectors@8.0.8(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11))': + '@wagmi/connectors@8.0.8(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76))': dependencies: - '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) + '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)) + viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) optionalDependencies: - '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11) - '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@4.1.11) - '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@4.1.11) + '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(typescript@5.9.3)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(typescript@5.9.3)(zod@3.25.76) typescript: 5.9.3 optional: true + '@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.3) + viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) + 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.101.2 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + '@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11))': dependencies: eventemitter3: 5.0.1 @@ -22133,14 +21927,14 @@ snapshots: w-json@1.3.11: {} - wagmi@3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)): + wagmi@3.7.1(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.2(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76)): dependencies: '@tanstack/react-query': 5.101.2(react@18.3.1) - '@wagmi/connectors': 8.0.22(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@4.1.11))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@4.1.11))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@4.1.11))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@4.1.11)) - '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@4.1.11)) + '@wagmi/connectors': 8.0.22(@coinbase/wallet-sdk@4.3.6(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(zod@3.25.76))(@safe-global/safe-apps-provider@0.18.6(typescript@5.9.3)(zod@3.25.76))(@safe-global/safe-apps-sdk@9.1.0(typescript@5.9.3)(zod@3.25.76))(@wagmi/core@3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)))(typescript@5.9.3)(viem@2.48.4(typescript@5.9.3)(zod@3.25.76)) + '@wagmi/core': 3.6.1(@tanstack/query-core@5.101.2)(@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.48.4(typescript@5.9.3)(zod@3.25.76)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - viem: 2.48.4(typescript@5.9.3)(zod@4.1.11) + viem: 2.48.4(typescript@5.9.3)(zod@3.25.76) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: From 0f38cc108acfde605fb9fbb502bcc000d8950669 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:57:32 +0000 Subject: [PATCH 2/4] fix: address viem fallback review feedback Co-authored-by: sirpy <671095+sirpy@users.noreply.github.com> --- packages/core/README.md | 2 + packages/core/package.json | 1 + packages/core/src/viemFallbackClient.test.ts | 156 +++++++++++++++++++ packages/core/src/viemFallbackClient.ts | 25 ++- 4 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/viemFallbackClient.test.ts diff --git a/packages/core/README.md b/packages/core/README.md index e7ed2fdd..d8f9828b 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -8,6 +8,8 @@ Runtime helpers for GoodWidget providers, host detection, wallet context, and vi The helper accepts a small key-value storage adapter. On initialization it reads cached RPC metadata from storage. If the cache is missing or older than 1 day, it starts a background refresh from Chainlist. Client creation waits for that first refresh only when no cached RPCs are available for the requested chain. +Caller-provided `fallbackRpcs` are always tried first, then the chain's own default RPC URLs, and finally any cached Chainlist URLs. The generated viem fallback transport enables ranking and a single retry so unhealthy discovered RPCs can be deprioritized during use instead of being trusted purely by cache order. + ```ts import { createViemFallbackClient } from '@goodwidget/core/viemFallbackClient' import { celo } from 'viem/chains' diff --git a/packages/core/package.json b/packages/core/package.json index c93e930d..f4f72c10 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,6 +26,7 @@ "build": "tsup", "dev": "tsup --watch", "lint": "eslint src/", + "test": "node --test --experimental-strip-types --test-force-exit src/**/*.test.ts", "clean": "rm -rf dist .turbo", "bump-version": "pnpm version patch --no-git-tag-version && git add package.json && git commit -m \"chore(release): bump @goodwidget/core to $(node -p 'JSON.parse(require(\"fs\").readFileSync(\"package.json\")).version')\"" }, diff --git a/packages/core/src/viemFallbackClient.test.ts b/packages/core/src/viemFallbackClient.test.ts new file mode 100644 index 00000000..52bf13a2 --- /dev/null +++ b/packages/core/src/viemFallbackClient.test.ts @@ -0,0 +1,156 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { celo } from 'viem/chains' + +import { createViemFallbackClient, type ViemFallbackStorage } from './viemFallbackClient.ts' + +function createStorage(initialValue?: string): ViemFallbackStorage { + let value = initialValue ?? null + + return { + async getItem() { + return value + }, + async setItem(_key, nextValue) { + value = nextValue + }, + } +} + +function createChainlistResponse(rpcs: string[]) { + return new Response( + JSON.stringify([ + { + chainId: celo.id, + rpc: rpcs, + }, + ]), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ) +} + +function createChain(defaultRpcs: string[]) { + return { + ...celo, + rpcUrls: { + ...celo.rpcUrls, + default: { + ...celo.rpcUrls.default, + http: defaultRpcs, + }, + }, + } +} + +test( + 'getRpcUrls prefers caller and chain defaults before cached Chainlist URLs', + { concurrency: false }, + async () => { + const client = createViemFallbackClient(createStorage(), { + fetch: async () => + createChainlistResponse(['https://cached.example', 'https://default.example']), + }) + + await client.ready + + const urls = await client.getRpcUrls( + createChain(['https://default.example']), + ['https://caller.example'], + ) + + assert.deepEqual(urls, [ + 'https://caller.example', + 'https://default.example', + 'https://cached.example', + ]) + }, +) + +test('refreshRpcs rejects non-HTTPS and non-Chainlist refresh URLs', { concurrency: false }, async () => { + const mockFetch = async () => createChainlistResponse([]) + + const insecureClient = createViemFallbackClient(createStorage(), { + chainlistRpcsUrl: 'http://chainlist.org/rpcs.json', + fetch: mockFetch, + }) + + await assert.rejects( + () => insecureClient.refreshRpcs(), + /chainlistRpcsUrl must use HTTPS/, + ) + + const unexpectedHostClient = createViemFallbackClient(createStorage(), { + chainlistRpcsUrl: 'https://example.com/rpcs.json', + fetch: mockFetch, + }) + + await assert.rejects( + () => unexpectedHostClient.refreshRpcs(), + /chainlistRpcsUrl must use chainlist\.org/, + ) +}) + +test( + 'createPublicClient falls back to a working discovered RPC when the first one fails', + { concurrency: false }, + async () => { + const originalFetch = globalThis.fetch + const badRpcCalls: string[] = [] + const goodRpcCalls: string[] = [] + + const mockFetch: typeof fetch = async (input, init) => { + const url = input instanceof Request ? input.url : String(input) + + if (url === 'https://chainlist.org/rpcs.json') { + return createChainlistResponse(['https://bad.example', 'https://good.example']) + } + + if (url === 'https://bad.example/' || url === 'https://bad.example') { + badRpcCalls.push(url) + return new Response('rate limited', { status: 429 }) + } + + if (url === 'https://good.example/' || url === 'https://good.example') { + goodRpcCalls.push(url) + const payload = JSON.parse(String(init?.body)) + + return new Response( + JSON.stringify({ + jsonrpc: '2.0', + id: payload.id, + result: '0xa4ec', + }), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ) + } + + throw new Error(`Unexpected fetch URL: ${url}`) + } + + globalThis.fetch = mockFetch + + try { + const client = createViemFallbackClient(createStorage(), { + fetch: mockFetch, + }) + const publicClient = await client.createPublicClient({ + chain: createChain([]), + }) + + const chainId = await publicClient.request({ method: 'eth_chainId' }) + + assert.equal(chainId, '0xa4ec') + assert.ok(badRpcCalls.length >= 1) + assert.ok(goodRpcCalls.length >= 1) + } finally { + globalThis.fetch = originalFetch + } + }, +) diff --git a/packages/core/src/viemFallbackClient.ts b/packages/core/src/viemFallbackClient.ts index 9606b2f7..d3a1f74c 100644 --- a/packages/core/src/viemFallbackClient.ts +++ b/packages/core/src/viemFallbackClient.ts @@ -18,6 +18,7 @@ const DEFAULT_CHAINLIST_RPCS_URL = 'https://chainlist.org/rpcs.json' const DEFAULT_CACHE_KEY = 'goodwidget:viem-rpcs' const DEFAULT_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 const DEFAULT_FETCH_TIMEOUT_MS = 10_000 +const CHAINLIST_HOST = 'chainlist.org' type MaybePromise = T | Promise @@ -92,7 +93,7 @@ export function createViemFallbackClient( const refreshRpcs = async (): Promise => { if (!fetchImpl) throw new Error('fetch is not available') - const chainlistUrl = new URL(chainlistRpcsUrl) + const chainlistUrl = parseChainlistUrl(chainlistRpcsUrl) const abortController = new AbortController() const timeout = setTimeout(() => abortController.abort(), fetchTimeoutMs) @@ -155,10 +156,12 @@ export function createViemFallbackClient( await refreshPromise } + // Prefer integrator-supplied RPCs and chain defaults before discovered Chainlist + // endpoints so callers control the primary transport order. return sanitizeRpcUrls([ - ...getCachedRpcUrls(cache, chain.id), ...fallbackRpcs, ...(chain.rpcUrls.default.http ?? []), + ...getCachedRpcUrls(cache, chain.id), ]) } @@ -168,7 +171,12 @@ export function createViemFallbackClient( ): Promise => { const rpcUrls = await getRpcUrls(chain, fallbackRpcs) const transports = rpcUrls.length > 0 ? rpcUrls.map((rpcUrl) => http(rpcUrl)) : [http()] - return fallback(transports) + // Let viem re-rank transports during use so stale cached endpoints do not remain + // the preferred RPCs after they start failing or slowing down. + return fallback(transports, { + rank: true, + retryCount: 1, + }) } return { @@ -291,6 +299,17 @@ function normalizeChainlistPayload(payload: unknown): CachedChainRpcs[] { .filter((entry) => Number.isInteger(entry.chainId) && entry.rpcs.length > 0) } +function parseChainlistUrl(value: string): URL { + const url = new URL(value) + if (url.protocol !== 'https:') { + throw new Error('chainlistRpcsUrl must use HTTPS') + } + if (url.hostname !== CHAINLIST_HOST) { + throw new Error(`chainlistRpcsUrl must use ${CHAINLIST_HOST}`) + } + return url +} + function getCachedRpcUrls(cache: ViemRpcCacheEntry | null, chainId: number): string[] { return cache?.rpcs.find((entry) => entry.chainId === chainId)?.rpcs ?? [] } From d9f364eb126c6378083d9faf67571f96951e6048 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:58:27 +0000 Subject: [PATCH 3/4] test: align viem fallback tests with review feedback Co-authored-by: sirpy <671095+sirpy@users.noreply.github.com> --- packages/core/src/viemFallbackClient.test.ts | 5 ++++- packages/core/src/viemFallbackClient.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/viemFallbackClient.test.ts b/packages/core/src/viemFallbackClient.test.ts index 52bf13a2..5f687ee7 100644 --- a/packages/core/src/viemFallbackClient.test.ts +++ b/packages/core/src/viemFallbackClient.test.ts @@ -5,6 +5,8 @@ import { celo } from 'viem/chains' import { createViemFallbackClient, type ViemFallbackStorage } from './viemFallbackClient.ts' +const chainlistUrl = 'https://chainlist.org/rpcs.json' + function createStorage(initialValue?: string): ViemFallbackStorage { let value = initialValue ?? null @@ -105,7 +107,7 @@ test( const mockFetch: typeof fetch = async (input, init) => { const url = input instanceof Request ? input.url : String(input) - if (url === 'https://chainlist.org/rpcs.json') { + if (url === chainlistUrl) { return createChainlistResponse(['https://bad.example', 'https://good.example']) } @@ -138,6 +140,7 @@ test( try { const client = createViemFallbackClient(createStorage(), { + chainlistRpcsUrl: chainlistUrl, fetch: mockFetch, }) const publicClient = await client.createPublicClient({ diff --git a/packages/core/src/viemFallbackClient.ts b/packages/core/src/viemFallbackClient.ts index d3a1f74c..f1f7f402 100644 --- a/packages/core/src/viemFallbackClient.ts +++ b/packages/core/src/viemFallbackClient.ts @@ -304,6 +304,8 @@ function parseChainlistUrl(value: string): URL { if (url.protocol !== 'https:') { throw new Error('chainlistRpcsUrl must use HTTPS') } + // Keep the refresh source pinned to Chainlist's canonical host so cached RPC + // metadata cannot be replaced by another origin. if (url.hostname !== CHAINLIST_HOST) { throw new Error(`chainlistRpcsUrl must use ${CHAINLIST_HOST}`) } From ea5e69115656128a774d444743629ee9b4d6c5eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:14:12 +0000 Subject: [PATCH 4/4] fix: remove chainlist URL validation Co-authored-by: sirpy <671095+sirpy@users.noreply.github.com> --- packages/core/README.md | 2 +- packages/core/src/viemFallbackClient.test.ts | 32 ++++++++------------ packages/core/src/viemFallbackClient.ts | 16 +--------- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index d8f9828b..de8d5b86 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -95,4 +95,4 @@ createViemFallbackClient(storage, { }) ``` -`chainlistRpcsUrl` is intended for Chainlist's HTTPS RPC JSON endpoint. The helper blocks non-HTTPS URLs and hosts outside the expected Chainlist domain. \ No newline at end of file +`chainlistRpcsUrl` defaults to Chainlist's RPC JSON endpoint, but can be overridden. Refresh requests still use a timeout and `redirect: 'error'`. \ No newline at end of file diff --git a/packages/core/src/viemFallbackClient.test.ts b/packages/core/src/viemFallbackClient.test.ts index 5f687ee7..b860bbcf 100644 --- a/packages/core/src/viemFallbackClient.test.ts +++ b/packages/core/src/viemFallbackClient.test.ts @@ -72,28 +72,22 @@ test( }, ) -test('refreshRpcs rejects non-HTTPS and non-Chainlist refresh URLs', { concurrency: false }, async () => { - const mockFetch = async () => createChainlistResponse([]) - - const insecureClient = createViemFallbackClient(createStorage(), { - chainlistRpcsUrl: 'http://chainlist.org/rpcs.json', - fetch: mockFetch, +test('refreshRpcs accepts a custom RPC list URL', { concurrency: false }, async () => { + const customUrl = 'http://example.com/rpcs.json' + const fetchCalls: string[] = [] + const client = createViemFallbackClient(createStorage(), { + chainlistRpcsUrl: customUrl, + fetch: async (input) => { + fetchCalls.push(input instanceof Request ? input.url : String(input)) + return createChainlistResponse(['https://cached.example']) + }, }) - await assert.rejects( - () => insecureClient.refreshRpcs(), - /chainlistRpcsUrl must use HTTPS/, - ) - - const unexpectedHostClient = createViemFallbackClient(createStorage(), { - chainlistRpcsUrl: 'https://example.com/rpcs.json', - fetch: mockFetch, - }) + await client.ready - await assert.rejects( - () => unexpectedHostClient.refreshRpcs(), - /chainlistRpcsUrl must use chainlist\.org/, - ) + assert.ok(fetchCalls.length >= 1) + assert.ok(fetchCalls.every((url) => url === customUrl)) + assert.deepEqual(await client.getRpcUrls(createChain([])), ['https://cached.example']) }) test( diff --git a/packages/core/src/viemFallbackClient.ts b/packages/core/src/viemFallbackClient.ts index f1f7f402..91f7d5c8 100644 --- a/packages/core/src/viemFallbackClient.ts +++ b/packages/core/src/viemFallbackClient.ts @@ -18,7 +18,6 @@ const DEFAULT_CHAINLIST_RPCS_URL = 'https://chainlist.org/rpcs.json' const DEFAULT_CACHE_KEY = 'goodwidget:viem-rpcs' const DEFAULT_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 const DEFAULT_FETCH_TIMEOUT_MS = 10_000 -const CHAINLIST_HOST = 'chainlist.org' type MaybePromise = T | Promise @@ -93,7 +92,7 @@ export function createViemFallbackClient( const refreshRpcs = async (): Promise => { if (!fetchImpl) throw new Error('fetch is not available') - const chainlistUrl = parseChainlistUrl(chainlistRpcsUrl) + const chainlistUrl = new URL(chainlistRpcsUrl) const abortController = new AbortController() const timeout = setTimeout(() => abortController.abort(), fetchTimeoutMs) @@ -299,19 +298,6 @@ function normalizeChainlistPayload(payload: unknown): CachedChainRpcs[] { .filter((entry) => Number.isInteger(entry.chainId) && entry.rpcs.length > 0) } -function parseChainlistUrl(value: string): URL { - const url = new URL(value) - if (url.protocol !== 'https:') { - throw new Error('chainlistRpcsUrl must use HTTPS') - } - // Keep the refresh source pinned to Chainlist's canonical host so cached RPC - // metadata cannot be replaced by another origin. - if (url.hostname !== CHAINLIST_HOST) { - throw new Error(`chainlistRpcsUrl must use ${CHAINLIST_HOST}`) - } - return url -} - function getCachedRpcUrls(cache: ViemRpcCacheEntry | null, chainId: number): string[] { return cache?.rpcs.find((entry) => entry.chainId === chainId)?.rpcs ?? [] }