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()) + }) + }) +})