diff --git a/.env.example b/.env.example index 63519f9..95da891 100644 --- a/.env.example +++ b/.env.example @@ -1,26 +1,5 @@ -PRIVATE_KEY="" -ETHEREUM_SEPOLIA_RPC_URL="" -OPTIMISM_GOERLI_RPC_URL="" -ARBITRUM_SEPOLIA_RPC_URL="" -AVALANCHE_FUJI_RPC_URL="" -POLYGON_MUMBAI_RPC_URL="" -BNB_CHAIN_TESTNET_RPC_URL="" -BASE_SEPOLIA_RPC_URL="" -METIS_SEPOLIA_RPC_URL="" -ZKSYNC_SEPOLIA_RPC_URL="" -SCROLL_SEPOLIA_RPC_URL="" -ZIRCUIT_SEPOLIA_RPC_URL="" -XLAYER_SEPOLIA_RPC_URL="" -POLYGON_ZKEVM_SEPOLIA_RPC_URL="" -POLKADOT_ASTAR_SHIBUYA_RPC_URL="" -MANTLE_SEPOLIA_RPC_URL="" -SONEIUM_MINATO_SEPOLIA_RPC_URL="" -BSQUARED_TESTNET_RPC_URL="" -BOB_SEPOLIA_RPC_URL="" -WORLDCHAIN_SEPOLIA_RPC_URL="" -SHIBARIUM_TESTNET_RPC_URL="" -BITLAYER_TESTNET_RPC_URL="" -FANTOM_SONIC_TESTNET_RPC_URL="" -CORN_TESTNET_RPC_URL="" -HASHKEY_SEPOLIA_RPC_URL="" -INK_SEPOLIA_RPC_URL="" +ETHEREUM_SEPOLIA_RPC_URL=https://your-ethereum-sepolia-rpc-url +AVALANCHE_FUJI_RPC_URL=https://your-avalanche-fuji-rpc-url +POLYGON_AMOY_RPC_URL=https://your-polygon-amoy-rpc-url +BASE_SEPOLIA_RPC_URL=https://your-base-sepolia-rpc-url + diff --git a/.gitignore b/.gitignore index 9fb4f5d..8dcdad8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,16 +3,16 @@ cache/ out/ # Ignores development broadcast logs -!/broadcast +/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ -broadcast/ # Docs -docs/ +book/book +*.tsv # Dotenv file .env -# Node modules -node_modules/ \ No newline at end of file +# Node +node_modules/ diff --git a/.gitmodules b/.gitmodules index 7358fee..fae5bf3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,30 +1,18 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/chainlink-evm"] path = lib/chainlink-evm url = https://github.com/smartcontractkit/chainlink-evm -[submodule "lib/openzeppelin-contracts-4.7.3"] - path = lib/openzeppelin-contracts-4.7.3 - url = https://github.com/openzeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-5.3.0"] + path = lib/openzeppelin-contracts-5.3.0 + url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/openzeppelin-contracts-4.8.3"] path = lib/openzeppelin-contracts-4.8.3 - url = https://github.com/openzeppelin/openzeppelin-contracts -[submodule "lib/openzeppelin-contracts-4.9.6"] - path = lib/openzeppelin-contracts-4.9.6 - url = https://github.com/openzeppelin/openzeppelin-contracts -[submodule "lib/openzeppelin-contracts-5.0.2"] - path = lib/openzeppelin-contracts-5.0.2 - url = https://github.com/openzeppelin/openzeppelin-contracts -[submodule "lib/openzeppelin-contracts-5.1.0"] - path = lib/openzeppelin-contracts-5.1.0 - url = https://github.com/openzeppelin/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/chainlink-ace"] + path = lib/chainlink-ace + url = https://github.com/smartcontractkit/chainlink-ace [submodule "lib/chainlink-ccip"] path = lib/chainlink-ccip url = https://github.com/smartcontractkit/chainlink-ccip -[submodule "lib/chainlink-local"] - path = lib/chainlink-local - url = https://github.com/smartcontractkit/chainlink-local diff --git a/README.md b/README.md index f2b700b..70e4851 100644 --- a/README.md +++ b/README.md @@ -1,745 +1,38 @@ -## Chainlink CCIP Starter Kit +# Chainlink CCIP Starter Kit V2 (Foundry) > **Note** > > _This repository represents an example of using a Chainlink product or service. It is provided to help you understand how to interact with Chainlink’s systems so that you can integrate them into your own. This template is provided "AS IS" without warranties of any kind, has not been audited, and may be missing key checks or error handling to make the usage of the product more clear. Take everything in this repository as an example and not something to be copy pasted into a production ready service._ -This project demonstrates a couple of basic Chainlink CCIP use cases. +Reference starter kit for learning and testing **CCIP v2** with Foundry scripts, Solidity contracts, and small SDK examples. -## Table of Contents +## Start Here -1. [Chainlink CCIP Starter Kit](#chainlink-ccip-starter-kit) - - [Note](#note) -2. [Prerequisites](#prerequisites) -3. [Getting Started](#getting-started) - - [Install packages](#install-packages) - - [Compile contracts](#compile-contracts) -4. [What is Chainlink CCIP?](#what-is-chainlink-ccip) -5. [Usage & Environment Variables](#usage--environment-variables) -6. [Chain Configurations](#chain-configurations) -7. [Testing](#local-testing) - [Faucet](#faucet) +The primary documentation for this repo lives in the book: +- https://smartcontractkit.github.io/ccip-starter-kit-foundry/ -8. [Production Best Practice](#production-best-practice) -9. [Example 1 - Transfer CCIP Test Tokens from EOA to EOA](#example-1---transfer-tokens-from-eoa-to-eoa) -10. [Example 2 - Transfer Tokens from EOA to Smart Contract](#example-2---transfer-tokens-from-eoa-to-smart-contract) -11. [Example 3 - Transfer Token(s) from Smart Contract to any destination](#example-3---transfer-tokens-from-smart-contract-to-any-destination) -12. [Example 4 - Send & Receive Tokens and Data](#example-4---send--receive-tokens-and-data) -13. [Example 5 - Send & Receive Cross-Chain Messages and Pay with Native Coins](#example-5---send--receive-cross-chain-messages-and-pay-with-native-coins) -14. [Example 6 - Send & Receive Cross-Chain Messages and Pay with LINK Tokens](#example-6---send--receive-cross-chain-messages-and-pay-with-link-tokens) -15. [Example 7 - Execute Received Message as a Function Call](#example-7---execute-received-message-as-a-function-call) -16. [CCT Standard](#cct-standard) +You can also run the book locally: -## Prerequisites - -- [Foundry](https://book.getfoundry.sh/getting-started/installation) - -## Getting Started - -1. Install packages - -``` -forge install -``` - -and - -``` -npm install -``` - -2. Compile contracts - -``` -forge build -``` - -## What is Chainlink CCIP? - -**Chainlink Cross-Chain Interoperability Protocol (CCIP)** provides a single, simple, and elegant interface through which dApps and web3 entrepreneurs can securely meet all their cross-chain needs, including token transfers and arbitrary messaging. - -![basic-architecture](./img/basic-architecture.png) - -With Chainlink CCIP, one can: - -- Transfer supported tokens -- Send messages (any data) -- Send messages and tokens - -CCIP receiver can be: - -- Smart contract that implements `CCIPReceiver.sol` -- EOA - -**Note**: If you send a message and token(s) to EOA, only tokens will arrive - -To use this project, you can consider CCIP as a "black-box" component and be aware of the Router contract only. If you want to dive deep into it, check the [Official Chainlink Documentation](https://docs.chain.link/ccip). - -## Usage & Environment Variables - -In the next section you can see a couple of basic Chainlink CCIP use case examples. But before that, you need to set up some environment variables. - -Create a new file by copying the `.env.example` file, and name it `.env`. Fill in your wallet's PRIVATE_KEY, and RPC URLs for at least two blockchains - -```shell -PRIVATE_KEY="" -ETHEREUM_SEPOLIA_RPC_URL="" -ARBITRUM_SEPOLIA_RPC_URL="" -AVALANCHE_FUJI_RPC_URL="" -BASE_SEPOLIA_RPC_URL="" -POLYGON_MUMBAI_RPC_URL="" -BNB_CHAIN_TESTNET_RPC_URL="" -WEMIX_TESTNET_RPC_URL="" -KROMA_SEPOLIA_TESTNET_RPC_URL="" -METIS_SEPOLIA_RPC_URL="" -ZKSYNC_SEPOLIA_RPC_URL="" -SCROLL_SEPOLIA_RPC_URL="" -ZIRCUIT_SEPOLIA_RPC_URL="" -XLAYER_SEPOLIA_RPC_URL="" -POLYGON_ZKEVM_SEPOLIA_RPC_URL="" -POLKADOT_ASTAR_SHIBUYA_RPC_URL="" -MANTLE_SEPOLIA_RPC_URL="" -SONEIUM_MINATO_SEPOLIA_RPC_URL="" -BSQUARED_TESTNET_RPC_URL="" -BOB_SEPOLIA_RPC_URL="" -WORLDCHAIN_SEPOLIA_RPC_URL="" -SHIBARIUM_TESTNET_RPC_URL="" -BITLAYER_TESTNET_RPC_URL="" -FANTOM_SONIC_TESTNET_RPC_URL="" -CORN_TESTNET_RPC_URL="" -HASHKEY_SEPOLIA_RPC_URL="" -INK_SEPOLIA_RPC_URL="" -``` - -Once that is done, to load the variables in the `.env` file, run the following command: - -```shell -source .env -``` - -## Chain Configurations - -Make yourself familiar with the [`Helper.sol`](./script/Helper.sol) smart contract. It contains all the necessary Chainlink CCIP config. If you ever need to adjust any of those parameters, go to the Helper contract. - -This contract also contains some enums, like `SupportedNetworks`: - -```solidity -enum SupportedNetworks { - ETHEREUM_SEPOLIA, // 0 - AVALANCHE_FUJI, // 1 - ARBITRUM_SEPOLIA, // 2 - POLYGON_MUMBAI, // 3 - BNB_CHAIN_TESTNET, // 4 - OPTIMISM_SEPOLIA, // 5 - BASE_SEPOLIA, // 6 - WEMIX_TESTNET, // 7 - KROMA_SEPOLIA_TESTNET, // 8 - METIS_SEPOLIA, // 9 - ZKSYNC_SEPOLIA // 10 - SCROLL_SEPOLIA, // 11 - ZIRCUIT_SEPOLIA, // 12 - XLAYER_SEPOLIA, // 13 - POLYGON_ZKEVM_SEPOLIA, // 14 - POLKADOT_ASTAR_SHIBUYA, // 15 - MANTLE_SEPOLIA, // 16 - SONEIUM_MINATO_SEPOLIA, // 17 - BSQUARED_TESTNET, // 18 - BOB_SEPOLIA, // 19 - WORLDCHAIN_SEPOLIA, // 20 - SHIBARIUM_TESTNET, // 21 - BITLAYER_TESTNET, // 22 - FANTOM_SONIC_TESTNET, // 23 - CORN_TESTNET, // 24 - HASHKEY_SEPOLIA, // 25 - INK_SEPOLIA // 26 -} -``` - -This means that if you want to perform some action from `AVALANCHE_FUJI` blockchain to `ETHEREUM_SEPOLIA` blockchain, for example, you will need to pass `2 (uint8)` as a source blockchain flag and `0 (uint8)` as a destination blockchain flag. - -Similarly, there is an `PayFeesIn` enum: - -```solidity -enum PayFeesIn { - Native, // 0 - LINK // 1 -} -``` - -So, if you want to pay for Chainlink CCIP fees in LINK token, you will pass `1 (uint8)` as a function argument. - -## Local testing - -The test files are located in the `test` folder. - -> **Note** -> Tests in the test folder have names like Example1.spec.ts, where `Example1` maps to the [#example-1](#example-1---transfer-tokens-from-eoa-to-eoa) scenario below. - -There are two types of tests: - -- **Test with [CCIPLocalSimulator](https://github.com/smartcontractkit/chainlink-local/blob/main/src/ccip/CCIPLocalSimulator.sol)**: These tests are used to test the CCIP functionality in your local environment. They are located in the `test/no-fork` folder. To run these tests, run the following command: - - ```shell - forge test --no-match-contract ".*ForkTest$" - ``` - -- **Test with [CCIPLocalSimulatorFork](https://github.com/smartcontractkit/chainlink-local/blob/main/src/ccip/CCIPLocalSimulatorFork.sol)**: These tests are used to test the CCIP functionality in a forked environment. They are located in the test/fork folder. To run these tests, run the following command: - - ```shell - forge test --match-contract ".*ForkTest$" - ``` - - **Note**: The fork tests send CCIP messages from Arbitrum Sepolia to Ethereum Sepolia, so make sure you have the _ETHEREUM_SEPOLIA_RPC_URL_ and _ARBITRUM_SEPOLIA_RPC_URL_ set in your .env file. - -## Faucet - -You will need test tokens for some of the examples in this Starter Kit. Public faucets sometimes limit how many tokens a user can create and token pools might not have enough liquidity. To resolve these issues, CCIP supports two test tokens that you can mint permissionlessly so you don't run out of tokens while testing different scenarios. - -To get 10\*\*18 units of each of these tokens, use the `script/Faucet.s.sol` smart contract. Keep in mind that the `CCIP-BnM` test token you can mint on all testnets, while `CCIP-LnM` you can mint only on Ethereum Sepolia. On other testnets, the `CCIP-LnM` token representation is a wrapped/synthetic asset called `clCCIP-LnM`. - -```solidity -function run(SupportedNetworks network) external; -``` - -For example, to mint 10\*\*18 units of both `CCIP-BnM` and `CCIP-LnM` test tokens on Ethereum Sepolia, run: - -```shell -forge script ./script/Faucet.s.sol -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -Or if you want to mint 10\*\*18 units of `CCIP-BnM` test token on Avalanche Fuji, run: - -```shell -forge script ./script/Faucet.s.sol -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -## Production Best Practice - -Most of these examples are simplified for educational purposes. For production code, please adhere to the following best practices (Refer to the [Best Practices](https://docs.chain.link/ccip/best-practices) guide from the Official Chainlink Documentation for more information.): - -- **Do Not Hardcode `extraArgs`**: In these examples, `extraArgs` are hardcoded within contracts for simplicity. It is recommended to make `extraArgs` mutable. For instance, you can construct `extraArgs` off-chain and pass them into your function calls, or store them in a storage variable that can be updated as needed. This approach ensures that `extraArgs` remain backward compatible with future CCIP upgrades. -- **Validate the Destination Chain**: Always ensure that the destination chain is valid and supported before sending messages. -- **Understand `allowOutOfOrderExecution` Usage**: This parameter is available only on lanes where the **Out of Order Execution** property is set to **Optional** or **Required**. Refer to the [CCIP Directory](https://docs.chain.link/ccip/directory) to determine if your target lane supports this feature. For lanes where this parameter is absent, you must use `extraArgsV1` instead. - -To help you with the off-chain encoding of `extraArgs`, we included the [`script/EncodeExtraArgsOffchain.s.sol`](script/EncodeExtraArgsOffchain.s.sol) helper script. To encode `extraArgs` as `EVMExtraArgsV1` run: - -```shell -forge script ./script/EncodeExtraArgsOffchain.s.sol -vvv --sig "encodeV1(uint256)" -- -``` - -To encode `extraArgs` as `EVMExtraArgsV2` run: - -```shell -forge script ./script/EncodeExtraArgsOffchain.s.sol -vvv --sig "encodeV2(uint256,bool)" -- -``` - -### Example 1 - Transfer Tokens from EOA to EOA - -To transfer tokens from one EOA on one blockchain to another EOA on another blockchain you can use the `script/Example01.s.sol` smart contract: - -```solidity -function run( - SupportedNetworks source, - SupportedNetworks destination, - address receiver, - address tokenToSend, - uint256 amount, - PayFeesIn payFeesIn -) external returns (bytes32 messageId); -``` - -For example, if you want to send 0.0000000000000001 CCIP-BnM from Avalanche Fuji to Ethereum Sepolia and to pay for CCIP fees in LINK, run: - -```shell -forge script ./script/Example01.s.sol -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8,uint8,address,address,uint256,uint8)" -- 1 0 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 100 1 -``` - -### Example 2 - Transfer Tokens from EOA to Smart Contract - -To transfer tokens from EOA from the source blockchain to the smart contract on the destination blockchain, follow the next steps: - -1. Deploy [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) to the **destination blockchain**, using the `script/Example02.s.sol:DeployBasicMessageReceiver` smart contract: - -```solidity -function run(SupportedNetworks destination) external; -``` - -For example, to deploy it to Ethereum Sepolia, run: - -```shell -forge script ./script/Example02.s.sol:DeployBasicMessageReceiver -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -2. Transfer tokens, from the **source blockchain** to the deployed BasicMessageReceiver smart contract using the `script/Example02.s.sol:CCIPTokenTransfer` smart contract: - -```solidity -function run( - SupportedNetworks source, - SupportedNetworks destination, - address basicMessageReceiver, - address tokenToSend, - uint256 amount, - PayFeesIn payFeesIn -) external returns (bytes32 messageId); -``` - -For example, if you want to send 0.0000000000000001 CCIP-BnM from Avalanche Fuji to Ethereum Sepolia and to pay for CCIP fees in native coin (Test AVAX), run: - -```shell -forge script ./script/Example02.s.sol:CCIPTokenTransfer -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8,uint8,address,address,uint256,uint8)" -- 1 0 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 100 0 -``` - -3. Once the CCIP message is finalized on the destination blockchain, you can see the details about the latest message using the `script/Example02.s.sol:GetLatestMessageDetails` smart contract: - -```solidity -function run(address basicMessageReceiver) external view; -``` - -For example, - -```shell -forge script ./script/Example02.s.sol:GetLatestMessageDetails -vvv --broadcast --rpc-url ethereumSepolia --sig "run(address)" -- -``` - -4. Finally, you can always withdraw received tokens from the [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) smart contract using the `cast send` command. - -For example, to withdraw 100 units of CCIP-BnM previously sent, run: - -```shell -cast send --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "withdrawToken(address,address)" 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 -``` - -### Example 3 - Transfer Token(s) from Smart Contract to any destination - -To transfer a token or batch of tokens from a single, universal, smart contract to any address on the destination blockchain follow the next steps: - -1. Deploy [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) to the **source blockchain**, using the `script/Example03.s.sol:DeployBasicTokenSender` smart contract: - -```solidity -function run(SupportedNetworks source) external; -``` - -For example, if you want to send tokens from Avalanche Fuji to Ethereum Sepolia, run: - -```shell -forge script ./script/Example03.s.sol:DeployBasicTokenSender -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -2. [OPTIONAL] If you want to send tokens to the smart contract, instead of EOA, you will need to deploy [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) to the **destination blockchain**. For this purpose, you can reuse the `script/Example02.s.sol:DeployBasicMessageReceiver` smart contract from the previous example: - -```solidity -function run(SupportedNetworks destination) external; -``` - -For example, to deploy it to Ethereum Sepolia, run: - -```shell -forge script ./script/Example02.s.sol:DeployBasicMessageReceiver -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -3. Fill the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) with tokens/coins for fees (you can always withdraw it later). You can do it manually from your wallet or by using the `cast send` command. - -For example, if you want to pay for Chainlink CCIP Fees in LINK tokens, you can fill the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) smart contract with 1 Avalanche Fuji LINK by running: - -```shell -cast send 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 "transfer(address,uint256)" 5000000000000000000 --rpc-url avalancheFuji --private-key=$PRIVATE_KEY -``` - -Or, if you want to pay for Chainlink CCIP Fees in Native coins, you can fill the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) smart contract with 0.1 Avalanche Fuji AVAX by running: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY --value 0.1ether -``` - -4. For each token you want to send, you will need to approve the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) to spend it on your behalf, by using the `cast send` command. - -For example, if you want to send 0.0000000000000001 CCIP-BnM using the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) you will first need to approve that amount: - -```shell -cast send 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 "approve(address,uint256)" 100 --rpc-url avalancheFuji --private-key=$PRIVATE_KEY -``` - -5. Finally, send tokens by providing the array of `Client.EVMTokenAmount {address token; uint256 amount;}` objects, using the `script/Example03.s.sol:SendBatch` smart contract: - -```solidity -function run( - SupportedNetworks destination, - address payable basicTokenSenderAddres, - address receiver, - Client.EVMTokenAmount[] memory tokensToSendDetails, - BasicTokenSender.PayFeesIn payFeesIn -) external; -``` - -For example, to send CCIP-BnM token amounts you previously approved from Avalanche Fuji to Ethereum Sepolia, and pay for Chainlink CCIP fees in LINK tokens, run: - -```shell -forge script ./script/Example03.s.sol:SendBatch -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8,address,address,(address,uint256)[],uint8)" -- 0 "[(0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4,100)]" 1 -``` - -6. Of course, you can always withdraw tokens you sent to the [`BasicTokenSender.sol`](./src/BasicTokenSender.sol) for fees, or from [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) if you received them there. - -For example, to withdraw ERC20 tokens, run: - -```shell -cast send --rpc-url --private-key=$PRIVATE_KEY "withdrawToken(address,address)" -``` - -And to withdraw Native coins, run: - -```shell -cast send --rpc-url --private-key=$PRIVATE_KEY "withdraw(address)" -``` - -### Example 4 - Send & Receive Tokens and Data - -To transfer tokens and data across multiple chains, follow the next steps: - -1. Deploy the [`ProgrammableTokenTransfers.sol`](./src/ProgrammableTokenTransfers.sol) smart contract to the **source blockchain**, using the `script/Example04.s.sol:DeployProgrammableTokenTransfers` smart contract: - -```solidity -function run(SupportedNetworks network) external; +```bash +mdbook serve book --open ``` -For example, if you want to send a message from Avalanche Fuji to Ethereum Sepolia type: +## Repo Scope -```shell -forge script ./script/Example04.s.sol:DeployProgrammableTokenTransfers -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -2. Open Metamask and fund your contract with Native tokens. For example, if you want to send a message from Avalanche Fuji to Ethereum Sepolia, you can send 1 Fuji AVAX to your contract. You can also do the same thing using the `cast send` command: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY --value 1ether -``` - -3. Open Metamask and fund your contract with LINK tokens. For example, if you want to send a message from Avalanche Fuji to Ethereum Sepolia, you can send a 1 Fuji BnM to your contract. You can also do the same thing using the `cast send` command: - -```shell -cast send 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 "transfer(address,uint256)" 1000000000000000000 --rpc-url avalancheFuji --private-key=$PRIVATE_KEY -``` - -4. Deploy the [`ProgrammableTokenTransfers.sol`](./src/ProgrammableTokenTransfers.sol) smart contract to the **destination blockchain**, using the `script/Example04.s.sol:DeployProgrammableTokenTransfers` smart contract, as you did in step number one. - -For example, if you want to receive a message from Avalanche Fuji on Ethereum Sepolia type: - -```shell -forge script ./script/Example04.s.sol:DeployProgrammableTokenTransfers -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -At this point, you have one **sender** contract on the source blockchain, and one **receiver** contract on the destination blockchain. Please note that [`ProgrammableTokenTransfers.sol`](./src/ProgrammableTokenTransfers.sol) can both send & receive tokens and data, hence we have two identical instances on both source and destination blockchains. - -5. Send a message, using the `script/Example04.s.sol:SendTokensAndData` smart contract: - -```solidity -function run( - address payable sender, - SupportedNetworks destination, - address receiver, - string memory message, - address token, - uint256 amount -) external; -``` - -For example, if you want to send a "Hello World" message alongside 100 units of CCIP-BnM from Avalanche Fuji to Ethereum Sepolia, type: - -```shell -forge script ./script/Example04.s.sol:SendTokensAndData -vvv --broadcast --rpc-url avalancheFuji --sig "run(address,uint8,address,string,address,uint256)" -- 0 "Hello World" 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 100 -``` - -6. Once the CCIP message is finalized on the destination blockchain, you can see the details of the latest CCIP message received, by running the following command: - -```shell -cast call "getLastReceivedMessageDetails()" --rpc-url ethereumSepolia -``` - -### Example 5 - Send & Receive Cross-Chain Messages and Pay with Native Coins - -To send simple Text Cross-Chain Messages and pay for CCIP fees in Native Tokens, follow the next steps: - -1. Deploy the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract on the **source blockchain**, using the `script/Example05.s.sol:DeployBasicMessageSender` smart contract: - -```solidity -function run(SupportedNetworks source) external; -``` - -For example, if you want to send a simple cross-chain message from Avalanche Fuji, run: - -```shell -forge script ./script/Example05.s.sol:DeployBasicMessageSender -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -2. Fund the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract with Native Coins, either manually using your wallet or by using the `cast send` command. For example, if you want to send 1 Fuji AVAX, run: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY --value 1ether -``` - -3. Deploy the [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) smart contract to the **destination blockchain**. For this purpose, you can reuse the `script/Example02.s.sol:DeployBasicMessageReceiver` smart contract from the second example: - -```solidity -function run(SupportedNetworks destination) external; -``` - -For example, to deploy it to Ethereum Sepolia, run: +- `script/examples`: Foundry CCIP v2 examples (core learning path) +- `src`: sender/receiver contracts used by examples +- `book/src`: step-by-step tutorial chapters +- `sdk-examples`: minimal CCIP SDK demos -```shell -forge script ./script/Example02.s.sol:DeployBasicMessageReceiver -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -4. Finally, send a cross-chain message using the `script/Example05.s.sol:SendMessage` smart contract: - -```solidity -function run( - address payable sender, - SupportedNetworks destination, - address receiver, - string memory message, - BasicMessageSender.PayFeesIn payFeesIn -) external; -``` - -For example, if you want to send a "Hello World" message type: - -```shell -forge script ./script/Example05.s.sol:SendMessage -vvv --broadcast --rpc-url avalancheFuji --sig "ru -n(address,uint8,address,string,uint8)" -- 0 "Hello World" -0 -``` - -5. Once the CCIP message is finalized on the destination blockchain, you can see the details about the latest message using the `script/Example02.s.sol:GetLatestMessageDetails` smart contract: - -```solidity -function run(address basicMessageReceiver) external view; -``` - -For example, - -```shell -forge script ./script/Example02.s.sol:GetLatestMessageDetails -vvv --broadcast --rpc-url ethereumSepolia --sig "run(address)" -- -``` - -6. You can always withdraw tokens for Chainlink CCIP fees from the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract using the `cast send` command: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "withdraw(address)" -``` - -### Example 6 - Send & Receive Cross-Chain Messages and Pay with LINK Tokens - -To send simple Text Cross-Chain Messages and pay for CCIP fees in LINK Tokens, follow the next steps: +## Minimal Setup -1. Deploy the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract on the **source blockchain**, using the `script/Example05.s.sol:DeployBasicMessageSender` smart contract: - -```solidity -function run(SupportedNetworks source) external; -``` - -For example, if you want to send a simple cross-chain message from Avalanche Fuji, run: - -```shell -forge script ./script/Example05.s.sol:DeployBasicMessageSender -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -2. Fund the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract with Testnet LINKs, either manually using your wallet or by using the `cast send` command. For example, if you want to send 1 Fuji LINK, run: - -```shell -cast send 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 "transfer(address,uint256)" 1000000000000000000 --rpc-url avalancheFuji --private-key=$PRIVATE_KEY -``` - -3. Deploy the [`BasicMessageReceiver.sol`](./src/BasicMessageReceiver.sol) smart contract to the **destination blockchain**. For this purpose, you can reuse the `script/Example02.s.sol:DeployBasicMessageReceiver` smart contract from the second example: - -```solidity -function run(SupportedNetworks destination) external; +```bash +cp .env.example .env ``` -For example, to deploy it to Ethereum Sepolia, run: - -```shell -forge script ./script/Example02.s.sol:DeployBasicMessageReceiver -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -4. Finally, send a cross-chain message using the `script/Example05.s.sol:SendMessage` smart contract: - -```solidity -function run( - address payable sender, - SupportedNetworks destination, - address receiver, - string memory message, - BasicMessageSender.PayFeesIn payFeesIn -) external; -``` - -For example, if you want to send a "Hello World" message type: - -```shell -forge script ./script/Example05.s.sol:SendMessage -vvv --broadcast --rpc-url avalancheFuji --sig "ru -n(address,uint8,address,string,uint8)" -- 0 "Hello World" -1 -``` +Set at least: -5. Once the CCIP message is finalized on the destination blockchain, you can see the details about the latest message using the `script/Example02.s.sol:GetLatestMessageDetails` smart contract: +- `AVALANCHE_FUJI_RPC_URL` +- `ETHEREUM_SEPOLIA_RPC_URL` -```solidity -function run(address basicMessageReceiver) external view; -``` - -For example, - -```shell -forge script ./script/Example02.s.sol:GetLatestMessageDetails -vvv --broadcast --rpc-url ethereumSepolia --sig "run(address)" -- -``` - -6. You can always withdraw tokens for Chainlink CCIP fees from the [`BasicMessageSender.sol`](./src/BasicMessageSender.sol) smart contract using the `cast send` command: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "withdrawToken(address,address)" 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 -``` - -### Example 7 - Execute Received Message as a Function Call - -Our goal for this example is to mint an NFT on the destination blockchain by sending the `to` address from the source blockchain. It is extremely simple so we can understand the basic concepts, but you can expand it to accept payment for minting on the source blockchain, add extra features, etc. - -The basic architecture diagram of what we want to accomplish looks like this: - -```mermaid -flowchart LR -subgraph "Source Blockchain" -a("SourceMinter.sol") -- "`send abi.encodeWithSignature('mint(address)', msg.sender);`" --> b("Source Router") -end - -b("Source Router") --> c("CCIP") - -c("CCIP") --> d("Destination Router") - -subgraph "Destination Blockchain" -d("Destination Router") -- "`receive abi.encodeWithSignature('mint(address)', msg.sender);`" --> e("DestinationMinter.sol") -e("DestinationMinter.sol") -- "`call mint(to)`" --> f("MyNFT.sol") -end -``` - -1. Deploy the [`MyNFT.sol`](./src/cross-chain-nft-minter/MyNFT.sol) and [`DestinationMinter.sol`](./src/cross-chain-nft-minter/DestinationMinter.sol) smart contracts from the `./src/cross-chain-nft-minter` folder on the **destination blockchain**, by using the `script/CrossChainNFT.s.sol:DeployDestination` smart contract: - -```solidity -function run(SupportedNetworks destination) external; -``` - -For example, if you want to have an NFT collection on Ethereum Sepolia, run: - -```shell -forge script ./script/CrossChainNFT.s.sol:DeployDestination -vvv --broadcast --rpc-url ethereumSepolia --sig "run(uint8)" -- 0 -``` - -2. Deploy the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract on the **source blockchain**, by using the `script/CrossChainNFT.s.sol:DeploySource` smart contract: - -```solidity -function run(SupportedNetworks source) external; -``` - -For example, if you want to mint NFTs on Ethereum Sepolia from Avalanche Fuji, run: - -```shell -forge script ./script/CrossChainNFT.s.sol:DeploySource -vvv --broadcast --rpc-url avalancheFuji --sig "run(uint8)" -- 1 -``` - -3. Fund the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract with tokens for CCIP fees. - -- If you want to pay for CCIP fees in Native tokens: - - Open Metamask and fund your contract with Native tokens. For example, if you want to mint from Avalanche Fuji to Ethereum Sepolia, you can send 1 Fuji AVAX to the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract. - - Or, you can use the `cast send` command: - - ```shell - cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY --value 1ether - ``` - -- If you want to pay for CCIP fees in LINK tokens: - - Open Metamask and fund your contract with LINK tokens. For example, if you want to mint from Avalanche Fuji to Ethereum Sepolia, you can send 1 Fuji LINK to the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract. - - Or, you can use the `cast send` command: - - ```shell - cast send 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 "transfer(address,uint256)" 1000000000000000000 --rpc-url avalancheFuji --private-key=$PRIVATE_KEY - ``` - -4. Mint NFTs by calling the `mint()` function of the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract on the **source blockchain**. It will send the CCIP Cross-Chain Message with the ABI-encoded mint function signature from the [`MyNFT.sol`](./src/cross-chain-nft-minter/MyNFT.sol) smart contract. The [`DestinationMinter.sol`](./src/cross-chain-nft-minter/DestinationMinter.sol) smart contracts will receive the CCIP Cross-Chain Message with the ABI-encoded mint function signature as a payload and call the [`MyNFT.sol`](./src/cross-chain-nft-minter/MyNFT.sol) smart contract using it. The [`MyNFT.sol`](./src/cross-chain-nft-minter/MyNFT.sol) smart contract will then mint the new NFT to the `msg.sender` account from the `mint()` function of the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract, a.k.a to the account from which you will call the following command: - -```solidity -function run( - address payable sourceMinterAddress, - SupportedNetworks destination, - address destinationMinterAddress, - SourceMinter.PayFeesIn payFeesIn -) external; -``` - -For example, if you want to mint NFTs on Ethereum Sepolia by sending requests from Avalanche Fuji, run: - -```shell -forge script ./script/CrossChainNFT.s.sol:Mint -vvv --broadcast --rpc-url avalancheFuji --sig "run(a -ddress,uint8,address,uint8)" -- 0 0 -``` - -5. Once the CCIP message is finalized on the destination blockchain, you can query the MyNFTs balance of your account, using the `cast call` command. - -![ccip-explorer](./img/ccip-explorer.png) - -For example, to verify that the new MyNFT was minted, type: - -```shell -cast call "balanceOf(address)" --rpc-url ethereumSepolia -``` - -Of course, you can see your newly minted NFT on popular NFT Marketplaces, like OpenSea for instance: - -![opensea](./img/opensea.png) - -6. You can always withdraw tokens for Chainlink CCIP fees from the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) smart contract using the `cast send` command. - -For example, to withdraw tokens previously sent for Chainlink CCIP fees, run: - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "withdraw(address)" -``` - -or - -```shell -cast send --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "withdrawToken(address,address)" 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 -``` - -depending on whether you filled the [`SourceMinter.sol`](./src/cross-chain-nft-minter/SourceMinter.sol) contract with `Native (0)` or `LINK (1)` in step number 3. - -## CCT Standard - -The [Cross-Chain Token (CCT)](https://docs.chain.link/ccip/concepts/cross-chain-token/overview) standard provides a standard for transferring tokens between different blockchains using Chainlink’s Cross-Chain Interoperability Protocol (CCIP). - -By implementing the CCT standard, you can enable seamless cross-chain transfers for your own "custom" ERC20 tokens through CCIP. - -This repository includes examples of CCT-enabled ERC-20 tokens with implementations of: - -1. `getCCIPAdmin()` function in `MockERC20BurnAndMintToken` in [`CCIPv1_5BurnMintPoolFork.t.sol`](./test/fork/CCIPv1_5BurnMintPoolFork.t.sol) file -2. `owner()` function in `MockERC20TokenOwner` in [`CCIPv1_5LockReleasePoolFork.t.sol`](./test/fork/CCIPv1_5LockReleasePoolFork.t.sol) file - -To run CCT examples make sure you have the following environment variables set in your `.env` file: - -```shell -ETHEREUM_SEPOLIA_RPC_URL="" -AVALANCHE_FUJI_RPC_URL="" -``` - -1. To run the ERC-20 CCT example with `getCCIPAdmin()` function and Burn & Mint token pools, run the following command: - -```shell -forge test --mp ./test/fork/CCIPv1_5BurnMintPoolFork.t.sol -``` - -2. To run the ERC-20 CCT example with `owner()` function and Lock & Release token pools, run the following command: - -```shell -forge test --mp ./test/fork/CCIPv1_5LockReleasePoolFork.t.sol -``` +Then follow the book chapters for exact run commands. diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 0000000..beda952 --- /dev/null +++ b/book/book.toml @@ -0,0 +1,8 @@ +[book] +title = "Chainlink CCIP Starter Kit V2 (Foundry)" +language = "en" +multilingual = false +src = "src" + +[build] +build-dir = "book" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 100644 index 0000000..68fe67d --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,28 @@ +# Summary + +# Overview +- [Overview](./index.md) + +# CCIP v2.0 +- [Example 01: Token Transfer + Faster Than Finality](./examples/example01-token-transfer-faster-than-finality.md) +- [Example 02: Hello World to BasicMessageReceiverWithCCVs (Faster Than Finality)](./examples/example02-hello-world-to-basic-message-receiver.md) +- [Example 03: Programmable Token Transfer (Faster Than Finality)](./examples/example03-programmable-token-transfer-data-and-token.md) +- [Example 04: Programmable Token Transfer (Default Finality + Sender Contract)](./examples/example04-programmable-token-transfer-default-finality-sender-contract.md) +- [Example 05: ExtraArgsV3 No-Execution-Tag (Manual Execution Path)](./examples/example05-extraargsv3-no-execution-tag-manual-execution.md) +- [Example 06: CCT Burn and Mint](./examples/example06-cct01-burnmint-token-and-pool-setup.md) +- [Example 07: CCT Lock and Release](./examples/example07-cct02-lockrelease-token-and-pool-setup.md) +- [Example 08: CCT Burn and Mint With AdvancedPoolHooks](./examples/example08-cct03-burnmint-with-advanced-pool-hooks.md) + +# CCIP API +- [Using the CCIP API](./tools/using-the-api.md) + +# CCIP SDK +- [CCIP SDK: `ccip-send` (Data, Token, Token+Data)](./tools/ccip-sdk-ccip-send.md) +- [CCIP SDK: `ccip-track` (Tx Hash, Message ID, Sender)](./tools/ccip-sdk-ccip-track.md) + +# CCIP CLI +- [Using the CCIP CLI](./tools/using-the-cli.md) + +# Legacy Examples +- [Legacy 01: ExtraArgsV1 Token Transfer](./examples/legacy01-extraargsv1-data-message.md) +- [Legacy 02: ExtraArgsV2 Token Transfer (CLI `allowOutOfOrderExecution`)](./examples/legacy02-extraargsv2-data-message.md) diff --git a/book/src/examples/example01-token-transfer-faster-than-finality.md b/book/src/examples/example01-token-transfer-faster-than-finality.md new file mode 100644 index 0000000..36581c0 --- /dev/null +++ b/book/src/examples/example01-token-transfer-faster-than-finality.md @@ -0,0 +1,89 @@ +# Example 01: Token Transfer + Faster Than Finality + +This example sends one token transfer message using `ExtraArgsV3` with Faster Than Finality (`blockConfirmations > 0`). + +Script path: `script/examples/Example01.s.sol` + +## What You Will Do + +1. Mint 1 CCIP-BnM token to your EOA with `script/Faucet.s.sol`. +2. (Optional) Read the executor allowed finality config (`FinalityCodec` `bytes4`) on the source chain. +3. Send that token from source chain to destination chain with `script/examples/Example01.s.sol`. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + + +## Step 1: Get 1 CCIP-BnM Token + +Use the faucet script on the source chain: + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +## Step 2: (Optional) Check Executor Allowed Finality + +Before choosing a Faster Than Finality depth in `ExtraArgsV3`, you can read what finality modes the **Executor** allows. The on-chain API is **`getAllowedFinalityConfig() → bytes4`**, encoded with **`FinalityCodec`** (same family of values as `requestedFinalityConfig` in your message’s ExtraArgs). + +From a shell (replace RPC and address): + +```bash +cast call "getAllowedFinalityConfig()(bytes4)" --rpc-url +``` + +Why this matters: + +- Your requested finality (derived from `blockConfirmations` in `EncodeExtraArgsOffchain` / `Example01` via **`FinalityCodec._encodeBlockDepth`**) must be **permitted** by the executor’s dynamic config; otherwise the send can revert. + +## Step 3: Send Token With Faster Than Finality + +Run `Example01`: + +```bash +forge script script/examples/Example01.s.sol:Example01 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint32,uint16,address)" \ + \ + \ + \ + \ + \ + \ + \ + +``` + +Parameter notes: + +- ``: token amount in token decimals (for 18 decimals, `1e18` is 1 token). +- ``: set to `0` for token-only transfer to an EOA receiver. +- ``: must be `> 0` in this Faster Than Finality example; pick a depth consistent with the executor’s allowed finality from Step 2 and lane policy. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` + +## Verify Result + +`Example01` logs the message ID. + +Use that ID in the CCIP Explorer to monitor status: + +- https://ccip.chain.link diff --git a/book/src/examples/example02-hello-world-to-basic-message-receiver.md b/book/src/examples/example02-hello-world-to-basic-message-receiver.md new file mode 100644 index 0000000..6f0ab90 --- /dev/null +++ b/book/src/examples/example02-hello-world-to-basic-message-receiver.md @@ -0,0 +1,169 @@ +# Example 02: Hello World to BasicMessageReceiverWithCCVs (Faster Than Finality) + +This example deploys a destination `BasicMessageReceiverWithCCVs` contract and sends a Hello World data message from an EOA using Faster Than Finality (`blockConfirmations > 0`). + +Scripts used: + +- `script/examples/Example02.s.sol:DeployBasicMessageReceiverWithCCVs` +- `script/examples/Example02.s.sol:SetBasicMessageReceiverWithCCVsMinBlockDepth` +- `script/examples/Example02.s.sol:Example02` + +## What You Will Do + +1. Deploy `BasicMessageReceiverWithCCVs` on destination chain. +2. Configure minimum block depth for the source chain. +3. Send a Hello World CCIP data message from source chain to `BasicMessageReceiverWithCCVs`. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +## Receiver Contract Context + +### Pre-v2.0 + +To receive CCIP messages (data or data+tokens), contracts implemented `IAny2EVMMessageReceiver`: + +```ts +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +interface IAny2EVMMessageReceiver { + function ccipReceive( + Client.Any2EVMMessage calldata message + ) external; +} +``` + +As a convenience, applications generally inherited `CCIPReceiver.sol` and implemented `_ccipReceive`. + +### CCIP v2.0 + +In v2.0, receivers expose Cross Chain Verifiers and finality requirements via `getCCVsAndFinalityConfig` (encoded with `FinalityCodec` as `bytes4 allowedFinalityConfig`): + +```ts +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Client} from "../libraries/Client.sol"; + +interface IAny2EVMMessageReceiverV2 { + function ccipReceive( + Client.Any2EVMMessage calldata message + ) external; + + function getCCVsAndFinalityConfig( + uint64 sourceChainSelector, + bytes calldata sender + ) + external + view + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ); +} +``` + +The receiver controls two independent dimensions: + +- `requiredCCVs` and `optionalCCVs` define additional verifiers required for acceptance. +- `allowedFinalityConfig` defines which requested finality modes are accepted (see `FinalityCodec` in chainlink-ccip). A zero value (`WAIT_FOR_FINALITY_FLAG`) means only fully finalized messages are accepted. + +You can combine these independently (for example, add verifiers while requiring full finality, or allow faster-than-finality modes when policy allows). + +In this starter kit, `src/BasicMessageReceiverWithCCVs.sol` adds configurable verifier sets and stores a per-chain minimum block depth, which it exposes via `FinalityCodec._encodeBlockDepth` in `getCCVsAndFinalityConfig`. + +## Step 1: Deploy `BasicMessageReceiverWithCCVs` on Destination Chain + +Run: + +```bash +forge script script/examples/Example02.s.sol:DeployBasicMessageReceiverWithCCVs \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +Save the deployed address as ``. + +By default, this receiver starts with `minBlockDepth = 0` (default finality only) for all source chains. + +## Step 2: Configure Minimum Block Depth for Your Source Chain + +Run: + +```bash +forge script script/examples/Example02.s.sol:SetBasicMessageReceiverWithCCVsMinBlockDepth \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,uint16)" \ + \ + \ + +``` + +For this chapter, use ` = 1`. + +- `0` means deafault finality-only behavior. +- `> 0` enables Faster Than Finality with that minimum depth. + +## Step 3: Send Hello World Data Message from EOA + +Run: + +```bash +forge script script/examples/Example02.s.sol:Example02 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,string,uint32,uint16,address)" \ + \ + \ + \ + "Hello, World" \ + \ + \ + +``` + +Parameter notes: + +- `` must be `> 0` because the destination receiver callback needs gas. +- `` must be `> 0` for Faster Than Finality. +- `` should be greater than or equal to ``. +- Executor may enforce a minimum block confirmations value and revert if too low. +- If requested confirmations exceed chain finality, default finality is used. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` + +## Verify Result + +`Example02` logs a CCIP message ID. + +Use that ID in the CCIP Explorer: + +- https://ccip.chain.link + +You can inspect receiver state on destination chain: + +```bash +cast call "latestMessage()(bytes)" --rpc-url ethereumSepolia +``` diff --git a/book/src/examples/example03-programmable-token-transfer-data-and-token.md b/book/src/examples/example03-programmable-token-transfer-data-and-token.md new file mode 100644 index 0000000..939a8df --- /dev/null +++ b/book/src/examples/example03-programmable-token-transfer-data-and-token.md @@ -0,0 +1,123 @@ +# Example 03: Programmable Token Transfer (Faster Than Finality) + +This example sends a programmable token transfer from an EOA on Avalanche Fuji to a receiver that supports Faster Than Finality on Ethereum Sepolia: + +- Data payload: `"Hello, World"` +- Tokens: `CCIP-BnM` + +Script path: `script/examples/Example03.s.sol` + +## What You Will Do + +1. Ensure a destination `BasicMessageReceiverWithCCVs` exists on Sepolia. +2. Mint 1 `CCIP-BnM` on Fuji using `script/Faucet.s.sol`. +3. Send one CCIP message containing both data and tokens. + +## Receiver Compatibility Note + +This chapter uses Faster Than Finality (`blockConfirmations > 0`), so destination receiver should return a non-zero minimum block depth. + +- If your receiver is default-finality-only, this message can fail on destination. +- Deploy `BasicMessageReceiverWithCCVs` before running this chapter. +- For default-finality programmable token transfer (`blockConfirmations = 0`), use Example04. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +> **If you do not have a receiver deployed yet** +> +> Run: +> +> ```bash +> forge script script/examples/Example02.s.sol:DeployBasicMessageReceiverWithCCVs \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address)" \ +> +> ``` +> +> Then configure min block depth for your source chain: +> +> ```bash +> forge script script/examples/Example02.s.sol:SetBasicMessageReceiverWithCCVsMinBlockDepth \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address,uint64,uint16)" \ +> \ +> \ +> +> ``` +> +> For this chapter (which sends Faster Than Finality), use ` > 0`. +> +> The script argument accepts `` (a `uint16` passed to `BasicMessageReceiverWithCCVs.setMinBlockDepth`). On-chain, the receiver does not return that integer directly to CCIP: `getCCVsAndFinalityConfig` sets `allowedFinalityConfig` to `FinalityCodec._encodeBlockDepth(minBlockDepth)` — the same `bytes4` finality encoding CCIP 2.0 uses elsewhere for allowed finality (depth `0` means wait for full/default finality). +> +> Save the receiver address from the `[RESULT]` log and use it in Step 2 below. + +## Step 1: Get 1 CCIP-BnM Token on Fuji + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +## Step 2: Send Hello World + CCIP-BnM + +```bash +forge script script/examples/Example03.s.sol:Example03 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,string,address,uint256,uint32,uint16,address)" \ + \ + \ + \ + "Hello, World" \ + \ + \ + \ + \ + +``` + +Parameter notes: + +- `` uses token decimals (`1e18` is 1 token for 18-decimal tokens). +- `` must be `> 0` because the receiver contract callback handles data. +- `` must be `> 0` for Faster Than Finality. +- `` should be greater than or equal to `` (the depth you stored on the receiver; CCIP compares it against your message’s `requestedFinalityConfig` after both sides use `FinalityCodec` encoding). +- Executor may enforce a minimum block confirmation value and revert if too low. +- If requested confirmations exceed chain finality, default finality is used. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` + +## Verify Result + +`Example03` logs a CCIP message ID. Track it in the CCIP Explorer: + +- https://ccip.chain.link + +Optional receiver checks on Sepolia: + +```bash +cast call "latestMessage()(bytes)" --rpc-url ethereumSepolia +cast call "latestSender()(address)" --rpc-url ethereumSepolia +cast call "latestSourceChainSelector()(uint64)" --rpc-url ethereumSepolia +``` diff --git a/book/src/examples/example04-programmable-token-transfer-default-finality-sender-contract.md b/book/src/examples/example04-programmable-token-transfer-default-finality-sender-contract.md new file mode 100644 index 0000000..0ab0755 --- /dev/null +++ b/book/src/examples/example04-programmable-token-transfer-default-finality-sender-contract.md @@ -0,0 +1,117 @@ +# Example 04: Programmable Token Transfer (Default Finality + Sender Contract) + +This example sends a programmable token transfer from Avalanche Fuji to Ethereum Sepolia using a deployed `BasicMessageSender` contract: + +- Data payload: `"Hello, World"` +- Tokens: `CCIP-BnM` +- Finality mode: default finality (`blockConfirmations = 0`) + +Because this chapter uses default finality, the baseline `BasicMessageReceiver` is sufficient on destination. +If you want the Faster Than Finality variant (`blockConfirmations > 0`), use Example03 with `BasicMessageReceiverWithCCVs`. + +Scripts used: + +- `script/examples/Example04.s.sol:DeployBasicMessageSender` +- `script/examples/Example04.s.sol:Example04` + +## What You Will Do + +1. Deploy `BasicMessageSender` on source chain (Fuji). +2. Ensure destination `BasicMessageReceiver` exists on Sepolia. +3. Mint 1 `CCIP-BnM` on Fuji. +4. Quote fee, fund sender contract with that exact fee amount, then send data+token. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +> **If you do not have a receiver deployed yet** +> +> Run: +> +> ```bash +> forge script script/examples/Example04.s.sol:DeployBasicMessageReceiver \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address)" \ +> +> ``` +> +> Save the receiver address from the `[RESULT]` log and use it in Step 3. + +## Step 1: Deploy `BasicMessageSender` on Fuji + +```bash +forge script script/examples/Example04.s.sol:DeployBasicMessageSender \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address)" \ + \ + +``` + +Save the deployed sender address from the `[RESULT]` log. + +## Step 2: Get 1 CCIP-BnM Token on Fuji + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +## Step 3: Send Hello World + CCIP-BnM Through Sender Contract + +```bash +forge script script/examples/Example04.s.sol:Example04 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,uint64,address,string,address,uint256,uint32)" \ + \ + \ + \ + \ + "Hello, World" \ + \ + \ + +``` + +Parameter notes: + +- This example always uses default finality by setting `blockConfirmations = 0` in the script. +- `` uses token decimals (`1e18` is 1 token for 18-decimal tokens). +- `` must be `> 0` because the receiver contract callback handles data. +- The script first quotes the fee, then funds sender contract with that exact amount before calling `send`. +- If fee conditions change between quote and send, rerun the script to re-quote and retry. + +## Verify Result + +`Example04` logs a CCIP message ID. Track it in the CCIP Explorer: + +- https://ccip.chain.link + +Optional receiver checks on Sepolia: + +```bash +cast call "latestMessage()(bytes)" --rpc-url ethereumSepolia +cast call "latestSender()(address)" --rpc-url ethereumSepolia +cast call "latestSourceChainSelector()(uint64)" --rpc-url ethereumSepolia +``` diff --git a/book/src/examples/example05-extraargsv3-no-execution-tag-manual-execution.md b/book/src/examples/example05-extraargsv3-no-execution-tag-manual-execution.md new file mode 100644 index 0000000..ee3bf4e --- /dev/null +++ b/book/src/examples/example05-extraargsv3-no-execution-tag-manual-execution.md @@ -0,0 +1,140 @@ +# Example 05: ExtraArgsV3 No-Execution-Tag (Manual Execution Path) + +This example sends a Hello World data message from an EOA, but disables automatic execution by setting `executor` to `NO_EXECUTION_ADDRESS` inside `ExtraArgsV3`. + +Script path: `script/examples/Example05.s.sol` + +## What You Will Do + +1. Ensure a compatible destination receiver exists (default finality or Faster Than Finality based on your `blockConfirmations`). +2. Send a CCIP data message with `ExtraArgsV3` no-execution-tag. +3. Observe pending execution in CCIP Explorer and execute manually. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +> **If you do not have a receiver deployed yet** +> +> Choose deployment based on your `blockConfirmations`: +> +> ```bash +> forge script script/examples/Example04.s.sol:DeployBasicMessageReceiver \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address)" \ +> +> ``` +> +> Use this for default finality (`blockConfirmations = 0`). Save this as ``. +> +> ```bash +> forge script script/examples/Example02.s.sol:DeployBasicMessageReceiverWithCCVs \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address)" \ +> +> ``` +> +> If you use Faster Than Finality (`blockConfirmations > 0`), configure min block depth for your source chain: +> +> ```bash +> forge script script/examples/Example02.s.sol:SetBasicMessageReceiverWithCCVsMinBlockDepth \ +> --rpc-url ethereumSepolia \ +> --account myAccount \ +> --broadcast \ +> --sig "run(address,uint64,uint16)" \ +> \ +> \ +> +> ``` +> +> Use ` > 0` for Faster Than Finality. +> +> Use this for Faster Than Finality (`blockConfirmations > 0`). Save this as ``. + +## Understanding ExtraArgsV3 + +The `ExtraArgsV3` is a structured set of delivery/execution options encoded and attached to a CCIP message. + +```solidity +struct GenericExtraArgsV3 { + uint32 gasLimit; + bytes4 requestedFinalityConfig; + address[] ccvs; + bytes[] ccvArgs; + address executor; + bytes executorArgs; + bytes tokenReceiver; + bytes tokenArgs; +} +``` + +(On-chain this struct lives in `ExtraArgsCodec` from `@chainlink/contracts-ccip`.) + +- `gasLimit`: gas allocated for callback execution on destination. If `0` and message data is empty, no callback executes. +- `requestedFinalityConfig`: `bytes4` finality mode + parameters per `FinalityCodec` (not a bare `uint16`). All-zero means wait for default/lane finality. For block-depth style faster-than-finality, encode with `FinalityCodec._encodeBlockDepth(uint16)` (see CCIP / `FinalityCodec` NatSpec). **Example05** and `EncodeExtraArgsOffchain.encodeV3` / `encodeV3Basic` take a `blockConfirmations` argument only as a convenience and set this field to `FinalityCodec._encodeBlockDepth(blockConfirmations)`. +- `ccvs`: list of cross-chain verifier addresses. Empty means default verifiers. +- `ccvArgs`: optional arguments for each CCV. Must match `ccvs` length. +- `executor`: executor address on source chain. `address(0)` uses default executor. +- `executorArgs`: chain/executor-specific args (format depends on chain family/executor implementation). +- `tokenReceiver`: encoded destination token receiver. If empty, receiver address is used. +- `tokenArgs`: extra token transfer args (pool-specific format). + +Related helpers in `script/EncodeExtraArgsOffchain.s.sol`: + +- `encodeV3Basic(gasLimit, blockConfirmations)` for a minimal V3 payload. +- `getNoExecutionAddress()` returns `Client.NO_EXECUTION_ADDRESS` for manual execution path. + +## Step 1: Send Message With No-Execution-Tag + +```bash +forge script script/examples/Example05.s.sol:Example05 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,string,uint32,uint16,address)" \ + \ + \ + \ + "Hello, World" \ + \ + \ + +``` + +Parameter notes: + +- `` must be `> 0` because the receiver callback needs gas. +- `` can be `0` (default finality) or `> 0`. +- If ` > 0`, destination receiver must accept Faster Than Finality (`minBlockDepth > 0`) for this source chain. +- If ` = 0`, a default-finality receiver is sufficient. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` +- This example sets executor to `NO_EXECUTION_ADDRESS`, so execution is not automatic. + +## Verify Result + +`Example05` logs a CCIP message ID. + +Use that ID in CCIP Explorer: + +- https://ccip.chain.link + +Expected behavior: + +- Message is delivered to a pending/manual execution state because automatic execution is disabled. + +![ccip-explorer]() diff --git a/book/src/examples/example06-cct01-burnmint-token-and-pool-setup.md b/book/src/examples/example06-cct01-burnmint-token-and-pool-setup.md new file mode 100644 index 0000000..db5e0e8 --- /dev/null +++ b/book/src/examples/example06-cct01-burnmint-token-and-pool-setup.md @@ -0,0 +1,183 @@ +# Example 06: CCT Burn and Mint + +This example covers the full BurnMint CCT flow on Fuji -> Sepolia: + +1. Deploy [`CrossChainToken`](https://github.com/smartcontractkit/chainlink-ccip/blob/develop/chains/evm/contracts/tokens/CrossChainToken.sol) + BurnMint pool on both chains. +2. Configure pools to trust each other. +3. Send a token transfer across the lane. +4. Verify BurnMint behavior (burn on source, mint on destination). + +Scripts used: + +- `script/examples/Example06.s.sol:DeployCCTBurnMintTokenAndPool` +- `script/examples/Example06.s.sol:Example06` +- `script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality` + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +## Script Defaults + +`DeployCCTBurnMintTokenAndPool` deploys a **CrossChainToken** (via `BaseERC20.ConstructorParams`) with: + +- `name`: `TestToken` +- `symbol`: `TEST` +- `decimals`: `18` +- `preMint`: `1_000_000 * 1e18` +- `maxSupply`: `100_000_000 * 1e18` +- `preMintRecipient`, `ccipAdmin`, burn/mint role admin, and AccessControl owner: the broadcasting EOA + +Token admin registration uses `RegistryModuleOwnerCustom.registerAdminViaGetCCIPAdmin` (CrossChainToken exposes `getCCIPAdmin()` from `BaseERC20`, not `owner()`). + +The deployment also uses: + +- BurnMint pool (no advanced pool hook in this example) +- disabled rate limiter config in chain updates + +> **Important: Optimizer is required for CCT pool deployment** +> +> `BurnMintTokenPool` can exceed EVM max code size if compiled without optimizer. +> This repo includes a dedicated CCT profile in `foundry.toml`: +> +> ```toml +> [profile.cct] +> optimizer = true +> optimizer_runs = 200 +> ``` +> +> For this chapter, use `FOUNDRY_PROFILE=cct` in all commands. + +## Step 1: Deploy Token + Pool on Fuji + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:DeployCCTBurnMintTokenAndPool \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address)" \ + \ + \ + \ + +``` + +Save from logs: + +- `` +- `` + +## Step 2: Deploy Token + Pool on Sepolia + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:DeployCCTBurnMintTokenAndPool \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address)" \ + \ + \ + \ + +``` + +Save from logs: + +- `` +- `` + +## Step 3: Configure Fuji Pool With Sepolia Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:Example06 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 4: Configure Sepolia Pool With Fuji Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:Example06 \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 5: Send BurnMint Transfer (ExtraArgsV3 + Default Finality) + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint32,address)" \ + \ + \ + \ + \ + \ + \ + +``` + +Parameter notes: + +- `` uses token decimals (`1e18` is 1 token for 18-decimal token). +- For token-only transfer to EOA receiver, use `` = `0`. +- This sender uses `ExtraArgsV3` with `blockConfirmations = 0` (default finality). +- ``: + - native fee: `0x0000000000000000000000000000000000000000` + - LINK fee: LINK token address on Fuji + +## Step 6: Verify BurnMint Behavior + +Verify token->pool registration: + +```bash +cast call "getPool(address)(address)" --rpc-url avalancheFuji +cast call "getPool(address)(address)" --rpc-url ethereumSepolia +``` + +Verify remote lane mapping: + +```bash +cast call "getRemoteToken(uint64)(bytes)" --rpc-url avalancheFuji +cast call "getRemotePools(uint64)(bytes[])" --rpc-url avalancheFuji +cast call "getRemoteToken(uint64)(bytes)" --rpc-url ethereumSepolia +cast call "getRemotePools(uint64)(bytes[])" --rpc-url ethereumSepolia +``` + +Verify balances and supplies after transfer finalizes: + +```bash +cast call "balanceOf(address)(uint256)" --rpc-url avalancheFuji +cast call "balanceOf(address)(uint256)" --rpc-url ethereumSepolia +cast call "totalSupply()(uint256)" --rpc-url avalancheFuji +cast call "totalSupply()(uint256)" --rpc-url ethereumSepolia +``` + +Monitor message status with the message ID on: + +- https://ccip.chain.link diff --git a/book/src/examples/example07-cct02-lockrelease-token-and-pool-setup.md b/book/src/examples/example07-cct02-lockrelease-token-and-pool-setup.md new file mode 100644 index 0000000..30717e4 --- /dev/null +++ b/book/src/examples/example07-cct02-lockrelease-token-and-pool-setup.md @@ -0,0 +1,202 @@ +# Example 07: CCT Lock and Release + +This example covers the full LockRelease CCT flow on Fuji -> Sepolia: + +1. Deploy token + lock box + LockRelease pool on both chains. +2. Configure pools to trust each other. +3. Fund destination lock box liquidity. +4. Send a token transfer across the lane. +5. Verify lock/release behavior. + +Scripts used: + +- `script/examples/Example07.s.sol:DeployCCTLockReleaseTokenAndPool` +- `script/examples/Example07.s.sol:Example07` +- `script/examples/Example07.s.sol:FundCCTLockBoxLiquidity` +- `script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality` (reused for the final transfer step) + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +## Script Defaults + +`DeployCCTLockReleaseTokenAndPool` deploys a **CrossChainToken** (same defaults as Example 06) for the underlying ERC-20, with: + +- `name`: `TestToken` +- `symbol`: `TEST` +- `decimals`: `18` +- `preMint`: `1_000_000 * 1e18` +- `maxSupply`: `100_000_000 * 1e18` + +Admin registration uses `registerAdminViaGetCCIPAdmin` on the token (see Example 06). + +The deployment also uses: + +- LockRelease pool +- `ERC20LockBox` bound to the local token +- disabled rate limiter config in chain updates + +> **Important: Optimizer is required for CCT pool deployment** +> +> This repo includes a dedicated CCT profile in `foundry.toml`: +> +> ```toml +> [profile.cct] +> optimizer = true +> optimizer_runs = 200 +> ``` +> +> For this chapter, use `FOUNDRY_PROFILE=cct` in all commands. + +## Step 1: Deploy Token + Lock Box + Pool on Fuji + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example07.s.sol:DeployCCTLockReleaseTokenAndPool \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address)" \ + \ + \ + \ + +``` + +Save from logs: + +- `` +- `` +- `` + +## Step 2: Deploy Token + Lock Box + Pool on Sepolia + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example07.s.sol:DeployCCTLockReleaseTokenAndPool \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address)" \ + \ + \ + \ + +``` + +Save from logs: + +- `` +- `` +- `` + +## Step 3: Configure Fuji Pool With Sepolia Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example07.s.sol:Example07 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 4: Configure Sepolia Pool With Fuji Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example07.s.sol:Example07 \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 5: Fund Destination Lock Box Liquidity + +For Fuji -> Sepolia transfers, fund the Sepolia lock box first: + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example07.s.sol:FundCCTLockBoxLiquidity \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,uint256)" \ + \ + \ + +``` + +If you also want Sepolia -> Fuji transfers, fund Fuji lock box too with the same script. + +## Step 6: Send LockRelease Transfer (Reuse Example06 CCT Sender) + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint32,address)" \ + \ + \ + \ + \ + \ + 0 \ + +``` + +Parameter notes: + +- `` uses token decimals (`1e18` is 1 token for 18-decimal token). +- `gasLimit` is set to `0` in this command because this is token-only transfer to an EOA receiver. +- This sender uses `ExtraArgsV3` with `blockConfirmations = 0` (default finality). +- ``: + - native fee: `0x0000000000000000000000000000000000000000` + - LINK fee: LINK token address on Fuji + +## Step 7: Verify LockRelease Behavior + +Verify token->pool registration: + +```bash +cast call "getPool(address)(address)" --rpc-url avalancheFuji +cast call "getPool(address)(address)" --rpc-url ethereumSepolia +``` + +Verify remote lane mapping: + +```bash +cast call "getRemoteToken(uint64)(bytes)" --rpc-url avalancheFuji +cast call "getRemotePools(uint64)(bytes[])" --rpc-url avalancheFuji +cast call "getRemoteToken(uint64)(bytes)" --rpc-url ethereumSepolia +cast call "getRemotePools(uint64)(bytes[])" --rpc-url ethereumSepolia +``` + +Verify lock box balances and receiver balance after transfer finalizes: + +```bash +cast call "balanceOf(address)(uint256)" --rpc-url avalancheFuji +cast call "balanceOf(address)(uint256)" --rpc-url ethereumSepolia +cast call "balanceOf(address)(uint256)" --rpc-url ethereumSepolia +``` + +Monitor message status with the message ID on: + +- https://ccip.chain.link diff --git a/book/src/examples/example08-cct03-burnmint-with-advanced-pool-hooks.md b/book/src/examples/example08-cct03-burnmint-with-advanced-pool-hooks.md new file mode 100644 index 0000000..06009bc --- /dev/null +++ b/book/src/examples/example08-cct03-burnmint-with-advanced-pool-hooks.md @@ -0,0 +1,229 @@ +# Example 08: CCT Burn and Mint With AdvancedPoolHooks + +This example covers BurnMint CCT on Fuji -> Sepolia with `AdvancedPoolHooks` attached to the pool. + +## Token Pool Hooks Development + +Token pool hooks are standalone contracts used by token pools for additional checks and behavior. + +At a high level: + +- pool calls hook `preflightCheck(...)` on source-side lock/burn +- pool calls hook `postflightCheck(...)` on destination-side release/mint +- hook can enforce allowlists and CCV requirements +- hook can optionally call a policy engine + +For custom development, the pool-level integration point is `IAdvancedPoolHooks` and the pool constructor receives +the hook address. + +## Chainlink ACE Context + +`AdvancedPoolHooks` includes optional policy engine integration through `IPolicyEngine`. +In this example, policy engine is intentionally disabled (`address(0)`) to focus on hook attachment and allowlist flow. + +When building policy-engine-driven flows, Chainlink ACE provides the policy engine interfaces and implementation model. + +This flow still demonstrates the correct hook wiring pattern for ACE-enabled setups. + +## What This Example Covers + +1. Deploy CrossChainToken + `AdvancedPoolHooks` + BurnMint pool on both chains. +2. Configure pools to trust each other. +3. Send token transfer with the existing ExtraArgsV3 default-finality sender. +4. Verify hook attachment and allowlist settings. + +Scripts used: + +- `script/examples/Example08.s.sol:DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHook` +- `script/examples/Example08.s.sol:Example08` +- `script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality` + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +## Script Defaults + +`DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHook` deploys **CrossChainToken** with the same token defaults as Example 06 (`BaseERC20.ConstructorParams` + `registerAdminViaGetCCIPAdmin`): + +- `name`: `TestToken` +- `symbol`: `TEST` +- `decimals`: `18` +- `preMint`: `1_000_000 * 1e18` +- `maxSupply`: `100_000_000 * 1e18` + +Hook deployment defaults in this example: + +- allowlist enabled and seeded with broadcaster EOA +- `thresholdAmountForAdditionalCCVs` is provided via CLI (use `1` in this tutorial) +- `policyEngine = address(0)` (disabled) +- hook is configured to authorize the deployed pool as caller + +> **Important: Optimizer is required for CCT pool deployment** +> +> This repo includes a dedicated CCT profile in `foundry.toml`: +> +> ```toml +> [profile.cct] +> optimizer = true +> optimizer_runs = 200 +> ``` +> +> For this chapter, use `FOUNDRY_PROFILE=cct` in all commands. + +## Step 1: Deploy Token + Hook + Pool on Fuji + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHook \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address,uint256)" \ + \ + \ + \ + \ + 1 +``` + +Save from logs: + +- `` +- `` +- `` + +## Step 2: Deploy Token + Hook + Pool on Sepolia + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHook \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,address,address,address,uint256)" \ + \ + \ + \ + \ + 1 +``` + +Save from logs: + +- `` +- `` +- `` + +## Step 3: Configure Fuji Pool With Sepolia Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:Example08 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 4: Configure Sepolia Pool With Fuji Remote + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:Example08 \ + --rpc-url ethereumSepolia \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address)" \ + \ + \ + \ + +``` + +## Step 5: Send Transfer (Reuse Example06 Sender) + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example06.s.sol:SendCCTTokenWithExtraArgsV3DefaultFinality \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint32,address)" \ + \ + \ + \ + \ + \ + 0 \ + +``` + +## Optional: Update Hook Allowlist + +Use this if you want to add or remove senders in the allowlist after deployment. + +Add one sender address: + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:Example08UpdateAllowlist \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address[],address[])" \ + \ + "[]" \ + "[]" +``` + +Remove one sender address: + +```bash +FOUNDRY_PROFILE=cct forge script script/examples/Example08.s.sol:Example08UpdateAllowlist \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,address[],address[])" \ + \ + "[]" \ + "[]" +``` + +## Step 6: Verify Hook and Pool State + +Verify token->pool registration: + +```bash +cast call "getPool(address)(address)" --rpc-url avalancheFuji +cast call "getPool(address)(address)" --rpc-url ethereumSepolia +``` + +Verify attached hook on each pool: + +```bash +cast call "getAdvancedPoolHooks()(address)" --rpc-url avalancheFuji +cast call "getAdvancedPoolHooks()(address)" --rpc-url ethereumSepolia +``` + +Verify allowlist status: + +```bash +cast call "getAllowListEnabled()(bool)" --rpc-url avalancheFuji +cast call "getAllowList()(address[])" --rpc-url avalancheFuji +cast call "getAllowListEnabled()(bool)" --rpc-url ethereumSepolia +cast call "getAllowList()(address[])" --rpc-url ethereumSepolia +``` + +Monitor message status with the message ID on: + +- https://ccip.chain.link diff --git a/book/src/examples/legacy01-extraargsv1-data-message.md b/book/src/examples/legacy01-extraargsv1-data-message.md new file mode 100644 index 0000000..48633c6 --- /dev/null +++ b/book/src/examples/legacy01-extraargsv1-data-message.md @@ -0,0 +1,69 @@ +# Legacy 01: ExtraArgsV1 Token Transfer + +This example sends a token transfer using `ExtraArgsV1` from an EOA. + +Script path: `script/examples/Legacy01.s.sol` + +## What You Will Do + +1. Mint test token(s) to your EOA on the source chain. +2. Send token(s) using `ExtraArgsV1`. +3. Observe the message on CCIP Explorer. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +You do not need to deploy a receiver contract for this token-only example. +Use a destination EOA as the receiver and pass `GAS_LIMIT = 0`. + +## Step 1: Get 1 CCIP-BnM Token on Source Chain + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +## Step 2: Send Token With ExtraArgsV1 + +```bash +forge script script/examples/Legacy01.s.sol:Legacy01 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint256,address)" \ + \ + \ + \ + \ + \ + \ + +``` + +Parameter notes: + +- ``: token amount in token decimals (for 18 decimals, `1e18` is 1 token). +- ``: set `0` for token-only transfer to an EOA receiver. Use `> 0` for contract callback. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` + +## Verify Result + +Use the logged message ID in: + +- https://ccip.chain.link diff --git a/book/src/examples/legacy02-extraargsv2-data-message.md b/book/src/examples/legacy02-extraargsv2-data-message.md new file mode 100644 index 0000000..7fbd30f --- /dev/null +++ b/book/src/examples/legacy02-extraargsv2-data-message.md @@ -0,0 +1,74 @@ +# Legacy 02: ExtraArgsV2 Token Transfer (CLI `allowOutOfOrderExecution`) + +This example sends a token transfer using `ExtraArgsV2` from an EOA. + +Script path: `script/examples/Legacy02.s.sol` + +You can toggle `allowOutOfOrderExecution` via CLI arg to cover both scenarios in one script. + +## What You Will Do + +1. Mint test token(s) to your EOA on the source chain. +2. Send token(s) using `ExtraArgsV2`. +3. Toggle out-of-order execution with a boolean CLI value. +4. Observe the message on CCIP Explorer. + +## Before You Start + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> This chapter assumes `--account myAccount`. + +You do not need to deploy a receiver contract for this token-only example. +Use a destination EOA as the receiver and pass `GAS_LIMIT = 0`. + +## Step 1: Get 1 CCIP-BnM Token on Source Chain + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +## Step 2: Send Token With ExtraArgsV2 + +```bash +forge script script/examples/Legacy02.s.sol:Legacy02 \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address,uint64,address,address,uint256,uint256,bool,address)" \ + \ + \ + \ + \ + \ + \ + \ + +``` + +Parameter notes: + +- ``: token amount in token decimals (for 18 decimals, `1e18` is 1 token). +- ``: set `0` for token-only transfer to an EOA receiver. Use `> 0` for contract callback. +- ``: `true` or `false`. +- ``: Pass the LINK token address on the source chain here. If you want to pay for CCIP fees in native coin instead, pass `0x0000000000000000000000000000000000000000` + +## Verify Result + +Use the logged message ID in: + +- https://ccip.chain.link diff --git a/book/src/index.md b/book/src/index.md new file mode 100644 index 0000000..bce4b0a --- /dev/null +++ b/book/src/index.md @@ -0,0 +1,38 @@ +# Overview + +This book showcases practical usage of CCIP v2.0 in this Foundry starter kit. + +The goal is not to re-teach CCIP fundamentals. It is to provide runnable example flows you can execute and adapt. + +## Before You Run Any Example + +> **Important: Keystore first** +> +> Use a local keystore account for script execution: +> +> ```bash +> cast wallet import myAccount --interactive +> Enter private key: +> Enter password: +> `myAccount` keystore was saved successfully. Address: +> ``` +> +> Examples in this book assume `--account myAccount`. + +### RPC URLs + +Set RPC URLs in a local `.env` file (do not commit secrets): + +```bash +cp .env.example .env +``` + +Populate RPC URL variables only for the chains you actually plan to run. + +In `foundry.toml`, each chain alias points to an env var (for example `avalancheFuji = "${AVALANCHE_FUJI_RPC_URL}"`). + +If a variable is missing in `.env`, commands using the corresponding alias will fail. + +## Chain Configuration + +Always pull the latest values from the [CCIP Directory](https://docs.chain.link/ccip/directory/) before running examples. diff --git a/book/src/tools/ccip-sdk-ccip-send.md b/book/src/tools/ccip-sdk-ccip-send.md new file mode 100644 index 0000000..5b8157b --- /dev/null +++ b/book/src/tools/ccip-sdk-ccip-send.md @@ -0,0 +1,136 @@ +# CCIP SDK: `ccip-send` (Data, Token, Token+Data) + +> **CCIP SDK package** +> +> Install from NPM: `npm install @chainlink/ccip-sdk` +> +> NPM: [https://www.npmjs.com/package/@chainlink/ccip-sdk](https://www.npmjs.com/package/@chainlink/ccip-sdk) +> +> Docs: [https://docs.chain.link/ccip/tools/sdk](https://docs.chain.link/ccip/tools/sdk) + +This chapter shows how to run the SDK script for the three message shapes used in CCIP v2 examples: + +1. data-only +2. token-only +3. token+data + +The script lives in `sdk-examples/src/ccip-send.ts`. + +## Receiver Compatibility for Faster Than Finality + +When using `--block-confirmations > 0` (Faster Than Finality), receiver compatibility matters for message modes that execute receiver callbacks. + +| Mode | `--block-confirmations 0` | `--block-confirmations > 0` | +|---|---|---| +| `data` | baseline receiver works | destination receiver must allow the requested finality (see `getCCVsAndFinalityConfig` / `allowedFinalityConfig`) | +| `token-data` | baseline receiver works | destination receiver must allow the requested finality (see `getCCVsAndFinalityConfig` / `allowedFinalityConfig`) | +| `token` with `--gas-limit 0` | callback not executed (EOA-style token receive path) | callback not executed (EOA-style token receive path) | + +Terminology note: + +- Sender side (`ExtraArgsV3`): requested finality (e.g. `FinalityCodec` / block depth in `requestedFinalityConfig`) +- Receiver side (`getCCVsAndFinalityConfig`): `allowedFinalityConfig` (`bytes4`, `FinalityCodec`) + +## How This Script Uses the SDK + +At a high level, the script builds `EVMChain` instances for source and destination, asks the SDK for a fee quote, then submits the message via the SDK send call. + +```ts +import { EVMChain } from '@chainlink/ccip-sdk' + +const source = await EVMChain.fromUrl(sourceRpcUrl) +const dest = await EVMChain.fromUrl(destRpcUrl) + +const fee = await source.getFee({ router, destChainSelector: dest.network.chainSelector, message }) +await source.sendMessage({ router, destChainSelector: dest.network.chainSelector, message: { ...message, fee }, wallet }) +``` + +## Prerequisites + +From repo root: + +```bash +npm --prefix sdk-examples install +``` + +For actual sends (non-`--dry-run`), set `USER_KEY` (or `PRIVATE_KEY`) in the root `.env`. + +## RPC Configuration + +`ccip-send` accepts RPC URLs either: + +- directly via flags (`--source-rpc-url`, `--dest-rpc-url`) +- or from `.env`: + - `CCIP_SOURCE_RPC_URL`, `CCIP_DEST_RPC_URL` + - fallback: `AVALANCHE_FUJI_RPC_URL`, `ETHEREUM_SEPOLIA_RPC_URL` + + +## 1) Data-Only + +Faster Than Finality (`blockConfirmations=1`): + +```bash +npm --prefix sdk-examples run ccip-send -- \ + --mode data \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --dest-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \ + --router \ + --receiver \ + --block-confirmations 1 +``` + +Default finality (`blockConfirmations=0`): + +```bash +npm --prefix sdk-examples run ccip-send -- \ + --mode data \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --dest-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \ + --router \ + --receiver \ + --block-confirmations 0 +``` + +## 2) Token-Only + +```bash +npm --prefix sdk-examples run ccip-send -- \ + --mode token \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --dest-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \ + --router \ + --receiver \ + --token \ + --amount 1 \ + --block-confirmations 1 +``` + +## 3) Token + Data + +```bash +npm --prefix sdk-examples run ccip-send -- \ + --mode token-data \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --dest-rpc-url "$ETHEREUM_SEPOLIA_RPC_URL" \ + --router \ + --receiver \ + --token \ + --amount 1 \ + --data "hello + token" \ + --block-confirmations 1 +``` + +## If You Need Testnet Tokens + +You can mint one CCIP-BnM token to your sender with the existing Foundry faucet script: + +```bash +forge script script/Faucet.s.sol:Faucet \ + --rpc-url avalancheFuji \ + --account myAccount \ + --broadcast \ + --sig "run(address)" \ + +``` + +Repeat as needed to accumulate test tokens. diff --git a/book/src/tools/ccip-sdk-ccip-track.md b/book/src/tools/ccip-sdk-ccip-track.md new file mode 100644 index 0000000..daece44 --- /dev/null +++ b/book/src/tools/ccip-sdk-ccip-track.md @@ -0,0 +1,84 @@ +# CCIP SDK: `ccip-track` (Tx Hash, Message ID, Sender) + +> **CCIP SDK package** +> +> Install from NPM: `npm install @chainlink/ccip-sdk` +> +> NPM: [https://www.npmjs.com/package/@chainlink/ccip-sdk](https://www.npmjs.com/package/@chainlink/ccip-sdk) +> +> Docs: [https://docs.chain.link/ccip/tools/sdk](https://docs.chain.link/ccip/tools/sdk) + +This chapter shows how to track CCIP v2 messages from source-chain activity using the SDK script: + +- `sdk-examples/src/ccip-track.ts` + +## How This Script Uses the SDK + +The tracking script uses the SDK in three paths: fetch messages from a source tx hash, fetch by message ID, and stream sender history from source-chain logs. + +```ts +import { EVMChain, getMessagesForSender } from '@chainlink/ccip-sdk' + +const source = await EVMChain.fromUrl(sourceRpcUrl) +const byTx = await source.getMessagesInTx(sourceTxHash) +const byId = await source.getMessageById(messageId) + +for await (const msg of getMessagesForSender(source, sender, { startBlock })) { + console.log(msg.message.messageId) +} +``` + +## Prerequisites + +From repo root: + +```bash +npm --prefix sdk-examples install +``` + +## RPC Configuration + +`ccip-track` reads source RPC URL from: + +- `--source-rpc-url ` flag, or +- `.env`: + - `CCIP_SOURCE_RPC_URL` + - fallback: `AVALANCHE_FUJI_RPC_URL` + +```bash +source .env +``` + +## 1) Track by Source Transaction Hash + +Use a source tx hash from your Foundry run (for example `Example01`-`Example08`): + +```bash +npm --prefix sdk-examples run ccip-track -- \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --tx-hash +``` + +## 2) Track by Message ID + +```bash +npm --prefix sdk-examples run ccip-track -- \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --message-id +``` + +Output prints the same concise SDK summary for that message ID. + +## 3) Track by Sender + +```bash +npm --prefix sdk-examples run ccip-track -- \ + --source-rpc-url "$AVALANCHE_FUJI_RPC_URL" \ + --limit 10 \ + --sender +``` + +Arguments: + +- `--limit`: max rows to print (default `10`) +- `--start-block`: first source block to scan (default `0`) diff --git a/book/src/tools/using-the-api.md b/book/src/tools/using-the-api.md new file mode 100644 index 0000000..1e1497e --- /dev/null +++ b/book/src/tools/using-the-api.md @@ -0,0 +1,111 @@ +# Using the CCIP API + +> CCIP API is available at: +> +> [https://api.ccip.chain.link/](https://api.ccip.chain.link/) +> +> Documentation at: +> +> [https://docs.chain.link/ccip/tools/api/](https://docs.chain.link/ccip/tools/api/) + +With the CCIP REST API you can: + +- fetch one message by `messageId` +- search your messages by sender, chain selectors, or source tx hash +- paginate through large result sets with `cursor` +- filter messages that are ready for manual execution +- read lane latency (`totalMs`) to estimate route timing + +--- + +## Set your variables once + +Paste this once per terminal session, replace with your values, then run the commands below. +Use the same EOA and `messageId` you got from your Foundry examples. + +```bash +export MY_EOA="0xYourEoaAddressHere" +export MESSAGE_ID="0xYourMessageIdFromExample01Logs" # optional +``` + +--- + +## Get one message by ID + +After [Example 01](../examples/example01-token-transfer-faster-than-finality.md) (or any send), the script logs a message ID. Fetch its status and full details: + +```bash +curl -s -X 'GET' \ + "https://api.ccip.chain.link/v2/messages/${MESSAGE_ID}" \ + -H 'accept: application/json' | jq +``` + +Or open in browser: + +```text +https://api.ccip.chain.link/v2/messages/ +``` + +You get `status`, source/dest network, `sendTransactionHash`, `receiptTransactionHash`, token amounts, fees. If you didn’t set `MESSAGE_ID`, set it to a real ID from your logs and run again. + +--- + +## List your messages (search, paginate, filter) + +After you run a few examples, this is the fastest way to inspect only your own CCIP traffic. + +**Last 10 messages:** + +```bash +curl -s -X 'GET' \ + "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&limit=10" \ + -H 'accept: application/json' | jq +``` + +The response has `data` (array of message summaries) and `pagination` with `limit`, `hasNextPage`, and `cursor`. Each item has a `messageId`; use the “Get one message” curl above with that ID to see full details and `status`. + +**Next page:** copy the `cursor` value from the response, then run: + +```bash +export CURSOR="paste_the_cursor_value_here" +``` +```bash +curl -s -X 'GET' \ + "https://api.ccip.chain.link/v2/messages?cursor=${CURSOR}&limit=10" \ + -H 'accept: application/json' | jq +``` + +Don’t mix `cursor` with other query params (sender, chain selectors, etc.). + +**Filter by:** + +- **Ready for manual execution** (e.g. from [Example 05](../examples/example05-extraargsv3-no-execution-tag-manual-execution.md)): add `&readyForManualExecOnly=true` +- **Source chain** or **destination chain**: add `&sourceChainSelector=...` or `&destChainSelector=...` (values from [CCIP Directory](https://docs.chain.link/ccip/directory)) + +Examples: + +```bash +# Only messages ready for manual execution +curl -s -X 'GET' \ + "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&readyForManualExecOnly=true&limit=10" \ + -H 'accept: application/json' | jq + +# Only messages from a given source chain (selector from CCIP Directory) +curl -s -X 'GET' \ + "https://api.ccip.chain.link/v2/messages?sender=${MY_EOA}&sourceChainSelector=14767482510784806043&limit=10" \ + -H 'accept: application/json' | jq +``` + +--- + +## Lane latency + +To show “about X minutes” for a route, call the latency endpoint with source and destination chain selectors ([CCIP Directory](https://docs.chain.link/ccip/directory)). This one needs no variables—paste and run: + +```bash +curl -s -X 'GET' \ + 'https://api.ccip.chain.link/v2/lanes/latency?sourceChainSelector=14767482510784806043&destChainSelector=16015286601757825753' \ + -H 'accept: application/json' | jq +``` + +Response includes `totalMs` (p90 in milliseconds). Swap the selectors for the same chains you use in your examples. diff --git a/book/src/tools/using-the-cli.md b/book/src/tools/using-the-cli.md new file mode 100644 index 0000000..ee9cc8a --- /dev/null +++ b/book/src/tools/using-the-cli.md @@ -0,0 +1,127 @@ +# Using the CCIP CLI + +> **CCIP CLI package** +> +> Install globally from NPM: `npm install -g @chainlink/ccip-cli` +> +> NPM: [https://www.npmjs.com/package/@chainlink/ccip-cli](https://www.npmjs.com/package/@chainlink/ccip-cli) +> +> Docs: [https://docs.chain.link/ccip/tools/cli](https://docs.chain.link/ccip/tools/cli) + +This chapter covers: + +1. CLI install +2. observability commands after Foundry/SDK sends +3. send parity commands for data-only, token-only, and token+data + +## Install and Verify + +```bash +npm install -g @chainlink/ccip-cli +``` + +```bash +ccip-cli --version +ccip-cli --help +``` + +## Observability + +### 1) Inspect by source transaction hash + +```bash +ccip-cli show \ + --rpcs \ + --rpcs +``` + +`` is the source-chain transaction hash. +`` and `` are RPC endpoints for the source and destination chains. + +Wait for delivery from the same command: + +```bash +ccip-cli show \ + --rpcs \ + --rpcs \ + --wait +``` + +`--wait` keeps polling until destination execution status is available. + +### 2) Inspect by `messageId` + +`tx-hash` positional becomes `messageId` when `--id-from-source` is used: + +```bash +ccip-cli show \ + --id-from-source \ + --rpcs \ + --rpcs +``` + +`` is the CCIP message ID. +`` is the source CCIP chain selector (example: Avalanche Fuji `14767482510784806043`). + +## Send Parity (CLI) + +### TO DO: Adapt these examples to use ExtraArgsV3 + +### 1) Data-only + +```bash +ccip-cli send -s -d -r \ + --rpcs \ + --rpcs \ + --wallet \ + --receiver \ + --data "hello from ccip-cli" \ + --gas-limit 200000 \ + --wait +``` + +`` and `` can be chain ID, chain selector, or chain name. +Chain ID examples: Avalanche Fuji `43113`, Ethereum Sepolia `11155111`. +`` is the source-chain router for the selected lane. + +### 2) Token-only + +```bash +ccip-cli send -s -d -r \ + --rpcs \ + --rpcs \ + --wallet \ + --receiver \ + --transfer-tokens = \ + --gas-limit 0 \ + --wait +``` + +`=` uses `token=amount` format. +`` is human-readable; CLI converts it using token decimals. + +### 3) Token + data + +```bash +ccip-cli send -s -d -r \ + --rpcs \ + --rpcs \ + --wallet \ + --receiver \ + --data "hello + token" \ + --transfer-tokens = \ + --gas-limit 200000 \ + --wait +``` + +Same chain/router placeholders as above; `--transfer-tokens` uses the same `token=amount` format. + +## Token Amount Format + +`--transfer-tokens` currently uses `token=amount` format. +Amount is human-readable and CLI converts it using token decimals. + +Examples: + +- `--transfer-tokens =1` means 1 whole token. +- `--transfer-tokens =1.5` means 1.5 tokens. diff --git a/foundry.lock b/foundry.lock index 229389c..bf52d4c 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,29 +1,26 @@ { + "lib/chainlink-ace": { + "tag": { + "name": "v1.0.0", + "rev": "288c393af4da077b49e5138ac47965aed9ca4b3a" + } + }, "lib/chainlink-ccip": { "tag": { - "name": "contracts-ccip-v1.6.2", - "rev": "0e3e0fc5c0f70f0d50dca66b139142ddf3009294" + "name": "contracts-ccip-v2.0.0-beta.1", + "rev": "c6c1ef8cb300b3c08b574c2053c82e4faf46f89c" } }, "lib/chainlink-evm": { - "rev": "86aa5a1d34b20eda8d18fe6eb0e4882948e545ba" - }, - "lib/chainlink-local": { "tag": { - "name": "v0.2.7-beta.0", - "rev": "126337155f40ac9eb53f1f3483d104fd18caf380" + "name": "contracts-v1.5.0", + "rev": "86aa5a1d34b20eda8d18fe6eb0e4882948e545ba" } }, "lib/forge-std": { - "rev": "9b49a72cfdb36bcf195eb863f868f01a6d6d3186" - }, - "lib/openzeppelin-contracts": { - "rev": "96b95592c300a1b38953b276f0b6c77017f5c942" - }, - "lib/openzeppelin-contracts-4.7.3": { "tag": { - "name": "v4.7.3", - "rev": "ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d" + "name": "v1.14.0", + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" } }, "lib/openzeppelin-contracts-4.8.3": { @@ -32,22 +29,10 @@ "rev": "0a25c1940ca220686588c4af3ec526f725fe2582" } }, - "lib/openzeppelin-contracts-4.9.6": { - "tag": { - "name": "v4.9.6", - "rev": "dc44c9f1a4c3b10af99492eed84f83ed244203f6" - } - }, - "lib/openzeppelin-contracts-5.0.2": { - "tag": { - "name": "v5.0.2", - "rev": "dbb6104ce834628e473d2173bbc9d47f81a9eec3" - } - }, - "lib/openzeppelin-contracts-5.1.0": { + "lib/openzeppelin-contracts-5.3.0": { "tag": { - "name": "v5.1.0", - "rev": "69c8def5f222ff96f2b5beff05dfba996368aa79" + "name": "v5.3.0", + "rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079" } } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index cdf1e8b..c9edf3b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,36 +1,26 @@ [profile.default] -src = 'src' -out = 'out' -libs = ['node_modules', 'lib'] -remappings = [] -solc = '0.8.24' +src = "src" +out = "out" +libs = ["lib"] +solc_version = '0.8.26' evm_version = 'paris' +[profile.cct] +optimizer = true +optimizer_runs = 200 + [rpc_endpoints] ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}" -optimismGoerli = "${OPTIMISM_GOERLI_RPC_URL}" avalancheFuji = "${AVALANCHE_FUJI_RPC_URL}" -arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}" -polygonMumbai = "${POLYGON_MUMBAI_RPC_URL}" -bnbChainTestnet = "${BNB_CHAIN_TESTNET_RPC_URL}" +polygonAmoy = "${POLYGON_AMOY_RPC_URL}" baseSepolia = "${BASE_SEPOLIA_RPC_URL}" -metisSepolia = "${METIS_SEPOLIA_RPC_URL}" -zksyncSepolia = "${ZKSYNC_SEPOLIA_RPC_URL}" -scrollSepolia = "${SCROLL_SEPOLIA_RPC_URL}" -zircuitSepolia = "${ZIRCUIT_SEPOLIA_RPC_URL}" -xlayerSepolia = "${XLAYER_SEPOLIA_RPC_URL}" -polygonZkevmSepolia = "${POLYGON_ZKEVM_SEPOLIA_RPC_URL}" -polkadotAstarShibuya = "${POLKADOT_ASTAR_SHIBUYA_RPC_URL}" -mantleSepolia = "${MANTLE_SEPOLIA_RPC_URL}" -soneiumMinatoSepolia = "${SONEIUM_MINATO_SEPOLIA_RPC_URL}" -bsquaredTestnet = "${BSQUARED_TESTNET_RPC_URL}" -bobSepolia = "${BOB_SEPOLIA_RPC_URL}" -worldchainSepolia = "${WORLDCHAIN_SEPOLIA_RPC_URL}" -shibariumTestnet = "${SHIBARIUM_TESTNET_RPC_URL}" -bitlayerTestnet= "${BITLAYER_TESTNET_RPC_URL}" -fantomSonicTestnet = "${FANTOM_SONIC_TESTNET_RPC_URL}" -cornTestnet = "${CORN_TESTNET_RPC_URL}" -hashkeySepolia = "${HASHKEY_SEPOLIA_RPC_URL}" -inkSepolia = "${INK_SEPOLIA_RPC_URL}" -# See more config options https://github.com/foundry-rs/foundry/tree/master/config +[profile.lint] +exclude_lints = [ + "mixed-case-variable", + "pascal-case-struct", + "mixed-case-function", + "screaming-snake-case-immutable", +] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/img/basic-architecture.png b/img/basic-architecture.png deleted file mode 100644 index 12a0a2c..0000000 Binary files a/img/basic-architecture.png and /dev/null differ diff --git a/img/ccip-explorer.png b/img/ccip-explorer.png deleted file mode 100644 index 56ebf41..0000000 Binary files a/img/ccip-explorer.png and /dev/null differ diff --git a/img/opensea.png b/img/opensea.png deleted file mode 100644 index 858b869..0000000 Binary files a/img/opensea.png and /dev/null differ diff --git a/lib/chainlink-ace b/lib/chainlink-ace new file mode 160000 index 0000000..288c393 --- /dev/null +++ b/lib/chainlink-ace @@ -0,0 +1 @@ +Subproject commit 288c393af4da077b49e5138ac47965aed9ca4b3a diff --git a/lib/chainlink-ccip b/lib/chainlink-ccip index 0e3e0fc..c6c1ef8 160000 --- a/lib/chainlink-ccip +++ b/lib/chainlink-ccip @@ -1 +1 @@ -Subproject commit 0e3e0fc5c0f70f0d50dca66b139142ddf3009294 +Subproject commit c6c1ef8cb300b3c08b574c2053c82e4faf46f89c diff --git a/lib/chainlink-local b/lib/chainlink-local deleted file mode 160000 index 1263371..0000000 --- a/lib/chainlink-local +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 126337155f40ac9eb53f1f3483d104fd18caf380 diff --git a/lib/forge-std b/lib/forge-std index 9b49a72..1801b05 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 9b49a72cfdb36bcf195eb863f868f01a6d6d3186 +Subproject commit 1801b0541f4fda118a10798fd3486bb7051c5dd6 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 96b9559..0000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 96b95592c300a1b38953b276f0b6c77017f5c942 diff --git a/lib/openzeppelin-contracts-4.7.3 b/lib/openzeppelin-contracts-4.7.3 deleted file mode 160000 index ecd2ca2..0000000 --- a/lib/openzeppelin-contracts-4.7.3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d diff --git a/lib/openzeppelin-contracts-4.9.6 b/lib/openzeppelin-contracts-4.9.6 deleted file mode 160000 index dc44c9f..0000000 --- a/lib/openzeppelin-contracts-4.9.6 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/lib/openzeppelin-contracts-5.0.2 b/lib/openzeppelin-contracts-5.0.2 deleted file mode 160000 index dbb6104..0000000 --- a/lib/openzeppelin-contracts-5.0.2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/lib/openzeppelin-contracts-5.1.0 b/lib/openzeppelin-contracts-5.1.0 deleted file mode 160000 index 69c8def..0000000 --- a/lib/openzeppelin-contracts-5.1.0 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/lib/openzeppelin-contracts-5.3.0 b/lib/openzeppelin-contracts-5.3.0 new file mode 160000 index 0000000..e4f7021 --- /dev/null +++ b/lib/openzeppelin-contracts-5.3.0 @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/remappings.txt b/remappings.txt index 25426a4..2b55d93 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,12 +1,6 @@ -ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -@chainlink/contracts/=lib/chainlink-evm/contracts/ -@chainlink/contracts-ccip/=lib/chainlink-ccip/chains/evm/ -@chainlink/local/=lib/chainlink-local/ - -@openzeppelin/contracts@4.7.3/=lib/openzeppelin-contracts-4.7.3/contracts/ +@chainlink/contracts-ccip/=lib/chainlink-ccip/chains/evm +@chainlink/contracts/=lib/chainlink-evm/contracts +@chainlink/policy-management/=lib/chainlink-ace/packages/policy-management/src/ @openzeppelin/contracts@4.8.3/=lib/openzeppelin-contracts-4.8.3/contracts/ -@openzeppelin/contracts@4.9.6/=lib/openzeppelin-contracts-4.9.6/contracts/ -@openzeppelin/contracts@5.0.2/=lib/openzeppelin-contracts-5.0.2/contracts/ -@openzeppelin/contracts@5.1.0/=lib/openzeppelin-contracts-5.1.0/contracts/ \ No newline at end of file +@openzeppelin/contracts@5.3.0/=lib/openzeppelin-contracts-5.3.0/contracts/ +forge-std/=lib/forge-std/src/ diff --git a/script/CrossChainNFT.s.sol b/script/CrossChainNFT.s.sol deleted file mode 100644 index c659994..0000000 --- a/script/CrossChainNFT.s.sol +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {MyNFT} from "../src/cross-chain-nft-minter/MyNFT.sol"; -import {DestinationMinter} from "../src/cross-chain-nft-minter/DestinationMinter.sol"; -import {SourceMinter} from "../src/cross-chain-nft-minter/SourceMinter.sol"; - -contract DeployDestination is Script, Helper { - function run(SupportedNetworks destination) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (address router, , , ) = getConfigFromNetwork(destination); - - MyNFT myNFT = new MyNFT(); - - console.log( - "MyNFT deployed on ", - networks[destination], - "with address: ", - address(myNFT) - ); - - DestinationMinter destinationMinter = new DestinationMinter( - router, - address(myNFT) - ); - - console.log( - "DestinationMinter deployed on ", - networks[destination], - "with address: ", - address(destinationMinter) - ); - - myNFT.transferOwnership(address(destinationMinter)); - address minter = myNFT.owner(); - - console.log("Minter role granted to: ", minter); - - vm.stopBroadcast(); - } -} - -contract DeploySource is Script, Helper { - function run(SupportedNetworks source) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (address router, address link, , ) = getConfigFromNetwork(source); - - SourceMinter sourceMinter = new SourceMinter(router, link); - - console.log( - "SourceMinter deployed on ", - networks[source], - "with address: ", - address(sourceMinter) - ); - - vm.stopBroadcast(); - } -} - -contract Mint is Script, Helper { - function run( - address payable sourceMinterAddress, - SupportedNetworks destination, - address destinationMinterAddress, - SourceMinter.PayFeesIn payFeesIn - ) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - SourceMinter(sourceMinterAddress).mint( - destinationChainId, - destinationMinterAddress, - payFeesIn - ); - - vm.stopBroadcast(); - } -} diff --git a/script/EncodeExtraArgsOffchain.s.sol b/script/EncodeExtraArgsOffchain.s.sol index 78572f7..489cf77 100644 --- a/script/EncodeExtraArgsOffchain.s.sol +++ b/script/EncodeExtraArgsOffchain.s.sol @@ -1,7 +1,15 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; +pragma solidity ^0.8.26; import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {ExtraArgsCodec} from "@chainlink/contracts-ccip/contracts/libraries/ExtraArgsCodec.sol"; +import {FinalityCodec} from "@chainlink/contracts-ccip/contracts/libraries/FinalityCodec.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ /// @title EncodeExtraArgsOffchain /// @notice This contract is not intended to be deployed on-chain, it is simply a helper contract to encode the extraArgs. @@ -38,4 +46,65 @@ contract EncodeExtraArgsOffchain { Client.GenericExtraArgsV2({gasLimit: gasLimit, allowOutOfOrderExecution: allowOutOfOrderExecution}); extraArgsBytes = Client._argsToBytes(extraArgs); } + + /** + * @param gasLimit Gas limit allocated for the callback on the destination chain. If `0` **and** the message data length + * is `0`, no callback is executed (useful for token-only transfers or EOA receivers). + * **Note:** The sender is billed for the *specified* gas limit, not actual usage. Unused gas is **not** refunded. + * Estimate gas requirements carefully, as they vary by chain family (refer to chain-specific documentation). + * @param blockConfirmations Number of block confirmations to wait before execution. `0` uses the default finality + * defined by the CCV. Non-zero values may be rejected by CCVs/Pools/executor if considered too risky. + * @param ccvs Array of cross-chain verifier (CCV) addresses. If empty, default verifiers are used. + * @param ccvArgs Optional, uninterpreted arguments for each CCV in `ccvs`. Must match the length of `ccvs`. + * **Format:** Chain/CCV-specific (e.g., encoded structs or raw bytes). + * @param executor Address of the executor contract on the **source chain**. If `address(0)`, the default executor is used. + * The executor handles message execution on the destination chain after CCV verification. + * @param executorArgs Chain-family-specific arguments for the executor (e.g., Solana accounts, Sui object IDs). + * **Security:** Incorrect values may cause fund loss. Format is executor/chain-dependent. + * @param tokenReceiver Destination token receiver address in bytes. If empty, the message receiver address is used. + * **Behavior:** + * - If token transfer exists: Falls back to message receiver. + * - If no token transfer: Must be empty. + * @param tokenArgs Additional token transfer arguments (e.g., pool-specific metadata). Format depends on the token pool. + * @return extraArgsBytes Encoded ExtraArgsV3 + */ + function encodeV3( + uint32 gasLimit, + uint16 blockConfirmations, + address[] memory ccvs, + bytes[] memory ccvArgs, + address executor, + bytes memory executorArgs, + bytes memory tokenReceiver, + bytes memory tokenArgs + ) public pure returns (bytes memory extraArgsBytes) { + ExtraArgsCodec.GenericExtraArgsV3 memory extraArgs = ExtraArgsCodec.GenericExtraArgsV3({ + gasLimit: gasLimit, + requestedFinalityConfig: FinalityCodec._encodeBlockDepth(blockConfirmations), + ccvs: ccvs, + ccvArgs: ccvArgs, + executor: executor, + executorArgs: executorArgs, + tokenReceiver: tokenReceiver, + tokenArgs: tokenArgs + }); + + extraArgsBytes = ExtraArgsCodec._encodeGenericExtraArgsV3(extraArgs); + } + + /// @notice Creates a basic encoded GenericExtraArgsV3 with only gasLimit and blockConfirmations set. + function encodeV3Basic(uint32 gasLimit, uint16 blockConfirmations) + public + pure + returns (bytes memory extraArgsBytes) + { + bytes4 finalityConfig = FinalityCodec._encodeBlockDepth(blockConfirmations); + extraArgsBytes = ExtraArgsCodec._getBasicEncodedExtraArgsV3(gasLimit, finalityConfig); + } + + /// @notice Get the NO_EXECUTION_ADDRESS for manual execution. + /// @return The address that signals no automatic execution. + function getNoExecutionAddress() public pure returns (address) { + return Client.NO_EXECUTION_ADDRESS; + } } diff --git a/script/Example01.s.sol b/script/Example01.s.sol deleted file mode 100644 index 7823e68..0000000 --- a/script/Example01.s.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {IERC20} from "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; - -contract CCIPTokenTransfer is Script, Helper { - function run( - SupportedNetworks source, - SupportedNetworks destination, - address receiver, - address tokenToSend, - uint256 amount, - PayFeesIn payFeesIn - ) external returns (bytes32 messageId) { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (address sourceRouter, address linkToken, , ) = getConfigFromNetwork( - source - ); - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - IERC20(tokenToSend).approve(sourceRouter, amount); - - Client.EVMTokenAmount[] - memory tokensToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenToSendDetails = Client - .EVMTokenAmount({token: tokenToSend, amount: amount}); - - tokensToSendDetails[0] = tokenToSendDetails; - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), - data: "", - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes( - Client.EVMExtraArgsV1({gasLimit: 0}) - ), - feeToken: payFeesIn == PayFeesIn.LINK ? linkToken : address(0) - }); - - uint256 fees = IRouterClient(sourceRouter).getFee( - destinationChainId, - message - ); - - if (payFeesIn == PayFeesIn.LINK) { - IERC20(linkToken).approve(sourceRouter, fees); - messageId = IRouterClient(sourceRouter).ccipSend( - destinationChainId, - message - ); - } else { - messageId = IRouterClient(sourceRouter).ccipSend{value: fees}( - destinationChainId, - message - ); - } - - console.log( - "You can now monitor the status of your Chainlink CCIP Message via https://ccip.chain.link using CCIP Message ID: " - ); - console.logBytes32(messageId); - - vm.stopBroadcast(); - } -} diff --git a/script/Example02.s.sol b/script/Example02.s.sol deleted file mode 100644 index 4a7b5ed..0000000 --- a/script/Example02.s.sol +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {BasicMessageReceiver} from "../src/BasicMessageReceiver.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {IERC20} from "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; - -contract DeployBasicMessageReceiver is Script, Helper { - function run(SupportedNetworks destination) external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - - (address router, , , ) = getConfigFromNetwork(destination); - - BasicMessageReceiver basicMessageReceiver = new BasicMessageReceiver( - router - ); - - console.log( - "Basic Message Receiver deployed on ", - networks[destination], - "with address: ", - address(basicMessageReceiver) - ); - - vm.stopBroadcast(); - } -} - -contract CCIPTokenTransfer is Script, Helper { - function run( - SupportedNetworks source, - SupportedNetworks destination, - address basicMessageReceiver, - address tokenToSend, - uint256 amount, - PayFeesIn payFeesIn - ) external returns (bytes32 messageId) { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (address sourceRouter, address linkToken, , ) = getConfigFromNetwork( - source - ); - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - IERC20(tokenToSend).approve(sourceRouter, amount); - - Client.EVMTokenAmount[] - memory tokensToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenToSendDetails = Client - .EVMTokenAmount({token: tokenToSend, amount: amount}); - - tokensToSendDetails[0] = tokenToSendDetails; - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(basicMessageReceiver), - data: "", - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes( - Client.GenericExtraArgsV2({ - gasLimit: 0, - allowOutOfOrderExecution: true - }) - ), - feeToken: payFeesIn == PayFeesIn.LINK ? linkToken : address(0) - }); - - uint256 fees = IRouterClient(sourceRouter).getFee( - destinationChainId, - message - ); - - if (payFeesIn == PayFeesIn.LINK) { - IERC20(linkToken).approve(sourceRouter, fees); - messageId = IRouterClient(sourceRouter).ccipSend( - destinationChainId, - message - ); - } else { - messageId = IRouterClient(sourceRouter).ccipSend{value: fees}( - destinationChainId, - message - ); - } - - console.log( - "You can now monitor the status of your Chainlink CCIP Message via https://ccip.chain.link using CCIP Message ID: " - ); - console.logBytes32(messageId); - - vm.stopBroadcast(); - } -} - -contract GetLatestMessageDetails is Script, Helper { - function run(address basicMessageReceiver) external view { - ( - bytes32 latestMessageId, - uint64 latestSourceChainSelector, - address latestSender, - string memory latestMessage - ) = BasicMessageReceiver(basicMessageReceiver).getLatestMessageDetails(); - - console.log("Latest Message ID: "); - console.logBytes32(latestMessageId); - console.log("Latest Source Chain Selector: "); - console.log(latestSourceChainSelector); - console.log("Latest Sender: "); - console.log(latestSender); - console.log("Latest Message: "); - console.log(latestMessage); - } -} diff --git a/script/Example03.s.sol b/script/Example03.s.sol deleted file mode 100644 index 4bd355d..0000000 --- a/script/Example03.s.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {BasicTokenSender} from "../src/BasicTokenSender.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; - -contract DeployBasicTokenSender is Script, Helper { - function run(SupportedNetworks source) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (address router, address linkToken, , ) = getConfigFromNetwork(source); - - BasicTokenSender basicTokenSender = new BasicTokenSender( - router, - linkToken - ); - - console.log( - "Basic Token Sender deployed on ", - networks[source], - "with address: ", - address(basicTokenSender) - ); - - vm.stopBroadcast(); - } -} - -contract SendBatch is Script, Helper { - function run( - SupportedNetworks destination, - address payable basicTokenSenderAddres, - address receiver, - Client.EVMTokenAmount[] memory tokensToSendDetails, - BasicTokenSender.PayFeesIn payFeesIn - ) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - BasicTokenSender(basicTokenSenderAddres).send( - destinationChainId, - receiver, - tokensToSendDetails, - payFeesIn - ); - - vm.stopBroadcast(); - } -} diff --git a/script/Example04.s.sol b/script/Example04.s.sol deleted file mode 100644 index 1e09e2c..0000000 --- a/script/Example04.s.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {ProgrammableTokenTransfers} from "../src/ProgrammableTokenTransfers.sol"; - -contract DeployProgrammableTokenTransfers is Script, Helper { - function run(SupportedNetworks network) external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - - (address router, , , ) = getConfigFromNetwork(network); - - ProgrammableTokenTransfers programmableTokenTransfers = new ProgrammableTokenTransfers( - router - ); - - console.log( - "ProgrammableTokenTransfers contract deployed on ", - networks[network], - "with address: ", - address(programmableTokenTransfers) - ); - - vm.stopBroadcast(); - } -} - -contract SendTokensAndData is Script, Helper { - function run( - address payable sender, - SupportedNetworks destination, - address receiver, - string memory message, - address token, - uint256 amount - ) external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - bytes32 messageId = ProgrammableTokenTransfers(sender).sendMessage( - destinationChainId, - receiver, - message, - token, - amount - ); - - console.log( - "You can now monitor the status of your Chainlink CCIP Message via https://ccip.chain.link using CCIP Message ID: " - ); - console.logBytes32(messageId); - - vm.stopBroadcast(); - } -} diff --git a/script/Example05.s.sol b/script/Example05.s.sol deleted file mode 100644 index cb7af9d..0000000 --- a/script/Example05.s.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "forge-std/Script.sol"; -import "./Helper.sol"; -import {BasicMessageSender} from "../src/BasicMessageSender.sol"; - -contract DeployBasicMessageSender is Script, Helper { - function run(SupportedNetworks source) external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - - (address router, address link, , ) = getConfigFromNetwork(source); - - BasicMessageSender basicMessageSender = new BasicMessageSender( - router, - link - ); - - console.log( - "BasicMessageSender contract deployed on ", - networks[source], - "with address: ", - address(basicMessageSender) - ); - - vm.stopBroadcast(); - } -} - -contract SendMessage is Script, Helper { - function run( - address payable sender, - SupportedNetworks destination, - address receiver, - string memory message, - BasicMessageSender.PayFeesIn payFeesIn - ) external { - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(deployerPrivateKey); - - (, , , uint64 destinationChainId) = getConfigFromNetwork(destination); - - bytes32 messageId = BasicMessageSender(sender).send( - destinationChainId, - receiver, - message, - payFeesIn - ); - - console.log( - "You can now monitor the status of your Chainlink CCIP Message via https://ccip.chain.link using CCIP Message ID: " - ); - console.logBytes32(messageId); - - vm.stopBroadcast(); - } -} diff --git a/script/Faucet.s.sol b/script/Faucet.s.sol index f9efd44..02a6c75 100644 --- a/script/Faucet.s.sol +++ b/script/Faucet.s.sol @@ -1,26 +1,20 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.0; -import "forge-std/Script.sol"; -import "./Helper.sol"; +import {Script, console2} from "forge-std/Script.sol"; -interface ICCIPToken { +interface BurnMintERC20WithDrip { + // Gives one full token to any given address. function drip(address to) external; } -contract Faucet is Script, Helper { - function run(SupportedNetworks network) external { - uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY"); - vm.startBroadcast(senderPrivateKey); - address senderAddress = vm.addr(senderPrivateKey); +contract Faucet is Script { + function run(address ccipBnM) external { + vm.startBroadcast(); - (address ccipBnm, address ccipLnm) = getDummyTokensFromNetwork(network); - - ICCIPToken(ccipBnm).drip(senderAddress); - - if (network == SupportedNetworks.ETHEREUM_SEPOLIA) { - ICCIPToken(ccipLnm).drip(senderAddress); - } + (, address broadcaster,) = vm.readCallers(); + BurnMintERC20WithDrip(ccipBnM).drip(broadcaster); + console2.log("[INFO] Minting 1 CCIP-BnM token (", ccipBnM, ") to address:", broadcaster); vm.stopBroadcast(); } diff --git a/script/Helper.sol b/script/Helper.sol deleted file mode 100644 index 23e3696..0000000 --- a/script/Helper.sol +++ /dev/null @@ -1,369 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -contract Helper { - // Supported Networks - enum SupportedNetworks { - ETHEREUM_SEPOLIA, // 0 - AVALANCHE_FUJI, // 1 - ARBITRUM_SEPOLIA, // 2 - POLYGON_MUMBAI, // 3 - BNB_CHAIN_TESTNET, // 4 - OPTIMISM_SEPOLIA, // 5 - BASE_SEPOLIA, // 6 - WEMIX_TESTNET, // 7 - KROMA_SEPOLIA_TESTNET, // 8 - METIS_SEPOLIA, // 9 - ZKSYNC_SEPOLIA, // 10 - SCROLL_SEPOLIA, // 11 - ZIRCUIT_SEPOLIA, // 12 - XLAYER_SEPOLIA, // 13 - POLYGON_ZKEVM_SEPOLIA, // 14 - POLKADOT_ASTAR_SHIBUYA, // 15 - MANTLE_SEPOLIA, // 16 - SONEIUM_MINATO_SEPOLIA, // 17 - BSQUARED_TESTNET, // 18 - BOB_SEPOLIA, // 19 - WORLDCHAIN_SEPOLIA, // 20 - SHIBARIUM_TESTNET, // 21 - BITLAYER_TESTNET, // 22 - FANTOM_SONIC_TESTNET, // 23 - CORN_TESTNET, // 24 - HASHKEY_SEPOLIA, // 25 - INK_SEPOLIA // 26 - - } - - mapping(SupportedNetworks enumValue => string humanReadableName) public networks; - - enum PayFeesIn { - Native, - LINK - } - - // Chain IDs - uint64 constant chainIdEthereumSepolia = 16015286601757825753; - uint64 constant chainIdAvalancheFuji = 14767482510784806043; - uint64 constant chainIdArbitrumSepolia = 3478487238524512106; - uint64 constant chainIdPolygonMumbai = 12532609583862916517; - uint64 constant chainIdBnbChainTestnet = 13264668187771770619; - uint64 constant chainIdOptimismSepolia = 5224473277236331295; - uint64 constant chainIdBaseSepolia = 10344971235874465080; - uint64 constant chainIdWemixTestnet = 9284632837123596123; - uint64 constant chainIdKromaSepoliaTestnet = 5990477251245693094; - uint64 constant chainIdMetisSepolia = 3777822886988675105; - uint64 constant chainIdZksyncSepolia = 6898391096552792247; - uint64 constant chainIdScrollSepolia = 2279865765895943307; - uint64 constant chainIdZircuitSepolia = 4562743618362911021; - uint64 constant chainIdXlayerSepolia = 2066098519157881736; - uint64 constant chainIdPolygonZkevmSepolia = 1654667687261492630; - uint64 constant chainIdPolkadotAstarShibuya = 6955638871347136141; - uint64 constant chainIdMantleSepolia = 8236463271206331221; - uint64 constant chainIdSoneiumMinatoSepolia = 686603546605904534; - uint64 constant chainIdBsquaredTestnet = 1948510578179542068; - uint64 constant chainIdBobSepolia = 5535534526963509396; - uint64 constant chainIdWorldchainSepolia = 5299555114858065850; - uint64 constant chainIdShibariumTestnet = 17833296867764334567; - uint64 constant chainIdBitlayerTestnet = 3789623672476206327; // https://github.com/smartcontractkit/reference-data-directory-ccip-prod-testnet/blob/SHIP-3965/bitlayer-testnet-deployment/bitcoin-testnet-bitlayer-1/ccip/metadata.json - uint64 constant chainIdFantomSonicTestnet = 3676871237479449268; - uint64 constant chainIdCornTestnet = 1467427327723633929; // https://github.com/smartcontractkit/reference-data-directory-ccip-prod-testnet/blob/corn-testnet-chain/ethereum-testnet-sepolia-corn-1/ccip/metadata.json - uint64 constant chainIdHashkeySepolia = 4356164186791070119; - uint64 constant chainIdInkSepolia = 9763904284804119144; - - // Router addresses - address constant routerEthereumSepolia = 0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59; - address constant routerAvalancheFuji = 0xF694E193200268f9a4868e4Aa017A0118C9a8177; - address constant routerArbitrumSepolia = 0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165; - address constant routerPolygonMumbai = 0x1035CabC275068e0F4b745A29CEDf38E13aF41b1; - address constant routerBnbChainTestnet = 0xE1053aE1857476f36A3C62580FF9b016E8EE8F6f; - address constant routerOptimismSepolia = 0x114A20A10b43D4115e5aeef7345a1A71d2a60C57; - address constant routerBaseSepolia = 0xD3b06cEbF099CE7DA4AcCf578aaebFDBd6e88a93; - address constant routerWemixTestnet = 0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D; - address constant routerKromaSepoliaTestnet = 0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D; - address constant routerMetisSepolia = 0xaCdaBa07ECad81dc634458b98673931DD9d3Bc14; - address constant routerZksyncSepolia = 0xA1fdA8aa9A8C4b945C45aD30647b01f07D7A0B16; - address constant routerScrollSepolia = 0x6aF501292f2A33C81B9156203C9A66Ba0d8E3D21; - address constant routerZircuitSepolia = 0x20bC4Ec73C6aE9Dc71f79Eb8470c542f71441bf5; - address constant routerXlayerSepolia = 0xc5F5330C4793AF46872a9eC15b76a007A96a4152; - address constant routerPolygonZkevmSepolia = 0x91A7f913EEF5E3058AD1Bf8842C294f7219C7271; - address constant routerPolkadotAstarShibuya = 0x22aE550d87eBf775E0c1fDc8881121c8A51F5903; - address constant routerMantleSepolia = 0xFd33fd627017fEf041445FC19a2B6521C9778f86; - address constant routerSoneiumMinatoSepolia = 0x443a1bce545d56E2c3f20ED32eA588395FFce0f4; - address constant routerBsquaredTestnet = 0x34A49Eb641daF64d61be00Aa7F759f8225351101; - address constant routerBobSepolia = 0x7808184405d6Cbc663764003dE21617fa640bc82; - address constant routerWorldchainSepolia = 0x47693fc188b2c30078F142eadc2C009E8D786E8d; - address constant routerShibariumTestnet = 0x449E234FEDF3F907b9E9Dd6BAf1ddc36664097E5; - address constant routerBitlayerTestnet = 0x3dfbe078277609D34c8ef015c61f23A9BeDE61BB; // TODO - address constant routerFantomSonicTestnet = 0x2fBd4659774D468Db5ca5bacE37869905d8EfA34; - address constant routerCornTestnet = 0x9981250f56d4d0Fa9736343659B4890ebbb94110; - address constant routerHashkeySepolia = 0x1360c71dd2458B6d4A5Ad5946d9011BafA0435d7; - address constant routerInkSepolia = 0x17fCda531D8E43B4e2a2A2492FBcd4507a1685A1; - - // Link addresses (can be used as fee) - address constant linkEthereumSepolia = 0x779877A7B0D9E8603169DdbD7836e478b4624789; - address constant linkAvalancheFuji = 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846; - address constant linkArbitrumSepolia = 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E; - address constant linkPolygonMumbai = 0x326C977E6efc84E512bB9C30f76E30c160eD06FB; - address constant linkBnbChainTestnet = 0x84b9B910527Ad5C03A9Ca831909E21e236EA7b06; - address constant linkOptimismSepolia = 0xE4aB69C077896252FAFBD49EFD26B5D171A32410; - address constant linkBaseSepolia = 0xE4aB69C077896252FAFBD49EFD26B5D171A32410; - address constant linkWemixTestnet = 0x3580c7A817cCD41f7e02143BFa411D4EeAE78093; - address constant linkKromaSepoliaTestnet = 0xa75cCA5b404ec6F4BB6EC4853D177FE7057085c8; - address constant linkMetisSepolia = 0x9870D6a0e05F867EAAe696e106741843F7fD116D; - address constant linkZksyncSepolia = 0x23A1aFD896c8c8876AF46aDc38521f4432658d1e; - address constant linkScrollSepolia = 0x7273ebbB21F8D8AcF2bC12E71a08937712E9E40c; - address constant linkZircuitSepolia = 0xDEE94506570cA186BC1e3516fCf4fd719C312cCD; - address constant linkXlayerSepolia = 0x724593f6FCb0De4E6902d4C55D7C74DaA2AF0E55; - address constant linkPolygonZkevmSepolia = 0x5576815a38A3706f37bf815b261cCc7cCA77e975; - address constant linkPolkadotAstarShibuya = 0xe74037112db8807B3B4B3895F5790e5bc1866a29; - address constant linkMantleSepolia = 0x22bdEdEa0beBdD7CfFC95bA53826E55afFE9DE04; - address constant linkSoneiumMinatoSepolia = 0x7ea13478Ea3961A0e8b538cb05a9DF0477c79Cd2; - address constant linkBsquaredTestnet = 0x436a1907D9e6a65E6db73015F08f9C66F6B63E45; - address constant linkBobSepolia = 0xcd2AfB2933391E35e8682cbaaF75d9CA7339b183; - address constant linkWorldchainSepolia = 0xC82Ea35634BcE95C394B6BC00626f827bB0F4801; - address constant linkShibariumTestnet = 0x44637eEfD71A090990f89faEC7022fc74B2969aD; - address constant linkBitlayerTestnet = 0x2A5bACb2440BC17D53B7b9Be73512dDf92265e48; // TODO - address constant linkFantomSonicTestnet = 0x61876F0429726D7777B46f663e1C9ab75d08Fc56; - address constant linkCornTestnet = 0x996EfAb6011896Be832969D91E9bc1b3983cfdA1; - address constant linkHashkeySepolia = 0x8418c4d7e8e17ab90232DC72150730E6c4b84F57; - address constant linkInkSepolia = 0x3423C922911956b1Ccbc2b5d4f38216a6f4299b4; - - // Wrapped native addresses - address constant wethEthereumSepolia = 0x097D90c9d3E0B50Ca60e1ae45F6A81010f9FB534; - address constant wavaxAvalancheFuji = 0xd00ae08403B9bbb9124bB305C09058E32C39A48c; - address constant wethArbitrumSepolia = 0xE591bf0A0CF924A0674d7792db046B23CEbF5f34; - address constant wmaticPolygonMumbai = 0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889; - address constant wbnbBnbChainTestnet = 0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd; - address constant wethOptimismSepolia = 0x4200000000000000000000000000000000000006; - address constant wethBaseSepolia = 0x4200000000000000000000000000000000000006; - address constant wwemixWemixTestnet = 0xbE3686643c05f00eC46e73da594c78098F7a9Ae7; - address constant wethKromaSepoliaTestnet = 0x4200000000000000000000000000000000000001; - address constant wethMetisSepolia = 0x5c48e07062aC4E2Cf4b9A768a711Aef18e8fbdA0; - address constant wethZksyncSepolia = 0x4317b2eCD41851173175005783322D29E9bAee9E; - address constant wethScrollSepolia = 0x5300000000000000000000000000000000000004; - address constant wethZircuitSepolia = 0x4200000000000000000000000000000000000006; - address constant wokbXlayerSepolia = 0xa7b9C3a116b20bEDDdBE4d90ff97157f67F0bD97; - address constant wethPolygonZkevmSepolia = 0x1CE28d5C81B229c77C5651feB49c4C489f8c52C4; - address constant wsbyPolkadotAstarShibuya = 0xbd5F3751856E11f3e80dBdA567Ef91Eb7e874791; - address constant wmntMantleSepolia = 0x19f5557E23e9914A18239990f6C70D68FDF0deD5; - address constant wethSoneiumMinatoSepolia = 0x4200000000000000000000000000000000000006; - address constant wbtcBsquaredTestnet = 0x4200000000000000000000000000000000000006; - address constant wethBobSepolia = 0x4200000000000000000000000000000000000006; - address constant wethWorldchainSepolia = 0x4200000000000000000000000000000000000006; - address constant wboneShibariumTestnet = 0x41c3F37587EBcD46C0F85eF43E38BcfE1E70Ab56; - address constant wbtcBitlayerTestnet = 0x3e57d6946f893314324C975AA9CEBBdF3232967E; // TODO double check - address constant wethFantomSonicTestnet = 0x917FE4b784d1895187Df169aeCc687C03ba12662; - address constant wbtcCornTestnet = 0x1cAa492a1B39D4867253FC27C4fBEE7b0DbAf575; - address constant whskHashkeySepolia = address(0); // TODO - address constant wethInkSepolia = 0x4200000000000000000000000000000000000006; - - // CCIP-BnM addresses - address constant ccipBnMEthereumSepolia = 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05; - address constant ccipBnMArbitrumSepolia = 0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D; - address constant ccipBnMAvalancheFuji = 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4; - address constant ccipBnMPolygonMumbai = 0xf1E3A5842EeEF51F2967b3F05D45DD4f4205FF40; - address constant ccipBnMBnbChainTestnet = 0xbFA2ACd33ED6EEc0ed3Cc06bF1ac38d22b36B9e9; - address constant ccipBnMOptimismSepolia = 0x8aF4204e30565DF93352fE8E1De78925F6664dA7; - address constant ccipBnMBaseSepolia = 0x88A2d74F47a237a62e7A51cdDa67270CE381555e; - address constant ccipBnMWemixTestnet = 0xF4E4057FbBc86915F4b2d63EEFFe641C03294ffc; - address constant ccipBnMKromaSepoliaTestnet = 0x6AC3e353D1DDda24d5A5416024d6E436b8817A4e; - address constant ccipBnMMetisSepolia = 0x20Aa09AAb761e2E600d65c6929A9fd1E59821D3f; - address constant ccipBnMScrollSepolia = 0x231d45b53C905c3d6201318156BDC725c9c3B9B1; - address constant ccipBnMZircuitSepolia = 0xB6eC69D477F8FAeDCE1c6d322a7842D1b4D1B08e; - address constant ccipBnMPolkadotAstarShibuya = 0xc49ec0eB4beb48B8Da4cceC51AA9A5bD0D0A4c43; - address constant ccipBnMMantleSepolia = 0xEA8cA8AE1c54faB8D185FC1fd7C2d70Bee8a417e; - address constant ccipBnMBsquaredTestnet = 0x0643fD73C261eC4B369C3a8C5c0eC8c57485E32d; - address constant ccipBnMBobSepolia = 0x3B7d0d0CeC08eBF8dad58aCCa4719791378b2329; - address constant ccipBnMWorldchainSepolia = 0x8fdE0C794fDA5a7A303Ce216f79B9695a7714EcB; - address constant ccipBnMShibariumTestnet = 0x81249b4bD91A8706eE67a2f422DB82258D4947ad; - address constant ccipBnMBitlayerTestnet = address(0); // TODO obtain - address constant ccipBnMSoneiumMinatoSepolia = address(0); // TODO obtain - address constant ccipBnMFantomSonicTestnet = 0x230c46b9a7c8929A80863bDe89082B372a4c7A99; - address constant ccipBnMCornTestnet = 0x996EfAb6011896Be832969D91E9bc1b3983cfdA1; - address constant ccipBnMHashkeySepolia = 0xB0F91Ce2ECAa3555D4b1fD4489bD9a207a7844f0; - address constant ccipBnMInkSepolia = 0x414dbe1d58dd9BA7C84f7Fc0e4f82bc858675d37; - - // CCIP-LnM addresses - address constant ccipLnMEthereumSepolia = 0x466D489b6d36E7E3b824ef491C225F5830E81cC1; - address constant clCcipLnMArbitrumSepolia = 0x139E99f0ab4084E14e6bb7DacA289a91a2d92927; - address constant clCcipLnMAvalancheFuji = 0x70F5c5C40b873EA597776DA2C21929A8282A3b35; - address constant clCcipLnMPolygonMumbai = 0xc1c76a8c5bFDE1Be034bbcD930c668726E7C1987; - address constant clCcipLnMBnbChainTestnet = 0x79a4Fc27f69323660f5Bfc12dEe21c3cC14f5901; - address constant clCcipLnMOptimismSepolia = 0x044a6B4b561af69D2319A2f4be5Ec327a6975D0a; - address constant clCcipLnMBaseSepolia = 0xA98FA8A008371b9408195e52734b1768c0d1Cb5c; - address constant clCcipLnMWemixTestnet = 0xcb342aE3D65E3fEDF8F912B0432e2B8F88514d5D; - address constant clCcipLnMKromaSepoliaTestnet = 0x835fcBB6770E1246CfCf52F83cDcec3177d0bb6b; - address constant clCcipLnMMetisSepolia = 0x705b364CadE0e515577F2646529e3A417473a155; - address constant clCcipLnMZircuitSepolia = 0x3210D3244B29535724e19159288323d86287195c; - address constant clCcipLnMPolkadotAstarShibuya = 0xB9d4e1141E67ECFedC8A8139b5229b7FF2BF16F5; - address constant clCcipLnMMantleSepolia = address(0); // TODO - address constant clCcipLnMMantleSepoliaShibuya = 0xCdeE7708A96479f6D029741144f458B7FA807A6C; - - // USDC addresses - address constant usdcAvalancheFuji = 0x5425890298aed601595a70AB815c96711a31Bc65; - address constant usdcPolygonMumbai = 0x9999f7Fea5938fD3b1E26A12c3f2fb024e194f97; - address constant usdcOptimismSepolia = 0x5fd84259d66Cd46123540766Be93DFE6D43130D7; - address constant usdcBaseSepolia = 0x036CbD53842c5426634e7929541eC2318f3dCF7e; - - // GHO Addresses - address constant ghoEthereumSepolia = 0xc4bF5CbDaBE595361438F8c6a187bDc330539c60; - address constant ghoArbitrumSepolia = 0xb13Cfa6f8B2Eed2C37fB00fF0c1A59807C585810; - - constructor() { - networks[SupportedNetworks.ETHEREUM_SEPOLIA] = "Ethereum Sepolia"; - networks[SupportedNetworks.AVALANCHE_FUJI] = "Avalanche Fuji"; - networks[SupportedNetworks.ARBITRUM_SEPOLIA] = "Arbitrum Sepolia"; - networks[SupportedNetworks.POLYGON_MUMBAI] = "Polygon Mumbai"; - networks[SupportedNetworks.BNB_CHAIN_TESTNET] = "BNB Chain Testnet"; - networks[SupportedNetworks.OPTIMISM_SEPOLIA] = "Optimism Sepolia"; - networks[SupportedNetworks.BASE_SEPOLIA] = "Base Sepolia"; - networks[SupportedNetworks.WEMIX_TESTNET] = "Wemix Testnet"; - networks[SupportedNetworks.KROMA_SEPOLIA_TESTNET] = "Kroma Sepolia Testnet"; - networks[SupportedNetworks.METIS_SEPOLIA] = "Metis Sepolia"; - networks[SupportedNetworks.ZKSYNC_SEPOLIA] = "zkSync Sepolia"; - networks[SupportedNetworks.SCROLL_SEPOLIA] = "Scroll Sepolia"; - networks[SupportedNetworks.ZIRCUIT_SEPOLIA] = "Zircuit Sepolia"; - networks[SupportedNetworks.XLAYER_SEPOLIA] = "Xlayer Sepolia"; - networks[SupportedNetworks.POLYGON_ZKEVM_SEPOLIA] = "Polygon ZKEVM Sepolia"; - networks[SupportedNetworks.POLKADOT_ASTAR_SHIBUYA] = "Polkadot Astar Shibuya"; - networks[SupportedNetworks.MANTLE_SEPOLIA] = "Mantle Sepolia"; - networks[SupportedNetworks.SONEIUM_MINATO_SEPOLIA] = "Soneium Minato Sepolia"; - networks[SupportedNetworks.BSQUARED_TESTNET] = "B-Squared Testnet"; - networks[SupportedNetworks.BOB_SEPOLIA] = "BoB Sepolia"; - networks[SupportedNetworks.WORLDCHAIN_SEPOLIA] = "World Chain Sepolia"; - networks[SupportedNetworks.SHIBARIUM_TESTNET] = "Shibarium Testnet"; - networks[SupportedNetworks.BITLAYER_TESTNET] = "Bitlayer Testnet"; - networks[SupportedNetworks.FANTOM_SONIC_TESTNET] = "Fantom Sonic Testnet"; - networks[SupportedNetworks.CORN_TESTNET] = "Corn Testnet"; - networks[SupportedNetworks.HASHKEY_SEPOLIA] = "Hashkey Sepolia"; - networks[SupportedNetworks.INK_SEPOLIA] = "Ink Sepolia"; - } - - function getDummyTokensFromNetwork(SupportedNetworks network) - internal - pure - returns (address ccipBnM, address ccipLnM) - { - if (network == SupportedNetworks.ETHEREUM_SEPOLIA) { - return (ccipBnMEthereumSepolia, ccipLnMEthereumSepolia); - } else if (network == SupportedNetworks.ARBITRUM_SEPOLIA) { - return (ccipBnMArbitrumSepolia, clCcipLnMArbitrumSepolia); - } else if (network == SupportedNetworks.AVALANCHE_FUJI) { - return (ccipBnMAvalancheFuji, clCcipLnMAvalancheFuji); - } else if (network == SupportedNetworks.POLYGON_MUMBAI) { - return (ccipBnMPolygonMumbai, clCcipLnMPolygonMumbai); - } else if (network == SupportedNetworks.BNB_CHAIN_TESTNET) { - return (ccipBnMBnbChainTestnet, clCcipLnMBnbChainTestnet); - } else if (network == SupportedNetworks.OPTIMISM_SEPOLIA) { - return (ccipBnMOptimismSepolia, clCcipLnMOptimismSepolia); - } else if (network == SupportedNetworks.WEMIX_TESTNET) { - return (ccipBnMWemixTestnet, clCcipLnMWemixTestnet); - } else if (network == SupportedNetworks.KROMA_SEPOLIA_TESTNET) { - return (ccipBnMKromaSepoliaTestnet, clCcipLnMKromaSepoliaTestnet); - } else if (network == SupportedNetworks.METIS_SEPOLIA) { - return (ccipBnMMetisSepolia, clCcipLnMMetisSepolia); - } else if (network == SupportedNetworks.ZIRCUIT_SEPOLIA) { - return (ccipBnMZircuitSepolia, clCcipLnMZircuitSepolia); - } else if (network == SupportedNetworks.POLKADOT_ASTAR_SHIBUYA) { - return (ccipBnMPolkadotAstarShibuya, clCcipLnMPolkadotAstarShibuya); - } else if (network == SupportedNetworks.MANTLE_SEPOLIA) { - return (ccipBnMMantleSepolia, clCcipLnMMantleSepolia); - } else if (network == SupportedNetworks.BSQUARED_TESTNET) { - return (ccipBnMBsquaredTestnet, address(0)); - } else if (network == SupportedNetworks.BOB_SEPOLIA) { - return (ccipBnMBobSepolia, address(0)); - } else if (network == SupportedNetworks.WORLDCHAIN_SEPOLIA) { - return (ccipBnMWorldchainSepolia, address(0)); - } else if (network == SupportedNetworks.SHIBARIUM_TESTNET) { - return (ccipBnMShibariumTestnet, address(0)); - } else if (network == SupportedNetworks.BITLAYER_TESTNET) { - return (ccipBnMBitlayerTestnet, address(0)); - } else if (network == SupportedNetworks.SONEIUM_MINATO_SEPOLIA) { - return (ccipBnMSoneiumMinatoSepolia, address(0)); - } else if (network == SupportedNetworks.FANTOM_SONIC_TESTNET) { - return (ccipBnMFantomSonicTestnet, address(0)); - } else if (network == SupportedNetworks.CORN_TESTNET) { - return (ccipBnMCornTestnet, address(0)); - } else if (network == SupportedNetworks.HASHKEY_SEPOLIA) { - return (ccipBnMHashkeySepolia, address(0)); - } else if (network == SupportedNetworks.INK_SEPOLIA) { - return (ccipBnMInkSepolia, address(0)); - } - } - - function getConfigFromNetwork(SupportedNetworks network) - internal - pure - returns (address router, address linkToken, address wrappedNative, uint64 chainId) - { - if (network == SupportedNetworks.ETHEREUM_SEPOLIA) { - return (routerEthereumSepolia, linkEthereumSepolia, wethEthereumSepolia, chainIdEthereumSepolia); - } else if (network == SupportedNetworks.ARBITRUM_SEPOLIA) { - return (routerArbitrumSepolia, linkArbitrumSepolia, wethArbitrumSepolia, chainIdArbitrumSepolia); - } else if (network == SupportedNetworks.AVALANCHE_FUJI) { - return (routerAvalancheFuji, linkAvalancheFuji, wavaxAvalancheFuji, chainIdAvalancheFuji); - } else if (network == SupportedNetworks.POLYGON_MUMBAI) { - return (routerPolygonMumbai, linkPolygonMumbai, wmaticPolygonMumbai, chainIdPolygonMumbai); - } else if (network == SupportedNetworks.BNB_CHAIN_TESTNET) { - return (routerBnbChainTestnet, linkBnbChainTestnet, wbnbBnbChainTestnet, chainIdBnbChainTestnet); - } else if (network == SupportedNetworks.OPTIMISM_SEPOLIA) { - return (routerOptimismSepolia, linkOptimismSepolia, wethOptimismSepolia, chainIdOptimismSepolia); - } else if (network == SupportedNetworks.BASE_SEPOLIA) { - return (routerBaseSepolia, linkBaseSepolia, wethBaseSepolia, chainIdBaseSepolia); - } else if (network == SupportedNetworks.WEMIX_TESTNET) { - return (routerWemixTestnet, linkWemixTestnet, wwemixWemixTestnet, chainIdWemixTestnet); - } else if (network == SupportedNetworks.KROMA_SEPOLIA_TESTNET) { - return ( - routerKromaSepoliaTestnet, linkKromaSepoliaTestnet, wethKromaSepoliaTestnet, chainIdKromaSepoliaTestnet - ); - } else if (network == SupportedNetworks.METIS_SEPOLIA) { - return (routerMetisSepolia, linkMetisSepolia, wethMetisSepolia, chainIdMetisSepolia); - } else if (network == SupportedNetworks.ZKSYNC_SEPOLIA) { - return (routerZksyncSepolia, linkZksyncSepolia, wethZksyncSepolia, chainIdZksyncSepolia); - } else if (network == SupportedNetworks.SCROLL_SEPOLIA) { - return (routerScrollSepolia, linkScrollSepolia, wethScrollSepolia, chainIdScrollSepolia); - } else if (network == SupportedNetworks.ZIRCUIT_SEPOLIA) { - return (routerZircuitSepolia, linkZircuitSepolia, wethZircuitSepolia, chainIdZircuitSepolia); - } else if (network == SupportedNetworks.XLAYER_SEPOLIA) { - return (routerXlayerSepolia, linkXlayerSepolia, wokbXlayerSepolia, chainIdXlayerSepolia); - } else if (network == SupportedNetworks.POLYGON_ZKEVM_SEPOLIA) { - return ( - routerPolygonZkevmSepolia, linkPolygonZkevmSepolia, wethPolygonZkevmSepolia, chainIdPolygonZkevmSepolia - ); - } else if (network == SupportedNetworks.POLKADOT_ASTAR_SHIBUYA) { - return ( - routerPolkadotAstarShibuya, - linkPolkadotAstarShibuya, - wsbyPolkadotAstarShibuya, - chainIdPolkadotAstarShibuya - ); - } else if (network == SupportedNetworks.MANTLE_SEPOLIA) { - return (routerMantleSepolia, linkMantleSepolia, wmntMantleSepolia, chainIdMantleSepolia); - } else if (network == SupportedNetworks.SONEIUM_MINATO_SEPOLIA) { - return ( - routerSoneiumMinatoSepolia, - linkSoneiumMinatoSepolia, - wethSoneiumMinatoSepolia, - chainIdSoneiumMinatoSepolia - ); - } else if (network == SupportedNetworks.BSQUARED_TESTNET) { - return (routerBsquaredTestnet, linkBsquaredTestnet, wbtcBsquaredTestnet, chainIdBsquaredTestnet); - } else if (network == SupportedNetworks.BOB_SEPOLIA) { - return (routerBobSepolia, linkBobSepolia, wethBobSepolia, chainIdBobSepolia); - } else if (network == SupportedNetworks.WORLDCHAIN_SEPOLIA) { - return (routerWorldchainSepolia, linkWorldchainSepolia, wethWorldchainSepolia, chainIdWorldchainSepolia); - } else if (network == SupportedNetworks.SHIBARIUM_TESTNET) { - return (routerShibariumTestnet, linkShibariumTestnet, wboneShibariumTestnet, chainIdShibariumTestnet); - } else if (network == SupportedNetworks.BITLAYER_TESTNET){ - return (routerBitlayerTestnet, linkBitlayerTestnet, wbtcBitlayerTestnet, chainIdBitlayerTestnet); - } else if (network == SupportedNetworks.FANTOM_SONIC_TESTNET){ - return (routerFantomSonicTestnet, linkFantomSonicTestnet, wethFantomSonicTestnet, chainIdFantomSonicTestnet); - } else if (network == SupportedNetworks.CORN_TESTNET){ - return (routerCornTestnet, linkCornTestnet, wbtcCornTestnet, chainIdCornTestnet); - } else if (network == SupportedNetworks.HASHKEY_SEPOLIA){ - return (routerHashkeySepolia, linkHashkeySepolia, whskHashkeySepolia, chainIdHashkeySepolia); - } else if (network == SupportedNetworks.INK_SEPOLIA){ - return (routerInkSepolia, linkInkSepolia, wethInkSepolia, chainIdInkSepolia); - } - } -} diff --git a/script/examples/Example01.s.sol b/script/examples/Example01.s.sol new file mode 100644 index 0000000..744da17 --- /dev/null +++ b/script/examples/Example01.s.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract Example01 is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + address tokenToSend, + uint256 amount, + uint32 gasLimit, + uint16 blockConfirmations, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require( + blockConfirmations > 0, "blockConfirmations == 0 means default finality; use > 0 for Faster Than Finality" + ); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + + console2.log("[INFO] Example01: Token transfer + ExtraArgsV3 + Faster Than Finality (EOA sender)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] Use gasLimit 0 if the receiver is an EOA (token-only transfer)."); + console2.log("[WARN] Executor may enforce a minimum block confirmation value and revert if too low."); + console2.log("[WARN] If requested confirmations exceed chain finality, default finality is used."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Example02.s.sol b/script/examples/Example02.s.sol new file mode 100644 index 0000000..11c05dc --- /dev/null +++ b/script/examples/Example02.s.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {BasicMessageReceiverWithCCVs} from "src/BasicMessageReceiverWithCCVs.sol"; +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract DeployBasicMessageReceiverWithCCVs is Script { + function run(address ccipRouter) external returns (address receiverAddress) { + vm.startBroadcast(); + + BasicMessageReceiverWithCCVs basicMessageReceiverWithCCVs = new BasicMessageReceiverWithCCVs(ccipRouter); + receiverAddress = address(basicMessageReceiverWithCCVs); + console2.log( + "[RESULT] BasicMessageReceiverWithCCVs deployed to chain ID:", block.chainid, "with address:", receiverAddress + ); + console2.log("[INFO] Default min block depth is 0 (finality-only) until configured per source chain."); + + vm.stopBroadcast(); + } +} + +contract SetBasicMessageReceiverWithCCVsMinBlockDepth is Script { + function run(address receiver, uint64 sourceChainSelector, uint16 minBlockDepth) external { + require(receiver != address(0), "receiver cannot be zero"); + require(sourceChainSelector != 0, "sourceChainSelector cannot be zero"); + + vm.startBroadcast(); + BasicMessageReceiverWithCCVs(receiver).setMinBlockDepth(sourceChainSelector, minBlockDepth); + vm.stopBroadcast(); + + console2.log("[RESULT] BasicMessageReceiverWithCCVs source chain selector:", sourceChainSelector); + console2.log("[RESULT] BasicMessageReceiverWithCCVs min block depth:", minBlockDepth); + } +} + +contract Example02 is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + string calldata messageText, + uint32 gasLimit, + uint16 blockConfirmations, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(bytes(messageText).length > 0, "messageText cannot be empty"); + require(gasLimit > 0, "gasLimit must be > 0 for contract callback"); + require( + blockConfirmations > 0, "blockConfirmations == 0 means default finality; use > 0 for Faster Than Finality" + ); + + console2.log("[INFO] Example02: Hello World data message + Faster Than Finality (EOA sender)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] Executor may enforce a minimum block confirmation value and revert if too low."); + console2.log("[WARN] If requested confirmations exceed chain finality, default finality is used."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: abi.encode(messageText), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Example03.s.sol b/script/examples/Example03.s.sol new file mode 100644 index 0000000..372d204 --- /dev/null +++ b/script/examples/Example03.s.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract Example03 is Script { + function _buildMessage( + address receiver, + string calldata messageText, + address tokenToSend, + uint256 amount, + bytes memory extraArgs, + address feeTokenAddress + ) internal pure returns (Client.EVM2AnyMessage memory ccipMessage) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: abi.encode(messageText), + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + } + + function _sendMessage( + address sourceRouter, + uint64 destinationChainSelector, + address tokenToSend, + uint256 amount, + address feeTokenAddress, + Client.EVM2AnyMessage memory ccipMessage + ) internal returns (bytes32 messageId) { + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + } + + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + string calldata messageText, + address tokenToSend, + uint256 amount, + uint32 gasLimit, + uint16 blockConfirmations, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(bytes(messageText).length > 0, "messageText cannot be empty"); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + require(gasLimit > 0, "gasLimit must be > 0 for contract callback"); + require( + blockConfirmations > 0, "blockConfirmations == 0 means default finality; use > 0 for Faster Than Finality" + ); + + console2.log("[INFO] Example03: Programmable token transfer (data + token) + Faster Than Finality (EOA sender)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Message:", messageText); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] Executor may enforce a minimum block confirmation value and revert if too low."); + console2.log("[WARN] If requested confirmations exceed chain finality, default finality is used."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations); + + Client.EVM2AnyMessage memory ccipMessage = + _buildMessage(receiver, messageText, tokenToSend, amount, extraArgs, feeTokenAddress); + messageId = + _sendMessage(sourceRouter, destinationChainSelector, tokenToSend, amount, feeTokenAddress, ccipMessage); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Example04.s.sol b/script/examples/Example04.s.sol new file mode 100644 index 0000000..4785601 --- /dev/null +++ b/script/examples/Example04.s.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {BasicMessageReceiver} from "src/BasicMessageReceiver.sol"; +import {BasicMessageSender} from "src/BasicMessageSender.sol"; +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract DeployBasicMessageReceiver is Script { + function run(address ccipRouter) external returns (address receiverAddress) { + vm.startBroadcast(); + + BasicMessageReceiver basicMessageReceiver = new BasicMessageReceiver(ccipRouter); + receiverAddress = address(basicMessageReceiver); + console2.log( + "[RESULT] BasicMessageReceiver deployed to chain ID:", block.chainid, "with address:", receiverAddress + ); + + vm.stopBroadcast(); + } +} + +contract DeployBasicMessageSender is Script { + function run(address ccipRouter, address linkToken) external returns (address senderAddress) { + vm.startBroadcast(); + + BasicMessageSender basicMessageSender = new BasicMessageSender(ccipRouter, linkToken); + senderAddress = address(basicMessageSender); + console2.log("[RESULT] BasicMessageSender deployed to chain ID:", block.chainid, "with address:", senderAddress); + + vm.stopBroadcast(); + } +} + +contract Example04 is Script { + struct Example04Config { + address basicMessageSender; + address sourceRouter; + uint64 destinationChainSelector; + address receiver; + string messageText; + address tokenToSend; + uint256 amount; + uint32 gasLimit; + } + + function _buildMessage( + address receiver, + string memory messageText, + address tokenToSend, + uint256 amount, + bytes memory extraArgs + ) internal pure returns (Client.EVM2AnyMessage memory ccipMessage, Client.EVMTokenAmount[] memory tokenAmounts) { + tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: abi.encode(messageText), + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: address(0) + }); + } + + function _run(Example04Config memory cfg) internal returns (bytes32 messageId) { + require(cfg.basicMessageSender != address(0), "basicMessageSender cannot be zero"); + require(cfg.sourceRouter != address(0), "sourceRouter cannot be zero"); + require(cfg.receiver != address(0), "receiver cannot be zero"); + require(bytes(cfg.messageText).length > 0, "messageText cannot be empty"); + require(cfg.tokenToSend != address(0), "token cannot be zero"); + require(cfg.amount > 0, "amount must be > 0"); + require(cfg.gasLimit > 0, "gasLimit must be > 0 for contract callback"); + + uint16 blockConfirmations = 0; + + console2.log( + "[INFO] Example04: Programmable token transfer (data + token) + default finality (sender contract)" + ); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Sender contract:", cfg.basicMessageSender); + console2.log("[INFO] Source router:", cfg.sourceRouter); + console2.log("[INFO] Destination selector:", cfg.destinationChainSelector); + console2.log("[INFO] Receiver:", cfg.receiver); + console2.log("[INFO] Message:", cfg.messageText); + console2.log("[INFO] Token:", cfg.tokenToSend); + console2.log("[INFO] Amount:", cfg.amount); + console2.log("[INFO] Gas limit:", cfg.gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations, "(default finality)"); + console2.log("[INFO] Fee token: native"); + console2.log("[INFO] This example funds sender contract with exact quoted fee before send."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(cfg.gasLimit, blockConfirmations); + + (Client.EVM2AnyMessage memory ccipMessage, Client.EVMTokenAmount[] memory tokenAmounts) = + _buildMessage(cfg.receiver, cfg.messageText, cfg.tokenToSend, cfg.amount, extraArgs); + uint256 fee = IRouterClient(cfg.sourceRouter).getFee(cfg.destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + console2.log("[INFO] Funding sender contract with exact quoted fee (no refund pattern in this example)"); + + vm.startBroadcast(); + IERC20(cfg.tokenToSend).approve(cfg.basicMessageSender, cfg.amount); + + (bool funded,) = payable(cfg.basicMessageSender).call{value: fee}(""); + require(funded, "fee funding transfer failed"); + + messageId = BasicMessageSender(payable(cfg.basicMessageSender)).send( + cfg.destinationChainSelector, cfg.receiver, abi.encode(cfg.messageText), tokenAmounts, extraArgs, address(0) + ); + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } + + function run( + address basicMessageSender, + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + string calldata messageText, + address tokenToSend, + uint256 amount, + uint32 gasLimit + ) external returns (bytes32 messageId) { + Example04Config memory cfg = Example04Config({ + basicMessageSender: basicMessageSender, + sourceRouter: sourceRouter, + destinationChainSelector: destinationChainSelector, + receiver: receiver, + messageText: messageText, + tokenToSend: tokenToSend, + amount: amount, + gasLimit: gasLimit + }); + + messageId = _run(cfg); + } +} diff --git a/script/examples/Example05.s.sol b/script/examples/Example05.s.sol new file mode 100644 index 0000000..581c3cf --- /dev/null +++ b/script/examples/Example05.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract Example05 is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + string calldata messageText, + uint32 gasLimit, + uint16 blockConfirmations, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(bytes(messageText).length > 0, "messageText cannot be empty"); + require(gasLimit > 0, "gasLimit must be > 0 for contract callback"); + + console2.log("[INFO] Example05: ExtraArgsV3 no-execution-tag (manual execution path)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Message:", messageText); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[INFO] Executor tag: NO_EXECUTION_ADDRESS (disable automatic execution)"); + console2.log("[WARN] Message delivery stays pending until manually executed."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + address noExecutionAddress = extraArgsEncoder.getNoExecutionAddress(); + bytes memory extraArgs = extraArgsEncoder.encodeV3( + gasLimit, blockConfirmations, new address[](0), new bytes[](0), noExecutionAddress, "", "", "" + ); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: abi.encode(messageText), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Example06.s.sol b/script/examples/Example06.s.sol new file mode 100644 index 0000000..9194225 --- /dev/null +++ b/script/examples/Example06.s.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {BaseERC20} from "@chainlink/contracts-ccip/contracts/tokens/BaseERC20.sol"; +import {CrossChainToken} from "@chainlink/contracts-ccip/contracts/tokens/CrossChainToken.sol"; +import {BurnMintTokenPool} from "@chainlink/contracts-ccip/contracts/pools/BurnMintTokenPool.sol"; +import {TokenPool} from "@chainlink/contracts-ccip/contracts/pools/TokenPool.sol"; +import {ITokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/interfaces/ITokenAdminRegistry.sol"; +import {IBurnMintERC20} from "@chainlink/contracts-ccip/contracts/interfaces/IBurnMintERC20.sol"; +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {RegistryModuleOwnerCustom} from + "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; +import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract DeployCCTBurnMintTokenAndPool is Script { + string internal constant TOKEN_NAME = "TestToken"; + string internal constant TOKEN_SYMBOL = "TEST"; + uint8 internal constant TOKEN_DECIMALS = 18; + uint256 internal constant TOKEN_PREMINT = 1_000_000 ether; + uint256 internal constant TOKEN_MAX_SUPPLY = 100_000_000 ether; + + function run(address tokenAdminRegistry, address registryModuleOwnerCustom, address armProxy, address router) + external + returns (address token, address pool) + { + require(tokenAdminRegistry != address(0), "tokenAdminRegistry cannot be zero"); + require(registryModuleOwnerCustom != address(0), "registryModuleOwnerCustom cannot be zero"); + require(armProxy != address(0), "armProxy cannot be zero"); + require(router != address(0), "router cannot be zero"); + + console2.log("[INFO] Example06 (CCT 01): Deploy CrossChainToken + BurnMint pool"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] TokenAdminRegistry:", tokenAdminRegistry); + console2.log("[INFO] RegistryModuleOwnerCustom:", registryModuleOwnerCustom); + console2.log("[INFO] ARM proxy:", armProxy); + console2.log("[INFO] Router:", router); + console2.log("[INFO] Token name:", TOKEN_NAME); + console2.log("[INFO] Token symbol:", TOKEN_SYMBOL); + console2.log("[INFO] Token decimals:", uint256(TOKEN_DECIMALS)); + console2.log("[INFO] Token preMint:", TOKEN_PREMINT); + console2.log("[INFO] Token maxSupply:", TOKEN_MAX_SUPPLY); + + vm.startBroadcast(); + (, address broadcaster,) = vm.readCallers(); + + BaseERC20.ConstructorParams memory tokenParams = BaseERC20.ConstructorParams({ + name: TOKEN_NAME, + symbol: TOKEN_SYMBOL, + maxSupply: TOKEN_MAX_SUPPLY, + preMint: TOKEN_PREMINT, + preMintRecipient: broadcaster, + decimals: TOKEN_DECIMALS, + ccipAdmin: broadcaster + }); + CrossChainToken tokenContract = new CrossChainToken(tokenParams, broadcaster, broadcaster); + + BurnMintTokenPool poolContract = new BurnMintTokenPool( + IBurnMintERC20(address(tokenContract)), + TOKEN_DECIMALS, + address(0), // No advanced pool hook in CCT 01 + armProxy, + router + ); + + // Token pool needs mint and burn roles on the local token. + tokenContract.grantMintAndBurnRoles(address(poolContract)); + + // Register token admin and attach pool in TokenAdminRegistry (CrossChainToken uses getCCIPAdmin, not owner()). + RegistryModuleOwnerCustom(registryModuleOwnerCustom).registerAdminViaGetCCIPAdmin(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).acceptAdminRole(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).setPool(address(tokenContract), address(poolContract)); + + vm.stopBroadcast(); + + token = address(tokenContract); + pool = address(poolContract); + + console2.log("[RESULT] Local CrossChainToken deployed:", token); + console2.log("[RESULT] Local CCT BurnMint pool deployed:", pool); + console2.log("[WARN] Remote chain configuration is not done yet."); + console2.log("[WARN] Run Example06.run on both chains to link pools and tokens."); + } +} + +contract Example06 is Script { + function run(address localPool, uint64 remoteChainSelector, address remoteToken, address remotePool) external { + require(localPool != address(0), "localPool cannot be zero"); + require(remoteChainSelector != 0, "remoteChainSelector cannot be zero"); + require(remoteToken != address(0), "remoteToken cannot be zero"); + require(remotePool != address(0), "remotePool cannot be zero"); + + console2.log("[INFO] Example06 (CCT 01): Configure BurnMint pool remote lane"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Local pool:", localPool); + console2.log("[INFO] Remote chain selector:", remoteChainSelector); + console2.log("[INFO] Remote token:", remoteToken); + console2.log("[INFO] Remote pool:", remotePool); + + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(remotePool); + + RateLimiter.Config memory disabledRateLimiter = RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: remoteChainSelector, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(remoteToken), + outboundRateLimiterConfig: disabledRateLimiter, + inboundRateLimiterConfig: disabledRateLimiter + }); + + bool chainAlreadyConfigured = TokenPool(localPool).isSupportedChain(remoteChainSelector); + uint64[] memory remoteChainSelectorsToRemove = chainAlreadyConfigured ? new uint64[](1) : new uint64[](0); + if (chainAlreadyConfigured) { + remoteChainSelectorsToRemove[0] = remoteChainSelector; + console2.log("[WARN] Existing config detected for this remote chain selector; replacing it."); + } else { + console2.log("[INFO] No existing config for this remote chain selector; adding new one."); + } + + vm.startBroadcast(); + TokenPool(localPool).applyChainUpdates(remoteChainSelectorsToRemove, chainUpdates); + vm.stopBroadcast(); + + bytes memory configuredRemoteTokenBytes = TokenPool(localPool).getRemoteToken(remoteChainSelector); + bytes[] memory configuredRemotePools = TokenPool(localPool).getRemotePools(remoteChainSelector); + address configuredRemoteToken = abi.decode(configuredRemoteTokenBytes, (address)); + address configuredRemotePool = abi.decode(configuredRemotePools[0], (address)); + + console2.log("[RESULT] Remote token configured on pool:", configuredRemoteToken); + console2.log("[RESULT] Remote pool configured on pool:", configuredRemotePool); + console2.log("[RESULT] BurnMint pool lane setup completed for remote chain selector:", remoteChainSelector); + } +} + +/// @notice Sets `TokenPool` allowed finality (`FinalityCodec` bytes4). Use `FinalityCodec._encodeBlockDepth(depth)` for a depth-only policy; `bytes4(0)` waits for full finality. +contract Example06SetPoolAllowedFinalityConfig is Script { + function run(address localPool, bytes4 allowedFinality) external returns (bytes4 updatedAllowedFinality) { + require(localPool != address(0), "localPool cannot be zero"); + + console2.log("[INFO] Example06 (CCT 01): Set pool allowed finality config"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Local pool:", localPool); + console2.logBytes4(TokenPool(localPool).getAllowedFinalityConfig()); + console2.logBytes4(allowedFinality); + + vm.startBroadcast(); + TokenPool(localPool).setAllowedFinalityConfig(allowedFinality); + vm.stopBroadcast(); + + updatedAllowedFinality = TokenPool(localPool).getAllowedFinalityConfig(); + console2.logBytes4(updatedAllowedFinality); + } +} + +contract Example06GetPoolAllowedFinalityConfig is Script { + function run(address localPool) external view returns (bytes4 allowedFinality) { + require(localPool != address(0), "localPool cannot be zero"); + + allowedFinality = TokenPool(localPool).getAllowedFinalityConfig(); + + console2.log("[INFO] Example06 (CCT 01): Read pool allowed finality config"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Local pool:", localPool); + console2.logBytes4(allowedFinality); + } +} + +contract SendCCTTokenWithExtraArgsV3DefaultFinality is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + address tokenToSend, + uint256 amount, + uint32 gasLimit, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + + uint16 blockConfirmations = 0; // Default finality + + console2.log("[INFO] CCT sender: ExtraArgsV3 token transfer (default finality)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] blockConfirmations = 0 means default finality."); + console2.log("[WARN] Use gasLimit 0 if receiver is an EOA."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} + +contract SendCCTTokenWithExtraArgsV3CustomFinality is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + address tokenToSend, + uint256 amount, + uint32 gasLimit, + uint16 blockConfirmations, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + require(blockConfirmations > 0, "blockConfirmations must be > 0 in custom-finality sender"); + + console2.log("[INFO] CCT sender: ExtraArgsV3 token transfer (custom finality)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Block confirmations:", blockConfirmations); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] This can revert if pool/executor min confirmations are higher than requested."); + console2.log("[WARN] Use gasLimit 0 if receiver is an EOA."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV3Basic(gasLimit, blockConfirmations); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Example07.s.sol b/script/examples/Example07.s.sol new file mode 100644 index 0000000..4687cb5 --- /dev/null +++ b/script/examples/Example07.s.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {BaseERC20} from "@chainlink/contracts-ccip/contracts/tokens/BaseERC20.sol"; +import {CrossChainToken} from "@chainlink/contracts-ccip/contracts/tokens/CrossChainToken.sol"; +import {LockReleaseTokenPool} from "@chainlink/contracts-ccip/contracts/pools/LockReleaseTokenPool.sol"; +import {ERC20LockBox} from "@chainlink/contracts-ccip/contracts/pools/ERC20LockBox.sol"; +import {TokenPool} from "@chainlink/contracts-ccip/contracts/pools/TokenPool.sol"; +import {ITokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/interfaces/ITokenAdminRegistry.sol"; +import {RegistryModuleOwnerCustom} from + "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; +import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol"; + +import {AuthorizedCallers} from "@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract DeployCCTLockReleaseTokenAndPool is Script { + string internal constant TOKEN_NAME = "TestToken"; + string internal constant TOKEN_SYMBOL = "TEST"; + uint8 internal constant TOKEN_DECIMALS = 18; + uint256 internal constant TOKEN_PREMINT = 1_000_000 ether; + uint256 internal constant TOKEN_MAX_SUPPLY = 100_000_000 ether; + + function run(address tokenAdminRegistry, address registryModuleOwnerCustom, address armProxy, address router) + external + returns (address token, address lockBox, address pool) + { + require(tokenAdminRegistry != address(0), "tokenAdminRegistry cannot be zero"); + require(registryModuleOwnerCustom != address(0), "registryModuleOwnerCustom cannot be zero"); + require(armProxy != address(0), "armProxy cannot be zero"); + require(router != address(0), "router cannot be zero"); + + console2.log("[INFO] Example07 (CCT 02): Deploy LockRelease token + lock box + pool"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] TokenAdminRegistry:", tokenAdminRegistry); + console2.log("[INFO] RegistryModuleOwnerCustom:", registryModuleOwnerCustom); + console2.log("[INFO] ARM proxy:", armProxy); + console2.log("[INFO] Router:", router); + console2.log("[INFO] Token name:", TOKEN_NAME); + console2.log("[INFO] Token symbol:", TOKEN_SYMBOL); + console2.log("[INFO] Token decimals:", uint256(TOKEN_DECIMALS)); + console2.log("[INFO] Token preMint:", TOKEN_PREMINT); + console2.log("[INFO] Token maxSupply:", TOKEN_MAX_SUPPLY); + + vm.startBroadcast(); + (, address broadcaster,) = vm.readCallers(); + + BaseERC20.ConstructorParams memory tokenParams = BaseERC20.ConstructorParams({ + name: TOKEN_NAME, + symbol: TOKEN_SYMBOL, + maxSupply: TOKEN_MAX_SUPPLY, + preMint: TOKEN_PREMINT, + preMintRecipient: broadcaster, + decimals: TOKEN_DECIMALS, + ccipAdmin: broadcaster + }); + CrossChainToken tokenContract = new CrossChainToken(tokenParams, broadcaster, broadcaster); + ERC20LockBox lockBoxContract = new ERC20LockBox(address(tokenContract)); + LockReleaseTokenPool poolContract = new LockReleaseTokenPool( + IERC20(address(tokenContract)), + TOKEN_DECIMALS, + address(0), // No advanced pool hook in CCT 02 + armProxy, + router, + address(lockBoxContract) + ); + + address[] memory addedCallers = new address[](1); + addedCallers[0] = address(poolContract); + lockBoxContract.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: addedCallers, removedCallers: new address[](0)}) + ); + + // Register token admin and attach pool in TokenAdminRegistry (CrossChainToken uses getCCIPAdmin, not owner()). + RegistryModuleOwnerCustom(registryModuleOwnerCustom).registerAdminViaGetCCIPAdmin(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).acceptAdminRole(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).setPool(address(tokenContract), address(poolContract)); + + vm.stopBroadcast(); + + token = address(tokenContract); + lockBox = address(lockBoxContract); + pool = address(poolContract); + + console2.log("[RESULT] Local CrossChainToken (LockRelease flow) deployed:", token); + console2.log("[RESULT] Local CCT lock box deployed:", lockBox); + console2.log("[RESULT] Local CCT LockRelease pool deployed:", pool); + console2.log("[WARN] Remote chain configuration is not done yet."); + console2.log("[WARN] Run Example07.run on both chains to link pools and tokens."); + } +} + +contract FundCCTLockBoxLiquidity is Script { + function run(address token, address lockBox, uint256 amount) external { + require(token != address(0), "token cannot be zero"); + require(lockBox != address(0), "lockBox cannot be zero"); + require(amount > 0, "amount must be > 0"); + + console2.log("[INFO] Example07 (CCT 02): Fund lock box liquidity"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Token:", token); + console2.log("[INFO] Lock box:", lockBox); + console2.log("[INFO] Amount:", amount); + + vm.startBroadcast(); + bool success = IERC20(token).transfer(lockBox, amount); + require(success, "lockBox funding failed"); + vm.stopBroadcast(); + + uint256 lockBoxBalance = IERC20(token).balanceOf(lockBox); + console2.log("[RESULT] Lock box funded. Current lock box balance:", lockBoxBalance); + } +} + +contract Example07 is Script { + function run(address localPool, uint64 remoteChainSelector, address remoteToken, address remotePool) external { + require(localPool != address(0), "localPool cannot be zero"); + require(remoteChainSelector != 0, "remoteChainSelector cannot be zero"); + require(remoteToken != address(0), "remoteToken cannot be zero"); + require(remotePool != address(0), "remotePool cannot be zero"); + + console2.log("[INFO] Example07 (CCT 02): Configure LockRelease pool remote lane"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Local pool:", localPool); + console2.log("[INFO] Remote chain selector:", remoteChainSelector); + console2.log("[INFO] Remote token:", remoteToken); + console2.log("[INFO] Remote pool:", remotePool); + + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(remotePool); + + RateLimiter.Config memory disabledRateLimiter = RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: remoteChainSelector, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(remoteToken), + outboundRateLimiterConfig: disabledRateLimiter, + inboundRateLimiterConfig: disabledRateLimiter + }); + + bool chainAlreadyConfigured = TokenPool(localPool).isSupportedChain(remoteChainSelector); + uint64[] memory remoteChainSelectorsToRemove = chainAlreadyConfigured ? new uint64[](1) : new uint64[](0); + if (chainAlreadyConfigured) { + remoteChainSelectorsToRemove[0] = remoteChainSelector; + console2.log("[WARN] Existing config detected for this remote chain selector; replacing it."); + } else { + console2.log("[INFO] No existing config for this remote chain selector; adding new one."); + } + + vm.startBroadcast(); + TokenPool(localPool).applyChainUpdates(remoteChainSelectorsToRemove, chainUpdates); + vm.stopBroadcast(); + + bytes memory configuredRemoteTokenBytes = TokenPool(localPool).getRemoteToken(remoteChainSelector); + bytes[] memory configuredRemotePools = TokenPool(localPool).getRemotePools(remoteChainSelector); + address configuredRemoteToken = abi.decode(configuredRemoteTokenBytes, (address)); + address configuredRemotePool = abi.decode(configuredRemotePools[0], (address)); + + console2.log("[RESULT] Remote token configured on pool:", configuredRemoteToken); + console2.log("[RESULT] Remote pool configured on pool:", configuredRemotePool); + console2.log("[RESULT] LockRelease pool lane setup completed for remote chain selector:", remoteChainSelector); + } +} diff --git a/script/examples/Example08.s.sol b/script/examples/Example08.s.sol new file mode 100644 index 0000000..7e3825f --- /dev/null +++ b/script/examples/Example08.s.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {BaseERC20} from "@chainlink/contracts-ccip/contracts/tokens/BaseERC20.sol"; +import {CrossChainToken} from "@chainlink/contracts-ccip/contracts/tokens/CrossChainToken.sol"; +import {AdvancedPoolHooks} from "@chainlink/contracts-ccip/contracts/pools/AdvancedPoolHooks.sol"; +import {BurnMintTokenPool} from "@chainlink/contracts-ccip/contracts/pools/BurnMintTokenPool.sol"; +import {TokenPool} from "@chainlink/contracts-ccip/contracts/pools/TokenPool.sol"; +import {ITokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/interfaces/ITokenAdminRegistry.sol"; +import {IBurnMintERC20} from "@chainlink/contracts-ccip/contracts/interfaces/IBurnMintERC20.sol"; +import {RegistryModuleOwnerCustom} from + "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; +import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol"; +import {AuthorizedCallers} from "@chainlink/contracts/src/v0.8/shared/access/AuthorizedCallers.sol"; + +contract DeployCCTBurnMintTokenAndPoolWithAdvancedPoolHook is Script { + string internal constant TOKEN_NAME = "TestToken"; + string internal constant TOKEN_SYMBOL = "TEST"; + uint8 internal constant TOKEN_DECIMALS = 18; + uint256 internal constant TOKEN_PREMINT = 1_000_000 ether; + uint256 internal constant TOKEN_MAX_SUPPLY = 100_000_000 ether; + + function run( + address tokenAdminRegistry, + address registryModuleOwnerCustom, + address armProxy, + address router, + uint256 thresholdAmountForAdditionalCCVs + ) + external + returns (address token, address advancedPoolHook, address pool) + { + require(tokenAdminRegistry != address(0), "tokenAdminRegistry cannot be zero"); + require(registryModuleOwnerCustom != address(0), "registryModuleOwnerCustom cannot be zero"); + require(armProxy != address(0), "armProxy cannot be zero"); + require(router != address(0), "router cannot be zero"); + + console2.log("[INFO] Example08 (CCT 03): Deploy CrossChainToken + AdvancedPoolHooks + BurnMint pool"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] TokenAdminRegistry:", tokenAdminRegistry); + console2.log("[INFO] RegistryModuleOwnerCustom:", registryModuleOwnerCustom); + console2.log("[INFO] ARM proxy:", armProxy); + console2.log("[INFO] Router:", router); + console2.log("[INFO] Token name:", TOKEN_NAME); + console2.log("[INFO] Token symbol:", TOKEN_SYMBOL); + console2.log("[INFO] Token decimals:", uint256(TOKEN_DECIMALS)); + console2.log("[INFO] Token preMint:", TOKEN_PREMINT); + console2.log("[INFO] Token maxSupply:", TOKEN_MAX_SUPPLY); + console2.log("[INFO] Hook thresholdAmountForAdditionalCCVs:", thresholdAmountForAdditionalCCVs); + + vm.startBroadcast(); + (, address broadcaster,) = vm.readCallers(); + + // Enable allowlist on the hook and seed it with broadcaster for immediate testing UX. + address[] memory allowlist = new address[](1); + allowlist[0] = broadcaster; + address[] memory authorizedCallers = new address[](0); + AdvancedPoolHooks advancedPoolHooks = new AdvancedPoolHooks( + allowlist, + thresholdAmountForAdditionalCCVs, + address(0), // policyEngine disabled + authorizedCallers // authorized callers disabled + ); + + BaseERC20.ConstructorParams memory tokenParams = BaseERC20.ConstructorParams({ + name: TOKEN_NAME, + symbol: TOKEN_SYMBOL, + maxSupply: TOKEN_MAX_SUPPLY, + preMint: TOKEN_PREMINT, + preMintRecipient: broadcaster, + decimals: TOKEN_DECIMALS, + ccipAdmin: broadcaster + }); + CrossChainToken tokenContract = new CrossChainToken(tokenParams, broadcaster, broadcaster); + BurnMintTokenPool poolContract = new BurnMintTokenPool( + IBurnMintERC20(address(tokenContract)), TOKEN_DECIMALS, address(advancedPoolHooks), armProxy, router + ); + + // AdvancedPoolHooks requires the pool to be an authorized caller for preflight/postflight checks. + address[] memory addedCallers = new address[](1); + addedCallers[0] = address(poolContract); + advancedPoolHooks.applyAuthorizedCallerUpdates( + AuthorizedCallers.AuthorizedCallerArgs({addedCallers: addedCallers, removedCallers: new address[](0)}) + ); + + // Token pool needs mint and burn roles on the local token. + tokenContract.grantMintAndBurnRoles(address(poolContract)); + + // Register token admin and attach pool in TokenAdminRegistry (CrossChainToken uses getCCIPAdmin, not owner()). + RegistryModuleOwnerCustom(registryModuleOwnerCustom).registerAdminViaGetCCIPAdmin(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).acceptAdminRole(address(tokenContract)); + ITokenAdminRegistry(tokenAdminRegistry).setPool(address(tokenContract), address(poolContract)); + + vm.stopBroadcast(); + + token = address(tokenContract); + advancedPoolHook = address(advancedPoolHooks); + pool = address(poolContract); + + console2.log("[RESULT] Local CrossChainToken deployed:", token); + console2.log("[RESULT] Local AdvancedPoolHooks deployed:", advancedPoolHook); + console2.log("[RESULT] Local CCT BurnMint pool deployed:", pool); + console2.log("[RESULT] Authorized caller added to hook:", pool); + console2.log("[WARN] Remote chain configuration is not done yet."); + console2.log("[WARN] Run Example08.run on both chains to link pools and tokens."); + } +} + +contract Example08UpdateAllowlist is Script { + function run(address advancedPoolHook, address[] calldata removes, address[] calldata adds) external { + require(advancedPoolHook != address(0), "advancedPoolHook cannot be zero"); + + console2.log("[INFO] Example08 (CCT 03): Update AdvancedPoolHooks allowlist"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] AdvancedPoolHooks:", advancedPoolHook); + console2.log("[INFO] Removes count:", removes.length); + console2.log("[INFO] Adds count:", adds.length); + + bool allowListEnabled = AdvancedPoolHooks(advancedPoolHook).getAllowListEnabled(); + require(allowListEnabled, "allowlist is disabled on hook"); + + vm.startBroadcast(); + AdvancedPoolHooks(advancedPoolHook).applyAllowListUpdates(removes, adds); + vm.stopBroadcast(); + + address[] memory currentAllowList = AdvancedPoolHooks(advancedPoolHook).getAllowList(); + console2.log("[RESULT] Allowlist updated. Current entries:", currentAllowList.length); + for (uint256 i = 0; i < currentAllowList.length; ++i) { + console2.log("[RESULT] Allowlist entry:", currentAllowList[i]); + } + } +} + +contract Example08 is Script { + function run(address localPool, uint64 remoteChainSelector, address remoteToken, address remotePool) external { + require(localPool != address(0), "localPool cannot be zero"); + require(remoteChainSelector != 0, "remoteChainSelector cannot be zero"); + require(remoteToken != address(0), "remoteToken cannot be zero"); + require(remotePool != address(0), "remotePool cannot be zero"); + + console2.log("[INFO] Example08 (CCT 03): Configure BurnMint pool remote lane"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Local pool:", localPool); + console2.log("[INFO] Remote chain selector:", remoteChainSelector); + console2.log("[INFO] Remote token:", remoteToken); + console2.log("[INFO] Remote pool:", remotePool); + + bytes[] memory remotePoolAddresses = new bytes[](1); + remotePoolAddresses[0] = abi.encode(remotePool); + + RateLimiter.Config memory disabledRateLimiter = RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}); + TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1); + chainUpdates[0] = TokenPool.ChainUpdate({ + remoteChainSelector: remoteChainSelector, + remotePoolAddresses: remotePoolAddresses, + remoteTokenAddress: abi.encode(remoteToken), + outboundRateLimiterConfig: disabledRateLimiter, + inboundRateLimiterConfig: disabledRateLimiter + }); + + bool chainAlreadyConfigured = TokenPool(localPool).isSupportedChain(remoteChainSelector); + uint64[] memory remoteChainSelectorsToRemove = chainAlreadyConfigured ? new uint64[](1) : new uint64[](0); + if (chainAlreadyConfigured) { + remoteChainSelectorsToRemove[0] = remoteChainSelector; + console2.log("[WARN] Existing config detected for this remote chain selector; replacing it."); + } else { + console2.log("[INFO] No existing config for this remote chain selector; adding new one."); + } + + vm.startBroadcast(); + TokenPool(localPool).applyChainUpdates(remoteChainSelectorsToRemove, chainUpdates); + vm.stopBroadcast(); + + bytes memory configuredRemoteTokenBytes = TokenPool(localPool).getRemoteToken(remoteChainSelector); + bytes[] memory configuredRemotePools = TokenPool(localPool).getRemotePools(remoteChainSelector); + address configuredRemoteToken = abi.decode(configuredRemoteTokenBytes, (address)); + address configuredRemotePool = abi.decode(configuredRemotePools[0], (address)); + + console2.log("[RESULT] Remote token configured on pool:", configuredRemoteToken); + console2.log("[RESULT] Remote pool configured on pool:", configuredRemotePool); + console2.log("[RESULT] BurnMint pool lane setup completed for remote chain selector:", remoteChainSelector); + } +} diff --git a/script/examples/Legacy01.s.sol b/script/examples/Legacy01.s.sol new file mode 100644 index 0000000..65408b5 --- /dev/null +++ b/script/examples/Legacy01.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract Legacy01 is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + address tokenToSend, + uint256 amount, + uint256 gasLimit, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + + console2.log("[INFO] Legacy01: ExtraArgsV1 token transfer (EOA sender)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] Use gasLimit 0 if receiver is an EOA."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV1(gasLimit); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/script/examples/Legacy02.s.sol b/script/examples/Legacy02.s.sol new file mode 100644 index 0000000..6f8a1f9 --- /dev/null +++ b/script/examples/Legacy02.s.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; + +import {EncodeExtraArgsOffchain} from "../EncodeExtraArgsOffchain.s.sol"; + +import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; + +contract Legacy02 is Script { + function run( + address sourceRouter, + uint64 destinationChainSelector, + address receiver, + address tokenToSend, + uint256 amount, + uint256 gasLimit, + bool allowOutOfOrderExecution, + address feeTokenAddress + ) external returns (bytes32 messageId) { + require(sourceRouter != address(0), "sourceRouter cannot be zero"); + require(receiver != address(0), "receiver cannot be zero"); + require(tokenToSend != address(0), "token cannot be zero"); + require(amount > 0, "amount must be > 0"); + + console2.log("[INFO] Legacy02: ExtraArgsV2 token transfer (EOA sender)"); + console2.log("[INFO] Source chain ID:", block.chainid); + console2.log("[INFO] Source router:", sourceRouter); + console2.log("[INFO] Destination selector:", destinationChainSelector); + console2.log("[INFO] Receiver:", receiver); + console2.log("[INFO] Token:", tokenToSend); + console2.log("[INFO] Amount:", amount); + console2.log("[INFO] Gas limit:", gasLimit); + console2.log("[INFO] Allow out-of-order execution:", allowOutOfOrderExecution); + console2.log("[INFO] Fee token:", feeTokenAddress); + console2.log("[WARN] Use gasLimit 0 if receiver is an EOA."); + + EncodeExtraArgsOffchain extraArgsEncoder = new EncodeExtraArgsOffchain(); + bytes memory extraArgs = extraArgsEncoder.encodeV2(gasLimit, allowOutOfOrderExecution); + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + tokenAmounts[0] = Client.EVMTokenAmount({token: tokenToSend, amount: amount}); + + Client.EVM2AnyMessage memory ccipMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: extraArgs, + feeToken: feeTokenAddress + }); + + uint256 fee = IRouterClient(sourceRouter).getFee(destinationChainSelector, ccipMessage); + console2.log("[INFO] Quoted fee:", fee); + + vm.startBroadcast(); + IERC20(tokenToSend).approve(sourceRouter, amount); + if (feeTokenAddress == address(0)) { + console2.log("[INFO] Sending message, paying CCIP fee in native token"); + messageId = IRouterClient(sourceRouter).ccipSend{value: fee}(destinationChainSelector, ccipMessage); + } else { + console2.log("[INFO] Sending message, paying CCIP fee in LINK"); + IERC20(feeTokenAddress).approve(sourceRouter, fee); + messageId = IRouterClient(sourceRouter).ccipSend(destinationChainSelector, ccipMessage); + } + vm.stopBroadcast(); + + console2.log("[RESULT] Monitor message status at https://ccip.chain.link using message ID:"); + console2.logBytes32(messageId); + } +} diff --git a/sdk-examples/README.md b/sdk-examples/README.md new file mode 100644 index 0000000..c1d958d --- /dev/null +++ b/sdk-examples/README.md @@ -0,0 +1,41 @@ +# CCIP Examples Workspace + +This directory contains TypeScript examples built with `@chainlink/ccip-sdk`. + +## Install + +From repo root: + +```bash +npm --prefix sdk-examples install +``` + +Add private key: + +```bash +USER_KEY=0x... +``` + +```bash +source .env +``` + +Run available scripts: + +```bash +npm --prefix sdk-examples run ccip-send -- --help +npm --prefix sdk-examples run ccip-track -- --help +``` + +## API vs CLI vs SDK + +- `CCIP API`: HTTP endpoints for querying message/transfer state and metadata +- `CCIP CLI`: terminal commands for inspect/send/manual-exec workflows +- `CCIP SDK`: TypeScript library for building CCIP features directly in your app/scripts + +## Official Documentation + +- Tools overview: https://docs.chain.link/ccip/tools/ +- CCIP API docs: https://docs.chain.link/ccip/tools/api +- CCIP CLI docs: https://docs.chain.link/ccip/tools/cli +- CCIP SDK docs: https://docs.chain.link/ccip/tools/sdk diff --git a/sdk-examples/package-lock.json b/sdk-examples/package-lock.json new file mode 100644 index 0000000..57c86da --- /dev/null +++ b/sdk-examples/package-lock.json @@ -0,0 +1,2957 @@ +{ + "name": "ccip-sdk-examples", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ccip-sdk-examples", + "version": "0.1.0", + "dependencies": { + "@chainlink/ccip-sdk": "^0.97.0", + "dotenv": "^16.6.1", + "viem": "^2.39.1" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "tsx": "^4.20.6", + "typescript": "^5.9.2" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", + "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.15.2.tgz", + "integrity": "sha512-Ys031WnS3sTQQBtRTkQsYnw372OlW72ais4sp0oh2UMPRNyxxnq85zRfU4PIdoy9kWriysPT5BYAkgIxhbonFA==", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@aptos-labs/aptos-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@aptos-labs/aptos-cli/-/aptos-cli-1.1.1.tgz", + "integrity": "sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==", + "license": "Apache-2.0", + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "aptos": "dist/aptos.js" + } + }, + "node_modules/@aptos-labs/aptos-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aptos-labs/aptos-client/-/aptos-client-2.1.0.tgz", + "integrity": "sha512-ttdY0qclRvbYAAwzijkFeipuqTfLFJnoXlNIm58tIw3DKhIlfYdR6iLqTeCpI23oOPghnO99FZecej/0MTrtuA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "got": "^11.8.6" + } + }, + "node_modules/@aptos-labs/ts-sdk": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@aptos-labs/ts-sdk/-/ts-sdk-5.2.1.tgz", + "integrity": "sha512-kazYjqfsPCBx2UJI+nYUOb6Ov7q7brSgYEfxp2sP27IeJWdDNa50lfs0WIpDJ92kQxdtlm9q3ZWw7Toh9f1gxQ==", + "license": "Apache-2.0", + "dependencies": { + "@aptos-labs/aptos-cli": "^1.0.2", + "@aptos-labs/aptos-client": "^2.1.0", + "@noble/curves": "^1.9.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", + "eventemitter3": "^5.0.1", + "js-base64": "^3.7.7", + "jwt-decode": "^4.0.0", + "poseidon-lite": "^0.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chainlink/ccip-sdk": { + "version": "0.97.0", + "resolved": "https://registry.npmjs.org/@chainlink/ccip-sdk/-/ccip-sdk-0.97.0.tgz", + "integrity": "sha512-vfp6r8Rmb4HPXXi+XG4gw/xATJq2wVXOXyI5/guSOfx701z9U+Y7TDrB88/cAPNfSrm1nDJMkN1idwq4qU99yw==", + "license": "MIT", + "dependencies": { + "@aptos-labs/ts-sdk": "^5.2.1", + "@coral-xyz/anchor": "^0.29.0", + "@mysten/bcs": "^1.9.2", + "@mysten/sui": "^1.45.2", + "@solana/spl-token": "0.4.14", + "@solana/web3.js": "^1.98.4", + "@ton/core": "0.63.1", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^16.2.2", + "abitype": "1.2.3", + "bn.js": "^5.2.3", + "borsh": "^2.0.0", + "bs58": "^6.0.0", + "buffer": "^6.0.3", + "ethers": "6.16.0", + "got": "^11.8.6", + "micro-memoize": "^5.1.1", + "type-fest": "^5.4.4", + "yaml": "2.8.2" + }, + "peerDependencies": { + "viem": "^2.0.0" + }, + "peerDependenciesMeta": { + "viem": { + "optional": true + } + } + }, + "node_modules/@coral-xyz/anchor": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.29.0.tgz", + "integrity": "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@coral-xyz/borsh": "^0.29.0", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.68.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/@coral-xyz/borsh": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.29.0.tgz", + "integrity": "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.68.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.7.2.tgz", + "integrity": "sha512-Qbc7hbLvCz6IliIJpJuKJa9p05b2Jona7ov7+qofCsMRxHRZE1kpAmZMvL8JCI4c0IagpIlWNaMizXEQUe8XjQ==", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/internal": "1.0.8", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/svelte-support": "1.0.1", + "@gql.tada/vue-support": "1.0.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", + "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.5" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@mysten/bcs": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-1.9.2.tgz", + "integrity": "sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "0.2.0", + "@scure/base": "^1.2.6" + } + }, + "node_modules/@mysten/sui": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-1.45.2.tgz", + "integrity": "sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "1.9.2", + "@mysten/utils": "0.2.0", + "@noble/curves": "=1.9.4", + "@noble/hashes": "^1.8.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^1.2.6", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "gql.tada": "^1.8.13", + "graphql": "^16.11.0", + "poseidon-lite": "0.2.1", + "valibot": "^1.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mysten/sui/node_modules/@noble/curves": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.2.0.tgz", + "integrity": "sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^1.2.6" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.11.1.tgz", + "integrity": "sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/buffer-layout-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", + "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@solana/codecs": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", + "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/options": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", + "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", + "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", + "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-strings": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", + "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": ">=5" + } + }, + "node_modules/@solana/errors": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", + "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/options": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", + "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/spl-token": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.14.tgz", + "integrity": "sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-group": "^0.0.7", + "@solana/spl-token-metadata": "^0.1.6", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.5" + } + }, + "node_modules/@solana/spl-token-group": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", + "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/spl-token-metadata": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz", + "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@solana/web3.js/node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@solana/web3.js/node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ton/core": { + "version": "0.63.1", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", + "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", + "license": "MIT", + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", + "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", + "license": "MIT", + "dependencies": { + "@ton/crypto-primitives": "2.1.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", + "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", + "license": "MIT", + "dependencies": { + "jssha": "3.2.0" + } + }, + "node_modules/@ton/ton": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-16.2.2.tgz", + "integrity": "sha512-yEOw4IW3gpRZxJAcILMI4dQ1d5/eAAbD2VU/Iwc6z7f2jt1mLDWVED8yn2vLNucQfZr+1eaqYHLztYVFZ7PKmw==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.7", + "dataloader": "^2.0.0", + "zod": "^3.21.4" + }, + "peerDependencies": { + "@ton/core": ">=0.63.0 <1.0.0", + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", + "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==", + "license": "Apache-2.0" + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-layout": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/buffer-layout/-/buffer-layout-1.2.2.tgz", + "integrity": "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==", + "license": "MIT", + "engines": { + "node": ">=4.5" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/crypto-hash": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/crypto-hash/-/crypto-hash-1.3.0.tgz", + "integrity": "sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/fast-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fast-stringify/-/fast-stringify-4.0.0.tgz", + "integrity": "sha512-lE2DIivBaLysf6hK5WH/VfMgqRbvBVHcpGVVTmA5Zi8oWIjq9YxIt6lYGdUgP1HNSXxTIat7HEIDnrSvXSeKQw==", + "license": "MIT" + }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/gql.tada": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.9.0.tgz", + "integrity": "sha512-1LMiA46dRs5oF7Qev6vMU32gmiNvM3+3nHoQZA9K9j2xQzH8xOAWnnJrLSbZOFHTSdFxqn86TL6beo1/7ja/aA==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.5", + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/cli-utils": "1.7.2", + "@gql.tada/internal": "1.0.8" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/graphql": { + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.0.tgz", + "integrity": "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micro-memoize": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-5.1.1.tgz", + "integrity": "sha512-QDwluos8YeMijiKxZGwaV4f4tzj0soS6+xcsJhJ3+4wdEIHMyKbIKVUziebOgWX3e6yiijdoaHo+9tyhbnaWXA==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.3.3", + "fast-stringify": "^4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/ox": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.4.tgz", + "integrity": "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/poseidon-lite": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.1.tgz", + "integrity": "sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rpc-websockets": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.3.tgz", + "integrity": "sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.46.3", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.3.tgz", + "integrity": "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.12.4", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/sdk-examples/package.json b/sdk-examples/package.json new file mode 100644 index 0000000..09c8826 --- /dev/null +++ b/sdk-examples/package.json @@ -0,0 +1,21 @@ +{ + "name": "ccip-sdk-examples", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "ccip-send": "node --import tsx src/ccip-send.ts", + "ccip-track": "node --import tsx src/ccip-track.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@chainlink/ccip-sdk": "^0.97.0", + "dotenv": "^16.6.1", + "viem": "^2.39.1" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "tsx": "^4.20.6", + "typescript": "^5.9.2" + } +} diff --git a/sdk-examples/src/ccip-send.ts b/sdk-examples/src/ccip-send.ts new file mode 100644 index 0000000..39c9206 --- /dev/null +++ b/sdk-examples/src/ccip-send.ts @@ -0,0 +1,155 @@ +import { EVMChain } from '@chainlink/ccip-sdk' +import { viemWallet } from '@chainlink/ccip-sdk/viem' +import { createWalletClient, defineChain, http, parseUnits, toHex } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { getArg, hasFlag, parseBlockConfirmations, requireArg, requireUserKey, resolveRpcUrl } from './helpers.js' + +type SendMode = 'data' | 'token' | 'token-data' + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +function printUsage(): void { + console.log('CCIP v2 send demo (generic lane)') + console.log('') + console.log('Usage:') + console.log( + ' npm --prefix sdk-examples run ccip-send -- --mode --router --receiver [options]', + ) + console.log('') + console.log('Lane options:') + console.log(' --source-rpc-url source RPC URL (or env CCIP_SOURCE_RPC_URL / AVALANCHE_FUJI_RPC_URL)') + console.log(' --dest-rpc-url destination RPC URL (or env CCIP_DEST_RPC_URL / ETHEREUM_SEPOLIA_RPC_URL)') + console.log('') + console.log('Message options:') + console.log(' --block-confirmations default: 1 (0 = default finality, >0 = Faster Than Finality)') + console.log(' --token required for token modes') + console.log(' --amount token amount in human units (default: 1)') + console.log(' --data payload text (default: hello from sdk-examples)') + console.log(' --fee-token default: native') + console.log(' --gas-limit default: 0 for token-only, 200000 otherwise') + console.log(' --dry-run quote only, do not send') +} + +function normalizeMode(input: string): SendMode { + if (input === 'data' || input === 'token' || input === 'token-data') return input + throw new Error(`Unsupported mode: ${input}. Use one of: data, token, token-data`) +} + +async function resolveFeeToken(source: EVMChain, router: string, feeTokenInput: string): Promise { + if (feeTokenInput === 'native') return ZERO_ADDRESS + if (feeTokenInput.startsWith('0x')) return feeTokenInput + if (feeTokenInput === 'link') { + const feeTokens = await source.getFeeTokens(router) + for (const [address, info] of Object.entries(feeTokens)) { + if (info.symbol.toUpperCase() === 'LINK') return address + } + throw new Error('LINK fee token not found for source router') + } + throw new Error(`Unsupported --fee-token value: ${feeTokenInput}`) +} + +async function main(): Promise { + if (hasFlag('--help')) { + printUsage() + return + } + + const mode = normalizeMode(requireArg('--mode')) + const router = requireArg('--router') + const receiver = requireArg('--receiver') + const sourceRpcUrl = resolveRpcUrl('--source-rpc-url') + const destRpcUrl = resolveRpcUrl('--dest-rpc-url') + + const token = getArg('--token') + const amountInput = getArg('--amount') ?? '1' + const payload = getArg('--data') ?? 'hello from sdk-examples' + const feeTokenInput = (getArg('--fee-token') ?? 'native').toLowerCase() + const blockConfirmations = parseBlockConfirmations(getArg('--block-confirmations') ?? '1') + const dryRun = hasFlag('--dry-run') + const gasLimit = BigInt(getArg('--gas-limit') ?? (mode === 'token' ? '0' : '200000')) + + if ((mode === 'token' || mode === 'token-data') && !token) { + throw new Error('--token is required for token modes') + } + + const source = await EVMChain.fromUrl(sourceRpcUrl) + const dest = await EVMChain.fromUrl(destRpcUrl) + const destChainSelector = dest.network.chainSelector + const feeToken = await resolveFeeToken(source, router, feeTokenInput) + + const tokenAmounts: Array<{ token: string; amount: bigint }> = [] + if (mode === 'token' || mode === 'token-data') { + const tokenInfo = await source.getTokenInfo(token!) + tokenAmounts.push({ + token: token!, + amount: parseUnits(amountInput, tokenInfo.decimals), + }) + } + + const message = { + receiver, + data: mode === 'token' ? '0x' : toHex(payload), + tokenAmounts, + feeToken, + extraArgs: { + gasLimit, + blockConfirmations, + }, + } + + const fee = await source.getFee({ + router, + destChainSelector, + message, + }) + + console.log({ + mode, + sourceNetwork: source.network.name, + sourceChainSelector: source.network.chainSelector.toString(), + destNetwork: dest.network.name, + destChainSelector: destChainSelector.toString(), + router, + receiver, + blockConfirmations, + feeToken, + fee: fee.toString(), + }) + + if (dryRun) return + + const account = privateKeyToAccount(requireUserKey()) + const sourceViemChain = defineChain({ + id: source.network.chainId, + name: source.network.name, + nativeCurrency: { name: 'Native', symbol: 'NATIVE', decimals: 18 }, + rpcUrls: { default: { http: [sourceRpcUrl] } }, + testnet: source.network.networkType === 'TESTNET', + }) + const walletClient = createWalletClient({ + account, + chain: sourceViemChain, + transport: http(sourceRpcUrl), + }) + + const request = await source.sendMessage({ + router, + destChainSelector, + message: { + ...message, + fee, + }, + wallet: viemWallet(walletClient), + }) + + console.log({ + txHash: request.tx.hash, + messageId: request.message.messageId, + }) +} + +main().catch((error) => { + console.error('CCIP v2 send demo failed') + console.error(error) + process.exitCode = 1 +}) diff --git a/sdk-examples/src/ccip-track.ts b/sdk-examples/src/ccip-track.ts new file mode 100644 index 0000000..972efbe --- /dev/null +++ b/sdk-examples/src/ccip-track.ts @@ -0,0 +1,181 @@ +import { EVMChain, getMessagesForSender } from '@chainlink/ccip-sdk' +import { inspect } from 'node:util' +import { getArg, hasFlag, resolveRpcUrl } from './helpers.js' + +function printUsage(): void { + console.log('CCIP v2 tracking demo (source chain)') + console.log('') + console.log('Usage:') + console.log(' npm --prefix sdk-examples run ccip-track -- --tx-hash [--source-rpc-url ]') + console.log(' npm --prefix sdk-examples run ccip-track -- --message-id [--source-rpc-url ]') + console.log( + ' npm --prefix sdk-examples run ccip-track -- --sender [--limit 10] [--start-block 0] [--source-rpc-url ]', + ) +} + +function parseNonNegativeInt(name: string, input: string, fallback: number): number { + const raw = input.trim() + if (!raw) return fallback + if (!/^\d+$/.test(raw)) throw new Error(`${name} must be a non-negative integer`) + const parsed = Number(raw) + if (!Number.isSafeInteger(parsed)) throw new Error(`${name} must be a safe integer`) + return parsed +} + +function pruneUndefined(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(pruneUndefined) + } + if (!value || typeof value !== 'object') { + return value + } + const out: Record = {} + for (const [k, v] of Object.entries(value)) { + if (v === undefined) continue + const pruned = pruneUndefined(v) + if (pruned !== undefined) out[k] = pruned + } + return out +} + +function asNonNegativeInt(value: unknown): number | undefined { + if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return Math.trunc(value) + if (typeof value === 'bigint' && value >= 0n) return Number(value) + if (typeof value === 'string' && /^\d+$/.test(value)) return Number(value) + return undefined +} + +function summaryOfRequest(request: any): Record { + const finalityThreshold = + request?.message?.blockConfirmations ?? + request?.message?.extraArgs?.blockConfirmations ?? + request?.message?.finality + const finalityThresholdInt = asNonNegativeInt(finalityThreshold) + + const transactionSpeed = + finalityThresholdInt === undefined + ? undefined + : finalityThresholdInt === 0 + ? 'default finality' + : 'faster than finality' + + const tokenAmounts = Array.isArray(request?.message?.tokenAmounts) + ? request.message.tokenAmounts + .map((tokenAmount: any) => + pruneUndefined({ + token: tokenAmount?.token ?? tokenAmount?.sourceTokenAddress, + amount: tokenAmount?.amount, + }), + ) + .filter((tokenAmount: any) => tokenAmount.token || tokenAmount.amount) + : undefined + + const ccipFees = pruneUndefined({ + feeToken: request?.message?.feeToken, + amount: request?.message?.feeTokenAmount, + }) as Record + + const verifiers = + (Array.isArray(request?.message?.ccvs) && request.message.ccvs.length > 0 ? request.message.ccvs : undefined) ?? + (Array.isArray(request?.message?.verifiers) && request.message.verifiers.length > 0 + ? request.message.verifiers + : undefined) + + return pruneUndefined({ + messageId: request?.message?.messageId, + sourceTxHash: request?.tx?.hash ?? request?.log?.transactionHash ?? request?.message?.sendTransactionHash, + destTxHash: request?.metadata?.receiptTransactionHash ?? request?.message?.receiptTransactionHash, + status: request?.metadata?.status ?? request?.message?.status, + sourceChain: + request?.metadata?.sourceNetworkInfo?.name ?? + request?.metadata?.sourceNetworkInfo?.chainSelector ?? + request?.lane?.sourceChainSelector, + destChain: + request?.metadata?.destNetworkInfo?.name ?? + request?.metadata?.destNetworkInfo?.chainSelector ?? + request?.lane?.destChainSelector, + txTimestamp: request?.tx?.timestamp ?? request?.message?.sendTimestamp, + origin: request?.message?.origin ?? request?.message?.sender, + from: request?.tx?.from ?? request?.message?.sender, + to: request?.tx?.to ?? request?.log?.address ?? request?.message?.receiver, + ccipFees: Object.keys(ccipFees).length > 0 ? ccipFees : undefined, + tokensAndAmountsSent: tokenAmounts && tokenAmounts.length > 0 ? tokenAmounts : undefined, + data: request?.message?.data, + senderNonce: request?.message?.nonce, + gasLimit: request?.message?.gasLimit ?? request?.message?.extraArgs?.gasLimit, + sequenceNumber: request?.message?.sequenceNumber, + transactionSpeed, + finalityThreshold: finalityThresholdInt ?? finalityThreshold, + verifiers, + executor: request?.message?.executor, + }) as Record +} + +function printSummary(prefix: string, request: unknown): void { + const useColors = !process.env.NO_COLOR + const title = useColors ? `\x1b[36m${prefix}\x1b[0m` : prefix + console.log(title) + console.log( + inspect(summaryOfRequest(request), { + colors: useColors, + depth: null, + compact: false, + sorted: false, + }), + ) +} + +async function main(): Promise { + if (hasFlag('--help')) { + printUsage() + return + } + + const txHash = getArg('--tx-hash') + const messageId = getArg('--message-id') + const sender = getArg('--sender') + const sourceRpcUrl = resolveRpcUrl('--source-rpc-url') + + const selectedModes = [Boolean(txHash), Boolean(messageId), Boolean(sender)].filter(Boolean).length + if (selectedModes === 0) { + printUsage() + process.exitCode = 1 + return + } + if (selectedModes > 1) { + throw new Error('Provide exactly one of --tx-hash, --message-id, or --sender') + } + + const chain = await EVMChain.fromUrl(sourceRpcUrl) + + if (txHash) { + const requests = await chain.getMessagesInTx(txHash) + console.log(`found ${requests.length} message(s) in tx ${txHash}`) + for (let i = 0; i < requests.length; i++) { + printSummary(`request ${i + 1}:`, requests[i]) + } + return + } + + if (messageId) { + const request = await chain.getMessageById(messageId) + printSummary('request:', request) + return + } + + const limit = parseNonNegativeInt('--limit', getArg('--limit') ?? '10', 10) + const startBlock = parseNonNegativeInt('--start-block', getArg('--start-block') ?? '0', 0) + + let count = 0 + for await (const request of getMessagesForSender(chain, sender!, { startBlock })) { + printSummary(`request ${count + 1}:`, request) + count++ + if (count >= limit) break + } +} + +main().catch((error) => { + console.error('CCIP v2 tracking demo failed') + console.error(error) + process.exitCode = 1 +}) diff --git a/sdk-examples/src/helpers.ts b/sdk-examples/src/helpers.ts new file mode 100644 index 0000000..4224b78 --- /dev/null +++ b/sdk-examples/src/helpers.ts @@ -0,0 +1,71 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import dotenv from 'dotenv' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') }) + +export function hasFlag(name: string): boolean { + return process.argv.includes(name) +} + +export function getArg(name: string): string | undefined { + const idx = process.argv.indexOf(name) + if (idx === -1 || idx + 1 >= process.argv.length) return undefined + const value = process.argv[idx + 1] + if (!value || value.startsWith('--')) return undefined + return value +} + +export function requireArg(name: string): string { + const value = getArg(name) + if (!value) throw new Error(`Missing required argument: ${name}`) + return value +} + +export function getEnvFirst(names: string[]): string | undefined { + for (const name of names) { + const value = process.env[name] + if (value) return value + } + return undefined +} + +export function resolveRpcUrl(flagName: '--source-rpc-url' | '--dest-rpc-url'): string { + const fromFlag = getArg(flagName) + if (fromFlag) return fromFlag + + const fromEnv = + flagName === '--source-rpc-url' + ? getEnvFirst(['CCIP_SOURCE_RPC_URL', 'AVALANCHE_FUJI_RPC_URL']) + : getEnvFirst(['CCIP_DEST_RPC_URL', 'ETHEREUM_SEPOLIA_RPC_URL']) + if (fromEnv) return fromEnv + + if (flagName === '--source-rpc-url') { + throw new Error( + 'Missing source RPC URL. Pass --source-rpc-url or set CCIP_SOURCE_RPC_URL (fallback AVALANCHE_FUJI_RPC_URL).', + ) + } + throw new Error( + 'Missing destination RPC URL. Pass --dest-rpc-url or set CCIP_DEST_RPC_URL (fallback ETHEREUM_SEPOLIA_RPC_URL).', + ) +} + +export function parseBlockConfirmations(input: string): number { + if (!/^\d+$/.test(input)) throw new Error(`Invalid --block-confirmations value: ${input}`) + const parsed = Number(input) + if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) { + throw new Error('--block-confirmations must be an integer between 0 and 65535') + } + return parsed +} + +export function requireUserKey(): `0x${string}` { + const key = process.env.USER_KEY ?? process.env.PRIVATE_KEY + if (!key) { + throw new Error('Missing USER_KEY (or PRIVATE_KEY) in environment') + } + return key.startsWith('0x') ? (key as `0x${string}`) : (`0x${key}` as `0x${string}`) +} diff --git a/sdk-examples/tsconfig.json b/sdk-examples/tsconfig.json new file mode 100644 index 0000000..057ce7b --- /dev/null +++ b/sdk-examples/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"], + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"] +} diff --git a/src/BasicMessageReceiver.sol b/src/BasicMessageReceiver.sol index 8d6956d..b891ed9 100644 --- a/src/BasicMessageReceiver.sol +++ b/src/BasicMessageReceiver.sol @@ -1,56 +1,32 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity 0.8.26; import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol"; import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {Withdraw} from "./utils/Withdraw.sol"; /** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. */ -contract BasicMessageReceiver is CCIPReceiver, Withdraw { - bytes32 latestMessageId; - uint64 latestSourceChainSelector; - address latestSender; - string latestMessage; +contract BasicMessageReceiver is CCIPReceiver { + bytes32 public latestMessageId; + uint64 public latestSourceChainSelector; + address public latestSender; + bytes public latestMessage; event MessageReceived( - bytes32 latestMessageId, - uint64 latestSourceChainSelector, - address latestSender, - string latestMessage + bytes32 latestMessageId, uint64 latestSourceChainSelector, address latestSender, bytes latestMessage ); constructor(address router) CCIPReceiver(router) {} - function _ccipReceive( - Client.Any2EVMMessage memory message - ) internal override { + function _ccipReceive(Client.Any2EVMMessage memory message) internal override { latestMessageId = message.messageId; latestSourceChainSelector = message.sourceChainSelector; latestSender = abi.decode(message.sender, (address)); - latestMessage = abi.decode(message.data, (string)); + latestMessage = message.data; - emit MessageReceived( - latestMessageId, - latestSourceChainSelector, - latestSender, - latestMessage - ); - } - - function getLatestMessageDetails() - public - view - returns (bytes32, uint64, address, string memory) - { - return ( - latestMessageId, - latestSourceChainSelector, - latestSender, - latestMessage - ); + emit MessageReceived(latestMessageId, latestSourceChainSelector, latestSender, latestMessage); } } diff --git a/src/BasicMessageReceiverWithCCVs.sol b/src/BasicMessageReceiverWithCCVs.sol new file mode 100644 index 0000000..aa55fcd --- /dev/null +++ b/src/BasicMessageReceiverWithCCVs.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {BasicMessageReceiver} from "./BasicMessageReceiver.sol"; + +import {FinalityCodec} from "@chainlink/contracts-ccip/contracts/libraries/FinalityCodec.sol"; +import {Ownable, Ownable2Step} from "@openzeppelin/contracts@5.3.0/access/Ownable2Step.sol"; + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ +contract BasicMessageReceiverWithCCVs is BasicMessageReceiver, Ownable2Step { + /// @notice CCV configuration for a source chain. + /// @dev For incoming messages, this receiver will require this CCV criteria to be met. + /// Required CCVs must all pass verification. >= optionalThreshold of the optional CCVs must pass verification. + struct CCVConfig { + address[] requiredCCVs; + address[] optionalCCVs; + uint8 optionalThreshold; + } + + /// @notice Arguments required to add a CCV configuration for a source chain. + struct CCVConfigArgs { + address[] requiredCCVs; + address[] optionalCCVs; + uint64 sourceChainSelector; + uint8 optionalThreshold; + } + + error DuplicateCCV(uint64 sourceChainSelector, address ccv); + error InvalidOptionalThreshold(uint64 sourceChainSelector, uint8 optionalThreshold); + error ZeroAddressNotAllowedAsOptional(); + + event CCVConfigSet( + uint64 indexed sourceChainSelector, address[] requiredCCVs, address[] optionalCCVs, uint8 optionalThreshold + ); + event MinBlockDepthSet(uint64 indexed sourceChainSelector, uint16 minBlockDepth); + + mapping(uint64 sourceChainSelector => CCVConfig ccvConfig) internal s_ccvConfigs; + mapping(uint64 sourceChainSelector => uint16 minBlockDepth) internal s_minBlockDepths; + + constructor(address router) BasicMessageReceiver(router) Ownable(msg.sender) {} + + /// @notice Set minimum accepted block depth for a source chain. + /// @dev 0 means Default Finality is required for that source chain. + /// Non-zero values allow Faster Than Finality with a minimum required depth - WARNING only use Faster Than Finality + /// when you use a trusted sender on the source chain that manages the finality risk when sending messages. + function setMinBlockDepth(uint64 sourceChainSelector, uint16 minBlockDepth) external onlyOwner { + s_minBlockDepths[sourceChainSelector] = minBlockDepth; + emit MinBlockDepthSet(sourceChainSelector, minBlockDepth); + } + + /// @notice Returns CCV config and allowed finality for a source chain (see `CCIPReceiver.getCCVsAndFinalityConfig`). + /// @dev Maps stored min block depth to `FinalityCodec` encoding (0 depth => wait for full finality). + function getCCVsAndFinalityConfig( + uint64 sourceChainSelector, + bytes calldata /*sender*/ + ) + external + view + override + returns ( + address[] memory requiredCCVs, + address[] memory optionalCCVs, + uint8 optionalThreshold, + bytes4 allowedFinalityConfig + ) + { + CCVConfig memory config = s_ccvConfigs[sourceChainSelector]; + uint16 minBlockDepth = s_minBlockDepths[sourceChainSelector]; + allowedFinalityConfig = FinalityCodec._encodeBlockDepth(minBlockDepth); + return (config.requiredCCVs, config.optionalCCVs, config.optionalThreshold, allowedFinalityConfig); + } + + /// @notice Set CCV configurations for source chains. + /// @param ccvConfigsToSet List of CCV configs to set. + function applyCCVConfigUpdates(CCVConfigArgs[] calldata ccvConfigsToSet) external virtual onlyOwner { + for (uint256 i = 0; i < ccvConfigsToSet.length; ++i) { + CCVConfigArgs memory args = ccvConfigsToSet[i]; + // If optionalThreshold > optionalCCVs.length, then it's impossible to satisfy the optional CCV requirement. + // If optionalThreshold == optionalCCVs.length, then optional CCVs are essentially required, they should instead + // be defined as required CCVs. + if (args.optionalCCVs.length > 0) { + if (args.optionalThreshold >= args.optionalCCVs.length) { + revert InvalidOptionalThreshold(args.sourceChainSelector, args.optionalThreshold); + } + } else { + if (args.optionalThreshold > 0) { + revert InvalidOptionalThreshold(args.sourceChainSelector, args.optionalThreshold); + } + } + uint256 requiredCCVLength = args.requiredCCVs.length; + uint256 optionalCCVLength = args.optionalCCVs.length; + uint256 totalCCVLength = requiredCCVLength + optionalCCVLength; + for (uint256 j = 0; j < totalCCVLength; ++j) { + address ccvAddressJ = + j < requiredCCVLength ? args.requiredCCVs[j] : args.optionalCCVs[j - requiredCCVLength]; + // address(0) is a valid required CCV address, but not a valid optional CCV address. + // This is because address(0) signals to always enforce the default CCVs for the lane. + if (j >= requiredCCVLength && ccvAddressJ == address(0)) { + revert ZeroAddressNotAllowedAsOptional(); + } + + for (uint256 k = j + 1; k < totalCCVLength; ++k) { + address ccvAddressK = + k < requiredCCVLength ? args.requiredCCVs[k] : args.optionalCCVs[k - requiredCCVLength]; + if (ccvAddressK == ccvAddressJ) { + revert DuplicateCCV(args.sourceChainSelector, ccvAddressK); + } + } + } + s_ccvConfigs[args.sourceChainSelector] = CCVConfig({ + requiredCCVs: args.requiredCCVs, + optionalCCVs: args.optionalCCVs, + optionalThreshold: args.optionalThreshold + }); + emit CCVConfigSet(args.sourceChainSelector, args.requiredCCVs, args.optionalCCVs, args.optionalThreshold); + } + } +} diff --git a/src/BasicMessageSender.sol b/src/BasicMessageSender.sol index 5172533..de6f961 100644 --- a/src/BasicMessageSender.sol +++ b/src/BasicMessageSender.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity 0.8.26; + +import {Withdraw, IERC20, SafeERC20} from "./utils/Withdraw.sol"; -import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {Withdraw} from "./utils/Withdraw.sol"; /** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. @@ -12,10 +12,7 @@ import {Withdraw} from "./utils/Withdraw.sol"; * DO NOT USE THIS CODE IN PRODUCTION. */ contract BasicMessageSender is Withdraw { - enum PayFeesIn { - Native, - LINK - } + using SafeERC20 for IERC20; address immutable i_router; address immutable i_link; @@ -29,25 +26,47 @@ contract BasicMessageSender is Withdraw { receive() external payable {} - function send(uint64 destinationChainSelector, address receiver, string memory messageText, PayFeesIn payFeesIn) - external - returns (bytes32 messageId) - { + /** + * @notice Sends a CCIP message. + * + * @param destinationChainSelector CCIP Chain selector of the destination chain - find it at https://docs.chain.link/ccip/directory/ + * @param receiver Receiver's address on the destination chain. + * @param dataToSend ABI-encoded payload to include in the CCIP message. If you don't want to send any payload, pass 0x => `bytes memory dataToSend = "";` + * @param tokensToSend Token amounts to send with the message. If you don't want to send any tokens, pass empty array => `Client.EVMTokenAmount[] memory tokensToSend = new Client.EVMTokenAmount[](0);` + * @param extraArgs Encoded CCIP extra args (use EncodeExtraArgsOffchain helper if needed). + * @param feeTokenAddress Token address used to pay CCIP fees. If you want to pay in native gas token pass `address(0)`, otherwise pass LINK token address. + * + * @return messageId Unique CCIP message ID returned by the router. Use it to monitor your message in real time at https://ccip.chain.link/ + */ + function send( + uint64 destinationChainSelector, + address receiver, + bytes memory dataToSend, + Client.EVMTokenAmount[] memory tokensToSend, + bytes memory extraArgs, + address feeTokenAddress + ) external returns (bytes32 messageId) { + if (tokensToSend.length > 0) { + // Only one token can be send per message + IERC20(tokensToSend[0].token).safeTransferFrom(msg.sender, address(this), tokensToSend[0].amount); + IERC20(tokensToSend[0].token).approve(i_router, tokensToSend[0].amount); + } + Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ receiver: abi.encode(receiver), - data: abi.encode(messageText), - tokenAmounts: new Client.EVMTokenAmount[](0), - extraArgs: Client._argsToBytes(Client.GenericExtraArgsV2({gasLimit: 200_000, allowOutOfOrderExecution: true})), - feeToken: payFeesIn == PayFeesIn.LINK ? i_link : address(0) + data: dataToSend, + tokenAmounts: tokensToSend, + extraArgs: extraArgs, + feeToken: feeTokenAddress }); uint256 fee = IRouterClient(i_router).getFee(destinationChainSelector, message); - if (payFeesIn == PayFeesIn.LINK) { - LinkTokenInterface(i_link).approve(i_router, fee); - messageId = IRouterClient(i_router).ccipSend(destinationChainSelector, message); - } else { + if (feeTokenAddress == address(0)) { messageId = IRouterClient(i_router).ccipSend{value: fee}(destinationChainSelector, message); + } else { + IERC20(i_link).approve(i_router, fee); + messageId = IRouterClient(i_router).ccipSend(destinationChainSelector, message); } emit MessageSent(messageId); diff --git a/src/BasicTokenSender.sol b/src/BasicTokenSender.sol deleted file mode 100644 index 8ac792f..0000000 --- a/src/BasicTokenSender.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; -import {IERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; -import {SafeERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/utils/SafeERC20.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {Withdraw} from "./utils/Withdraw.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ -contract BasicTokenSender is Withdraw { - using SafeERC20 for IERC20; - - enum PayFeesIn { - Native, - LINK - } - - address immutable i_router; - address immutable i_link; - - event MessageSent(bytes32 messageId); - - constructor(address router, address link) { - i_router = router; - i_link = link; - } - - receive() external payable {} - - function send( - uint64 destinationChainSelector, - address receiver, - Client.EVMTokenAmount[] memory tokensToSendDetails, - PayFeesIn payFeesIn - ) external { - uint256 length = tokensToSendDetails.length; - - for (uint256 i = 0; i < length;) { - IERC20(tokensToSendDetails[i].token).safeTransferFrom( - msg.sender, address(this), tokensToSendDetails[i].amount - ); - IERC20(tokensToSendDetails[i].token).approve(i_router, tokensToSendDetails[i].amount); - - unchecked { - ++i; - } - } - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), - data: "", - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes(Client.GenericExtraArgsV2({gasLimit: 0, allowOutOfOrderExecution: true})), - feeToken: payFeesIn == PayFeesIn.LINK ? i_link : address(0) - }); - - uint256 fee = IRouterClient(i_router).getFee(destinationChainSelector, message); - - bytes32 messageId; - - if (payFeesIn == PayFeesIn.LINK) { - LinkTokenInterface(i_link).approve(i_router, fee); - messageId = IRouterClient(i_router).ccipSend(destinationChainSelector, message); - } else { - messageId = IRouterClient(i_router).ccipSend{value: fee}(destinationChainSelector, message); - } - - emit MessageSent(messageId); - } -} diff --git a/src/ProgrammableTokenTransfers.sol b/src/ProgrammableTokenTransfers.sol deleted file mode 100644 index 2c36030..0000000 --- a/src/ProgrammableTokenTransfers.sol +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol"; -import {IERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; -import {SafeERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/utils/SafeERC20.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ - -/// @title - A simple messenger contract for sending/receiving messages and tokens across chains. -/// Pay using native tokens (e.g, ETH in Ethereum) -contract ProgrammableTokenTransfers is CCIPReceiver, OwnerIsCreator { - using SafeERC20 for IERC20; - - // Custom errors to provide more descriptive revert messages. - error NoMessageReceived(); // Used when trying to access a message but no messages have been received. - error IndexOutOfBound(uint256 providedIndex, uint256 maxIndex); // Used when the provided index is out of bounds. - error MessageIdNotExist(bytes32 messageId); // Used when the provided message ID does not exist. - error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw. - error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails. - error InsufficientFeeTokenAmount(); // Used when the contract balance isn't enough to pay fees. - - // Event emitted when a message is sent to another chain. - // The chain selector of the destination chain. - // The address of the receiver on the destination chain. - // The message being sent. - // The token amount that was sent. - // The fees paid for sending the message. - event MessageSent( // The unique ID of the message. - bytes32 indexed messageId, - uint64 indexed destinationChainSelector, - address receiver, - string message, - Client.EVMTokenAmount tokenAmount, - uint256 fees - ); - - // Event emitted when a message is received from another chain. - // The chain selector of the source chain. - // The address of the sender from the source chain. - // The message that was received. - // The token amount that was received. - event MessageReceived( // The unique ID of the message. - bytes32 indexed messageId, - uint64 indexed sourceChainSelector, - address sender, - string message, - Client.EVMTokenAmount tokenAmount - ); - - // Struct to hold details of a message. - struct Message { - uint64 sourceChainSelector; // The chain selector of the source chain. - address sender; // The address of the sender. - string message; // The content of the message. - address token; // received token. - uint256 amount; // received amount. - } - - // Storage variables. - bytes32[] public receivedMessages; // Array to keep track of the IDs of received messages. - mapping(bytes32 => Message) public messageDetail; // Mapping from message ID to Message struct, storing details of each received message. - - /// @notice Constructor initializes the contract with the router address. - /// @param router The address of the router contract. - constructor(address router) CCIPReceiver(router) {} - - /// @notice Sends data to receiver on the destination chain. - /// @dev Assumes your contract has sufficient native asset (e.g, ETH on Ethereum, MATIC on Polygon...). - /// @param destinationChainSelector The identifier (aka selector) for the destination blockchain. - /// @param receiver The address of the recipient on the destination blockchain. - /// @param message The string message to be sent. - /// @param token token address. - /// @param amount token amount. - /// @return messageId The ID of the message that was sent. - function sendMessage( - uint64 destinationChainSelector, - address receiver, - string calldata message, - address token, - uint256 amount - ) external returns (bytes32 messageId) { - // set the tokent amounts - Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: token, amount: amount}); - tokenAmounts[0] = tokenAmount; - // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message - Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), // ABI-encoded receiver address - data: abi.encode(message), // ABI-encoded string message - tokenAmounts: tokenAmounts, // Tokens amounts - extraArgs: Client._argsToBytes( - Client.GenericExtraArgsV2({ - gasLimit: 200_000, - allowOutOfOrderExecution: true - }) - ), - feeToken: address(0) // Setting feeToken to zero address, indicating native asset will be used for fees - }); - - // Initialize a router client instance to interact with cross-chain router - IRouterClient router = IRouterClient(this.getRouter()); - - // approve the Router to spend tokens on contract's behalf. I will spend the amount of the given token - IERC20(token).approve(address(router), amount); - - // Get the fee required to send the message - uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage); - - // Reverts if this Contract doesn't have enough native tokens - if (address(this).balance < fees) revert InsufficientFeeTokenAmount(); - - // Send the message through the router and store the returned message ID - messageId = router.ccipSend{value: fees}(destinationChainSelector, evm2AnyMessage); - - // Emit an event with message details - emit MessageSent(messageId, destinationChainSelector, receiver, message, tokenAmount, fees); - - // Return the message ID - return messageId; - } - - /// handle a received message - function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { - bytes32 messageId = any2EvmMessage.messageId; // fetch the messageId - uint64 sourceChainSelector = any2EvmMessage.sourceChainSelector; // fetch the source chain identifier (aka selector) - address sender = abi.decode(any2EvmMessage.sender, (address)); // abi-decoding of the sender address - Client.EVMTokenAmount[] memory tokenAmounts = any2EvmMessage.destTokenAmounts; - address token = tokenAmounts[0].token; // we expect one token to be transfered at once but of course, you can transfer several tokens. - uint256 amount = tokenAmounts[0].amount; // we expect one token to be transfered at once but of course, you can transfer several tokens. - string memory message = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent string message - receivedMessages.push(messageId); - Message memory detail = Message(sourceChainSelector, sender, message, token, amount); - messageDetail[messageId] = detail; - - emit MessageReceived(messageId, sourceChainSelector, sender, message, tokenAmounts[0]); - } - - /// @notice Get the total number of received messages. - /// @return number The total number of received messages. - function getNumberOfReceivedMessages() external view returns (uint256 number) { - return receivedMessages.length; - } - - /// @notice Fetches details of a received message by message ID. - /// @dev Reverts if the message ID does not exist. - /// @param messageId The ID of the message whose details are to be fetched. - /// @return sourceChainSelector The source chain identifier (aka selector). - /// @return sender The address of the sender. - /// @return message The received message. - /// @return token The received token. - /// @return amount The received token amount. - function getReceivedMessageDetails(bytes32 messageId) - external - view - returns (uint64 sourceChainSelector, address sender, string memory message, address token, uint256 amount) - { - Message memory detail = messageDetail[messageId]; - if (detail.sender == address(0)) revert MessageIdNotExist(messageId); - return (detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount); - } - - /// @notice Fetches details of a received message by its position in the received messages list. - /// @dev Reverts if the index is out of bounds. - /// @param index The position in the list of received messages. - /// @return messageId The ID of the message. - /// @return sourceChainSelector The source chain identifier (aka selector). - /// @return sender The address of the sender. - /// @return message The received message. - /// @return token The received token. - /// @return amount The received token amount. - function getReceivedMessageAt(uint256 index) - external - view - returns ( - bytes32 messageId, - uint64 sourceChainSelector, - address sender, - string memory message, - address token, - uint256 amount - ) - { - if (index >= receivedMessages.length) { - revert IndexOutOfBound(index, receivedMessages.length - 1); - } - messageId = receivedMessages[index]; - Message memory detail = messageDetail[messageId]; - return (messageId, detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount); - } - - /// @notice Fetches the details of the last received message. - /// @dev Reverts if no messages have been received yet. - /// @return messageId The ID of the last received message. - /// @return sourceChainSelector The source chain identifier (aka selector) of the last received message. - /// @return sender The address of the sender of the last received message. - /// @return message The last received message. - /// @return token The last transferred token. - /// @return amount The last transferred token amount. - function getLastReceivedMessageDetails() - external - view - returns ( - bytes32 messageId, - uint64 sourceChainSelector, - address sender, - string memory message, - address token, - uint256 amount - ) - { - // Revert if no messages have been received - if (receivedMessages.length == 0) revert NoMessageReceived(); - - // Fetch the last received message ID - messageId = receivedMessages[receivedMessages.length - 1]; - - // Fetch the details of the last received message - Message memory detail = messageDetail[messageId]; - - return (messageId, detail.sourceChainSelector, detail.sender, detail.message, detail.token, detail.amount); - } - - /// @notice Fallback function to allow the contract to receive Ether. - /// @dev This function has no function body, making it a default function for receiving Ether. - /// It is automatically called when Ether is sent to the contract without any data. - receive() external payable {} - - /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract. - /// @dev This function reverts if there are no funds to withdraw or if the transfer fails. - /// It should only be callable by the owner of the contract. - /// @param beneficiary The address to which the Ether should be sent. - function withdraw(address beneficiary) public onlyOwner { - // Retrieve the balance of this contract - uint256 amount = address(this).balance; - - // Revert if there is nothing to withdraw - if (amount == 0) revert NothingToWithdraw(); - - // Attempt to send the funds, capturing the success status and discarding any return data - (bool sent,) = beneficiary.call{value: amount}(""); - - // Revert if the send failed, with information about the attempted transfer - if (!sent) revert FailedToWithdrawEth(msg.sender, beneficiary, amount); - } - - /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token. - /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw. - /// @param beneficiary The address to which the tokens will be sent. - /// @param token The contract address of the ERC20 token to be withdrawn. - function withdrawToken(address beneficiary, address token) public onlyOwner { - // Retrieve the balance of this contract - uint256 amount = IERC20(token).balanceOf(address(this)); - - // Revert if there is nothing to withdraw - if (amount == 0) revert NothingToWithdraw(); - - IERC20(token).safeTransfer(beneficiary, amount); - } -} diff --git a/src/cross-chain-nft-minter/DestinationMinter.sol b/src/cross-chain-nft-minter/DestinationMinter.sol deleted file mode 100644 index 147b35f..0000000 --- a/src/cross-chain-nft-minter/DestinationMinter.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {CCIPReceiver} from "@chainlink/contracts-ccip/contracts/applications/CCIPReceiver.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {MyNFT} from "./MyNFT.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ -contract DestinationMinter is CCIPReceiver { - MyNFT nft; - - event MintCallSuccessfull(); - - constructor(address router, address nftAddress) CCIPReceiver(router) { - nft = MyNFT(nftAddress); - } - - function _ccipReceive( - Client.Any2EVMMessage memory message - ) internal override { - (bool success, ) = address(nft).call(message.data); - require(success); - emit MintCallSuccessfull(); - } -} diff --git a/src/cross-chain-nft-minter/MyNFT.sol b/src/cross-chain-nft-minter/MyNFT.sol deleted file mode 100644 index 2e83ed6..0000000 --- a/src/cross-chain-nft-minter/MyNFT.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ -contract MyNFT is ERC721URIStorage, Ownable { - string constant TOKEN_URI = - "https://ipfs.io/ipfs/QmYuKY45Aq87LeL1R5dhb1hqHLp6ZFbJaCP8jxqKM1MX6y/babe_ruth_1.json"; - uint256 internal tokenId; - - constructor() ERC721("MyNFT", "MNFT"){} - - function mint(address to) public onlyOwner { - _safeMint(to, tokenId); - _setTokenURI(tokenId, TOKEN_URI); - unchecked { - tokenId++; - } - } -} diff --git a/src/cross-chain-nft-minter/SourceMinter.sol b/src/cross-chain-nft-minter/SourceMinter.sol deleted file mode 100644 index 6681499..0000000 --- a/src/cross-chain-nft-minter/SourceMinter.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {Withdraw} from "../utils/Withdraw.sol"; - -/** - * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. - * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. - * DO NOT USE THIS CODE IN PRODUCTION. - */ -contract SourceMinter is Withdraw { - enum PayFeesIn { - Native, - LINK - } - - address immutable i_router; - address immutable i_link; - - event MessageSent(bytes32 messageId); - - constructor(address router, address link) { - i_router = router; - i_link = link; - } - - receive() external payable {} - - function mint( - uint64 destinationChainSelector, - address receiver, - PayFeesIn payFeesIn - ) external { - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(receiver), - data: abi.encodeWithSignature("mint(address)", msg.sender), - tokenAmounts: new Client.EVMTokenAmount[](0), - extraArgs: "", - feeToken: payFeesIn == PayFeesIn.LINK ? i_link : address(0) - }); - - uint256 fee = IRouterClient(i_router).getFee( - destinationChainSelector, - message - ); - - bytes32 messageId; - - if (payFeesIn == PayFeesIn.LINK) { - LinkTokenInterface(i_link).approve(i_router, fee); - messageId = IRouterClient(i_router).ccipSend( - destinationChainSelector, - message - ); - } else { - messageId = IRouterClient(i_router).ccipSend{value: fee}( - destinationChainSelector, - message - ); - } - - emit MessageSent(messageId); - } -} diff --git a/src/utils/Withdraw.sol b/src/utils/Withdraw.sol index 47778ba..0550030 100644 --- a/src/utils/Withdraw.sol +++ b/src/utils/Withdraw.sol @@ -1,32 +1,29 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity 0.8.26; -import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; -import {IERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/IERC20.sol"; -import {SafeERC20} from - "@openzeppelin/contracts@4.8.3/token/ERC20/utils/SafeERC20.sol"; +import {Ownable, Ownable2Step} from "@openzeppelin/contracts@5.3.0/access/Ownable2Step.sol"; +import {IERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts@5.3.0/token/ERC20/utils/SafeERC20.sol"; /** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. */ -contract Withdraw is OwnerIsCreator { +contract Withdraw is Ownable2Step { using SafeERC20 for IERC20; error FailedToWithdrawEth(address owner, address target, uint256 value); + constructor() Ownable(msg.sender) {} + function withdraw(address beneficiary) public onlyOwner { uint256 amount = address(this).balance; - (bool sent, ) = beneficiary.call{value: amount}(""); + (bool sent,) = beneficiary.call{value: amount}(""); if (!sent) revert FailedToWithdrawEth(msg.sender, beneficiary, amount); } - function withdrawToken( - address beneficiary, - address token - ) public onlyOwner { + function withdrawToken(address beneficiary, address token) public onlyOwner { uint256 amount = IERC20(token).balanceOf(address(this)); IERC20(token).safeTransfer(beneficiary, amount); } diff --git a/test/fork/CCIPv1_5BurnMintPoolFork.t.sol b/test/fork/CCIPv1_5BurnMintPoolFork.t.sol deleted file mode 100644 index 3eb0c14..0000000 --- a/test/fork/CCIPv1_5BurnMintPoolFork.t.sol +++ /dev/null @@ -1,284 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Test, Vm} from "forge-std/Test.sol"; -import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"; -import {BurnMintTokenPool, TokenPool} from "@chainlink/contracts-ccip/contracts/pools/BurnMintTokenPool.sol"; -import {IBurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol"; -import {RegistryModuleOwnerCustom} from - "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; -import {TokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/TokenAdminRegistry.sol"; -import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; - -import { - ERC20, - ERC20Burnable, - IERC20 -} from - "@openzeppelin/contracts@4.8.3/token/ERC20/extensions/ERC20Burnable.sol"; -import {AccessControl} from - "@openzeppelin/contracts@4.8.3/access/AccessControl.sol"; - -contract MockERC20BurnAndMintToken is IBurnMintERC20, ERC20Burnable, AccessControl { - address internal immutable i_CCIPAdmin; - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); - - constructor() ERC20("MockERC20BurnAndMintToken", "MTK") { - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - _grantRole(MINTER_ROLE, msg.sender); - _grantRole(BURNER_ROLE, msg.sender); - i_CCIPAdmin = msg.sender; - } - - function mint(address account, uint256 amount) public onlyRole(MINTER_ROLE) { - _mint(account, amount); - } - - function burn(uint256 amount) public override(IBurnMintERC20, ERC20Burnable) onlyRole(BURNER_ROLE) { - super.burn(amount); - } - - function burnFrom(address account, uint256 amount) - public - override(IBurnMintERC20, ERC20Burnable) - onlyRole(BURNER_ROLE) - { - super.burnFrom(account, amount); - } - - function burn(address account, uint256 amount) public virtual override { - burnFrom(account, amount); - } - - function getCCIPAdmin() public view returns (address) { - return i_CCIPAdmin; - } -} - -contract CCIPv1_5BurnMintPoolFork is Test { - CCIPLocalSimulatorFork public ccipLocalSimulatorFork; - MockERC20BurnAndMintToken public mockERC20TokenEthSepolia; - MockERC20BurnAndMintToken public mockERC20TokenAvalancheFuji; - BurnMintTokenPool public burnMintTokenPoolEthSepolia; - BurnMintTokenPool public burnMintTokenPoolAvalancheFuji; - - Register.NetworkDetails ethSepoliaNetworkDetails; - Register.NetworkDetails avalancheFujiNetworkDetails; - - uint256 ethSepoliaFork; - uint256 avalancheFujiFork; - - address alice; - - function setUp() public { - alice = makeAddr("alice"); - - string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL"); - string memory AVALANCHE_FUJI_RPC_URL = vm.envString("AVALANCHE_FUJI_RPC_URL"); - ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL); - avalancheFujiFork = vm.createFork(AVALANCHE_FUJI_RPC_URL); - - ccipLocalSimulatorFork = new CCIPLocalSimulatorFork(); - vm.makePersistent(address(ccipLocalSimulatorFork)); - - // Step 1) Deploy token on Ethereum Sepolia - vm.startPrank(alice); - mockERC20TokenEthSepolia = new MockERC20BurnAndMintToken(); - vm.stopPrank(); - - // Step 2) Deploy token on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - mockERC20TokenAvalancheFuji = new MockERC20BurnAndMintToken(); - vm.stopPrank(); - } - - function test_forkSupportNewCCIPToken() public { - // Step 3) Deploy BurnMintTokenPool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - address[] memory allowlist = new address[](0); - uint8 localTokenDecimals = 18; - - vm.startPrank(alice); - burnMintTokenPoolEthSepolia = new BurnMintTokenPool( - IBurnMintERC20(address(mockERC20TokenEthSepolia)), - localTokenDecimals, - allowlist, - ethSepoliaNetworkDetails.rmnProxyAddress, - ethSepoliaNetworkDetails.routerAddress - ); - vm.stopPrank(); - - // Step 4) Deploy BurnMintTokenPool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - avalancheFujiNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - - vm.startPrank(alice); - burnMintTokenPoolAvalancheFuji = new BurnMintTokenPool( - IBurnMintERC20(address(mockERC20TokenAvalancheFuji)), - localTokenDecimals, - allowlist, - avalancheFujiNetworkDetails.rmnProxyAddress, - avalancheFujiNetworkDetails.routerAddress - ); - vm.stopPrank(); - - // Step 5) Grant Mint and Burn roles to BurnMintTokenPool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - vm.startPrank(alice); - mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.MINTER_ROLE(), address(burnMintTokenPoolEthSepolia)); - mockERC20TokenEthSepolia.grantRole(mockERC20TokenEthSepolia.BURNER_ROLE(), address(burnMintTokenPoolEthSepolia)); - vm.stopPrank(); - - // Step 6) Grant Mint and Burn roles to BurnMintTokenPool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - mockERC20TokenAvalancheFuji.grantRole( - mockERC20TokenAvalancheFuji.MINTER_ROLE(), address(burnMintTokenPoolAvalancheFuji) - ); - mockERC20TokenAvalancheFuji.grantRole( - mockERC20TokenAvalancheFuji.BURNER_ROLE(), address(burnMintTokenPoolAvalancheFuji) - ); - vm.stopPrank(); - - // Step 7) Claim Admin role on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - RegistryModuleOwnerCustom registryModuleOwnerCustomEthSepolia = - RegistryModuleOwnerCustom(ethSepoliaNetworkDetails.registryModuleOwnerCustomAddress); - - vm.startPrank(alice); - registryModuleOwnerCustomEthSepolia.registerAdminViaGetCCIPAdmin(address(mockERC20TokenEthSepolia)); - vm.stopPrank(); - - // Step 8) Claim Admin role on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - RegistryModuleOwnerCustom registryModuleOwnerCustomAvalancheFuji = - RegistryModuleOwnerCustom(avalancheFujiNetworkDetails.registryModuleOwnerCustomAddress); - - vm.startPrank(alice); - registryModuleOwnerCustomAvalancheFuji.registerAdminViaGetCCIPAdmin(address(mockERC20TokenAvalancheFuji)); - vm.stopPrank(); - - // Step 9) Accept Admin role on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - TokenAdminRegistry tokenAdminRegistryEthSepolia = - TokenAdminRegistry(ethSepoliaNetworkDetails.tokenAdminRegistryAddress); - - vm.startPrank(alice); - tokenAdminRegistryEthSepolia.acceptAdminRole(address(mockERC20TokenEthSepolia)); - vm.stopPrank(); - - // Step 10) Accept Admin role on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - TokenAdminRegistry tokenAdminRegistryAvalancheFuji = - TokenAdminRegistry(avalancheFujiNetworkDetails.tokenAdminRegistryAddress); - - vm.startPrank(alice); - tokenAdminRegistryAvalancheFuji.acceptAdminRole(address(mockERC20TokenAvalancheFuji)); - vm.stopPrank(); - - // Step 11) Link token to pool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - vm.startPrank(alice); - tokenAdminRegistryEthSepolia.setPool(address(mockERC20TokenEthSepolia), address(burnMintTokenPoolEthSepolia)); - vm.stopPrank(); - - // Step 12) Link token to pool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - tokenAdminRegistryAvalancheFuji.setPool( - address(mockERC20TokenAvalancheFuji), address(burnMintTokenPoolAvalancheFuji) - ); - vm.stopPrank(); - - // Step 13) Configure Token Pool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - vm.startPrank(alice); - TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1); - bytes[] memory remotePoolAddressesEthSepolia = new bytes[](1); - remotePoolAddressesEthSepolia[0] = abi.encode(address(burnMintTokenPoolEthSepolia)); - chains[0] = TokenPool.ChainUpdate({ - remoteChainSelector: avalancheFujiNetworkDetails.chainSelector, - remotePoolAddresses: remotePoolAddressesEthSepolia, - remoteTokenAddress: abi.encode(address(mockERC20TokenAvalancheFuji)), - outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100_000, rate: 167}), - inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100_000, rate: 167}) - }); - uint64[] memory remoteChainSelectorsToRemove = new uint64[](0); - burnMintTokenPoolEthSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains); - vm.stopPrank(); - - // Step 14) Configure Token Pool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - chains = new TokenPool.ChainUpdate[](1); - bytes[] memory remotePoolAddressesAvalancheFuji = new bytes[](1); - remotePoolAddressesAvalancheFuji[0] = abi.encode(address(burnMintTokenPoolEthSepolia)); - chains[0] = TokenPool.ChainUpdate({ - remoteChainSelector: ethSepoliaNetworkDetails.chainSelector, - remotePoolAddresses: remotePoolAddressesAvalancheFuji, - remoteTokenAddress: abi.encode(address(mockERC20TokenEthSepolia)), - outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100_000, rate: 167}), - inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: 100_000, rate: 167}) - }); - burnMintTokenPoolAvalancheFuji.applyChainUpdates(remoteChainSelectorsToRemove, chains); - vm.stopPrank(); - - // Step 15) Mint tokens on Ethereum Sepolia and transfer them to Avalanche Fuji - vm.selectFork(ethSepoliaFork); - - address linkSepolia = ethSepoliaNetworkDetails.linkAddress; - ccipLocalSimulatorFork.requestLinkFromFaucet(address(alice), 20 ether); - - uint256 amountToSend = 100; - Client.EVMTokenAmount[] memory tokenToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenAmount = - Client.EVMTokenAmount({token: address(mockERC20TokenEthSepolia), amount: amountToSend}); - tokenToSendDetails[0] = tokenAmount; - - vm.startPrank(alice); - mockERC20TokenEthSepolia.mint(address(alice), amountToSend); - - mockERC20TokenEthSepolia.approve(ethSepoliaNetworkDetails.routerAddress, amountToSend); - IERC20(linkSepolia).approve(ethSepoliaNetworkDetails.routerAddress, 20 ether); - - uint256 balanceOfAliceBeforeEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice); - - IRouterClient routerEthSepolia = IRouterClient(ethSepoliaNetworkDetails.routerAddress); - routerEthSepolia.ccipSend( - avalancheFujiNetworkDetails.chainSelector, - Client.EVM2AnyMessage({ - receiver: abi.encode(address(alice)), - data: "", - tokenAmounts: tokenToSendDetails, - extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), - feeToken: linkSepolia - }) - ); - - uint256 balanceOfAliceAfterEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice); - vm.stopPrank(); - - assertEq(balanceOfAliceAfterEthSepolia, balanceOfAliceBeforeEthSepolia - amountToSend); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(avalancheFujiFork); - - uint256 balanceOfAliceAfterAvalancheFuji = mockERC20TokenAvalancheFuji.balanceOf(alice); - assertEq(balanceOfAliceAfterAvalancheFuji, amountToSend); - } -} diff --git a/test/fork/CCIPv1_5LockReleasePoolFork.t.sol b/test/fork/CCIPv1_5LockReleasePoolFork.t.sol deleted file mode 100644 index a1ff308..0000000 --- a/test/fork/CCIPv1_5LockReleasePoolFork.t.sol +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Test, Vm} from "forge-std/Test.sol"; -import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"; -import {LockReleaseTokenPool, TokenPool} from "@chainlink/contracts-ccip/contracts/pools/LockReleaseTokenPool.sol"; -import {RegistryModuleOwnerCustom} from - "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol"; -import {TokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/TokenAdminRegistry.sol"; -import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; - -import { - ERC20, - IERC20 -} from "@openzeppelin/contracts@4.8.3/token/ERC20/ERC20.sol"; -import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol"; - -contract MockERC20TokenOwner is ERC20, OwnerIsCreator { - constructor() ERC20("MockERC20Token", "MTK") {} - - function mint(address account, uint256 amount) public onlyOwner { - _mint(account, amount); - } -} - -contract CCIPv1_5LockReleasePoolFork is Test { - CCIPLocalSimulatorFork public ccipLocalSimulatorFork; - MockERC20TokenOwner public mockERC20TokenEthSepolia; - MockERC20TokenOwner public mockERC20TokenAvalancheFuji; - LockReleaseTokenPool public lockReleaseTokenPoolEthSepolia; - LockReleaseTokenPool public lockReleaseTokenPoolAvalancheFuji; - - Register.NetworkDetails ethSepoliaNetworkDetails; - Register.NetworkDetails avalancheFujiNetworkDetails; - - uint256 ethSepoliaFork; - uint256 avalancheFujiFork; - - address alice; - - function setUp() public { - alice = makeAddr("alice"); - - string memory ETHEREUM_SEPOLIA_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL"); - string memory AVALANCHE_FUJI_RPC_URL = vm.envString("AVALANCHE_FUJI_RPC_URL"); - ethSepoliaFork = vm.createSelectFork(ETHEREUM_SEPOLIA_RPC_URL); - avalancheFujiFork = vm.createFork(AVALANCHE_FUJI_RPC_URL); - - ccipLocalSimulatorFork = new CCIPLocalSimulatorFork(); - vm.makePersistent(address(ccipLocalSimulatorFork)); - - // Step 1) Deploy token on Ethereum Sepolia - vm.startPrank(alice); - mockERC20TokenEthSepolia = new MockERC20TokenOwner(); - vm.stopPrank(); - - // Step 2) Deploy token on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - mockERC20TokenAvalancheFuji = new MockERC20TokenOwner(); - vm.stopPrank(); - } - - function test_forkSupportNewCCIPToken() public { - // Step 3) Deploy LockReleaseTokenPool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - address[] memory allowlist = new address[](0); - uint8 localTokenDecimals = 18; - - vm.startPrank(alice); - lockReleaseTokenPoolEthSepolia = new LockReleaseTokenPool( - IERC20(address(mockERC20TokenEthSepolia)), - localTokenDecimals, - allowlist, - ethSepoliaNetworkDetails.rmnProxyAddress, - ethSepoliaNetworkDetails.routerAddress - ); - vm.stopPrank(); - - // Step 4) Deploy LockReleaseTokenPool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - avalancheFujiNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - - vm.startPrank(alice); - lockReleaseTokenPoolAvalancheFuji = new LockReleaseTokenPool( - IERC20(address(mockERC20TokenAvalancheFuji)), - localTokenDecimals, - allowlist, - avalancheFujiNetworkDetails.rmnProxyAddress, - avalancheFujiNetworkDetails.routerAddress - ); - vm.stopPrank(); - - // Step 5) Set the LiquidityManager address and Add liquidity to the pool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - uint256 amountToMint = 1_000_000; - uint128 liquidityAmount = 100_000; - - vm.startPrank(alice); - mockERC20TokenEthSepolia.mint(address(alice), amountToMint); - mockERC20TokenEthSepolia.approve(address(lockReleaseTokenPoolEthSepolia), liquidityAmount); - lockReleaseTokenPoolEthSepolia.setRebalancer(address(alice)); - lockReleaseTokenPoolEthSepolia.provideLiquidity(liquidityAmount); - vm.stopPrank(); - - // Step 6) Set the LiquidityManager address and Add liquidity to the pool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - mockERC20TokenAvalancheFuji.mint(address(alice), amountToMint); - mockERC20TokenAvalancheFuji.approve(address(lockReleaseTokenPoolAvalancheFuji), liquidityAmount); - lockReleaseTokenPoolAvalancheFuji.setRebalancer(address(alice)); - lockReleaseTokenPoolAvalancheFuji.provideLiquidity(liquidityAmount); - vm.stopPrank(); - - // Step 7) Claim Admin role on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - RegistryModuleOwnerCustom registryModuleOwnerCustomEthSepolia = - RegistryModuleOwnerCustom(ethSepoliaNetworkDetails.registryModuleOwnerCustomAddress); - - vm.startPrank(alice); - registryModuleOwnerCustomEthSepolia.registerAdminViaOwner(address(mockERC20TokenEthSepolia)); - vm.stopPrank(); - - // Step 8) Claim Admin role on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - RegistryModuleOwnerCustom registryModuleOwnerCustomAvalancheFuji = - RegistryModuleOwnerCustom(avalancheFujiNetworkDetails.registryModuleOwnerCustomAddress); - - vm.startPrank(alice); - registryModuleOwnerCustomAvalancheFuji.registerAdminViaOwner(address(mockERC20TokenAvalancheFuji)); - vm.stopPrank(); - - // Step 9) Accept Admin role on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - TokenAdminRegistry tokenAdminRegistryEthSepolia = - TokenAdminRegistry(ethSepoliaNetworkDetails.tokenAdminRegistryAddress); - - vm.startPrank(alice); - tokenAdminRegistryEthSepolia.acceptAdminRole(address(mockERC20TokenEthSepolia)); - vm.stopPrank(); - - // Step 10) Accept Admin role on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - TokenAdminRegistry tokenAdminRegistryAvalancheFuji = - TokenAdminRegistry(avalancheFujiNetworkDetails.tokenAdminRegistryAddress); - - vm.startPrank(alice); - tokenAdminRegistryAvalancheFuji.acceptAdminRole(address(mockERC20TokenAvalancheFuji)); - vm.stopPrank(); - - // Step 11) Link token to pool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - vm.startPrank(alice); - tokenAdminRegistryEthSepolia.setPool(address(mockERC20TokenEthSepolia), address(lockReleaseTokenPoolEthSepolia)); - vm.stopPrank(); - - // Step 12) Link token to pool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - tokenAdminRegistryAvalancheFuji.setPool( - address(mockERC20TokenAvalancheFuji), address(lockReleaseTokenPoolAvalancheFuji) - ); - vm.stopPrank(); - - // Step 13) Configure Token Pool on Ethereum Sepolia - vm.selectFork(ethSepoliaFork); - - vm.startPrank(alice); - TokenPool.ChainUpdate[] memory chains = new TokenPool.ChainUpdate[](1); - bytes[] memory remotePoolAddressesEthSepolia = new bytes[](1); - remotePoolAddressesEthSepolia[0] = abi.encode(address(lockReleaseTokenPoolAvalancheFuji)); - chains[0] = TokenPool.ChainUpdate({ - remoteChainSelector: avalancheFujiNetworkDetails.chainSelector, - remotePoolAddresses: remotePoolAddressesEthSepolia, - remoteTokenAddress: abi.encode(address(mockERC20TokenAvalancheFuji)), - outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: liquidityAmount, rate: 167}), - inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: liquidityAmount, rate: 167}) - }); - uint64[] memory remoteChainSelectorsToRemove = new uint64[](0); - lockReleaseTokenPoolEthSepolia.applyChainUpdates(remoteChainSelectorsToRemove, chains); - vm.stopPrank(); - - // Step 14) Configure Token Pool on Avalanche Fuji - vm.selectFork(avalancheFujiFork); - - vm.startPrank(alice); - chains = new TokenPool.ChainUpdate[](1); - bytes[] memory remotePoolAddressesAvalancheFuji = new bytes[](1); - remotePoolAddressesAvalancheFuji[0] = abi.encode(address(lockReleaseTokenPoolEthSepolia)); - chains[0] = TokenPool.ChainUpdate({ - remoteChainSelector: ethSepoliaNetworkDetails.chainSelector, - remotePoolAddresses: remotePoolAddressesAvalancheFuji, - remoteTokenAddress: abi.encode(address(mockERC20TokenEthSepolia)), - outboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: liquidityAmount, rate: 167}), - inboundRateLimiterConfig: RateLimiter.Config({isEnabled: true, capacity: liquidityAmount, rate: 167}) - }); - lockReleaseTokenPoolAvalancheFuji.applyChainUpdates(remoteChainSelectorsToRemove, chains); - vm.stopPrank(); - - // Step 15) Transfer tokens from Ethereum Sepolia to Avalanche Fuji - vm.selectFork(ethSepoliaFork); - - address linkEthSepoliaAddress = ethSepoliaNetworkDetails.linkAddress; - address routerEthSepoliaAddress = ethSepoliaNetworkDetails.routerAddress; - ccipLocalSimulatorFork.requestLinkFromFaucet(address(alice), 20 ether); - - uint256 amountToSend = 100; - Client.EVMTokenAmount[] memory tokenToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenAmount = - Client.EVMTokenAmount({token: address(mockERC20TokenEthSepolia), amount: amountToSend}); - tokenToSendDetails[0] = tokenAmount; - - vm.startPrank(alice); - - mockERC20TokenEthSepolia.approve(routerEthSepoliaAddress, amountToSend); - IERC20(linkEthSepoliaAddress).approve(routerEthSepoliaAddress, 20 ether); - - uint256 balanceOfAliceBeforeEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice); - - uint64 destinationChainSelector = avalancheFujiNetworkDetails.chainSelector; - IRouterClient routerEthSepolia = IRouterClient(routerEthSepoliaAddress); - routerEthSepolia.ccipSend( - destinationChainSelector, - Client.EVM2AnyMessage({ - receiver: abi.encode(address(alice)), - data: "", - tokenAmounts: tokenToSendDetails, - extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), - feeToken: linkEthSepoliaAddress - }) - ); - - uint256 balanceOfAliceAfterEthSepolia = mockERC20TokenEthSepolia.balanceOf(alice); - vm.stopPrank(); - - assertEq(balanceOfAliceAfterEthSepolia, balanceOfAliceBeforeEthSepolia - amountToSend); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(avalancheFujiFork); - - uint256 balanceOfAliceAfterAvalancheFuji = mockERC20TokenAvalancheFuji.balanceOf(alice); - assertEq(balanceOfAliceAfterAvalancheFuji, balanceOfAliceBeforeEthSepolia + amountToSend); - } -} diff --git a/test/fork/Example01Fork.t.sol b/test/fork/Example01Fork.t.sol deleted file mode 100644 index 22e3c28..0000000 --- a/test/fork/Example01Fork.t.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"; -import {BurnMintERC677Helper, IERC20} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; - -contract Example01ForkTest is Test { - CCIPLocalSimulatorFork public ccipLocalSimulatorFork; - uint256 public sourceFork; - uint256 public destinationFork; - address public alice; - address public bob; - IRouterClient public sourceRouter; - uint64 public destinationChainSelector; - BurnMintERC677Helper public sourceCCIPBnMToken; - BurnMintERC677Helper public destinationCCIPBnMToken; - IERC20 public sourceLinkToken; - - function setUp() public { - string memory DESTINATION_RPC_URL = vm.envString( - "ETHEREUM_SEPOLIA_RPC_URL" - ); - string memory SOURCE_RPC_URL = vm.envString("ARBITRUM_SEPOLIA_RPC_URL"); - destinationFork = vm.createSelectFork(DESTINATION_RPC_URL); - sourceFork = vm.createFork(SOURCE_RPC_URL); - - bob = makeAddr("bob"); - alice = makeAddr("alice"); - - ccipLocalSimulatorFork = new CCIPLocalSimulatorFork(); - vm.makePersistent(address(ccipLocalSimulatorFork)); - - Register.NetworkDetails - memory destinationNetworkDetails = ccipLocalSimulatorFork - .getNetworkDetails(block.chainid); - destinationCCIPBnMToken = BurnMintERC677Helper( - destinationNetworkDetails.ccipBnMAddress - ); - destinationChainSelector = destinationNetworkDetails.chainSelector; - - vm.selectFork(sourceFork); - Register.NetworkDetails - memory sourceNetworkDetails = ccipLocalSimulatorFork - .getNetworkDetails(block.chainid); - sourceCCIPBnMToken = BurnMintERC677Helper( - sourceNetworkDetails.ccipBnMAddress - ); - sourceLinkToken = IERC20(sourceNetworkDetails.linkAddress); - sourceRouter = IRouterClient(sourceNetworkDetails.routerAddress); - } - - function prepareScenario() - public - returns ( - Client.EVMTokenAmount[] memory tokensToSendDetails, - uint256 amountToSend - ) - { - vm.selectFork(sourceFork); - vm.startPrank(alice); - sourceCCIPBnMToken.drip(alice); - - amountToSend = 100; - sourceCCIPBnMToken.approve(address(sourceRouter), amountToSend); - - tokensToSendDetails = new Client.EVMTokenAmount[](1); - tokensToSendDetails[0] = Client.EVMTokenAmount({ - token: address(sourceCCIPBnMToken), - amount: amountToSend - }); - - vm.stopPrank(); - } - - function test_transferTokensFromEoaToEoaPayFeesInLink() external { - ( - Client.EVMTokenAmount[] memory tokensToSendDetails, - uint256 amountToSend - ) = prepareScenario(); - vm.selectFork(destinationFork); - uint256 balanceOfBobBefore = destinationCCIPBnMToken.balanceOf(bob); - - vm.selectFork(sourceFork); - uint256 balanceOfAliceBefore = sourceCCIPBnMToken.balanceOf(alice); - ccipLocalSimulatorFork.requestLinkFromFaucet(alice, 10 ether); - - vm.startPrank(alice); - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(bob), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes( - Client.EVMExtraArgsV1({gasLimit: 0}) - ), - feeToken: address(sourceLinkToken) - }); - - uint256 fees = sourceRouter.getFee(destinationChainSelector, message); - sourceLinkToken.approve(address(sourceRouter), fees); - sourceRouter.ccipSend(destinationChainSelector, message); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = sourceCCIPBnMToken.balanceOf(alice); - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationFork); - uint256 balanceOfBobAfter = destinationCCIPBnMToken.balanceOf(bob); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } - - function test_transferTokensFromEoaToEoaPayFeesInNative() external { - ( - Client.EVMTokenAmount[] memory tokensToSendDetails, - uint256 amountToSend - ) = prepareScenario(); - vm.selectFork(destinationFork); - uint256 balanceOfBobBefore = destinationCCIPBnMToken.balanceOf(bob); - - vm.selectFork(sourceFork); - uint256 balanceOfAliceBefore = sourceCCIPBnMToken.balanceOf(alice); - - vm.startPrank(alice); - deal(alice, 5 ether); - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(bob), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes( - Client.EVMExtraArgsV1({gasLimit: 0}) - ), - feeToken: address(0) - }); - - uint256 fees = sourceRouter.getFee(destinationChainSelector, message); - sourceRouter.ccipSend{value: fees}(destinationChainSelector, message); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = sourceCCIPBnMToken.balanceOf(alice); - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationFork); - uint256 balanceOfBobAfter = destinationCCIPBnMToken.balanceOf(bob); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } -} diff --git a/test/fork/Example02Fork.t.sol b/test/fork/Example02Fork.t.sol deleted file mode 100644 index 461bd09..0000000 --- a/test/fork/Example02Fork.t.sol +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {CCIPLocalSimulatorFork, Register} from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol"; -import {BurnMintERC677Helper, IERC20} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol"; -import {BasicMessageReceiver} from "../../src/BasicMessageReceiver.sol"; - -contract Example02ForkTest is Test { - CCIPLocalSimulatorFork public ccipLocalSimulatorFork; - uint256 public sourceFork; - uint256 public destinationFork; - address public alice; - BasicMessageReceiver public basicMessageReceiver; - IRouterClient public destinationRouter; - IRouterClient public sourceRouter; - uint64 public destinationChainSelector; - - BurnMintERC677Helper public sourceCCIPBnMToken; - BurnMintERC677Helper public destinationCCIPBnMToken; - IERC20 public sourceLinkToken; - - function setUp() public { - string memory DESTINATION_RPC_URL = vm.envString("ETHEREUM_SEPOLIA_RPC_URL"); - string memory SOURCE_RPC_URL = vm.envString("ARBITRUM_SEPOLIA_RPC_URL"); - - sourceFork = vm.createFork(SOURCE_RPC_URL); - destinationFork = vm.createFork(DESTINATION_RPC_URL); - - alice = makeAddr("alice"); // Sender. - - ccipLocalSimulatorFork = new CCIPLocalSimulatorFork(); - vm.makePersistent(address(ccipLocalSimulatorFork)); - - vm.selectFork(destinationFork); - Register.NetworkDetails memory destinationNetworkDetails = - ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - - destinationCCIPBnMToken = BurnMintERC677Helper(destinationNetworkDetails.ccipBnMAddress); - destinationChainSelector = destinationNetworkDetails.chainSelector; - destinationRouter = IRouterClient(destinationNetworkDetails.routerAddress); - - basicMessageReceiver = new BasicMessageReceiver(address(destinationRouter)); // Receiver - - vm.selectFork(sourceFork); - Register.NetworkDetails memory sourceNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); - sourceCCIPBnMToken = BurnMintERC677Helper(sourceNetworkDetails.ccipBnMAddress); - sourceLinkToken = IERC20(sourceNetworkDetails.linkAddress); - sourceRouter = IRouterClient(sourceNetworkDetails.routerAddress); - } - - function buildSendTokenData() - public - returns (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) - { - amountToSend = 100; - - vm.selectFork(sourceFork); - - vm.startPrank(alice); - sourceCCIPBnMToken.drip(alice); - sourceCCIPBnMToken.approve(address(sourceRouter), amountToSend); - - tokensToSendDetails = new Client.EVMTokenAmount[](1); - tokensToSendDetails[0] = Client.EVMTokenAmount({token: address(sourceCCIPBnMToken), amount: amountToSend}); - - vm.stopPrank(); - } - - function test_fork_transferTokensEoaToContractPayFeesInLink() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = buildSendTokenData(); - - vm.selectFork(destinationFork); - uint256 receiverContractBalanceBefore = destinationCCIPBnMToken.balanceOf(address(basicMessageReceiver)); - - vm.selectFork(sourceFork); - uint256 aliceBalanceBefore = sourceCCIPBnMToken.balanceOf(alice); - assertEq(aliceBalanceBefore, 1e18); - - uint256 linkRequestQty = 20 ether; // 10 LINK is generally enough but network spikes can cause "ERC20: transfer amount exceeds balance" on some networks. - ccipLocalSimulatorFork.requestLinkFromFaucet(alice, linkRequestQty); - - vm.startPrank(alice); - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(basicMessageReceiver), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: "", // This will use default gas limit of 200k gas. - feeToken: address(sourceLinkToken) - }); - - uint256 fees = sourceRouter.getFee(destinationChainSelector, message); - sourceLinkToken.approve(address(sourceRouter), fees); - - sourceRouter.ccipSend(destinationChainSelector, message); - vm.stopPrank(); - - uint256 aliceBalanceAfter = sourceCCIPBnMToken.balanceOf(alice); - assertEq(aliceBalanceBefore - amountToSend, aliceBalanceAfter); - assertEq(sourceLinkToken.balanceOf(alice), linkRequestQty - fees); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationFork); - - uint256 receiverContractBalanceAfter = destinationCCIPBnMToken.balanceOf(address(basicMessageReceiver)); - assertEq(receiverContractBalanceAfter, receiverContractBalanceBefore + amountToSend); - } - - function test_fork_transferTokensEoaToContractPayFeesInNative() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = buildSendTokenData(); - - vm.selectFork(destinationFork); - uint256 receiverContractBalanceBefore = destinationCCIPBnMToken.balanceOf(address(basicMessageReceiver)); - - vm.selectFork(sourceFork); - uint256 aliceBalanceBefore = sourceCCIPBnMToken.balanceOf(alice); - assertEq(aliceBalanceBefore, 1e18); - - ccipLocalSimulatorFork.requestLinkFromFaucet(alice, 10 ether); - - vm.startPrank(alice); - deal(alice, 5 ether); - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(basicMessageReceiver), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: "", // This will use default gas limit of 200k gas. - feeToken: address(0) - }); - - uint256 fees = sourceRouter.getFee(destinationChainSelector, message); - - sourceRouter.ccipSend{value: fees}(destinationChainSelector, message); - vm.stopPrank(); - - uint256 aliceBalanceAfter = sourceCCIPBnMToken.balanceOf(alice); - assertEq(aliceBalanceBefore - amountToSend, aliceBalanceAfter); - - ccipLocalSimulatorFork.switchChainAndRouteMessage(destinationFork); - - uint256 receiverContractBalanceAfter = destinationCCIPBnMToken.balanceOf(address(basicMessageReceiver)); - assertEq(receiverContractBalanceAfter, receiverContractBalanceBefore + amountToSend); - } -} diff --git a/test/no-fork/Example01.t.sol b/test/no-fork/Example01.t.sol deleted file mode 100644 index 90377ae..0000000 --- a/test/no-fork/Example01.t.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import { - CCIPLocalSimulator, - IRouterClient, - LinkToken, - BurnMintERC677Helper -} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; - -contract Example01Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - - address alice; - address bob; - IRouterClient router; - uint64 destinationChainSelector; - BurnMintERC677Helper ccipBnMToken; - LinkToken linkToken; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - (uint64 chainSelector, IRouterClient sourceRouter,,, LinkToken link, BurnMintERC677Helper ccipBnM,) = - ccipLocalSimulator.configuration(); - - alice = makeAddr("alice"); - bob = makeAddr("bob"); - - router = sourceRouter; - destinationChainSelector = chainSelector; - ccipBnMToken = ccipBnM; - linkToken = link; - } - - function prepareScenario() - public - returns (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) - { - vm.startPrank(alice); - - ccipBnMToken.drip(alice); - - amountToSend = 100; - ccipBnMToken.approve(address(router), amountToSend); - - tokensToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenToSendDetails = - Client.EVMTokenAmount({token: address(ccipBnMToken), amount: amountToSend}); - tokensToSendDetails[0] = tokenToSendDetails; - - vm.stopPrank(); - } - - function test_transferTokensFromEoaToEoaPayFeesInLink() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = prepareScenario(); - - uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobBefore = ccipBnMToken.balanceOf(bob); - - vm.startPrank(alice); - ccipLocalSimulator.requestLinkFromFaucet(alice, 5 ether); - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(bob), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), - feeToken: address(linkToken) - }); - - uint256 fees = router.getFee(destinationChainSelector, message); - linkToken.approve(address(router), fees); - - router.ccipSend(destinationChainSelector, message); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobAfter = ccipBnMToken.balanceOf(bob); - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } - - function test_transferTokensFromEoaToEoaPayFeesInNative() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = prepareScenario(); - - uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobBefore = ccipBnMToken.balanceOf(bob); - - vm.startPrank(alice); - deal(alice, 5 ether); - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(bob), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), - feeToken: address(0) - }); - - uint256 fees = router.getFee(destinationChainSelector, message); - router.ccipSend{value: fees}(destinationChainSelector, message); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobAfter = ccipBnMToken.balanceOf(bob); - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } -} diff --git a/test/no-fork/Example02.t.sol b/test/no-fork/Example02.t.sol deleted file mode 100644 index 6b248ae..0000000 --- a/test/no-fork/Example02.t.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import { - CCIPLocalSimulator, - IRouterClient, - LinkToken, - BurnMintERC677Helper -} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {BasicMessageReceiver} from "../../src/BasicMessageReceiver.sol"; - -contract Example02Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - BasicMessageReceiver public basicMessageReceiver; - address public alice; - - IRouterClient public router; - uint64 public destinationChainSelector; - BurnMintERC677Helper public ccipBnMToken; - LinkToken public linkToken; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - ( - uint64 chainSelector, - IRouterClient sourceRouter, - IRouterClient destinationRouter, - , - LinkToken link, - BurnMintERC677Helper ccipBnM, - ) = ccipLocalSimulator.configuration(); - - router = sourceRouter; - destinationChainSelector = chainSelector; - ccipBnMToken = ccipBnM; - linkToken = link; - - basicMessageReceiver = new BasicMessageReceiver(address(destinationRouter)); - - alice = makeAddr("alice"); - } - - function test_TransferTokensFromEoaToSmartContract() external { - ccipLocalSimulator.requestLinkFromFaucet(alice, 5 ether); - ccipBnMToken.drip(alice); - uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); - uint256 balanceOfReceiverBefore = ccipBnMToken.balanceOf(address(basicMessageReceiver)); - assertEq(balanceOfAliceBefore, 1 ether); - - vm.startPrank(alice); - - uint256 amount = 100; - ccipBnMToken.approve(address(router), amount); - - Client.EVMTokenAmount[] memory tokensToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenToSendDetails = - Client.EVMTokenAmount({token: address(ccipBnMToken), amount: amount}); - - tokensToSendDetails[0] = tokenToSendDetails; - - Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({ - receiver: abi.encode(basicMessageReceiver), - data: abi.encode(""), - tokenAmounts: tokensToSendDetails, - extraArgs: "", // This will use default gas limit of 200k gas. - feeToken: address(linkToken) - }); - - uint256 fees = router.getFee(destinationChainSelector, message); - linkToken.approve(address(router), fees); - - router.ccipSend(destinationChainSelector, message); - - vm.stopPrank(); - - uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); - uint256 balanceOfReceiverAfter = ccipBnMToken.balanceOf(address(basicMessageReceiver)); - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amount); - assertEq(balanceOfReceiverAfter, balanceOfReceiverBefore + amount); - } -} diff --git a/test/no-fork/Example03.t.sol b/test/no-fork/Example03.t.sol deleted file mode 100644 index dad7bfc..0000000 --- a/test/no-fork/Example03.t.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import { - CCIPLocalSimulator, - IRouterClient, - LinkToken, - BurnMintERC677Helper -} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol"; -import {BasicTokenSender} from "../../src/BasicTokenSender.sol"; - -contract Example03Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - BasicTokenSender public basicTokenSender; - address public alice; - address public bob; - - uint64 public destinationChainSelector; - BurnMintERC677Helper public ccipBnMToken; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - (uint64 chainSelector, IRouterClient sourceRouter,,, LinkToken link, BurnMintERC677Helper ccipBnM,) = - ccipLocalSimulator.configuration(); - - destinationChainSelector = chainSelector; - ccipBnMToken = ccipBnM; - - basicTokenSender = new BasicTokenSender(address(sourceRouter), address(link)); - - alice = makeAddr("alice"); - bob = makeAddr("bob"); - } - - function prepareScenario() - public - returns (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) - { - vm.startPrank(alice); - - ccipBnMToken.drip(alice); - - amountToSend = 100; - ccipBnMToken.approve(address(basicTokenSender), amountToSend); - - tokensToSendDetails = new Client.EVMTokenAmount[](1); - Client.EVMTokenAmount memory tokenToSendDetails = - Client.EVMTokenAmount({token: address(ccipBnMToken), amount: amountToSend}); - tokensToSendDetails[0] = tokenToSendDetails; - - vm.stopPrank(); - } - - function test_transferTokensFromSmartContractAndPayFeesInLink() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = prepareScenario(); - - uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobBefore = ccipBnMToken.balanceOf(bob); - - vm.startPrank(alice); - ccipLocalSimulator.requestLinkFromFaucet(address(basicTokenSender), 5 ether); - basicTokenSender.send(destinationChainSelector, bob, tokensToSendDetails, BasicTokenSender.PayFeesIn.LINK); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobAfter = ccipBnMToken.balanceOf(bob); - - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } - - function test_transferTokensFromSmartContractAndPayFeesInNative() external { - (Client.EVMTokenAmount[] memory tokensToSendDetails, uint256 amountToSend) = prepareScenario(); - - uint256 balanceOfAliceBefore = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobBefore = ccipBnMToken.balanceOf(bob); - - vm.startPrank(alice); - deal(address(basicTokenSender), 5 ether); - basicTokenSender.send(destinationChainSelector, bob, tokensToSendDetails, BasicTokenSender.PayFeesIn.Native); - vm.stopPrank(); - - uint256 balanceOfAliceAfter = ccipBnMToken.balanceOf(alice); - uint256 balanceOfBobAfter = ccipBnMToken.balanceOf(bob); - - assertEq(balanceOfAliceAfter, balanceOfAliceBefore - amountToSend); - assertEq(balanceOfBobAfter, balanceOfBobBefore + amountToSend); - } -} diff --git a/test/no-fork/Example04.t.sol b/test/no-fork/Example04.t.sol deleted file mode 100644 index 1a45d08..0000000 --- a/test/no-fork/Example04.t.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import { - CCIPLocalSimulator, IRouterClient, BurnMintERC677Helper -} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {ProgrammableTokenTransfers} from "../../src/ProgrammableTokenTransfers.sol"; - -contract Example04Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - ProgrammableTokenTransfers public sender; - ProgrammableTokenTransfers public receiver; - - uint64 public destinationChainSelector; - BurnMintERC677Helper public ccipBnMToken; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - (uint64 chainSelector, IRouterClient sourceRouter,,,, BurnMintERC677Helper ccipBnM,) = - ccipLocalSimulator.configuration(); - - sender = new ProgrammableTokenTransfers(address(sourceRouter)); - receiver = new ProgrammableTokenTransfers(address(sourceRouter)); - - destinationChainSelector = chainSelector; - ccipBnMToken = ccipBnM; - } - - function test_programmableTokenTransfers() external { - deal(address(sender), 1 ether); - ccipBnMToken.drip(address(sender)); - - uint256 balanceOfSenderBefore = ccipBnMToken.balanceOf(address(sender)); - uint256 balanceOfReceiverBefore = ccipBnMToken.balanceOf(address(receiver)); - - string memory messageToSend = "Hello, World!"; - uint256 amountToSend = 100; - - bytes32 messageId = sender.sendMessage( - destinationChainSelector, address(receiver), messageToSend, address(ccipBnMToken), amountToSend - ); - - ( - bytes32 latestMessageId, - uint64 latestMessageSourceChainSelector, - address latestMessageSender, - string memory latestMessage, - address latestMessageToken, - uint256 latestMessageAmount - ) = receiver.getLastReceivedMessageDetails(); - - uint256 balanceOfSenderAfter = ccipBnMToken.balanceOf(address(sender)); - uint256 balanceOfReceiverAfter = ccipBnMToken.balanceOf(address(receiver)); - - assertEq(latestMessageId, messageId); - assertEq(latestMessageSourceChainSelector, destinationChainSelector); - assertEq(latestMessageSender, address(sender)); - assertEq(latestMessage, messageToSend); - assertEq(latestMessageToken, address(ccipBnMToken)); - assertEq(latestMessageAmount, amountToSend); - - assertEq(balanceOfSenderAfter, balanceOfSenderBefore - amountToSend); - assertEq(balanceOfReceiverAfter, balanceOfReceiverBefore + amountToSend); - } -} diff --git a/test/no-fork/Example05.t.sol b/test/no-fork/Example05.t.sol deleted file mode 100644 index 2c469fd..0000000 --- a/test/no-fork/Example05.t.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {CCIPLocalSimulator, IRouterClient, LinkToken} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {BasicMessageSender} from "../../src/BasicMessageSender.sol"; -import {BasicMessageReceiver} from "../../src/BasicMessageReceiver.sol"; - -contract Example05Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - BasicMessageSender public sender; - BasicMessageReceiver public receiver; - - uint64 public destinationChainSelector; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - (uint64 chainSelector, IRouterClient sourceRouter, IRouterClient destinationRouter,, LinkToken link,,) = - ccipLocalSimulator.configuration(); - - sender = new BasicMessageSender(address(sourceRouter), address(link)); - receiver = new BasicMessageReceiver(address(destinationRouter)); - - destinationChainSelector = chainSelector; - } - - function test_sendAndReceiveCrossChainMessagePayFeesInNative() external { - deal(address(sender), 1 ether); - - string memory messageToSend = "Hello, World!"; - - bytes32 messageId = - sender.send(destinationChainSelector, address(receiver), messageToSend, BasicMessageSender.PayFeesIn.Native); - - (bytes32 latestMessageId, uint64 latestSourceChainSelector, address latestSender, string memory latestMessage) = - receiver.getLatestMessageDetails(); - - assertEq(latestMessageId, messageId); - assertEq(latestSourceChainSelector, destinationChainSelector); - assertEq(latestSender, address(sender)); - assertEq(latestMessage, messageToSend); - } -} diff --git a/test/no-fork/Example06.t.sol b/test/no-fork/Example06.t.sol deleted file mode 100644 index 0ff0bff..0000000 --- a/test/no-fork/Example06.t.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {CCIPLocalSimulator, IRouterClient, LinkToken} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {BasicMessageSender} from "../../src/BasicMessageSender.sol"; -import {BasicMessageReceiver} from "../../src/BasicMessageReceiver.sol"; - -contract Example06Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - BasicMessageSender public sender; - BasicMessageReceiver public receiver; - - uint64 public destinationChainSelector; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - (uint64 chainSelector, IRouterClient sourceRouter, IRouterClient destinationRouter,, LinkToken link,,) = - ccipLocalSimulator.configuration(); - - sender = new BasicMessageSender(address(sourceRouter), address(link)); - receiver = new BasicMessageReceiver(address(destinationRouter)); - - destinationChainSelector = chainSelector; - } - - function test_sendAndReceiveCrossChainMessagePayFeesInLink() external { - ccipLocalSimulator.requestLinkFromFaucet(address(sender), 5 ether); - - string memory messageToSend = "Hello, World!"; - - bytes32 messageId = - sender.send(destinationChainSelector, address(receiver), messageToSend, BasicMessageSender.PayFeesIn.Native); - - (bytes32 latestMessageId, uint64 latestSourceChainSelector, address latestSender, string memory latestMessage) = - receiver.getLatestMessageDetails(); - - assertEq(latestMessageId, messageId); - assertEq(latestSourceChainSelector, destinationChainSelector); - assertEq(latestSender, address(sender)); - assertEq(latestMessage, messageToSend); - } -} diff --git a/test/no-fork/Example07.t.sol b/test/no-fork/Example07.t.sol deleted file mode 100644 index e28cf5e..0000000 --- a/test/no-fork/Example07.t.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {CCIPLocalSimulator, IRouterClient, LinkToken} from "@chainlink/local/src/ccip/CCIPLocalSimulator.sol"; -import {MyNFT} from "../../src/cross-chain-nft-minter/MyNFT.sol"; -import {DestinationMinter} from "../../src/cross-chain-nft-minter/DestinationMinter.sol"; -import {SourceMinter} from "../../src/cross-chain-nft-minter/SourceMinter.sol"; - -contract Example07Test is Test { - CCIPLocalSimulator public ccipLocalSimulator; - MyNFT public myNFT; - DestinationMinter public destinationMinter; - SourceMinter public sourceMinter; - - address public alice; - uint64 public destinationChainSelector; - - function setUp() public { - ccipLocalSimulator = new CCIPLocalSimulator(); - - ( - uint64 chainSelector, - IRouterClient sourceRouter, - IRouterClient destinationRouter, - , - LinkToken link, - , - - ) = ccipLocalSimulator.configuration(); - - myNFT = new MyNFT(); - destinationMinter = new DestinationMinter( - address(destinationRouter), - address(myNFT) - ); - myNFT.transferOwnership(address(destinationMinter)); - - sourceMinter = new SourceMinter(address(sourceRouter), address(link)); - - alice = makeAddr("alice"); - destinationChainSelector = chainSelector; - } - - function test_executeReceivedMessageAsFunctionCall() external { - ccipLocalSimulator.requestLinkFromFaucet( - address(sourceMinter), - 5 ether - ); - - vm.startPrank(alice); - sourceMinter.mint( - destinationChainSelector, - address(destinationMinter), - SourceMinter.PayFeesIn.LINK - ); - vm.stopPrank(); - - assertEq(myNFT.balanceOf(alice), 1); - } -}