Skip to content

Commit b0aedbc

Browse files
authored
[TASK-14095] feat: add fallback transport to viem clients (#1131)
* feat: add fallback transport to viem clients Use viem fallback transport to handle RPC errors and fallback to other providers. * style: Apply prettier formatting * test: add fallback to viem mock
1 parent 6d71e01 commit b0aedbc

File tree

10 files changed

+47
-92
lines changed

10 files changed

+47
-92
lines changed

src/app/actions/claimLinks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const getLinkDetails = unstable_cache(
1515
const chainId = params.chainId
1616
const contractVersion = params.contractVersion
1717
const depositIdx = params.depositIdx
18-
const client = await getPublicClient(Number(chainId) as ChainId)
18+
const client = getPublicClient(Number(chainId) as ChainId)
1919
const peanutContractAddress = peanut.getContractAddress(chainId, contractVersion) as Address
2020
const peanutContractAbi = peanut.getContractAbi(contractVersion)
2121
const contract = getContract({
@@ -58,7 +58,7 @@ export const getLinkFromTx = unstable_cache(
5858
password: string
5959
}): Promise<string> => {
6060
const { chainId } = linkDetails
61-
const client = await getPublicClient(Number(chainId) as ChainId)
61+
const client = getPublicClient(Number(chainId) as ChainId)
6262
const txReceipt = await client.waitForTransactionReceipt({
6363
hash: txHash as `0x${string}`,
6464
})
@@ -68,7 +68,7 @@ export const getLinkFromTx = unstable_cache(
6868
)
6969

7070
export async function getNextDepositIndex(contractVersion: string): Promise<number> {
71-
const publicClient = await getPublicClient(PEANUT_WALLET_CHAIN.id)
71+
const publicClient = getPublicClient(PEANUT_WALLET_CHAIN.id)
7272
const contractAbi = peanut.getContractAbi(contractVersion)
7373
const contractAddress: Address = peanut.getContractAddress(
7474
PEANUT_WALLET_CHAIN.id.toString(),

src/app/actions/clients.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
1-
'use server'
2-
import type { PublicClient, Chain } from 'viem'
3-
import { createPublicClient, http, extractChain } from 'viem'
1+
import type { PublicClient, Chain, Transport } from 'viem'
2+
import { createPublicClient, http, extractChain, fallback } from 'viem'
43
import * as chains from 'viem/chains'
54

65
import { PUBLIC_CLIENTS_BY_CHAIN, rpcUrls } from '@/constants'
76

87
const allChains = Object.values(chains)
98
export type ChainId = (typeof allChains)[number]['id']
109

11-
export const getPublicClient = async (chainId: ChainId): Promise<PublicClient> => {
10+
/**
11+
* Returns viem transport with fallback
12+
*
13+
* @see https://viem.sh/docs/clients/transports/fallback#fallback-transport
14+
*/
15+
export function getTransportWithFallback(chainId: ChainId): Transport {
16+
const providerUrls = rpcUrls[chainId]
17+
if (!providerUrls) {
18+
// If no premium providers are configured, viem will use a default one
19+
return http()
20+
}
21+
return fallback(
22+
providerUrls.map((u) => http(u)),
23+
// Viem checks the status of the provider every 60 seconds and notes latency
24+
// and stability. The provider that viem will try first depend on this
25+
// ranking
26+
{ rank: { interval: 60_000 } }
27+
)
28+
}
29+
30+
export function getPublicClient(chainId: ChainId): PublicClient {
1231
let client: PublicClient | undefined = PUBLIC_CLIENTS_BY_CHAIN[chainId]?.client
1332
if (client) return client
1433
const chain: Chain = extractChain({ chains: allChains, id: chainId })
1534
if (!chain) throw new Error(`No chain found for chainId ${chainId}`)
1635
return createPublicClient({
17-
transport: http(rpcUrls[chainId][0]),
36+
transport: getTransportWithFallback(chainId),
1837
chain,
1938
})
2039
}

src/app/actions/tokens.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export const fetchTokenDetails = unstable_cache(
160160
console.log('chain id', chainId)
161161
const tokenDetails = getTokenDetails({ tokenAddress: tokenAddress as Address, chainId: chainId! })
162162
if (tokenDetails) return tokenDetails
163-
const client = await getPublicClient(Number(chainId) as ChainId)
163+
const client = getPublicClient(Number(chainId) as ChainId)
164164
console.log('token address', tokenAddress)
165165
const [symbol, name, decimals] = await Promise.all([
166166
client.readContract({
@@ -193,7 +193,7 @@ export const fetchTokenDetails = unstable_cache(
193193
*/
194194
const getCachedGasPrice = unstable_cache(
195195
async (chainId: string) => {
196-
const client = await getPublicClient(Number(chainId) as ChainId)
196+
const client = getPublicClient(Number(chainId) as ChainId)
197197
const gasPrice = await client.getGasPrice()
198198
return gasPrice.toString()
199199
},
@@ -208,7 +208,7 @@ const getCachedGasPrice = unstable_cache(
208208
*/
209209
const getCachedGasEstimate = unstable_cache(
210210
async (fromAddress: Address, contractAddress: Address, data: Hex, chainId: string) => {
211-
const client = await getPublicClient(Number(chainId) as ChainId)
211+
const client = getPublicClient(Number(chainId) as ChainId)
212212
const gasEstimate = await client.estimateGas({
213213
account: fromAddress,
214214
to: contractAddress,

src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jest.mock('viem', () => ({
4343
isAddress: (address: string) => address.startsWith('0x') && address.length === 42,
4444
http: jest.fn(),
4545
createPublicClient: jest.fn(),
46+
fallback: jest.fn(),
4647
}))
4748

4849
describe('GeneralRecipientInput Type Detection', () => {

src/constants/general.consts.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ export const rpcUrls: Record<number, string[]> = {
2222
[polygon.id]: [infuraUrl('polygon-mainnet'), alchemyUrl('polygon-mainnet')].filter(Boolean) as string[],
2323
[optimism.id]: [infuraUrl('optimism-mainnet'), alchemyUrl('opt-mainnet')].filter(Boolean) as string[],
2424
[base.id]: [infuraUrl('base-mainnet'), alchemyUrl('base-mainnet')].filter(Boolean) as string[],
25-
// Infura is returning weird estimations for BSC @2025-05-14
26-
//[bsc.id]: `https://bsc-mainnet.infura.io/v3/${INFURA_API_KEY}`,
2725
[bsc.id]: ['https://bsc-dataseed.bnbchain.org', infuraUrl('bsc-mainnet'), alchemyUrl('bsc-mainnet')].filter(
2826
Boolean
2927
) as string[],

src/constants/zerodev.consts.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { rpcUrls } from '@/constants/general.consts'
21
import { getEntryPoint, KERNEL_V3_1 } from '@zerodev/sdk/constants'
32
import type { Chain, PublicClient } from 'viem'
4-
import { createPublicClient, http } from 'viem'
3+
import { createPublicClient } from 'viem'
4+
import { getTransportWithFallback } from '@/app/actions/clients'
55
import { arbitrum, polygon } from 'viem/chains'
66

77
// consts needed to define low level SDK kernel
@@ -57,7 +57,7 @@ export const PUBLIC_CLIENTS_BY_CHAIN: Record<
5757
> = {
5858
[arbitrum.id]: {
5959
client: createPublicClient({
60-
transport: http(rpcUrls[arbitrum.id][0]),
60+
transport: getTransportWithFallback(arbitrum.id),
6161
chain: arbitrum,
6262
pollingInterval: 500,
6363
}),
@@ -67,7 +67,7 @@ export const PUBLIC_CLIENTS_BY_CHAIN: Record<
6767
},
6868
[polygon.id]: {
6969
client: createPublicClient({
70-
transport: http(rpcUrls[polygon.id][0]),
70+
transport: getTransportWithFallback(polygon.id),
7171
chain: polygon,
7272
pollingInterval: 2500,
7373
}),

src/services/swap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ async function checkTokenAllowance(
191191
spenderAddress: Address,
192192
chainId: string
193193
): Promise<bigint> {
194-
const client = await getPublicClient(Number(chainId) as ChainId)
194+
const client = getPublicClient(Number(chainId) as ChainId)
195195

196196
const allowance = await client.readContract({
197197
address: tokenAddress,

src/utils/ens.utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { JustaName } from '@justaname.id/sdk'
22
import { rpcUrls } from '@/constants/general.consts'
33
import { mainnet } from 'viem/chains'
4-
import { ethers } from 'ethers'
54

65
/**
76
* Resolves an Ethereum address to its username using JustaName ENS resolution
@@ -11,7 +10,7 @@ import { ethers } from 'ethers'
1110
*/
1211
export async function resolveAddressToUsername(address: string, siteUrl: string): Promise<string | null> {
1312
try {
14-
const mainnetRpcUrl = rpcUrls[mainnet.id]?.[0] ?? ethers.getDefaultProvider('mainnet')
13+
const mainnetRpcUrl = rpcUrls[mainnet.id]?.[0]!
1514

1615
const ensDomain = process.env.NEXT_PUBLIC_JUSTANAME_ENS_DOMAIN
1716

@@ -23,7 +22,7 @@ export async function resolveAddressToUsername(address: string, siteUrl: string)
2322
networks: [
2423
{
2524
chainId: 1, // Ethereum Mainnet
26-
providerUrl: mainnetRpcUrl || 'https://eth.llamarpc.com',
25+
providerUrl: mainnetRpcUrl,
2726
},
2827
],
2928
ensDomains: [

src/utils/general.utils.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import * as Sentry from '@sentry/nextjs'
1515
import peanut, { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk'
1616
import { SiweMessage } from 'siwe'
1717
import type { Address, TransactionReceipt } from 'viem'
18-
import { getAddress, isAddress } from 'viem'
18+
import { getAddress, isAddress, erc20Abi } from 'viem'
1919
import * as wagmiChains from 'wagmi/chains'
20-
import { getSDKProvider } from './sdk.utils'
20+
import { getPublicClient, type ChainId } from '@/app/actions/clients'
2121
import { NATIVE_TOKEN_ADDRESS, SQUID_ETH_ADDRESS } from './token.utils'
2222

2323
export function urlBase64ToUint8Array(base64String: string) {
@@ -1056,16 +1056,13 @@ export async function fetchTokenSymbol(tokenAddress: string, chainId: string): P
10561056
let tokenSymbol = getTokenSymbol(tokenAddress, chainId)
10571057
if (!tokenSymbol) {
10581058
try {
1059-
const provider = await getSDKProvider({ chainId })
1060-
if (!provider) {
1061-
console.error(`Failed to get provider for chain ID ${chainId}`)
1062-
return undefined
1063-
}
1064-
const contract = await peanut.getTokenContractDetails({
1065-
address: tokenAddress,
1066-
provider: provider,
1067-
})
1068-
tokenSymbol = contract?.symbol?.toUpperCase()
1059+
const client = getPublicClient(Number(chainId) as ChainId)
1060+
tokenSymbol = (await client.readContract({
1061+
address: tokenAddress as Address,
1062+
abi: erc20Abi,
1063+
functionName: 'symbol',
1064+
args: [],
1065+
})) as string
10691066
} catch (error) {
10701067
Sentry.captureException(error)
10711068
console.error('Error fetching token symbol:', error)

src/utils/sdk.utils.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)