diff --git a/jest.setup.ts b/jest.setup.ts index 29ae4162..75d33f46 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -14,3 +14,6 @@ global.console = { // warn: jest.fn(), // error: jest.fn(), } + +// mock console.warn +global.console.warn = jest.fn() diff --git a/src/utils/__tests__/chains.utils.test.ts b/src/utils/__tests__/chains.utils.test.ts new file mode 100644 index 00000000..9a8b6a51 --- /dev/null +++ b/src/utils/__tests__/chains.utils.test.ts @@ -0,0 +1,93 @@ +import { chains } from '@/constants/chains.consts' +import peanut from '@squirrel-labs/peanut-sdk' +import { getChainProvider } from '../chains.utils' + +// mock the chains array +jest.mock('@/constants/chains.consts', () => ({ + chains: [ + { + id: 1, + name: 'Ethereum', + rpcUrls: { + default: { http: ['https://eth-mainnet.infura.io/v3/your-key'] }, + public: { http: ['https://ethereum-rpc.publicnode.com'] }, + }, + }, + ], +})) + +// mock the peanut SDK +jest.mock('@squirrel-labs/peanut-sdk', () => ({ + getDefaultProvider: jest.fn(), +})) + +describe('Chain Utilities', () => { + // mock provider with required methods + const mockProvider = { + getNetwork: jest.fn(), + } + + beforeEach(() => { + // clear all mocks before each test + jest.clearAllMocks() + // reset the mock implementation + mockProvider.getNetwork.mockReset() + }) + + it('should successfully return provider when Infura RPC works', async () => { + // setup successful infura response + mockProvider.getNetwork.mockResolvedValueOnce({ chainId: 1 }) + ;(peanut.getDefaultProvider as jest.Mock).mockResolvedValueOnce(mockProvider) + + const provider = await getChainProvider('1') + + expect(provider).toBe(mockProvider) + expect(peanut.getDefaultProvider).toHaveBeenCalledWith('1') + expect(mockProvider.getNetwork).toHaveBeenCalled() + }) + + it('should fall back to public RPC when Infura fails', async () => { + // setup failed Infura response but successful public RPC + const failingProvider = { + getNetwork: jest.fn().mockRejectedValueOnce(new Error('Infura failed')), + } + const successfulPublicProvider = { + getNetwork: jest.fn().mockResolvedValueOnce({ chainId: 1 }), + } + + // first call fails (Infura), second call succeeds (public RPC) + ;(peanut.getDefaultProvider as jest.Mock) + .mockResolvedValueOnce(failingProvider) + .mockResolvedValueOnce(successfulPublicProvider) + + const provider = await getChainProvider('1') + + expect(provider).toBe(successfulPublicProvider) + expect(peanut.getDefaultProvider).toHaveBeenCalledTimes(2) + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Default RPC failed for chain 1')) + }) + + it('should throw error when chain is not found', async () => { + await expect(getChainProvider('999999')).rejects.toThrow('Chain 999999 not found') + }) + + it('should throw error when no public RPC is available', async () => { + // mock a chain without public RPC + const chainWithoutPublicRpc = { + ...chains[0], + rpcUrls: { + public: { http: [] }, + default: { http: ['some-url'] }, + }, + } + jest.spyOn(chains, 'find').mockReturnValueOnce(chainWithoutPublicRpc) + + // setup failed Infura response + const failingProvider = { + getNetwork: jest.fn().mockRejectedValueOnce(new Error('Infura failed')), + } + ;(peanut.getDefaultProvider as jest.Mock).mockResolvedValueOnce(failingProvider) + + await expect(getChainProvider('1')).rejects.toThrow('No public RPC URL found for chain 1') + }) +}) diff --git a/src/utils/chains.utils.ts b/src/utils/chains.utils.ts index cd9e8230..8551da28 100644 --- a/src/utils/chains.utils.ts +++ b/src/utils/chains.utils.ts @@ -1,5 +1,5 @@ import { chains } from '@/constants/chains.consts' -import { ethers } from 'ethers' +import peanut from '@squirrel-labs/peanut-sdk' /** * Generates an Infura API URL for supported networks @@ -11,20 +11,26 @@ export const getInfuraApiUrl = (network: string): string => { } /** - * Retrieves the JSON-RPC provider for a given blockchain network. - * - * @param chainId - The unique identifier of the blockchain network as a string. - * @returns An instance of `ethers.providers.JsonRpcProvider` configured with the network's RPC URL and chain ID. - * @throws Will throw an error if the chain with the specified `chainId` is not found. - * - * @remarks - * This function uses the `ethers` library to create a JSON-RPC provider, which is required by the Peanut SDK. + * Retrieves the JSON-RPC provider for a given blockchain network with fallback to public RPC. */ -export const getChainProvider = (chainId: string) => { +export const getChainProvider = async (chainId: string) => { const chain = chains.find((chain) => chain.id.toString() === chainId) - if (!chain) throw new Error(`Chain ${chainId} not found`) - // using ethers cuz peanut sdk accepts provider type to be from ethers atm 🫠 - return new ethers.providers.JsonRpcProvider(chain.rpcUrls.default.http[0], Number(chainId)) + // Try default (Infura) provider first + try { + const defaultProvider = await peanut.getDefaultProvider(chainId) + await defaultProvider.getNetwork() // Test the connection + return defaultProvider + } catch (error) { + console.warn(`Default RPC failed for chain ${chainId}, falling back to public RPC`) + + // Fallback to public RPC + const publicRpcUrl = chain.rpcUrls.public.http[0] + if (!publicRpcUrl) { + throw new Error(`No public RPC URL found for chain ${chainId}`) + } + + return peanut.getDefaultProvider(publicRpcUrl) + } }