diff --git a/packages/ccip-js/README.md b/packages/ccip-js/README.md
index 3cc7892..6f6bd94 100644
--- a/packages/ccip-js/README.md
+++ b/packages/ccip-js/README.md
@@ -1,6 +1,5 @@
# CCIP-JS
-
CCIP-JS is a TypeScript library that provides a client for managing cross-chain token transfers that use Chainlink's [Cross-Chain Interoperability Protocol (CCIP)](https://docs.chain.link/ccip) routers. The library utilizes types and helper functions from [Viem](https://viem.sh/).
To learn more about CCIP, refer to the [CCIP documentation](https://docs.chain.link/ccip).
@@ -41,7 +40,6 @@ To learn more about CCIP, refer to the [CCIP documentation](https://docs.chain.l
- [Contributing](#contributing)
- [License](#license)
-
## Why CCIP-JS?
CCIP-JS provides ready-to-use typesafe methods for every step of the token transfer process.
@@ -69,24 +67,25 @@ Additionally, after the transfer, you may need to check the transfer status.
To install the package, use the following command:
```sh
-npm install @chainlink/ccip-js viem
+npm install @chainlink/ccip-js
```
Or with Yarn:
```sh
-yarn add @chainlink/ccip-js viem
+yarn add @chainlink/ccip-js
```
Or with PNPM:
```sh
-pnpm add @chainlink/ccip-js viem
+pnpm add @chainlink/ccip-js
```
## Usage
This example code covers the following steps:
+
- Initialize CCIP-JS Client for mainnet
- Approve tokens for transfer
- Get fee for the transfer
@@ -593,11 +592,13 @@ pnpm build-ccip-js
#### Running tests
-```sh
-pnpm i -w
-anvil
-pnpm test
-```
+1. cd into `packages/ccip-js` and then run `pnpm install` OR from the project root you can run `pnpm i -w`
+
+2. open a new terminal window and run `anvil` - requires that you've [installed Foundry Anvil](https://book.getfoundry.sh/anvil/).
+
+3. Back in the first terminal, inside, `packages/ccip-js` run `pnpm test`
+
+Note: that Anvil is only needed for the tests inside `./test/integration-mocked.test.ts` which uses the [Chainlink Local](https://github.com/smartcontractkit/chainlink-local) simulator. Actual testnet and mainnet behavior may differ from time to time and passing these tests does not guarantee testnet or mainnet behavior.
### Contributing
diff --git a/packages/ccip-js/package.json b/packages/ccip-js/package.json
index bebda8e..25abd6a 100644
--- a/packages/ccip-js/package.json
+++ b/packages/ccip-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@chainlink/ccip-js",
- "version": "0.2.2",
+ "version": "0.2.3",
"private": false,
"main": "dist/api.js",
"types": "dist/api.d.ts",
@@ -15,7 +15,7 @@
"lint": "eslint 'src/**/*.{ts,js}'",
"format": "prettier --write 'src/**/*.{ts,js,json,md}'",
"pretest": "anvil --block-time 2",
- "t:int": "jest --coverage -u -t=\"Integration\"",
+ "t:int": "jest --coverage -u --testMatch=\"**/integration-*.test.ts\" --detectOpenHandles",
"t:unit": "jest --coverage -u -t=\"Unit\"",
"test": "jest --coverage",
"test:hh": "hardhat test"
diff --git a/packages/ccip-js/src/api.ts b/packages/ccip-js/src/api.ts
index 4d22722..e18d3c2 100644
--- a/packages/ccip-js/src/api.ts
+++ b/packages/ccip-js/src/api.ts
@@ -281,8 +281,7 @@ export interface Client {
* @param {Viem.Address} options.routerAddress - The address of the router contract on the source blockchain.
* @param {string} options.destinationChainSelector - The selector for the destination chain.
* @param {Viem.Address} options.tokenAddress - The address of the token contract on the source blockchain.
- * @returns {Promise} A promise that resolves to a boolean value indicating whether the token
- * is supported on the destination chain.
+ * @returns {Promise} A promise that resolves to the Token Admin Registry Contract address on the source chain.
* @example
* import { createPublicClient, http } from 'viem'
* import { mainnet } from 'viem/chains'
@@ -617,7 +616,7 @@ export const createClient = (): Client => {
const approveTxHash = await writeContract(options.client, {
chain: options.client.chain,
- account: options.client.account!.address,
+ account: options.client.account!,
abi: IERC20ABI,
address: options.tokenAddress,
functionName: 'approve',
@@ -852,7 +851,7 @@ export const createClient = (): Client => {
address: options.routerAddress,
functionName: 'ccipSend',
args: buildArgs(options),
- account: options.client.account!.address,
+ account: options.client.account!,
...(!options.feeTokenAddress && {
value: await getFee(options),
}),
@@ -905,7 +904,7 @@ export const createClient = (): Client => {
address: options.routerAddress,
functionName: 'ccipSend',
args: buildArgs(options),
- account: options.client.account!.address,
+ account: options.client.account!,
...(!options.feeTokenAddress && {
value: await getFee(options),
}),
diff --git a/packages/ccip-js/test/helpers/clients.ts b/packages/ccip-js/test/helpers/clients.ts
index d39fbd6..cac9184 100644
--- a/packages/ccip-js/test/helpers/clients.ts
+++ b/packages/ccip-js/test/helpers/clients.ts
@@ -1,9 +1,9 @@
import { account } from './constants'
import { createTestClient, http, publicActions, walletActions } from 'viem'
-import { hardhat, sepolia } from 'viem/chains'
+import { sepolia, anvil } from 'viem/chains'
export const testClient = createTestClient({
- chain: hardhat,
+ chain: anvil,
transport: http(),
mode: 'anvil',
account,
diff --git a/packages/ccip-js/test/helpers/constants.ts b/packages/ccip-js/test/helpers/constants.ts
index 8cb0b4a..384ff18 100644
--- a/packages/ccip-js/test/helpers/constants.ts
+++ b/packages/ccip-js/test/helpers/constants.ts
@@ -7,22 +7,26 @@ import routerJson from '../../artifacts-compile/Router.json'
import simulatorJson from '../../artifacts-compile/CCIPLocalSimulator.json'
import priceRegistryJson from '../../artifacts-compile/PriceRegistry.json'
-// load.env file for private key
+// load.env file for private key
// replace with your own private key (optional)
dotenv.config()
-// default anvil PK
-export const privateKey =
- (process.env.PRIVATE_KEY as Hex) || '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
-export const account = privateKeyToAccount(privateKey)
+if (process.env.PRIVATE_KEY?.slice(0, 2) !== '0x') {
+ process.env.PRIVATE_KEY = `0x${process.env.PRIVATE_KEY}`
+}
+
+export const DEFAULT_ANVIL_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
+export const account = privateKeyToAccount(DEFAULT_ANVIL_PRIVATE_KEY)
// bridge token contract
export const { bridgeTokenAbi, bridgeTokenBin } = bridgeJson['contracts']['src/contracts/BridgeToken.sol:BridgeToken']
// note: no need to deploy
export const { onRampAbi, onRampBin } = onRampJson['contracts']['src/contracts/EVM2EVMOnRamp.sol:EVM2EVMOnRamp']
export const { routerAbi, routerBin } = routerJson['contracts']['src/contracts/Router.sol:Router']
-export const { simulatorAbi, simulatorBin } = simulatorJson['contracts']['src/contracts/CCIPLocalSimulator.sol:CCIPLocalSimulator']
-export const { priceRegistryAbi, priceRegistryBin } = priceRegistryJson['contracts']['src/contracts/PriceRegistry.sol:PriceRegistry']
+export const { simulatorAbi, simulatorBin } =
+ simulatorJson['contracts']['src/contracts/CCIPLocalSimulator.sol:CCIPLocalSimulator']
+export const { priceRegistryAbi, priceRegistryBin } =
+ priceRegistryJson['contracts']['src/contracts/PriceRegistry.sol:PriceRegistry']
// CCIP testing data for simulations
export const ccipTxHash = '0xc55d92b1212dd24db843e1cbbcaebb1fffe3cd1751313e0fd02cf26bf72b359e'
@@ -119,4 +123,4 @@ export const ccipTxReceipt: TransactionReceipt = {
transactionHash: ccipTxHash,
transactionIndex: 0,
type: 'eip1559',
-}
\ No newline at end of file
+}
diff --git a/packages/ccip-js/test/integration.test.ts b/packages/ccip-js/test/integration-mocked.test.ts
similarity index 86%
rename from packages/ccip-js/test/integration.test.ts
rename to packages/ccip-js/test/integration-mocked.test.ts
index 78a542f..fcb2110 100644
--- a/packages/ccip-js/test/integration.test.ts
+++ b/packages/ccip-js/test/integration-mocked.test.ts
@@ -1,32 +1,16 @@
-import { jest, expect, it, describe, afterEach } from '@jest/globals'
+import { jest, expect, it, describe, afterEach, beforeAll } from '@jest/globals'
import * as CCIP from '../src/api'
import * as Viem from 'viem'
import * as viemActions from 'viem/actions'
-import {
- Address,
- encodeAbiParameters,
- encodeFunctionData,
- getContract,
- parseEther,
- zeroAddress,
-} from 'viem'
import { testClient } from './helpers/clients'
-import {
- account,
- ccipLog,
- ccipTxHash,
- ccipTxReceipt,
- onRampAbi,
- routerAbi,
-} from './helpers/constants'
+import { account, ccipLog, ccipTxHash, ccipTxReceipt, onRampAbi, routerAbi } from './helpers/constants'
import { getContracts, setOnRampAddress } from './helpers/contracts'
// getSupportedFeeTokens
import { mineBlock } from './helpers/utils'
import { expect as expectChai } from 'chai'
import { getSupportedFeeTokens } from './helpers/config'
-// import { readContract } from 'viem/actions'
// import { getTokenAdminRegistry } from './helpers/config'
const ccipClient = CCIP.createClient()
@@ -37,42 +21,66 @@ const writeContractMock = jest.spyOn(viemActions, 'writeContract')
const waitForTransactionReceiptMock = jest.spyOn(viemActions, 'waitForTransactionReceipt')
const parseEventLogsMock = jest.spyOn(Viem, 'parseEventLogs')
-describe('Integration', () => {
-
+describe('Integration- Using Mocks', () => {
afterEach(() => {
jest.clearAllMocks()
})
- describe('√ deploy on HH', () => {
- it("Should Deploy Router.sol", async function () {
+ beforeAll(async () => {
+ // Create a temporary public client to check if Anvil is running
+ const tempClient = Viem.createPublicClient({
+ transport: Viem.http('http://127.0.0.1:8545'),
+ })
+
+ // Try to get the chain ID and verify it's Anvil
+ try {
+ const chainId = await tempClient.getChainId()
+ if (chainId.toString() !== '31337') {
+ throw new Error(`Wrong chain ID ('${chainId}') detected on port 8545. Expected Anvil's '31337'`)
+ }
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('Wrong chain ID')) {
+ throw error
+ }
+
+ throw new Error(
+ '❌ Anvil is not running on port 8545. Please start Anvil first:\n' +
+ '1. Open a new terminal\n' +
+ '2. Run: anvil --port 8545\n' +
+ '3. Then run the tests again',
+ )
+ }
+ })
+
+ describe('√ deploy on Anvil', () => {
+ it('Should Deploy Router.sol', async function () {
const { router } = await getContracts()
- expectChai(router.address).to.not.equal(0);
- });
- it("Should Deploy BridgeToken.sol", async function () {
+ expectChai(router.address).to.not.equal(0)
+ })
+ it('Should Deploy BridgeToken.sol', async function () {
const { bridgeToken } = await getContracts()
- expectChai(bridgeToken.address).to.not.equal(0);
- });
- it("Should Deploy CCIPLocalSimulator.sol", async function () {
+ expectChai(bridgeToken.address).to.not.equal(0)
+ })
+ it('Should Deploy CCIPLocalSimulator.sol', async function () {
const { localSimulator } = await getContracts()
- expectChai(localSimulator.address).to.not.equal(0);
+ expectChai(localSimulator.address).to.not.equal(0)
})
- console.log('\u2705 | Deployed Smart Contracts on local Hardhat')
+ console.log('\u2705 | Deployed Smart Contracts on local Anvil')
})
describe('√ approve', () => {
-
it('√ should succeed with valid input', async () => {
const { bridgeToken, localSimulator, router } = await getContracts()
writeContractMock.mockResolvedValueOnce(ccipTxHash)
waitForTransactionReceiptMock.mockResolvedValue(ccipTxReceipt)
- const approvedAmount = parseEther('10')
+ const approvedAmount = Viem.parseEther('10')
// HH: Approval Transaction
await bridgeToken.write.approve([
- router.address, // spender
- approvedAmount // amount
+ router.address, // spender
+ approvedAmount, // amount
])
// CCIP: Approval Transaction
@@ -93,7 +101,7 @@ describe('Integration', () => {
// writeContractMock.mockResolvedValueOnce(ccipTxHash)
// waitForTransactionReceiptMock.mockResolvedValue(ccipTxReceipt)
const { bridgeToken, localSimulator, router } = await getContracts()
- const approvedAmount = parseEther('0')
+ const approvedAmount = Viem.parseEther('0')
const { txReceipt } = await ccipClient.approveRouter({
client: testClient,
@@ -131,19 +139,19 @@ describe('Integration', () => {
writeContractMock.mockResolvedValueOnce(ccipTxHash)
waitForTransactionReceiptMock.mockResolvedValue(ccipTxReceipt)
const { bridgeToken, router } = await getContracts()
- const approvedAmount = parseEther('10')
+ const approvedAmount = Viem.parseEther('10')
// HH: Approval Transaction
await bridgeToken.write.approve([
- router.address, // spender
- approvedAmount // amount
+ router.address, // spender
+ approvedAmount, // amount
])
mineBlock(isFork)
const hhApprovedAmount = await bridgeToken.read.allowance([
- account.address, // owner
- router.address // spender
+ account.address, // owner
+ router.address, // spender
])
await ccipClient.approveRouter({
@@ -155,8 +163,8 @@ describe('Integration', () => {
})
const ccipApprovedAmount = await bridgeToken.read.allowance([
- account.address, // owner
- router.address // spender
+ account.address, // owner
+ router.address, // spender
])
expect(hhApprovedAmount).toBe(approvedAmount)
@@ -168,7 +176,6 @@ describe('Integration', () => {
})
describe('√ getOnRampAddress', () => {
-
it('√ should return the address of the onRamp contract', async () => {
const { router } = await getContracts()
const expectedOnRampAddress = '0x8F35B097022135E0F46831f798a240Cc8c4b0B01'
@@ -194,7 +201,6 @@ describe('Integration', () => {
})
describe('√ getSupportedFeeTokens', () => {
-
it('√ should return supported fee tokens for valid chains', async () => {
const { router } = await getContracts()
const supportedFeeTokens = [
@@ -214,7 +220,7 @@ describe('Integration', () => {
const ccipSupportedFeeTokens = await ccipClient.getSupportedFeeTokens({
client: testClient,
routerAddress: router.address,
- destinationChainSelector: "16015286601757825753",
+ destinationChainSelector: '16015286601757825753',
})
expect(hhSupportedFeeTokens).toStrictEqual(supportedFeeTokens)
@@ -233,7 +239,7 @@ describe('Integration', () => {
// const data = encodeFunctionData({
// abi: CCIP.IERC20ABI,
// functionName: 'transfer',
- // args: [Viem.zeroAddress, Viem.parseEther('0.12')],
+ // args: [Viem.Viem.zeroAddress, Viem.parseEther('0.12')],
// })
// const hhFee = await router.read.getFee([
// '14767482510784806043', // destinationChainSelector: '14767482510784806043',
@@ -245,7 +251,7 @@ describe('Integration', () => {
// client: testClient,
// routerAddress: router.address,
// destinationChainSelector: '14767482510784806043',
- // destinationAccount: zeroAddress,
+ // destinationAccount: Viem.zeroAddress,
// amount: 1000000000000000000n,
// tokenAddress: '0x94095e6514411C65E7809761F21eF0febe69A977',
// })
@@ -318,7 +324,7 @@ describe('Integration', () => {
// const hhTransfer = await router.write.ccipSend([
// 14767482510784806043n, // destinationChainSelector
- // zeroAddress // destinationAccount
+ // Viem.zeroAddress // destinationAccount
// ])
// mineBlock(isFork)
// console.log({ hhTransfer })
@@ -327,7 +333,7 @@ describe('Integration', () => {
client: testClient,
routerAddress: '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59',
destinationChainSelector: '14767482510784806043',
- destinationAccount: zeroAddress,
+ destinationAccount: Viem.zeroAddress,
tokenAddress: '0x94095e6514411C65E7809761F21eF0febe69A977',
amount: 1000000000000000000n,
})
@@ -358,7 +364,6 @@ describe('Integration', () => {
})
})
describe('sendCCIPMessage', () => {
-
it('should successfully send message', async () => {
const { router } = await getContracts()
@@ -371,7 +376,7 @@ describe('Integration', () => {
client: testClient,
routerAddress: router.address,
destinationChainSelector: '14767482510784806043',
- destinationAccount: zeroAddress,
+ destinationAccount: Viem.zeroAddress,
data: Viem.encodeAbiParameters([{ type: 'string', name: 'data' }], ['Hello']),
})
expect(transfer.txHash).toEqual(ccipTxHash)
@@ -391,7 +396,7 @@ describe('Integration', () => {
client: testClient,
routerAddress: router.address,
destinationChainSelector: '14767482510784806043',
- destinationAccount: zeroAddress,
+ destinationAccount: Viem.zeroAddress,
feeTokenAddress: '0x94095e6514411C65E7809761F21eF0febe69A977',
data: Viem.encodeAbiParameters([{ type: 'string', name: 'data' }], ['Hello']),
})
diff --git a/packages/ccip-js/test/integration-testnet.test.ts b/packages/ccip-js/test/integration-testnet.test.ts
new file mode 100644
index 0000000..f1605ca
--- /dev/null
+++ b/packages/ccip-js/test/integration-testnet.test.ts
@@ -0,0 +1,293 @@
+import { jest, expect, it, beforeAll, describe, afterAll } from '@jest/globals'
+import * as CCIP from '../src/api'
+import * as Viem from 'viem'
+import { sepolia, avalancheFuji } from 'viem/chains'
+import { privateKeyToAccount } from 'viem/accounts'
+import bridgeTokenAbi from '@chainlink/contracts/abi/v0.8/BurnMintERC677.json'
+import { DEFAULT_ANVIL_PRIVATE_KEY } from './helpers/constants'
+import { parseEther } from 'viem'
+
+const ccipSdkClient = CCIP.createClient()
+
+const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL
+const AVALANCHE_FUJI_RPC_URL = process.env.AVALANCHE_FUJI_RPC_URL
+const SEPOLIA_CHAIN_SELECTOR = '16015286601757825753'
+const WRAPPED_NATIVE_AVAX = '0xd00ae08403B9bbb9124bB305C09058E32C39A48c'
+const LINK_TOKEN_FUJI = '0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846'
+
+// 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?
+const TIMEOUT = 180 * 1000 // 3m
+
+if (!SEPOLIA_RPC_URL) {
+ throw new Error('SEPOLIA_RPC_URL must be set')
+}
+if (!AVALANCHE_FUJI_RPC_URL) {
+ throw new Error('AVALANCHE_FUJI_RPC_URL must be set')
+}
+const privateKey = process.env.PRIVATE_KEY as `0x${string}`
+
+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",
+ )
+}
+
+jest.setTimeout(TIMEOUT)
+describe('Integration: Fuji -> Sepolia', () => {
+ let avalancheFujiClient: Viem.WalletClient
+ let sepoliaClient: Viem.WalletClient
+ let bnmToken_fuji: any
+ let _messageId: `0x${string}`
+ let ccipSend_txHash: `0x${string}`
+
+ const AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS = '0xF694E193200268f9a4868e4Aa017A0118C9a8177'
+ const approvedAmount = parseEther('0.000000005')
+
+ beforeAll(async () => {
+ avalancheFujiClient = Viem.createWalletClient({
+ chain: avalancheFuji,
+ transport: Viem.http(AVALANCHE_FUJI_RPC_URL),
+ account: privateKeyToAccount(privateKey),
+ })
+
+ sepoliaClient = Viem.createWalletClient({
+ chain: sepolia,
+ transport: Viem.http(SEPOLIA_RPC_URL),
+ account: privateKeyToAccount(privateKey),
+ })
+
+ bnmToken_fuji = Viem.getContract({
+ address: '0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4', // CCIP BnM on Avalanche Fuji
+ abi: bridgeTokenAbi,
+ client: avalancheFujiClient,
+ })
+
+ expect(bnmToken_fuji.address).toEqual('0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4')
+
+ const bnmBalance = await bnmToken_fuji.read.balanceOf([privateKeyToAccount(privateKey!).address])
+ if (parseInt(bnmBalance) < approvedAmount) {
+ await bnmToken_fuji.write.drip([privateKeyToAccount(privateKey!).address])
+ console.log(' ℹ️ | Dripped 1 CCIP BnM token to account: ', privateKeyToAccount(privateKey!).address)
+ }
+ })
+
+ describe('√ all critical functionality in CCIP Client', () => {
+ it('✅ should approve BnM spend, given valid input', async () => {
+ const ccipApprove = await ccipSdkClient.approveRouter({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ amount: approvedAmount,
+ tokenAddress: bnmToken_fuji.address,
+ 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 ccipSdkClient.getAllowance({
+ client: avalancheFujiClient,
+ account: avalancheFujiClient.account!.address,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ })
+ expect(allowance).toEqual(approvedAmount)
+ })
+
+ it('✅ returns on-ramp address', async function () {
+ const avalancheFujiOnRampAddress = await ccipSdkClient.getOnRampAddress({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ })
+ expect(avalancheFujiOnRampAddress).toEqual('0x75b9a75Ee1fFef6BE7c4F842a041De7c6153CF4E')
+ })
+
+ it('✅ lists supported fee tokens', async function () {
+ const result = await ccipSdkClient.getSupportedFeeTokens({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ })
+ expect(result.length).toEqual(2)
+ expect(result[1].toLocaleLowerCase()).toBe(WRAPPED_NATIVE_AVAX.toLowerCase())
+ expect(result[0].toLocaleLowerCase()).toBe(LINK_TOKEN_FUJI.toLowerCase())
+ })
+
+ it('✅ fetched lane rate refill limits are defined', async function () {
+ const { tokens, lastUpdated, isEnabled, capacity, rate } = await ccipSdkClient.getLaneRateRefillLimits({
+ client: avalancheFujiClient,
+ 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('number')
+ 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 ccipSdkClient.getTokenRateLimitByLane({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ supportedTokenAddress: bnmToken_fuji.address,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ })
+
+ // this implicitly asserts that the values are defined as well.
+ expect(typeof tokens).toBe('bigint')
+ expect(typeof lastUpdated).toBe('number')
+ 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 ccipSdkClient.getFee({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ amount: approvedAmount,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ destinationAccount: sepoliaClient.account!.address,
+ feeTokenAddress: LINK_TOKEN_FUJI,
+ })
+ const fee_native = await ccipSdkClient.getFee({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ amount: approvedAmount,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ destinationAccount: sepoliaClient.account!.address,
+ feeTokenAddress: WRAPPED_NATIVE_AVAX,
+ })
+
+ expect(fee_link).toBeGreaterThan(1000n)
+ expect(fee_native).toBeGreaterThan(1000n)
+ })
+ it('✅ returns token admin registry', async function () {
+ const result = await ccipSdkClient.getTokenAdminRegistry({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ })
+
+ const CCIP_ADMIN_REGISTRY_ADDRESS = '0xA92053a4a3922084d992fD2835bdBa4caC6877e6'
+ expect(result).toEqual(CCIP_ADMIN_REGISTRY_ADDRESS)
+ })
+
+ it('✅ checks if BnM token is supported for transfer', async function () {
+ const result = await ccipSdkClient.isTokenSupported({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ })
+ expect(result).toBe(true)
+ })
+
+ it('✅ transfers tokens | pay in LINK', async function () {
+ await ccipSdkClient.approveRouter({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ amount: approvedAmount,
+ tokenAddress: bnmToken_fuji.address,
+ waitForReceipt: true,
+ })
+
+ // approve LINK spend
+ const fee_link = await ccipSdkClient.getFee({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ amount: approvedAmount,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ destinationAccount: sepoliaClient.account!.address,
+ feeTokenAddress: LINK_TOKEN_FUJI,
+ })
+ await ccipSdkClient.approveRouter({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ amount: fee_link,
+ tokenAddress: LINK_TOKEN_FUJI,
+ waitForReceipt: true,
+ })
+ const allowance = await ccipSdkClient.getAllowance({
+ client: avalancheFujiClient,
+ account: avalancheFujiClient.account!.address,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ })
+
+ expect(allowance).toBeGreaterThanOrEqual(approvedAmount)
+
+ const result = await ccipSdkClient.transferTokens({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ destinationAccount: sepoliaClient.account!.address,
+ amount: approvedAmount,
+ feeTokenAddress: LINK_TOKEN_FUJI,
+ })
+
+ _messageId = result.messageId
+ ccipSend_txHash = result.txHash
+
+ expect(result.txReceipt!.status).toEqual('success')
+ })
+
+ it('✅ transfers tokens | pays in native token', async function () {
+ await ccipSdkClient.approveRouter({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ amount: approvedAmount,
+ tokenAddress: bnmToken_fuji.address,
+ waitForReceipt: true,
+ })
+
+ const result = await ccipSdkClient.transferTokens({
+ client: avalancheFujiClient,
+ routerAddress: AVALANCHE_FUJI_CCIP_ROUTER_ADDRESS,
+ tokenAddress: bnmToken_fuji.address,
+ destinationChainSelector: SEPOLIA_CHAIN_SELECTOR,
+ destinationAccount: sepoliaClient.account!.address,
+ amount: approvedAmount,
+ })
+
+ expect(result.txReceipt!.status).toEqual('success')
+ })
+
+ it('✅ gets transfer status & gets transaction receipt', async function () {
+ const ccipSend_txReceipt = await ccipSdkClient.getTransactionReceipt({
+ client: avalancheFujiClient,
+ hash: ccipSend_txHash,
+ })
+
+ const FUJI_CHAIN_SELECTOR = '14767482510784806043'
+ const SEPOLIA_ROUTER_ADDRESS = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
+
+ const transferStatus = await ccipSdkClient.getTransferStatus({
+ client: sepoliaClient, // from the destination chain
+ sourceChainSelector: FUJI_CHAIN_SELECTOR,
+ destinationRouterAddress: SEPOLIA_ROUTER_ADDRESS,
+ fromBlockNumber: ccipSend_txReceipt.blockNumber,
+ messageId: _messageId,
+ })
+
+ expect(transferStatus).toBeDefined()
+
+ 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())
+ })
+ })
+})