# Using Chainlink Local simulator in your Hardhat project
Source: https://docs.chain.link/chainlink-local/build/ccip/hardhat/local-simulator

> For the complete documentation index, see [llms.txt](/llms.txt).

> **NOTE: Integrate Chainlink Local into your project**
>
> ### npm
>
> If you use [NPM](https://www.npmjs.com/), install the [@chainlink/local NPM
> package](https://www.npmjs.com/package/@chainlink/local):
>
> ```shell
> npm install @chainlink/local@0.3.1
> ```
>
> ### yarn
>
> If you use [Yarn](https://yarnpkg.com/), install the [@chainlink/local NPM
> package](https://www.npmjs.com/package/@chainlink/local):
>
> ```shell
> yarn add @chainlink/local@0.3.1
> ```
>
> ### foundry
>
> If you use [Foundry](https://book.getfoundry.sh/):
>
> ```shell
> forge install smartcontractkit/chainlink-local@v0.3.1
> ```
>
> and then set remappings to: `@chainlink/local/=lib/chainlink-local/` in either `remappings.txt` or `foundry.toml` file
>
> ### RemixIDE
>
> Import the `CCIPLocalSimulator` contract from the `@chainlink/local` package:
>
> ```solidity
> import {CCIPLocalSimulator} from "@chainlink/local@0.3.1/src/ccip/CCIPLocalSimulator.sol";
> ```

You can use Chainlink Local to run CCIP 2.0 in a localhost environment within your Hardhat project. To get started quickly, you will use the
[CCIP Hardhat Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-hardhat). This project is a Hardhat 3 boilerplate that includes the Chainlink Local
package (V3, `v0.3.1`) and CCIP 2.0 examples (`Example01`-`Example10`) as Hardhat tasks and Ignition modules.

In local mode, the simulator delivers messages synchronously inside `ccipSend` and applies the CCIP 2.0 OnRamp and OffRamp message rules: finality, one token per message, no zero amounts, and receiver CCV config validation. Read [CCIP 2.0 behavior in local mode](/chainlink-local/learn/ccip-v2-local) for the full list of what is and is not enforced.

> **NOTE: Hardhat 3 required**
>
> Chainlink Local `0.3.x` does not compile under Hardhat 2 (the contracts use `@openzeppelin/contracts@4.8.3`-style
> imports that Hardhat 2 cannot resolve, `HH411`). Hardhat 2 users stay on the `0.2.x` line (`npm install
>   @chainlink/local@0.2.9`).

## Prerequisites

1. In a terminal, clone the [CCIP Hardhat Starter Kit](https://github.com/smartcontractkit/ccip-starter-kit-hardhat) repository and change directories:

   ```shell
   git clone https://github.com/smartcontractkit/ccip-starter-kit-hardhat.git && \
   cd ./ccip-starter-kit-hardhat/
   ```

2. Install the dependencies, including the [Chainlink Local](https://github.com/smartcontractkit/chainlink-local) package:

   ```shell
   npm install
   ```

3. Compile the contracts:

   ```shell
   npx hardhat build
   ```

The kit pins `solc 0.8.26` with `evmVersion: "cancun"` in `hardhat.config.ts`. CCIP 2.0 contracts need Cancun opcodes, so keep that setting in your own project. Contracts from npm packages (including the Chainlink Local simulator) are only compiled if listed in `solidity.npmFilesToBuild`.

## Test token transfers

You will run a test that transfers tokens between two accounts using `ExtraArgsV3` with Fast Transfers (non-zero block confirmations). The test file `Example01.test.ts` is located in the `./test` directory. It calls the real `example01` task action (`tasks/Example01.ts`) against the Chainlink Local simulator, so a regression in the task fails the test. The file contains four test cases:

1. **Transfer with native fees**: Transfers CCIP-BnM from the sender to a receiver EOA, paying the fee in native gas, and verifies the receiver was credited and the router collected the fee.
2. **Transfer with LINK fees**: Same transfer, paying the fee in LINK.
3. **Revert on zero amount**: The task rejects a zero amount before sending anything.
4. **Revert on zero receiver**: The task rejects a zero receiver.

For a detailed explanation of the test file, refer to the [Examine the code](#examine-the-code) section.

In your terminal, run the following command to execute the test:

```shell
npx hardhat test nodejs
```

Example output:

```text
$ npx hardhat test nodejs

Example01
  ✔ transfers token to an EOA paying the fee in native (1048ms)
  ✔ transfers token to an EOA paying the fee in LINK (166ms)
  ✔ rejects a zero amount before sending anything (105ms)
  ✔ rejects a zero receiver (96ms)

...

51 passing (3392ms)
```

No RPC URLs or keys are needed: the tests in `test/` run against the in-process Chainlink Local simulator.

## Examine the code

### Setup

The shared setup lives in `test/utils/localSimulator.ts`. Each call to `setupLocalSimulator()` returns an isolated in-process chain with the simulator deployed:

1. Connect to the in-process network and deploy the [Chainlink Local simulator](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#cciplocalsimulator) contract:

   ```typescript
   const connection = await hre.network.connect()
   const { viem } = connection
   const simulator = await viem.deployContract("CCIPLocalSimulator")
   ```

2. Read the simulator's [`configuration`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#configuration) and connect to the router, LINK and CCIP-BnM contracts:

   ```typescript
   const [chainSelector, routerAddress, , , linkAddress, bnmAddress] = await simulator.read.configuration()

   const router = await viem.getContractAt("CCIPLocalRouter", routerAddress)
   const link = await viem.getContractAt("LinkToken", linkAddress)
   const ccipBnM = await viem.getContractAt("BurnMintERC677Helper", bnmAddress)
   ```

   **Note**: The [`configuration`](/chainlink-local/api-reference/v0.3.1/ccip-local-simulator#configuration) call also returns the destination router, WETH9 and CCIP-LnM contracts, which these tests do not use. Hence, there are empty slots in the destructured array.

3. The example tasks open their own connection with `hre.network.connect()`, which would be a fresh chain. The setup hands them an hre whose `connect()` returns the test's connection instead, and exposes the first Hardhat wallet as `broadcaster` (the account every example task sends from):

   ```typescript
   const taskHre = Object.create(hre, { network: { value: { connect: async () => connection } } }) as typeof hre
   ```

4. `Example01`'s `beforeEach` funds the broadcaster with CCIP-BnM and picks the receiver:

   ```typescript
   s = await setupLocalSimulator()
   receiver = s.other.account.address
   await s.ccipBnM.write.drip([s.broadcaster.account.address])
   ```

### Test case 1: Transfer with native fees

The `transfers token to an EOA paying the fee in native` test case:

1. Set the fee the local router charges. The local router charges only what `router.write.setFee(...)` sets (default 0) and uses a mock message ID:

   ```typescript
   const fee = 10n ** 16n
   await s.router.write.setFee([fee])
   const senderBefore = await s.ccipBnM.read.balanceOf([s.broadcaster.account.address])
   ```

2. Run the task with the router, chain selector, receiver, token, amount, gas limit, block confirmations and fee token. The constants used:

   ```typescript
   const AMOUNT = 10n ** 17n
   const EOA_GAS_LIMIT = 0
   const BLOCK_CONFIRMATIONS = 5
   ```

   - `EOA_GAS_LIMIT` is `0` because the receiver is an EOA: there is no `ccipReceive` logic to execute. The sender is billed for the specified gas limit, not actual usage, so do not set a gas limit for token-only transfers.
   - `BLOCK_CONFIRMATIONS` is `5` (any value `> 0`): this requests a Fast Transfer, delivered before full chain finality. `0` would mean default finality.

   ```typescript
   await run()
   ```

   where `run()` calls the task action with `feeTokenAddress: ZERO_ADDRESS` and the test's `taskHre`.

3. Inside the task, the `ExtraArgsV3` are encoded with the `encodeV3Basic` helper (`scripts/CallEncodeExtraArgsOffchain.ts`), and the [Client.EVM2AnyMessage](/ccip/evm/api-reference/v2.0.0/client#evm2anymessage) is built with the receiver, token amounts, extraArgs and fee token:

   ```typescript
   const params: EncodeV3Params = { gasLimit, blockConfirmations }
   const extraArgs = await encodeV3Basic(params)

   const ccipMessage = {
     receiver: receiverEncoded,
     data: "0x" as `0x${string}`,
     tokenAmounts,
     extraArgs,
     feeToken: feeTokenAddress,
   }
   ```

   `encodeV3Basic` maps `blockConfirmations` to the `requestedFinalityConfig` with `FinalityCodec._encodeBlockDepth`. Read more about `extraArgs` formats in the [CCIP best practices](/ccip/evm/concepts/best-practices#using-extraargs).

4. The task quotes the fee, approves the router to spend the token, and sends. With `feeTokenAddress` set to the zero address the fee is paid in native gas:

   ```typescript
   const fee = await publicClient.readContract({
     address: sourceRouter,
     abi: routerArtifact.abi,
     functionName: "getFee",
     args: [destinationChainSelector, ccipMessage],
   })
   ```

5. The test verifies the transfer: the receiver was credited, the sender was debited, and the router collected the fee. Because local mode delivers synchronously inside `ccipSend`, the assertions run immediately after the task:

   ```typescript
   assert.equal(await s.ccipBnM.read.balanceOf([receiver]), AMOUNT)
   assert.equal(await s.ccipBnM.read.balanceOf([s.broadcaster.account.address]), senderBefore - AMOUNT)
   assert.equal(await s.publicClient.getBalance({ address: s.router.address }), fee)
   ```

### Test case 2: Transfer with LINK fees

The `transfers token to an EOA paying the fee in LINK` test case runs the same task with two differences:

1. Fund the broadcaster with LINK from the simulator faucet instead of relying on native gas:

   ```typescript
   await s.simulator.write.requestLinkFromFaucet([s.broadcaster.account.address, 10n ** 18n])
   ```

2. Pass the LINK token address as the fee token (`run({ feeTokenAddress: s.link.address })`), and verify the router collected the fee in LINK:

   ```typescript
   assert.equal(await s.link.read.balanceOf([s.router.address]), fee)
   ```

### Revert test cases

The remaining two test cases verify the task's argument validation, which mirrors the CCIP 2.0 send-time rules:

1. A zero amount is rejected before anything is sent, and no tokens move:

   ```typescript
   await assert.rejects(run({ amount: 0n }), /Required args must be non-zero/)
   assert.equal(await s.ccipBnM.read.balanceOf([receiver]), 0n)
   ```

2. A zero receiver is rejected the same way:

   ```typescript
   await assert.rejects(run({ receiver: ZERO_ADDRESS }), /Required args must be non-zero/)
   ```

## Next steps

- Send a data-only message to a receiver contract and see the local router enforce its CCV config and minimum block depth in the [receiver CCV config guide](/chainlink-local/build/ccip/hardhat/data-messages-receiver-ccv-config).
- For the other test files in the `./test` directory (`Example03.test.ts` programmable token transfers, `Example04.test.ts` sender contract, `Example05.test.ts` manual execution path), see the [CCIP starter kit examples map](/chainlink-local/learn/starter-kit-examples).
- To learn how to use Chainlink Local in forked environments,
  refer to the guide on [Using Chainlink Local Simulator in your Hardhat project with forked environments](/chainlink-local/build/ccip/hardhat/local-simulator-fork).