From 48e573fa3379f9ee25abd9c58bc610690b405ec8 Mon Sep 17 00:00:00 2001 From: zeuslawyer Date: Fri, 12 Sep 2025 15:35:12 +1000 Subject: [PATCH 1/4] Refactor Example/ Next.js example to demonstrate use of ccipClient that accepts Ethers.js signer. Update example nextjs app's routes to show wagmi use and etherjs use with ccipClient. Update dependencies - Updated `@chainlink/ccip-js` dependency in `examples/nextjs/package.json` to use workspace reference. This means it will use local built version of ccip-js package rather than published version. But will use published version when it itself is publised to npmjs. - Added new Ethers.js components and page routes to show the CCIP JS Package API. - Updated `pnpm-lock.yaml` to reflect new dependencies and versions. - Removed outdated Babel dependencies and updated node types across various packages. --- examples/nextjs/app/ccip-js-ethers/page.tsx | 10 + .../{ccip-js => ccip-js-ethers}/providers.tsx | 0 .../app/{ccip-js => ccip-js-wagmi}/page.tsx | 2 +- .../nextjs/app/ccip-js-wagmi/providers.tsx | 16 + examples/nextjs/app/layout.tsx | 10 +- .../nextjs/components/ccip-with-ethers.tsx | 1166 +++++++++++++++++ .../{ccip.tsx => ccip-with-wagmi.tsx} | 2 +- examples/nextjs/package.json | 5 +- packages/ccip-js/src/adapters/ethers.ts | 5 +- packages/ccip-js/src/api.ts | 3 +- .../ccip-js/test/integration-testnet.test.ts | 2 +- pnpm-lock.yaml | 163 +-- 12 files changed, 1251 insertions(+), 133 deletions(-) create mode 100644 examples/nextjs/app/ccip-js-ethers/page.tsx rename examples/nextjs/app/{ccip-js => ccip-js-ethers}/providers.tsx (100%) rename examples/nextjs/app/{ccip-js => ccip-js-wagmi}/page.tsx (73%) create mode 100644 examples/nextjs/app/ccip-js-wagmi/providers.tsx create mode 100644 examples/nextjs/components/ccip-with-ethers.tsx rename examples/nextjs/components/{ccip.tsx => ccip-with-wagmi.tsx} (99%) diff --git a/examples/nextjs/app/ccip-js-ethers/page.tsx b/examples/nextjs/app/ccip-js-ethers/page.tsx new file mode 100644 index 0000000..f0ad3c0 --- /dev/null +++ b/examples/nextjs/app/ccip-js-ethers/page.tsx @@ -0,0 +1,10 @@ +import { CCIPEthers } from "@/components/ccip-with-ethers"; +import { Providers } from "./providers"; + +export default function CCIPJsPage() { + return ( + + + + ); +} diff --git a/examples/nextjs/app/ccip-js/providers.tsx b/examples/nextjs/app/ccip-js-ethers/providers.tsx similarity index 100% rename from examples/nextjs/app/ccip-js/providers.tsx rename to examples/nextjs/app/ccip-js-ethers/providers.tsx diff --git a/examples/nextjs/app/ccip-js/page.tsx b/examples/nextjs/app/ccip-js-wagmi/page.tsx similarity index 73% rename from examples/nextjs/app/ccip-js/page.tsx rename to examples/nextjs/app/ccip-js-wagmi/page.tsx index 0b00b06..7c8b8e6 100644 --- a/examples/nextjs/app/ccip-js/page.tsx +++ b/examples/nextjs/app/ccip-js-wagmi/page.tsx @@ -1,4 +1,4 @@ -import { CCIP } from "@/components/ccip"; +import { CCIP } from "@/components/ccip-with-wagmi"; import { Providers } from "./providers"; export default function CCIPJsPage() { diff --git a/examples/nextjs/app/ccip-js-wagmi/providers.tsx b/examples/nextjs/app/ccip-js-wagmi/providers.tsx new file mode 100644 index 0000000..91787dc --- /dev/null +++ b/examples/nextjs/app/ccip-js-wagmi/providers.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { WagmiProvider } from 'wagmi'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactNode } from 'react'; +import { wagmiConfig } from '@/config/wagmiConfig'; + +const queryClient = new QueryClient(); + +export function Providers({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/examples/nextjs/app/layout.tsx b/examples/nextjs/app/layout.tsx index bf73ccd..8353c1b 100644 --- a/examples/nextjs/app/layout.tsx +++ b/examples/nextjs/app/layout.tsx @@ -34,9 +34,15 @@ export default function RootLayout({ - CCIP-JS + CCIP-JS (Wagmi) + + + CCIP-JS (Ethers)
diff --git a/examples/nextjs/components/ccip-with-ethers.tsx b/examples/nextjs/components/ccip-with-ethers.tsx new file mode 100644 index 0000000..a92e2c9 --- /dev/null +++ b/examples/nextjs/components/ccip-with-ethers.tsx @@ -0,0 +1,1166 @@ +"use client"; + +import { createClient, IERC20ABI, RateLimiterState, TransferStatus, SupportedClient } from "@chainlink/ccip-js"; +import { + Address, + encodeAbiParameters, + encodeFunctionData, + Hash, + Hex, + parseEther, + PublicClient, + TransactionReceipt, + WalletClient, +} from "viem"; +import { useState, useEffect } from "react"; // Added useEffect + +import { ethers, BrowserProvider, Signer } from "ethers"; // Added BrowserProvider, Signer + +const ccipClient = createClient(); + +export function CCIPEthers() { + const [ethersProvider, setEthersProvider] = useState(undefined); + const [ethersSigner, setEthersSigner] = useState(undefined); + const [ethersErrorMessage, setEthersErrorMessage] = useState(null); + + return ( +
+ {!ethersProvider && !ethersSigner && ( + + )} + {ethersErrorMessage && ( +
{ethersErrorMessage}
+ )} + {ethersProvider && ( + <> + + + + + + + + + + + )} + {ethersSigner && ( + <> + + + + + + )} +
+ ); +} + +interface ConnectWalletProps { + setEthersProvider: React.Dispatch>; + setEthersSigner: React.Dispatch>; + setEthersErrorMessage: React.Dispatch>; +} + +function ConnectWallet({ setEthersProvider, setEthersSigner, setEthersErrorMessage }: ConnectWalletProps) { + const [currentAccount, setCurrentAccount] = useState(null); + const [currentChainId, setCurrentChainId] = useState(null); + const [connectionStatus, setConnectionStatus] = useState("Disconnected"); + const [connectError, setConnectError] = useState(null); + const [switchChainError, setSwitchChainError] = useState(null); + + const availableChains = [ + { id: 43113, name: "Avalanche Fuji", hexId: "0xA28A" }, // Example: Avalanche Fuji testnet + { id: 11155111, name: "Sepolia", hexId: "0xAA36A7" }, // Example: Sepolia testnet + // Add other chains as needed + ]; + + useEffect(() => { + const initEthersConnection = async () => { + if (typeof window !== "undefined" && window.ethereum) { + try { + setConnectionStatus("Connecting..."); + const accounts = await window.ethereum.request({ method: "eth_accounts" }); + if (accounts.length > 0) { + setCurrentAccount(accounts[0]); + const provider = new ethers.BrowserProvider(window.ethereum); + setEthersProvider(provider); + const signer = await provider.getSigner(); + setEthersSigner(signer); + const network = await provider.getNetwork(); + setCurrentChainId(`${network.chainId}`); + setConnectionStatus("Connected"); + setConnectError(null); + } else { + setConnectionStatus("Disconnected"); + setCurrentAccount(null); + setEthersProvider(undefined); + setEthersSigner(undefined); + } + + const handleAccountsChanged = (accounts: string[]) => { + console.log('Ethers: Accounts changed:', accounts); + if (accounts.length === 0) { + setCurrentAccount(null); + setEthersProvider(undefined); + setEthersSigner(undefined); + setConnectionStatus("Disconnected"); + setEthersErrorMessage("MetaMask disconnected or no accounts available."); + } else { + setCurrentAccount(accounts[0]); + initEthersConnection(); // Re-initialize provider/signer with new account + setEthersErrorMessage(null); + } + }; + + const handleChainChanged = (chainId: string) => { + console.log('Ethers: Chain changed:', chainId); + setCurrentChainId(parseInt(chainId, 16).toString()); + initEthersConnection(); // Re-initialize provider/signer for new chain + setEthersErrorMessage(null); + }; + + window.ethereum.on('accountsChanged', handleAccountsChanged); + window.ethereum.on('chainChanged', handleChainChanged); + + return () => { + window.ethereum.removeListener('accountsChanged', handleAccountsChanged); + window.ethereum.removeListener('chainChanged', handleChainChanged); + }; + } catch (error: any) { + console.error("Ethers: Failed to connect or initialize:", error); + setConnectionStatus("Disconnected"); + const errorMessage = `Ethers: Failed to connect or initialize: ${error.message || error}`; + setConnectError(errorMessage); + setEthersErrorMessage(errorMessage); + } + } else { + setConnectionStatus("Disconnected"); + const errorMessage = "Ethers: MetaMask or compatible wallet not detected."; + setConnectError(errorMessage); + setEthersErrorMessage(errorMessage); + } + }; + + initEthersConnection(); + }, []); + + const handleConnectWallet = async () => { + if (typeof window !== "undefined" && window.ethereum) { + try { + setConnectionStatus("Connecting..."); + setConnectError(null); + await window.ethereum.request({ method: "eth_requestAccounts" }); + const provider = new ethers.BrowserProvider(window.ethereum); + setEthersProvider(provider); + const signer = await provider.getSigner(); + setEthersSigner(signer); + const network = await provider.getNetwork(); + setCurrentChainId(`${network.chainId}`); + setCurrentAccount(await signer.getAddress()); + setConnectionStatus("Connected"); + setEthersErrorMessage(null); + } catch (error: any) { + console.error("Ethers: Failed to connect to MetaMask:", error); + setConnectionStatus("Disconnected"); + const errorMessage = `Ethers: Failed to connect to MetaMask: ${error.message || error}`; + setConnectError(errorMessage); + setEthersErrorMessage(errorMessage); + } + } else { + const errorMessage = "Ethers: MetaMask or compatible wallet not detected."; + setConnectError(errorMessage); + setEthersErrorMessage(errorMessage); + } + }; + + const handleSwitchChain = async (newChainId: string) => { + if (window.ethereum) { + try { + setSwitchChainError(null); + const targetChain = availableChains.find(c => `${c.id}` === newChainId); + if (!targetChain) { + throw new Error(`Chain with id ${newChainId} not found.`); + } + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: targetChain.hexId }], + }); + // ChainChanged event listener will handle updating currentChainId and re-initializing provider/signer + } catch (error: any) { + console.error("Ethers: Failed to switch chain:", error); + const errorMessage = `Ethers: Failed to switch chain: ${error.message || error}`; + setSwitchChainError(errorMessage); + setEthersErrorMessage(errorMessage); + } + } + }; + + return ( +
+

Connect Wallet (Ethers.js):

+
+ +
+ {connectError &&

{connectError}

} + {currentAccount &&

{`Address: ${currentAccount}`}

} + {currentChainId && ( + <> +

{`Connected to Chain ID: ${currentChainId} (Status: ${connectionStatus})`}

+
+ + +
+ {switchChainError &&

{switchChainError}

} + + )} +
+ ); +} + +function ApproveRouter({ ethersSigner }: { ethersSigner: SupportedClient }) { // Changed WalletClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [amount, setAmount] = useState(); + const [txHash, setTxHash] = useState(); + + return ( +
+

Approve Transfer

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+
+ + setAmount(target.value)} + /> +
+ + {txHash && ( +
+ + {txHash} +
+ )} +
+ ); +} + +function TransferTokensAndMessage({ ethersSigner }: { ethersSigner: SupportedClient }) { // Changed WalletClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [amount, setAmount] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [destinationAccount, setDestinationAccount] = useState(); + const [data, setData] = useState(); + const [messageId, setMessageId] = useState(); + const [txHash, setTxHash] = useState(); + + return ( +
+

Transfer Tokens

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setDestinationAccount(target.value)} + /> +
+
+ + setData(encodeAbiParameters([{ type: "string", name: "data" }], [target.value]))} + /> +
+
+ + setAmount(target.value)} + /> +
+ + {txHash && ( +
+ + {txHash} +
+ )} + {messageId && ( +
+ + {messageId} +
+ )} +
+ ); +} + +function SendCCIPMessage({ ethersSigner }: { ethersSigner: SupportedClient }) { // Changed WalletClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [destinationAccount, setDestinationAccount] = useState(); + const [data, setData] = useState(); + const [messageId, setMessageId] = useState(); + const [txHash, setTxHash] = useState(); + + return ( +
+

Send Message

+
+ + setRouterAddress(target.value)} + /> +
+ +
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setDestinationAccount(target.value)} + /> +
+
+ + setData(encodeAbiParameters([{ type: "string", name: "data" }], [target.value]))} + /> +
+ + {txHash && ( +
+ + {txHash} +
+ )} + {messageId && ( +
+ + {messageId} +
+ )} +
+ ); +} + +function SendFunctionData({ ethersSigner }: { ethersSigner: SupportedClient }) { // Changed WalletClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [destinationAccount, setDestinationAccount] = useState(); + const [amount, setAmount] = useState(); + const [data, setData] = useState(); + const [messageId, setMessageId] = useState(); + const [txHash, setTxHash] = useState(); + + return ( +
+

Send Function Data

+

Using ERC20 transfer function

+
+ + setRouterAddress(target.value)} + /> +
+ +
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setDestinationAccount(target.value)} + /> +
+
+ + setAmount(target.value)} + /> +
+ + {txHash && ( +
+ + {txHash} +
+ )} + {messageId && ( +
+ + {messageId} +
+ )} +
+ ); +} + +function GetAllowance({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [account, setAccount] = useState(); + const [allowance, setAllowance] = useState(); + return ( +
+

Get allowance:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+ +
+ + setAccount(target.value)} + /> +
+ + + {allowance && ( +
+ + {allowance} +
+ )} +
+ ); +} + +function GetOnRampAddress({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [onRamp, setOnRamp] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + return ( +
+

Get On-ramp address:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+ + + {onRamp && ( +
+ + {onRamp} +
+ )} +
+ ); +} + +function GetSupportedFeeTokens({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [supportedFeeTokens, setSupportedFeeTokens] = useState(); + + return ( +
+

Get supported fee tokens:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+ + {supportedFeeTokens && supportedFeeTokens.length > 0 && ( +
+ + + {supportedFeeTokens.map(address => ( +
+                                {address}
+                            
+ ))} +
+
+ )} +
+ ); +} + +function GetLaneRateRefillLimits({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [rateLimits, setRateLimits] = useState(); + + return ( +
+

Get lane rate refil limits:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+ + {rateLimits && ( +
+ + +
+                            {`Tokens: ${rateLimits.tokens.toLocaleString()}`}
+                        
+
+                            {`Last updated: ${new Date(rateLimits.lastUpdated * 1000).toLocaleString()}`}
+                        
+
{`Is enabled: ${rateLimits.isEnabled.toString()}`}
+
{`Capacity: ${rateLimits.capacity.toLocaleString()}`}
+
{`Rate: ${rateLimits.rate.toLocaleString()}`}
+
+
+ )} +
+ ); +} + +function GetTokenRateLimitByLane({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [tokenRateLimits, setTokenRateLimits] = useState(); + + return ( +
+

Get token rate limit by lane:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+ + {tokenRateLimits && ( + <> +
+ + +
+                                {`Tokens: ${tokenRateLimits.tokens.toLocaleString()}`}
+                            
+
+                                {`Last updated: ${new Date(tokenRateLimits.lastUpdated * 1000).toLocaleString()}`}
+                            
+
{`Is enabled: ${tokenRateLimits.isEnabled.toString()}`}
+
{`Capacity: ${tokenRateLimits.capacity.toLocaleString()}`}
+
{`Rate: ${tokenRateLimits.rate.toLocaleString()}`}
+
+
+ + )} +
+ ); +} + +function IsTokenSupported({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [isTokenSupported, setIsTokenSupported] = useState(); + + return ( +
+

Is token supported:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+ + {isTokenSupported && ( +
+ + {isTokenSupported.toLocaleString()} +
+ )} +
+ ); +} + +function GetTokenAdminRegistry({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [tokenAdminRegistry, setTokenAdminRegistry] = useState(); + return ( +
+

Token admin registry:

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+ + {tokenAdminRegistry && ( +
+ + {tokenAdminRegistry.toLocaleString()} +
+ )} +
+ ); +} + +function GetTransactionReceipt({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [hash, setHash] = useState(); + const [transactionReceipt, setTransactionReceipt] = useState(); + + return ( +
+

Get transaction receipt:

+ +
+ + setHash(target.value)} + /> +
+ + + {transactionReceipt && ( + <> +

{`Block Number: ${transactionReceipt.blockNumber.toString()}`}

+

{`From: ${transactionReceipt.from}`}

+

{`To: ${transactionReceipt.to}`}

+

{`Status: ${transactionReceipt.status}`}

+
+ + +
+                                {`Block Number: ${transactionReceipt.blockNumber.toString()}`}
+                            
+
{`From: ${transactionReceipt.from}`}
+
{`To: ${transactionReceipt.to}`}
+
{`Status: ${transactionReceipt.status}`}
+
+
+ + )} +
+ ); +} + +function GetFee({ ethersProvider }: { ethersProvider: SupportedClient }) { // Changed PublicClient to SupportedClient + const [routerAddress, setRouterAddress] = useState(); + const [tokenAddress, setTokenAddress] = useState(); + const [amount, setAmount] = useState(); + const [destinationChainSelector, setDestinationChainSelector] = useState(); + const [destinationAccount, setDestinationAccount] = useState(); + const [data, setData] = useState(); + const [fee, setFee] = useState(); + + return ( +
+

Get fee

+
+ + setRouterAddress(target.value)} + /> +
+
+ + setTokenAddress(target.value)} + /> +
+
+ + setDestinationChainSelector(target.value)} + /> +
+
+ + setDestinationAccount(target.value)} + /> +
+
+ + setData(encodeAbiParameters([{ type: "string", name: "data" }], [target.value]))} + /> +
+
+ + setAmount(target.value)} + /> +
+ + {fee && ( +
+ + {fee} +
+ )} +
+ ); +} diff --git a/examples/nextjs/components/ccip.tsx b/examples/nextjs/components/ccip-with-wagmi.tsx similarity index 99% rename from examples/nextjs/components/ccip.tsx rename to examples/nextjs/components/ccip-with-wagmi.tsx index 7609f6a..fa5c83d 100644 --- a/examples/nextjs/components/ccip.tsx +++ b/examples/nextjs/components/ccip-with-wagmi.tsx @@ -23,7 +23,7 @@ export function CCIP() { return (
- + {!publicClient && !walletClient && } {publicClient && ( <> diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 9013ba3..31039d6 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -13,9 +13,10 @@ "lint": "next lint" }, "dependencies": { - "@chainlink/ccip-js": "^0.2.1", + "@chainlink/ccip-js": "workspace:^", "@chainlink/ccip-react-components": "^0.3.0", "@tanstack/react-query": "^5.37.1", + "ethers": "6.13.4", "next": "14.2.3", "react": "18", "react-dom": "18", @@ -32,4 +33,4 @@ "postcss": "^8", "tailwindcss": "^3.4.1" } -} +} \ No newline at end of file diff --git a/packages/ccip-js/src/adapters/ethers.ts b/packages/ccip-js/src/adapters/ethers.ts index 690febe..fb84db1 100644 --- a/packages/ccip-js/src/adapters/ethers.ts +++ b/packages/ccip-js/src/adapters/ethers.ts @@ -21,7 +21,7 @@ import { isAddressEqual, } from 'viem' import { toAccount } from 'viem/accounts' -import type { Provider, Signer, TypedDataField } from 'ethers' +import type { Provider, Signer, TypedDataField, BrowserProvider } from 'ethers' import { Contract, type TransactionReceipt as EthersTxReceipt, type TransactionResponse } from 'ethers' import { readContract as viemReadContract, @@ -262,7 +262,8 @@ export function isEthersSigner(signer: any): signer is Signer { /** * Union of supported client types: viem Client/WalletClient, ethers Provider, or ethers Signer. */ -export type SupportedClient = ViemClient | WalletClient | Provider | (Signer & { provider?: Provider }) + +export type SupportedClient = ViemClient | WalletClient | Provider | Signer | BrowserProvider /** * Attempts to adapt the provided client to a viem PublicClient if possible. diff --git a/packages/ccip-js/src/api.ts b/packages/ccip-js/src/api.ts index 100262a..93c0269 100644 --- a/packages/ccip-js/src/api.ts +++ b/packages/ccip-js/src/api.ts @@ -22,6 +22,7 @@ export { getTransactionReceiptCompat, getBlockNumberCompat, getLogsCompat, + SupportedClient, } from './adapters/ethers' import { @@ -848,7 +849,7 @@ export const createClient = (): Client => { try { const viemChain = (options.client as any)?.chain as Viem.Chain | undefined if (viemChain) return scaleFeeDecimals(fee, viemChain) - } catch {} + } catch { } return scaleFeeDecimals(fee) } diff --git a/packages/ccip-js/test/integration-testnet.test.ts b/packages/ccip-js/test/integration-testnet.test.ts index 33033f9..bb677e7 100644 --- a/packages/ccip-js/test/integration-testnet.test.ts +++ b/packages/ccip-js/test/integration-testnet.test.ts @@ -40,7 +40,7 @@ if (privateKey === DEFAULT_ANVIL_PRIVATE_KEY) { ) } -describe('Integration: Fuji -> Sepolia', () => { +describe.only('Integration: Fuji -> Sepolia', () => { let avalancheFujiClient: Viem.WalletClient let sepoliaClient: Viem.WalletClient let bnmToken_fuji: any diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b8c23a..c207993 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,14 +22,17 @@ importers: examples/nextjs: dependencies: '@chainlink/ccip-js': - specifier: ^0.2.1 - version: 0.2.4(a3c99c0fb0fe13a5c24ee996cead66b3) + specifier: workspace:^ + version: link:../../packages/ccip-js '@chainlink/ccip-react-components': specifier: ^0.3.0 version: 0.3.0(1ed1302eb82042231a950ad4c671505f) '@tanstack/react-query': specifier: ^5.37.1 version: 5.80.7(react@18.3.1) + ethers: + specifier: 6.13.4 + version: 6.13.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) next: specifier: 14.2.3 version: 14.2.3(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -326,10 +329,6 @@ packages: '@asamuzakjp/css-color@3.1.1': resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -342,10 +341,6 @@ packages: resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==} engines: {node: '>=6.9.0'} - '@babel/generator@7.26.10': - resolution: {integrity: sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.27.5': resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} engines: {node: '>=6.9.0'} @@ -515,12 +510,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.25.9': - resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} @@ -3779,9 +3768,6 @@ packages: bn.js@4.12.2: resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} - bn.js@5.2.1: - resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} - bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} @@ -8274,12 +8260,6 @@ snapshots: '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -8308,14 +8288,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.26.10': - dependencies: - '@babel/parser': 7.27.5 - '@babel/types': 7.26.10 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - '@babel/generator@7.27.5': dependencies: '@babel/parser': 7.27.5 @@ -8497,11 +8469,6 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.27.4)': - dependencies: - '@babel/core': 7.27.4 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.4)': dependencies: '@babel/core': 7.27.4 @@ -8623,44 +8590,6 @@ snapshots: - utf-8-validate - zod - '@chainlink/ccip-js@0.2.4(a3c99c0fb0fe13a5c24ee996cead66b3)': - dependencies: - '@nomicfoundation/hardhat-chai-matchers': 2.0.8(@nomicfoundation/hardhat-ethers@3.0.8(ethers@6.13.4(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)))(chai@5.2.0)(ethers@6.13.4(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@nomicfoundation/hardhat-ethers': 3.0.8(ethers@6.13.4(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10)) - '@nomicfoundation/hardhat-toolbox': 5.0.0(13680ddddf672fc71131616f3a7f7389) - '@nomicfoundation/hardhat-viem': 2.0.6(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(typescript@5.8.3)(viem@2.21.25(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67))(zod@3.25.67) - '@openzeppelin/contracts': 5.2.0 - chai: 5.2.0 - ethers: 6.13.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) - mocha: 11.7.0 - ts-jest: 29.2.6(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3) - typescript: 5.8.3 - viem: 2.21.25(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67) - transitivePeerDependencies: - - '@babel/core' - - '@jest/transform' - - '@jest/types' - - '@nomicfoundation/hardhat-ignition-ethers' - - '@nomicfoundation/hardhat-network-helpers' - - '@nomicfoundation/hardhat-verify' - - '@typechain/ethers-v6' - - '@typechain/hardhat' - - '@types/chai' - - '@types/mocha' - - '@types/node' - - babel-jest - - bufferutil - - esbuild - - hardhat - - hardhat-gas-reporter - - jest - - solidity-coverage - - supports-color - - ts-node - - typechain - - utf-8-validate - - zod - '@chainlink/ccip-react-components@0.3.0(1ed1302eb82042231a950ad4c671505f)': dependencies: '@chainlink/ccip-js': 0.2.4(5e3e05d9b50680c4de2bc0a3c2cf5c97) @@ -8836,7 +8765,7 @@ snapshots: package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.7.1 + semver: 7.7.2 spawndamnit: 3.0.1 term-size: 2.2.1 @@ -9359,7 +9288,7 @@ snapshots: dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/logger': 5.8.0 - bn.js: 5.2.1 + bn.js: 5.2.2 '@ethersproject/bytes@5.8.0': dependencies: @@ -9652,7 +9581,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -9665,14 +9594,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.15.32)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -9700,14 +9629,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.15.32)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@22.15.32)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@22.15.32)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -9736,7 +9665,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -9754,7 +9683,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.15.32 + '@types/node': 20.19.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -9776,7 +9705,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.15.32 + '@types/node': 20.19.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -9846,7 +9775,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.15.32 + '@types/node': 20.19.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -10441,16 +10370,6 @@ snapshots: - typescript - zod - '@nomicfoundation/hardhat-viem@2.0.6(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(typescript@5.8.3)(viem@2.21.25(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67))(zod@3.25.67)': - dependencies: - abitype: 0.9.10(typescript@5.8.3)(zod@3.25.67) - hardhat: 2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10) - lodash.memoize: 4.1.2 - viem: 2.21.25(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67) - transitivePeerDependencies: - - typescript - - zod - '@nomicfoundation/hardhat-viem@2.0.6(hardhat@2.24.3(bufferutil@4.0.9)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@22.15.32)(typescript@5.8.3))(typescript@5.8.3)(utf-8-validate@5.0.10))(typescript@5.8.3)(viem@2.21.25(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67))(zod@3.25.67)': dependencies: abitype: 0.9.10(typescript@5.8.3)(zod@3.25.67) @@ -11702,28 +11621,28 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.27.6 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.27.5 + '@babel/types': 7.27.6 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.27.6 '@types/bn.js@5.1.6': dependencies: - '@types/node': 22.15.32 + '@types/node': 20.19.1 '@types/bn.js@5.2.0': dependencies: @@ -11762,7 +11681,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.15.32 + '@types/node': 20.19.1 '@types/istanbul-lib-coverage@2.0.6': {} @@ -12695,7 +12614,7 @@ snapshots: acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.1: {} @@ -13004,8 +12923,6 @@ snapshots: bn.js@4.12.2: {} - bn.js@5.2.1: {} - bn.js@5.2.2: {} bowser@2.11.0: {} @@ -15317,7 +15234,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -15406,7 +15323,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.15.32)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3)): + jest-config@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@swc/core@1.12.1)(@types/node@22.15.32)(typescript@5.8.3)): dependencies: '@babel/core': 7.27.4 '@jest/test-sequencer': 29.7.0 @@ -15431,8 +15348,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.15.32 - ts-node: 10.9.2(@swc/core@1.12.1)(@types/node@20.19.1)(typescript@5.8.3) + '@types/node': 20.19.1 + ts-node: 10.9.2(@swc/core@1.12.1)(@types/node@22.15.32)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -15492,7 +15409,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -15502,7 +15419,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.15.32 + '@types/node': 20.19.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -15528,7 +15445,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -15541,7 +15458,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -15576,7 +15493,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -15604,7 +15521,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -15625,10 +15542,10 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.27.4 - '@babel/generator': 7.26.10 + '@babel/generator': 7.27.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.4) - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.27.4) - '@babel/types': 7.26.10 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.4) + '@babel/types': 7.27.6 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -15669,7 +15586,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.15.32 + '@types/node': 20.19.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -15678,7 +15595,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.15.32 + '@types/node': 20.19.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 From 31de2f39116c5afb6b020862d95bb47cef1fe477 Mon Sep 17 00:00:00 2001 From: zeuslawyer Date: Fri, 12 Sep 2025 20:20:07 +1000 Subject: [PATCH 2/4] *note* integration tests for ethersjs failing with provider related errors. Refactor Ethers.js adapter to use custom type definitions for contract calls, transactions, receipts, and logs. This change eliminates 'as any' casts, improving type safety and code clarity. Additionally, update integration tests to reflect these changes and ensure compatibility with the new type definitions. --- packages/ccip-js/src/adapters/ethers.ts | 53 +-- packages/ccip-js/src/adapters/types.ts | 43 +++ .../ccip-js/test/integration-testnet.test.ts | 335 +++++++++++++++++- 3 files changed, 399 insertions(+), 32 deletions(-) create mode 100644 packages/ccip-js/src/adapters/types.ts diff --git a/packages/ccip-js/src/adapters/ethers.ts b/packages/ccip-js/src/adapters/ethers.ts index fb84db1..defe9ab 100644 --- a/packages/ccip-js/src/adapters/ethers.ts +++ b/packages/ccip-js/src/adapters/ethers.ts @@ -9,6 +9,13 @@ import type { TransactionReceipt as ViemTransactionReceipt, AbiEvent, } from 'viem' + +import type { + ContractCallArgs, + TransactionArgs, + ReceiptArgs, + LogsArgs, +} from './types' import { createPublicClient, createWalletClient, @@ -300,13 +307,13 @@ async function toViemWalletClient(client: SupportedClient, chain?: Chain): Promi */ export async function readContractCompat( client: SupportedClient, - args: Parameters[1] & { chain?: Chain }, + args: ContractCallArgs, ) { if (isEthersProvider(client) || isEthersSigner(client)) { const provider: Provider | undefined = (isEthersSigner(client) ? (client as any).provider : client) as Provider if (!provider) throw new Error('Unsupported client for readContract: signer has no provider') - const contract = new Contract(args.address as Address, args.abi as any[], provider) - return (contract as any)[(args as any).functionName](...(((args as any).args as any[]) || [])) + const contract = new Contract(args.address, args.abi, provider) + return (contract as any)[args.functionName](...(args.args || [])) } const viemClient = toViemPublicClient(client, args.chain) if (!viemClient) throw new Error('Unsupported client for readContract') @@ -319,18 +326,18 @@ export async function readContractCompat( */ export async function writeContractCompat( client: SupportedClient, - args: Parameters[1] & { chain?: Chain }, + args: TransactionArgs, ) { if (isEthersSigner(client)) { const signer = client as Signer - const contract = new Contract(args.address as Address, args.abi as any[], signer) - const txResponse: TransactionResponse = await (contract as any)[(args as any).functionName]( - ...(((args as any).args as any[]) || []), + const contract = new Contract(args.address, args.abi, signer) + const txResponse: TransactionResponse = await (contract as any)[args.functionName]( + ...(args.args || []), { - value: (args as any).value !== undefined ? (args as any).value.toString() : undefined, - gasLimit: (args as any).gas, - gasPrice: (args as any).gasPrice, - nonce: (args as any).nonce, + value: args.value !== undefined ? args.value.toString() : undefined, + gasLimit: args.gas, + gasPrice: args.gasPrice, + nonce: args.nonce, }, ) return txResponse.hash as Hex @@ -346,15 +353,15 @@ export async function writeContractCompat( */ export async function waitForTransactionReceiptCompat( client: SupportedClient, - args: Parameters[1] & { chain?: Chain }, + args: ReceiptArgs, ) { if (isEthersProvider(client) || isEthersSigner(client)) { const provider: Provider | undefined = (isEthersSigner(client) ? (client as any).provider : client) as Provider if (!provider) throw new Error('Unsupported client for waitForTransactionReceipt: signer has no provider') const maybe = await provider.waitForTransaction( - (args as any).hash, - (args as any).confirmations, - (args as any).timeout, + args.hash, + args.confirmations, + args.timeout, ) if (!maybe) throw new Error('Transaction receipt not found') return formatEthersReceipt(maybe) @@ -370,12 +377,12 @@ export async function waitForTransactionReceiptCompat( */ export async function getTransactionReceiptCompat( client: SupportedClient, - args: Parameters[1] & { chain?: Chain }, + args: ReceiptArgs, ) { if (isEthersProvider(client) || isEthersSigner(client)) { const provider: Provider | undefined = (isEthersSigner(client) ? (client as any).provider : client) as Provider if (!provider) throw new Error('Unsupported client for getTransactionReceipt: signer has no provider') - const maybe = await provider.getTransactionReceipt((args as any).hash) + const maybe = await provider.getTransactionReceipt(args.hash) if (!maybe) throw new Error('Transaction receipt not found') return formatEthersReceipt(maybe) } @@ -406,17 +413,17 @@ export async function getBlockNumberCompat(client: SupportedClient, chain?: Chai */ export async function getLogsCompat( client: SupportedClient, - args: Parameters[1] & { chain?: Chain }, + args: LogsArgs, ) { if (isEthersProvider(client) || isEthersSigner(client)) { const provider: Provider | undefined = (isEthersSigner(client) ? (client as any).provider : client) as Provider if (!provider) throw new Error('Unsupported client for getLogs: signer has no provider') - const abiEvent = (args as any).event as AbiEvent | undefined - const address = (args as any).address as Address | undefined - const fromBlock = (args as any).fromBlock ? ((args as any).fromBlock as bigint).toString() : undefined - const toBlock = (args as any).toBlock ? ((args as any).toBlock as bigint).toString() : undefined + const abiEvent = args.event as AbiEvent | undefined + const address = args.address as Address | undefined + const fromBlock = args.fromBlock ? args.fromBlock.toString() : undefined + const toBlock = args.toBlock ? args.toBlock.toString() : undefined const topics: (string | null)[] | undefined = abiEvent - ? buildTopicsFromEventAndArgs(abiEvent, (args as any).args) + ? buildTopicsFromEventAndArgs(abiEvent, args.args) : undefined const filter: any = { address, topics, fromBlock, toBlock } const logs = await provider.getLogs(filter) diff --git a/packages/ccip-js/src/adapters/types.ts b/packages/ccip-js/src/adapters/types.ts new file mode 100644 index 0000000..6d322fe --- /dev/null +++ b/packages/ccip-js/src/adapters/types.ts @@ -0,0 +1,43 @@ +import type { Address, Chain, Hex, AbiEvent } from 'viem' + +// Custom types to eliminate 'as any' casts throughout the adapter layer +export interface ContractCallArgs { + address: Address + // Supports both readonly and mutable ABI arrays for maximum compatibility + // (readonly arrays come from parseAbi, static ABIs, etc.) + abi: readonly any[] | any[] + functionName: string + args?: any[] + chain?: Chain +} + +export interface TransactionArgs { + address: Address + // Supports both readonly and mutable ABI arrays for maximum compatibility + // (readonly arrays come from parseAbi, static ABIs, etc.) + abi: readonly any[] | any[] + functionName: string + args?: any[] + value?: bigint + gas?: bigint + gasPrice?: bigint + nonce?: number + chain?: Chain +} + +export interface ReceiptArgs { + hash: Hex + confirmations?: number + timeout?: number + chain?: Chain +} + +export interface LogsArgs { + address?: Address + event?: AbiEvent + args?: Record + fromBlock?: bigint + toBlock?: bigint + + chain?: Chain +} diff --git a/packages/ccip-js/test/integration-testnet.test.ts b/packages/ccip-js/test/integration-testnet.test.ts index bb677e7..932866e 100644 --- a/packages/ccip-js/test/integration-testnet.test.ts +++ b/packages/ccip-js/test/integration-testnet.test.ts @@ -7,6 +7,7 @@ import { sepolia, avalancheFuji, hederaTestnet } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' import bridgeToken from '../artifacts-compile/BridgeToken.json' import { DEFAULT_ANVIL_PRIVATE_KEY } from './helpers/constants' +import { JsonRpcProvider, Wallet, Contract } from 'ethers' const ccipClient = CCIP.createClient() const bridgeTokenAbi = bridgeToken.contracts['src/contracts/BridgeToken.sol:BridgeToken'].bridgeTokenAbi @@ -20,8 +21,7 @@ const WRAPPED_HBAR = '0xb1F616b8134F602c3Bb465fB5b5e6565cCAd37Ed' const LINK_TOKEN_FUJI = '0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846' const LINK_TOKEN_HEDERA = '0x90a386d59b9A6a4795a011e8f032Fc21ED6FEFb6' -// 6m to match https://viem.sh/docs/actions/public/waitForTransactionReceipt.html#timeout-optional, -// which is called in approveRouter() + // TODO @zeuslawyer: https://prajjwaldimri.medium.com/why-is-my-jest-runner-not-closing-bc4f6632c959 - tests are passing but jest is not closing. Viem transport issue? why? // currently timeout set to 180000ms in jest.config.js @@ -36,11 +36,11 @@ const privateKey = process.env.PRIVATE_KEY as Viem.Hex if (privateKey === DEFAULT_ANVIL_PRIVATE_KEY) { throw new Error( - "Developer's PRIVATE_KEY for Ethereum Sepolia and Avalanche Fuji must be set for integration testing on", + "Developer's PRIVATE_KEY for Ethereum Sepolia and Avalanche Fuji must be set in terminal for integration testing on testnet", ) } -describe.only('Integration: Fuji -> Sepolia', () => { +describe('[Viem]Integration: Fuji -> Sepolia', () => { let avalancheFujiClient: Viem.WalletClient let sepoliaClient: Viem.WalletClient let bnmToken_fuji: any @@ -108,7 +108,7 @@ describe.only('Integration: Fuji -> Sepolia', () => { routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, }) - expect(avalancheFujiOnRampAddress).toEqual('0x75b9a75Ee1fFef6BE7c4F842a041De7c6153CF4E') + expect(avalancheFujiOnRampAddress).toEqual('0x75b9a75Ee1fFef6BE7c4F842a041De7c6153CF4E' as `0x${string}`) }) it('lists supported fee tokens', async function () { @@ -131,7 +131,7 @@ describe.only('Integration: Fuji -> Sepolia', () => { // this implicitly asserts that the values are defined as well. expect(typeof tokens).toBe('bigint') - expect(typeof lastUpdated).toBe('number') + expect(typeof lastUpdated).toBe('bigint') // Changed to bigint expect(typeof isEnabled).toBe('boolean') expect(typeof capacity).toBe('bigint') expect(typeof rate).toBe('bigint') @@ -335,7 +335,7 @@ describe.only('Integration: Fuji -> Sepolia', () => { expect(ccipSend_txReceipt).toBeDefined() expect(ccipSend_txReceipt.status).toEqual('success') expect(ccipSend_txReceipt.from.toLowerCase()).toEqual(avalancheFujiClient.account!.address.toLowerCase()) - expect(ccipSend_txReceipt.to!.toLowerCase()).toEqual(AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS.toLowerCase()) + expect(ccipSend_txReceipt.to!.toLowerCase()).toEqual(AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS.toLowerCase() as `0x${string}`) }) }) @@ -344,7 +344,7 @@ describe.only('Integration: Fuji -> Sepolia', () => { }) }) -describe('√ (Hedera(custom decimals) -> Sepolia) all critical functionality in CCIP Client', () => { +describe('[Viem](Hedera(custom decimals) -> Sepolia) all critical functionality in CCIP Client', () => { let hederaTestnetClient: Viem.WalletClient let sepoliaClient: Viem.WalletClient let bnmToken_hedera: any @@ -561,6 +561,323 @@ describe('√ (Hedera(custom decimals) -> Sepolia) all critical functionality in expect(ccipSend_txReceipt).toBeDefined() expect(ccipSend_txReceipt.status).toEqual('success') expect(ccipSend_txReceipt.from.toLowerCase()).toEqual(hederaTestnetClient.account!.address.toLowerCase()) - expect(ccipSend_txReceipt.to!.toLowerCase()).toEqual(HEDERA_TESTNET_CCIP_ROUTER_ADDRESS.toLowerCase()) + expect(ccipSend_txReceipt.to!.toLowerCase()).toEqual(HEDERA_TESTNET_CCIP_ROUTER_ADDRESS.toLowerCase() as `0x${string}`) + }) +}) + +describe.only('[Ethers]Integration: Fuji -> Sepolia', () => { + let avalancheFujiProvider: JsonRpcProvider + let avalancheFujiSigner: Wallet + let sepoliaProvider: JsonRpcProvider + let sepoliaSigner: Wallet + let bnmToken_fuji: any + let _messageId: `0x${string}` + let tokenTransfer_txHash: `0x${string}` + + const AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS = '0xF694E193200268f9a4868e4Aa017A0118C9a8177' + const approvedAmount = parseEther('0.000000001') + + beforeAll(async () => { + avalancheFujiProvider = new JsonRpcProvider(AVALANCHE_FUJI_RPC_URL) + try { + await avalancheFujiProvider.ready + await avalancheFujiProvider.getBlockNumber() + } catch (error) { + console.log('ERROR : avalancheFujiProvider', error) + + + avalancheFujiSigner = new Wallet(privateKey, avalancheFujiProvider) + + sepoliaProvider = new JsonRpcProvider(SEPOLIA_RPC_URL) + sepoliaSigner = new Wallet(privateKey, sepoliaProvider) + + bnmToken_fuji = new Contract( + '0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4', // CCIP BnM on Avalanche Fuji + bridgeTokenAbi, + avalancheFujiSigner, + ) + + // Check that contract is properly instantiated + if (!bnmToken_fuji.target) { + throw new Error('Contract target is undefined - contract not properly instantiated') + } + expect(bnmToken_fuji.target).toEqual('0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4' as `0x${string}`) + + const bnmBalance = await bnmToken_fuji.balanceOf(avalancheFujiSigner.address) + if (parseInt(bnmBalance) <= approvedAmount) { + await bnmToken_fuji.drip(avalancheFujiSigner.address) + console.log(' ℹ️ | Dripped 1 CCIP BnM token to account: ', avalancheFujiSigner.address) + } + }) + + describe('√ (Fuji -> Sepolia) all critical functionality in CCIP Client', () => { + it('should approve BnM spend, given valid input', async () => { + const ccipApprove = await ccipClient.approveRouter({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + amount: approvedAmount, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + waitForReceipt: true, + }) + + // ccipApprove.txReceipt!.status == 'success' && console.log(' | Approved CCIP BnM token on Avalanche Fuji' + await expect(ccipApprove.txReceipt!.status).toEqual('success') + }) + + it('fetches token allowance', async function () { + const allowance = await ccipClient.getAllowance({ + client: avalancheFujiSigner, + account: avalancheFujiSigner.address as `0x${string}`, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + }) + expect(allowance).toEqual(approvedAmount) + }) + + it('returns on-ramp address', async function () { + const avalancheFujiOnRampAddress = await ccipClient.getOnRampAddress({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + expect(avalancheFujiOnRampAddress).toEqual('0x75b9a75Ee1fFef6BE7c4F842a041De7c6153CF4E' as `0x${string}`) + }) + + it.only('lists supported fee tokens', async function () { + const result = await ccipClient.getSupportedFeeTokens({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + expect(result.length).toEqual(2) + expect(result.includes(LINK_TOKEN_FUJI)).toBe(true) + expect(result.includes(WRAPPED_NATIVE_AVAX)).toBe(true) + }) + + it('fetched lane rate refill limits are defined', async function () { + const { tokens, lastUpdated, isEnabled, capacity, rate } = await ccipClient.getLaneRateRefillLimits({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + + // this implicitly asserts that the values are defined as well. + expect(typeof tokens).toBe('bigint') + expect(typeof lastUpdated).toBe('bigint') // Both Ethers and Viem return bigint for timestamps + expect(typeof isEnabled).toBe('boolean') + expect(typeof capacity).toBe('bigint') + expect(typeof rate).toBe('bigint') + }) + + it('returns token rate limit by lane', async function () { + const { tokens, lastUpdated, isEnabled, capacity, rate } = await ccipClient.getTokenRateLimitByLane({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + supportedTokenAddress: bnmToken_fuji.target as `0x${string}`, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + + // this implicitly asserts that the values are defined as well. + expect(typeof tokens).toBe('bigint') + expect(typeof lastUpdated).toBe('bigint') // Both Ethers and Viem return bigint for timestamps + expect(typeof isEnabled).toBe('boolean') + expect(typeof capacity).toBe('bigint') + expect(typeof rate).toBe('bigint') + }) + + it('returns fee estimate', async function () { + const fee_link = await ccipClient.getFee({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + amount: approvedAmount, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: sepoliaSigner.address as `0x${string}`, + feeTokenAddress: LINK_TOKEN_FUJI, + }) + const fee_native = await ccipClient.getFee({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + amount: approvedAmount, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: sepoliaSigner.address as `0x${string}`, + feeTokenAddress: WRAPPED_NATIVE_AVAX, + }) + + expect(fee_link).toBeGreaterThan(1000n) + expect(fee_native).toBeGreaterThan(1000n) + }) + it('returns token admin registry', async function () { + const result = await ccipClient.getTokenAdminRegistry({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + + const CCIP_ADMIN_REGISTRY_ADDRESS = '0xA92053a4a3922084d992fD2835bdBa4caC6877e6' + expect(result).toEqual(CCIP_ADMIN_REGISTRY_ADDRESS as `0x${string}`) + }) + + it('checks if BnM token is supported for transfer', async function () { + const result = await ccipClient.isTokenSupported({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + }) + expect(result).toBe(true) + }) + + it('transfers tokens | pay in LINK', async function () { + await ccipClient.approveRouter({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + amount: approvedAmount, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + waitForReceipt: true, + }) + + // approve LINK spend + const fee_link = await ccipClient.getFee({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + amount: approvedAmount, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: sepoliaSigner.address as `0x${string}`, + feeTokenAddress: LINK_TOKEN_FUJI, + }) + await ccipClient.approveRouter({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + amount: fee_link, + tokenAddress: LINK_TOKEN_FUJI, + waitForReceipt: true, + }) + const allowance = await ccipClient.getAllowance({ + client: avalancheFujiSigner, // Use signer for ethers + account: avalancheFujiSigner.address as `0x${string}`, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + }) + + expect(allowance).toBeGreaterThanOrEqual(approvedAmount) + + const result = await ccipClient.transferTokens({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: sepoliaSigner.address as `0x${string}`, + amount: approvedAmount, + feeTokenAddress: LINK_TOKEN_FUJI, + }) + + _messageId = result.messageId + tokenTransfer_txHash = result.txHash + + expect(result.txReceipt!.status).toEqual('success') + }) + + it('transfers tokens > pays in native token', async function () { + await ccipClient.approveRouter({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + amount: approvedAmount, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + waitForReceipt: true, + }) + + const result = await ccipClient.transferTokens({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + tokenAddress: bnmToken_fuji.target as `0x${string}`, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: sepoliaSigner.address as `0x${string}`, + amount: approvedAmount, + }) + + expect(result.txReceipt!.status).toEqual('success') + }) + + it('CCIP message (sending) tx OK > paid in LINK', async function () { + const testReceiverContract = '0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4' as `0x${string}` // using BnM token contract as receiver for testing + const testMessage = Viem.encodeAbiParameters( + [{ type: 'string', name: 'message' }], + ['Hello from Avalanche Fuji!'], + ) + + // Get fee in LINK and approve it + const fee_link = await ccipClient.getFee({ + client: avalancheFujiSigner, // Use signer for ethers + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: testReceiverContract, + data: testMessage, + feeTokenAddress: LINK_TOKEN_FUJI, + }) + + await ccipClient.approveRouter({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + amount: fee_link, + tokenAddress: LINK_TOKEN_FUJI, + waitForReceipt: true, + }) + + const result = await ccipClient.sendCCIPMessage({ + client: avalancheFujiSigner, + routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS, + destinationChainSelector: SEPOLIA_CHAIN_SELECTOR, + destinationAccount: testReceiverContract, + data: testMessage, + feeTokenAddress: LINK_TOKEN_FUJI, + }) + + console.info( + `Avalanche Fuji --> Sepolia sendCCIPMessage MessageId: ${result.messageId} <> Sent to: ${testReceiverContract} on Sepolia`, + ) + + expect(result.txReceipt!.status).toEqual('success') + expect(result.messageId).toBeDefined() + expect(result.txHash).toBeDefined() + }) + + it('gets transfer status & gets transaction receipt', async function () { + // Skip if tokenTransfer_txHash is not set (previous test failed) + if (!tokenTransfer_txHash) { + console.warn('Skipping transfer status test - tokenTransfer_txHash not set') + return + } + + const ccipSend_txReceipt = await ccipClient.getTransactionReceipt({ + client: avalancheFujiSigner, // Use signer for ethers + hash: tokenTransfer_txHash, + }) + + const FUJI_CHAIN_SELECTOR = '14767482510784806043' + const SEPOLIA_ROUTER_ADDRESS = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59' + + const transferStatus = await ccipClient.getTransferStatus({ + client: sepoliaSigner, // Use signer for ethers + sourceChainSelector: FUJI_CHAIN_SELECTOR, + destinationRouterAddress: SEPOLIA_ROUTER_ADDRESS as `0x${string}`, + fromBlockNumber: ccipSend_txReceipt.blockNumber ? ccipSend_txReceipt.blockNumber : undefined, + messageId: _messageId, + }) + + expect(transferStatus).toBeDefined() + + expect(ccipSend_txReceipt).toBeDefined() + expect(ccipSend_txReceipt.status).toEqual('success') + expect(ccipSend_txReceipt.from.toLowerCase()).toEqual(avalancheFujiSigner.address.toLowerCase()) + expect(ccipSend_txReceipt.to!.toLowerCase()).toEqual(AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS.toLowerCase() as `0x${string}`) + }) + }) + + afterAll(async () => { + console.info('✅ | Ethers Integration tests completed. Waiting for timeout...') }) }) From cbc408acb86f8dc0db33c3d8317dc659dfa837d1 Mon Sep 17 00:00:00 2001 From: Thomas Hodges Date: Fri, 12 Sep 2025 06:47:00 -0500 Subject: [PATCH 3/4] Close catch block --- packages/ccip-js/test/integration-testnet.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ccip-js/test/integration-testnet.test.ts b/packages/ccip-js/test/integration-testnet.test.ts index 932866e..c406063 100644 --- a/packages/ccip-js/test/integration-testnet.test.ts +++ b/packages/ccip-js/test/integration-testnet.test.ts @@ -584,7 +584,7 @@ describe.only('[Ethers]Integration: Fuji -> Sepolia', () => { await avalancheFujiProvider.getBlockNumber() } catch (error) { console.log('ERROR : avalancheFujiProvider', error) - + } avalancheFujiSigner = new Wallet(privateKey, avalancheFujiProvider) From 1199ed40817941757957f47f17c8792cfbdf8998 Mon Sep 17 00:00:00 2001 From: Thomas Hodges Date: Fri, 12 Sep 2025 06:50:55 -0500 Subject: [PATCH 4/4] Correct getFeeTokens signature --- packages/ccip-js/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ccip-js/src/api.ts b/packages/ccip-js/src/api.ts index 93c0269..fbbb5a8 100644 --- a/packages/ccip-js/src/api.ts +++ b/packages/ccip-js/src/api.ts @@ -735,7 +735,7 @@ export const createClient = (): Client => { ) const feeTokens = await readCompat(options.client as any, { - abi: parseAbi(['function getFeeTokens() returns (address[] feeTokens)']), // same signature for both PriceRegistry and FeeQuoter + abi: parseAbi(['function getFeeTokens() view returns (address[] feeTokens)']), // same signature for both PriceRegistry and FeeQuoter address: priceRegistryOrFeeQuoter as Viem.Address, functionName: 'getFeeTokens', })