Build a Full Stack Interchain dApp with Next.js, Solidity & Axelar
Tags: javascript, webdev, nextjs, solidity
Published: 2023-07-06 12:49:40

Today's blockchain ecosystem is characterized by many blockchain networks operating independently, each with unique features and functionalities. However, this fragmentation presents challenges regarding seamless communication and collaboration between these networks. To overcome these barriers, a vital element that has emerged is a programmable blockchain interoperability layer.

The programmable blockchain interoperability layer bridges different blockchain networks, facilitating the exchange of information and assets securely and efficiently. It enables smooth interaction by providing a standardized framework for cross-chain communication, allowing various blockchain networks to interoperate seamlessly.

In this tutorial, you will learn how to build a full-stack interchain decentralized application with Next.js, Solidity, and [Axelar General message passing](https://docs.axelar.dev/dev/general-message-passing/overview) to send messages from one blockchain to another.

For a quick start, you can find the complete code for this tutorial on [GitHub](https://github.com/axelarnetwork/fullstack-interchain-dapp). However, in the video provided below, you can see the completed app that offers users the ability to:

* Connect their wallet
    
* Enter their desired message for cross-chain interaction
    
* Send the message from Binance to Avalanche
    

{% embed https://youtu.be/o-OrYvyN38M %} 

## Getting Started with Axelar General Message Passing

[Axelar's General Message Passing (GMP)](https://docs.axelar.dev/dev/general-message-passing/overview) feature empowers developers by enabling them to call any function on interconnected chains seamlessly.

With GMP, developers gain the ability to:

1. Call a contract on chain A and interact with a contract on chain B.
    
2. Execute cross-chain transactions by calling a contract on chain A and sending tokens to chain B.
    

## Prerequisite

Before getting started, you need the following prerequisites:

* [**Node.js**](https://nodejs.org/en/) and its package manager NPM, version 18\*. Verify Node.js is installed by running the following terminal command: `node -v && npm -v`
    
* A basic understanding of [JavaScript](https://www.w3schools.com/js/default.asp), [Solidity](https://www.tutorialspoint.com/solidity/index.htm), and [React/Next.js](https://www.w3schools.com/REACT/DEFAULT.ASP)
    

## Project Setup and Installation

To start the project setup and installation quickly, clone this [**project on GitHub**](https://github.com/Olanetsoft/fullstack-interchain-dapp/tree/starter). Make sure you're on the `starter` branch using the following command:

```bash
git clone <https://github.com/axelarnetwork/fullstack-interchain-dapp>
```

Next, install the project locally after cloning it using the following command in your terminal.

Here's how you can install the project using `npm`:

```bash
cd fullstack-interchain-dapp && npm i && npm run dev
```

Next.js will start a hot-reloading development environment accessible by default at [http://localhost:3000](http://localhost:3000).

![Build a Full Stack Interchain dApp with Next.js, Solidity & Axelar](https://cdn.hashnode.com/res/hashnode/image/upload/v1686054110807/41e991f5-c25d-4e68-ba91-27b067becc23.png)

## Building a Smart Contract with Hardhat and Axelar GMP

In this section, you will build an interchain smart contract leveraging the Axelar GMP feature to send messages from one chain to another.

Navigate to the project's root folder you cloned in the previous step, and then run the following commands to create a new Hardhat project.

```bash
mkdir hardhat
cd hardhat
npm install --save-dev hardhat
```

Let's get a sample project by running the command below:

```bash
npx hardhat
```

We'll go with the following options:

`What do you want to do?`

`✔ Create A JavaScript Project`

`✔ Hardhat project root:`

`? Do you want to add a .gitignore? (Y/n) › y`

`? Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) › y`

The `@nomicfoundation/hardhat-toolbox` plugin bundles all the commonly used packages and Hardhat plugins recommended to start developing with Hardhat.

Just in case it didn't install automatically, install this other requirement with the following command:

```apache
npm i @nomicfoundation/hardhat-toolbox
```

Next, install `@axelar-network/axelar-gmp-sdk-solidity` for Axelar General Message Passing SDK in Solidity and `dotenv` with the following command:

```apache
npm i @axelar-network/axelar-gmp-sdk-solidity dotenv
```

To ensure everything works, run the command below in the `hardhat` directory.

```apache
npx hardhat test
```

You will see a passed test result in our console.

Delete `Lock.js` from the test folder and delete `deploy.js` from the `scripts` directory. After that, go to contracts and delete `Lock.sol.`

> The folders themselves should not be deleted!

Create a `SendMessage.sol` file inside the `contracts` directory and update it with the following code snippet. When using Hardhat, file layout is crucial, so pay attention!

```solidity
// SPDX-License-Identifier: MIT
// SPDX license identifier specifies which open-source license is being used for the contract
pragma solidity 0.8.9;

// Importing external contracts for dependencies
import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol';
import { IAxelarGateway } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol';
import { IAxelarGasService } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol';
import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol';

contract SendMessage is AxelarExecutable {

    string public value;
    string public sourceChain;
    string public sourceAddress;

    IAxelarGasService public immutable gasService;

    constructor(address gateway_, address gasReceiver_) AxelarExecutable(gateway_) {
        // Sets the immutable state variable to the address of gasReceiver_
        gasService = IAxelarGasService(gasReceiver_);
    }

    function sendMessage(
        string calldata destinationChain,
        string calldata destinationAddress,
        string calldata value_
    ) external payable {
   
        bytes memory payload = abi.encode(value_);

        if (msg.value > 0) {
            gasService.payNativeGasForContractCall{ value: msg.value }(
                address(this),
                destinationChain,
                destinationAddress,
                payload,
                msg.sender
            );
        }
        // Calls the Axelar gateway contract with the specified destination chain and address and sends the payload along with the call
        gateway.callContract(destinationChain, destinationAddress, payload);
    }

    function _execute(
        string calldata sourceChain_,
        string calldata sourceAddress_,
        bytes calldata payload_
    ) internal override {
        // Decodes the payload bytes into the string value and sets the state variable for this contract
        (value) = abi.decode(payload_, (string));
        
sourceChain = sourceChain_;
        sourceAddress = sourceAddress_;
    }
}
```

In the code snippet above we:

* Create a `SendMessage` contract that extends the `AxelarExecutable` contract
    
* Import `AxelarExecutable`, `IAxelarGateway`, `IAxelarGasService` from the `@axelar-network/axelar-gmp-sdk-solidity` library.
    
* Define `four` state variables: `value`, `sourceChain`, `sourceAddress`, and `gasService`. The `gasService` state variable is immutable and can only be set during contract deployment.
    
* Initialize the `gasService` variable with the provided `gasReceiver_` address.
    
* Create a `sendMessage` function that takes three string parameters: `destinationChain`, `destinationAddress`, `value_` and it utilizes `gasService.payNativeGasForContractCall` with native gas (Ether).
    
* Utilized the `gateway` contract's `callContract` function with the specified `destinationChain`, `destinationAddress`, and `payload` parameters.
    
* The `_execute` function decodes the payload bytes into the `value` string and updates the state variables `sourceChain` and `sourceAddress`.
    

### Setup Deployment Script

Next, create a `deploy.js` file in the `scripts` folder and add the following code snippet:

```jsx
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

async function main() {
  const SendMessage = await hre.ethers.getContractFactory("SendMessage");
  const sendMessage = await SendMessage.deploy(
    "",
    ""
  );

  await sendMessage.deployed();

  console.log(`SendMessage contract deployed to ${sendMessage.address}`);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

```

In the code snippet above:

* The `main` function has the `SendMessage` contract factory obtained using `hre.ethers.getContractFactory`.
    
* The `sendMessage` contract is deployed using the `SendMessage.deploy` method with two strings as arguments.
    
* `await sendMessage.deployed()` statement ensures that the deployment is completed before moving forward.
    
* The deployed contract's address is logged into the console.
    

### Setup Remote Procedure Call (RPC) to Testnet

Remote Procedure Call (RPC) is a protocol used for communication between client and server systems in a network or blockchain environment. It enables clients to execute procedures or functions on remote servers and receive the results. RPC abstracts the underlying network details and allows clients to invoke methods on servers as if they were local.

Before you proceed to set up RPC, create a `.env` file using the command below:

```apache
touch .env
```

> Ensure you are in the hardhat directory before running the command above.

Inside the `.env` file you just created, add the following key:

```apache
PRIVATE_KEY= // Add your account private key here
```

Getting your private account key is easy. Check out this [**post**](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key).

Next, set up RPC for Binance and Avalanche networks by updating the `hardhat.config.js` file with the following code snippet:

```javascript
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });
require("solidity-coverage");

const PRIVATE_KEY = process.env.PRIVATE_KEY;

// This is a sample Hardhat task. To learn how to create your own go to
// <https://hardhat.org/guides/create-task.html>
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to <https://hardhat.org/config/> to learn more

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
  networks: {
    bsc: {
      url: "<https://data-seed-prebsc-1-s1.binance.org:8545>",
      chainId: 97,
      accounts: [PRIVATE_KEY],
    },
    avalancheFujiTestnet: {
      url: "<https://avalanche-fuji-c-chain.publicnode.com>",
      chainId: 43113,
      accounts: [PRIVATE_KEY],
    },
  },
  mocha: {
    timeout: 10000000000,
  },
};

```

You have successfully configured RPC for Binance and Avalanche test networks, you will proceed with the smart contract deployment to those networks in the following step.

### Deploy Smart Contract to Binance and Avalanche Network

In this section, you will deploy the smart contract to Binance and Avalanche Testnet. However, before you proceed, you need to specify the Axelar `Gateway Service` and the `Gas Service` Contract in the `SendMessage.deploy()` method within the `deploy.js` file you created earlier.

You can find the Axelar Gas Service and Gateway contracts list for all the chains Axelar currently supports [here](https://docs.axelar.dev/resources/testnet).

You also need a faucet for your Binance and Avalanche accounts to ensure successful contract deployment. To obtain the Binance faucet, visit this [link](http://discord.gg/bnbchain), and for the Avalanche faucet, access it [here](https://faucet.avax.network/).

### Deploy to Binance Testnet

Update the `deploy.js` file inside the `scripts` folder to deploy to Binance testnet with the following code snippet:

```apache
//...

async function main() {
  //...

  // Update arguments with the Axelar gateway and
  // gas service on Binance testnet
  const sendMessage = await SendMessage.deploy(
    "0x4D147dCb984e6affEEC47e44293DA442580A3Ec0",
    "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6"
  );

  //...
}

//..
```

To deploy the contract on the Binance testnet, run the following command:

```apache
npx hardhat run scripts/deploy.js --network bsc
```

For example, the contract address will be displayed in your console: `0xC1b8fC9208E51aC997895626b0f384153E94f2A7`.

### Deploy to Avalanche Fuji Testnet

Update the `deploy.js` file inside the `scripts` folder to deploy to Avalanche testnet with the following code snippet:

```apache
//...

async function main() {
  //...

  // Update arguments with the Axelar gateway and
  // gas service on Avalanche testnet
  const sendMessage = await SendMessage.deploy(
    "0xC249632c2D40b9001FE907806902f63038B737Ab",
    "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6"
  );

  //...
}

//..
```

To deploy the contract on the Avalanche testnet, run the following command:

```apache
npx hardhat run scripts/deploy.js --network avalancheFujiTestnet
```

The contract address will be displayed on your console; for example, `0x2a03DCB9B24431d02839822209D58262f5e5df52`. Make sure to save both deployed contract addresses, as you will need them for front-end integration.

## Integrating a Nextjs Frontend Application with Smart Contract

In the previous steps, you successfully built and deployed the smart contract. Now, it's time to interact with it from the front end, just as you would typically interact with decentralized applications on the web.

You already have the Next.js frontend project cloned, and the configuration for `WAGMI` and `Rainbowkit` is set up. This means you can proceed to update the existing application and connect your smart contract for testing.

### Implementing Write Smart Contract Functionality

Interacting with our contract is quite simple from the front-end application, thanks to `WAGMI`, `RainbowKit`, and `ethers`.

Create a `.env.local` file in the root directory using the command below:

```apache
touch .env.local
```

> Ensure you are in the root directory before running the command above.

Inside the `.env.local` file you just created, add the following key:

```apache
NEXT_PUBLIC_AVALANCHE_RPC_URL=https://avalanche-fuji-c-chain.publicnode.com
NEXT_PUBLIC_BSC_CONTRACT_ADDRESS=<BSC_CONTRACT_ADDRESS>
NEXT_PUBLIC_AVALANCHE_CONTRACT_ADDRESS=<AVALANCHE_CONTRACT_ADDRESS>
```

Replace `<BSC_CONTRACT_ADDRESS>` with the contract address, you deployed to the Binance testnet and replace `<AVALANCHE_CONTRACT_ADDRESS>` with the contract address, you deployed to the Avalanche Fuji Testnet earlier in this tutorial.

Next, implement the write functionality for the smart contract, add the following code snippet to the `index.js` file located in the `pages` directory.

```javascript
//...

const BSC_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_BSC_CONTRACT_ADDRESS;
const AVALANCHE_CONTRACT_ADDRESS =
  process.env.NEXT_PUBLIC_AVALANCHE_CONTRACT_ADDRESS;
const AVALANCHE_RPC_URL = process.env.NEXT_PUBLIC_AVALANCHE_RPC_URL;

export default function Home() {
//...

const [message, setMessage] = useState(""); 
const [sourceChain, setSourceChain] = useState(""); 

const { config } = usePrepareContractWrite({ // Calling a hook to prepare the contract write configuration
  address: BSC_CONTRACT_ADDRESS, // Address of the BSC contract
  abi: SendMessageContract.abi, // ABI (Application Binary Interface) of the contract
  functionName: "sendMessage", // Name of the function to call on the contract
  args: ["Avalanche", AVALANCHE_CONTRACT_ADDRESS, message], // Arguments to pass to the contract function
  value: ethers.utils.parseEther("0.01"), // Value to send along with the contract call for gas fee
});

const { data: useContractWriteData, write } = useContractWrite(config); 

const { data: useWaitForTransactionData, isSuccess } = useWaitForTransaction({ 
  hash: useContractWriteData?.hash, // Hash of the transaction obtained from the contract write data
});

const handleSendMessage = () => {
  write(); // Initiating the contract call

  toast.info("Sending message...", {
    position: "top-right",
    autoClose: 5000,
    hideProgressBar: false,
    closeOnClick: true,
    pauseOnHover: false,
    draggable: true,
  });
};

useEffect(() => {
    const body = document.querySelector("body");
    darkMode ? body.classList.add("dark") : body.classList.remove("dark");

    isSuccess
      ? toast.success("Message sent!", {
          position: "top-right",
          autoClose: 7000,
          closeOnClick: true,
          pauseOnHover: false,
          draggable: true,
        })
      : useWaitForTransactionData?.error || useContractWriteData?.error
      ? toast.error("Error sending message")
      : null;
  }, [darkMode, useContractWriteData, useWaitForTransactionData]);

return (
    //..
   )
}
```

In the code snippet above:

1. Two state variables, `message` and `sourceChain`, are declared using the `useState` hook to hold the message content and source chain, respectively.
    
2. The `usePrepareContractWrite` hook is called to prepare the configuration for a contract write operation. It takes several parameters, such as the BSC contract address, ABI, function name, arguments, and value.
    
3. `useContractWrite` hook retrieves the contract write data and the write function based on the configuration obtained from the previous step.
    
4. `useWaitForTransaction` hook is called to wait for the transaction to be mined. It takes the hash of the transaction obtained from the contract write data.
    
5. `handleSendMessage` function is defined, which initiates the contract call by invoking the `write` function.
    
6. `useEffect` hook performs actions when certain dependencies change to display toast notifications for successful or failed message sending.
    

Update the `Send` button and `textarea` with the following code snippet to send messages and retrieve the data to be sent.

```apache
//...

return (
    //...
    <div className="border border-gray-300 rounded-lg p-8 m-2 ">
        <h2 className="text-2xl font-bold mb-4">Send Message 📓 </h2>
        <textarea
            //...
            onChange={(e) => setMessage(e.target.value)}
        />
        <button
            //...
            onClick={() => handleSendMessage()}
         >
            Send
        </button>
     </div>
    //...
  );
}
```

### Implementing Read Smart Contract Functionality

In the previous step, you implemented the functionality for writing to a smart contract from the front end. This section will teach you how to implement the functionality for reading data from a smart contract.

Update the `index.js` with the following code snippet:

```javascript
//...

export default function Home() {
  //...

  const [value, setValue] = useState("");

  const provider = new ethers.providers.JsonRpcProvider(AVALANCHE_RPC_URL); // Create an instance of JsonRpcProvider with Avalanche RPC URL

  const contract = new ethers.Contract(
    AVALANCHE_CONTRACT_ADDRESS, 
    SendMessageContract.abi,

    provider
  );

  async function readDestinationChainVariables() {
    try {
      const value = await contract.value(); 
      const sourceChain = await contract.sourceChain();

      setValue(value.toString()); // Convert the value to a string and store it
      setSourceChain(sourceChain); // Store the source chain
    } catch (error) {
      toast.error("Error reading message"); // Display an error toast if reading fails
    }
  }

  useEffect(() => {
    readDestinationChainVariables(); // Call the function to read destination chain variables

    //...
  }, [darkMode, useContractWriteData, useWaitForTransactionData]);

  return (
    //...

        {value ? ( // Add value here
              <>
                //...
              </>
            ) : (
              <span className="text-red-500 ">waiting for response...</span>
         )}
  );
}
```

In the code above,

1. An instance of the `JsonRpcProvider` class from the ethers.js library is created using the Avalanche RPC URL (`AVALANCHE_RPC_URL`) as the parameter.
    
2. An instance of the `Contract` class from the ethers.js library is created, representing a contract on the Avalanche network. It takes parameters such as the contract address (`AVALANCHE_CONTRACT_ADDRESS`), ABI (`SendMessageContract.abi`), and provider.
    
3. An asynchronous function named `readDestinationChainVariables` is defined. It attempts to read the contract's value and source chain variables using the `value()` and `sourceChain()` functions, respectively.
    
4. If an error occurs during the reading process, a toast notification with the message "Error reading message" is displayed.
    
5. The `useEffect` hook calls the `readDestinationChainVariables` function when certain dependencies change.
    

## Trying the Application

Hurray 🥳 , you have successfully built and deployed a full-stack interchain decentralized application.

![Build a Full Stack Interchain dApp with Next.js, Solidity & Axelar](https://cdn.hashnode.com/res/hashnode/image/upload/v1686152491196/0f6c1be9-75b5-4dc8-bb28-fe9b77cb0f84.png)

You can find the GMP transaction on Axelarscan Testnet [here](https://testnet.axelarscan.io/gmp/0x2fa441ef701ee830cd2e8d16fd78e5943e0d67af0787050edb68eacda18f3651) and the complete code for this project on [GitHub](https://github.com/Olanetsoft/fullstack-interchain-dapp).

## What Next?

This post covered the utilization of Axelar's General Message Passing with `callContract`, but that's not all the General Message Passing can do.

You can always explore other functionalities like [`callContractWithToken`](https://docs.axelar.dev/dev/general-message-passing/gmp-tokens-with-messages), [`SendToken`](https://docs.axelar.dev/dev/send-tokens/overview), [`Deposit addresses`](https://docs.axelar.dev/dev/send-tokens/overview#get-a-deposit-address), [`NFT Linker`](https://docs.axelar.dev/dev/general-message-passing/examples#nft-linker), [`JavaScript SDK`](https://docs.axelar.dev/dev/axelarjs-sdk/intro), etc.

If you've made it this far, you're awesome! You can also tweet about your experience building or following along with this tutorial to show your support to the author and tag [@axelarcore](https://twitter.com/axelarcore).

## Conclusion

This tutorial taught you how to build a full-stack interchain decentralized application using Next.js, Solidity, and Axelar's General Message Passing. You learned how to deploy and send messages from Binance to Avalanche test networks and interact with them through a Next.js frontend application.

## Reference

* [Axelar General Message Passing](https://docs.axelar.dev/dev/general-message-passing/overview)
    
* [Axelar General Message Passing Video Tutorial](https://youtu.be/htMVIYzGA34)
    
* [Axelar Documentation](https://docs.axelar.dev/)
    
* [Hardhat](https://hardhat.org/)
    
* [RainbowKit](https://www.rainbowkit.com/)
    
* [Next.js](https://nextjs.org/docs)