HOW TO BUILD A CROSS-CHAIN NFT USING HARDHAT AND AXELAR

279
1

INTRODUCTION

The ability to create digital assets that are one-of-a-kind and impossible to duplicate makes NFTs crucial. This makes them perfect for various uses, such as in-game objects, digital art, and collectibles. 

Enabling NFTs to be cross-chain is crucial as it facilitates their utilization across several blockchains. As a result, they become more liquid and more widely available to users.

Here are some of the benefits of cross-chain NFTs:

  • Increased liquidity: Cross-chain NFTs can be traded on multiple blockchains, which increases their liquidity and makes them easier to sell.
  • Increased accessibility: Cross-chain NFTs can be used on multiple blockchains, which makes them more accessible to a wider range of users.
  • Reduced fees: Cross-chain NFTs can be transferred between blockchains without the need for a centralized exchange, which can reduce fees.
  • Increased security: Cross-chain NFTs are secured by multiple blockchains, which makes them more secure than NFTs that are only on one blockchain.

In this article, you will learn how to build a cross-chain NFT contract using Solidity, Hardhat, and Axelar General Message Passing (GMP).

GETTING STARTED

Firstly, we will outline the steps:

  1. Initialize a hardhat project
  2. Write a basic NFT contract using solidity
  3. Write an NFTLinker contract to supercharge our NFT with Axelar’s GMP
  4. Write scripts to enable us to easily deploy the contracts on a local blockchain.

Without further ado, let’s get to it.

In order to initialize a hardhat project, you will need to run the following commands on your terminal.

mkdir Axelar-cross-chain-NFT
cd Axelar-cross-chain-NFT
npx hardhat

After running this command, we should see these results.

Follow the process in the screenshot above to complete the set up.

To begin developing with Hardhat, all of the frequently used packages and suggested Hardhat plugins are included in the @nomicfoundation/hardhat-toolbox plugin.

Use the following command to install this extra prerequisite if it wasn’t installed automatically:

npm i @nomicfoundation/hardhat-toolbox@3.0.0

Next, use the following command to install @axelar-network/axelar-gmp-sdk-solidity for the Axelar General Message Passing SDK in Solidity 

npm i @axelar-network/axelar-gmp-sdk-solidity@3.6.1

Now use the next command to install dotenv

Npm i dotenv

After installing the these packages, your hardhat project folder structure should look like this:

It’s crucial to tidy up the directory before beginning to construct from scratch. Delete Lock.js from the test folder and deploy.js from the scripts directory to complete this operation. Proceed to the contracts folder and delete Lock.sol from there.

Remember not to delete the folders themselves.

Next, create an ERC721Demo.sol file in the contracts folder and copy and paste the solidity code snippet below:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

//A simple ERC721 that allows users to mint NFTs as they please.

contract ERC721Demo is ERC721 {

constructor(

string memory name_,

string memory symbol_

) ERC721(name_, symbol_) {}

function mint(uint256 tokenId) external {

_safeMint(_msgSender(), tokenId);

}

}

This Solidity code above defines a simple ERC-721 non-fungible token (NFT) contract using the OpenZeppelin library.

import “@openzeppelin/contracts/token/ERC721/ERC721.sol”;

This line imports the ERC-721 contract implementation from the OpenZeppelin contracts library, which is a well-tested and secure library of smart contracts.

function mint(uint256 tokenId) external {

_safeMint(_msgSender(), tokenId);

}

This function above allows users to mint new tokens.

It is marked as external, meaning it can only be called from outside the contract.

_safeMint is a function from the OpenZeppelin ERC721 contract that safely mints a new token and assigns it to the caller of the mint function. It ensures that the token does not already exist and performs other safety checks.

_msgSender() is a function that returns the address of the entity that is called the mint function.

Next, we’ll write the Nftlinker.sol smart contract, copy and paste the code snippet below:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import {IERC20} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol";

import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol";

import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol";

import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol";

import {Upgradable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/Upgradable.sol";

import {StringToAddress, AddressToString} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/libs/AddressString.sol";

contract NftLinker is ERC721, AxelarExecutable, Upgradable {

using StringToAddress for string;

using AddressToString for address;

error AlreadyInitialized();

mapping(uint256 => bytes) public original; //abi.encode(originaChain, operator, tokenId);

string public chainName; //To check if we are the source chain.

IAxelarGasService public immutable gasService;

constructor(

address gateway_,

address gasReceiver_

) ERC721("Axelar NFT Linker", "ANL") AxelarExecutable(gateway_) {

gasService = IAxelarGasService(gasReceiver_);

}

function _setup(bytes calldata params) internal override {

string memory chainName_ = abi.decode(params, (string));

if (bytes(chainName).length != 0) revert AlreadyInitialized();

chainName = chainName_;

}

//The main function users will interract with.

function sendNFT(

address operator,

uint256 tokenId,

string memory destinationChain,

address destinationAddress

) external payable {

//If we are the operator then this is a minted token that lives remotely.

if (operator == address(this)) {

require(ownerOf(tokenId) == _msgSender(), "NOT_YOUR_TOKEN");

_sendMintedToken(tokenId, destinationChain, destinationAddress);

} else {

IERC721(operator).transferFrom(

_msgSender(),

address(this),

tokenId

);

_sendNativeToken(

operator,

tokenId,

destinationChain,

destinationAddress

);

}

}

//Burns and sends a token.

function _sendMintedToken(

uint256 tokenId,

string memory destinationChain,

address destinationAddress

) internal {

_burn(tokenId);

//Get the original information.

(

string memory originalChain,

address operator,

uint256 originalTokenId

) = abi.decode(original[tokenId], (string, address, uint256));

//Create the payload.

bytes memory payload = abi.encode(

originalChain,

operator,

originalTokenId,

destinationAddress

);

string memory stringAddress = address(this).toString();

//Pay for gas. We could also send the contract call here but then the sourceAddress will be that of the gas receiver which is a problem later.

gasService.payNativeGasForContractCall{value: msg.value}(

address(this),

destinationChain,

stringAddress,

payload,

msg.sender

);

//Call the remote contract.

gateway.callContract(destinationChain, stringAddress, payload);

}

//Locks and sends a token.

function _sendNativeToken(

address operator,

uint256 tokenId,

string memory destinationChain,

address destinationAddress

) internal {

//Create the payload.

bytes memory payload = abi.encode(

chainName,

operator,

tokenId,

destinationAddress

);

string memory stringAddress = address(this).toString();

//Pay for gas. We could also send the contract call here but then the sourceAddress will be that of the gas receiver which is a problem later.

gasService.payNativeGasForContractCall{value: msg.value}(

address(this),

destinationChain,

stringAddress,

payload,

msg.sender

);

//Call remote contract.

gateway.callContract(destinationChain, stringAddress, payload);

}

//This is automatically executed by Axelar Microservices since gas was payed for.

function _execute(

string calldata /*sourceChain*/,

string calldata sourceAddress,

bytes calldata payload

) internal override {

//Check that the sender is another token linker.

require(sourceAddress.toAddress() == address(this), "NOT_A_LINKER");

//Decode the payload.

(

string memory originalChain,

address operator,

uint256 tokenId,

address destinationAddress

) = abi.decode(payload, (string, address, uint256, address));

//If this is the original chain then we give the NFT locally.

if (keccak256(bytes(originalChain)) == keccak256(bytes(chainName))) {

IERC721(operator).transferFrom(

address(this),

destinationAddress,

tokenId

);

//Otherwise we need to mint a new one.

} else {

//We need to save all the relevant information.

bytes memory originalData = abi.encode(

originalChain,

operator,

tokenId

);

//Avoids tokenId collisions.

uint256 newTokenId = uint256(keccak256(originalData));

original[newTokenId] = originalData;

_safeMint(destinationAddress, newTokenId);

}

}

function contractId() external pure returns (bytes32) {

return keccak256("example");

}

}

This Solidity code above defines a smart contract named NftLinker, which is a type of ERC-721 non-fungible token (NFT) that can be used to link and transfer NFTs across different blockchain networks using Axelar Network’s cross-chain communication protocol.

The contract imports several external contracts and libraries:

ERC721: A standard implementation of an ERC-721 token from OpenZeppelin.

IERC20, IAxelarGasService, IAxelarGateway, AxelarExecutable, Upgradable: Interfaces and contracts from the Axelar network SDK, used for cross-chain communication and contract upgradability.

StringToAddress, AddressToString: Libraries for converting between addresses and their string representations.

original: A mapping to store the original chain, operator, and token ID of linked NFTs.

chainName: A string to store the name of the current blockchain network.

gasService: An instance of IAxelarGasService to handle gas payments for cross-chain calls.

The constructor initializes the ERC-721 token, the Axelar gateway, and the gas service.

sendNFT: The main function for users to link and send NFTs across chains.

_sendMintedToken: Handles the case where the NFT to be sent is a minted token that lives remotely.

_sendNativeToken: Handles the case where the NFT to be sent is a native token of the current chain.

Lastly, we will write a Proxy.sol contract, copy and paste the code snippet below:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {InitProxy} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/upgradable/InitProxy.sol";

contract ExampleProxy is InitProxy {

function contractId() internal pure override returns (bytes32) {

return keccak256("example");

}

}

This Solidity code defines a smart contract named ExampleProxy, which is a specific type of proxy contract based on the InitProxy contract from the Axelar network’s General Message Passing (GMP) SDK. 

The purpose of this contract is to serve as a proxy for another contract, enabling upgradeability and the ability to change the contract’s implementation while preserving its state.
This concludes the solidity contracts for this project.

Next, we have to set up our environment to ensure effective deployment and testing of our code using Axelar; it’s good to note that the procedure we are about to follow isn’t the regular hardhat contract deployment procedure.

Axelar has been kind to give us examples and procedures on how to set up a custom deployment environment and procedure, so most of the code we will use from this point is mostly derived from Axelar’s examples.

We’ll go ahead to create an index.js file inside the contracts and paste this code snippet:

'use strict';

const {

getDefaultProvider,

utils: { keccak256, defaultAbiCoder },

} = require('ethers');

const {

utils: { deployContract },

} = require('@axelar-network/axelar-local-dev');

const { deployUpgradable } = require('@axelar-network/axelar-gmp-sdk-solidity');

const ERC721 = rootRequire('./artifacts/contracts/ERC721Demo.sol/ERC721Deamo.json');

const ExampleProxy = rootRequire('./artifacts/contracts/Proxy.sol/ExampleProxy.json');

const NftLinker = rootRequire('./artifacts/contracts//NftLinker.sol/NftLinker.json');

const tokenId = 0;

async function deploy(chain, wallet) {

console.log(`Deploying ERC721Demo for ${chain.name}.`);

chain.erc721 = await deployContract(wallet, ERC721, ['Test', 'TEST']);

console.log(`Deployed ERC721Demo for ${chain.name} at ${chain.erc721.address}.`);

console.log(`Deploying NftLinker for ${chain.name}.`);

const provider = getDefaultProvider(chain.rpc);

chain.wallet = wallet.connect(provider);

chain.contract = await deployUpgradable(

chain.constAddressDeployer,

wallet.connect(provider),

NftLinker,

ExampleProxy,

[chain.gateway, chain.gasService],

[],

defaultAbiCoder.encode(['string'], [chain.name]),

);

console.log(`Deployed NftLinker for ${chain.name} at ${chain.contract.address}.`);

console.log(`Minting token ${tokenId} for ${chain.name}`);

await (await chain.erc721.mint(tokenId)).wait();

console.log(`Minted token ${tokenId} for ${chain.name}`);

}

async function execute(chains, wallet, options) {

const { source: originChain, destination, calculateBridgeFee } = options;

const ownerOf = async (chain = originChain) => {

const operator = chain.erc721;

const owner = await operator.ownerOf(tokenId);

if (owner !== chain.contract.address) {

return { chain: chain.name, address: owner, tokenId: BigInt(tokenId) };

}

const newTokenId = BigInt(

keccak256(defaultAbiCoder.encode(['string', 'address', 'uint256'], [chain.name, operator.address, tokenId])),

);

for (const checkingChain of chains) {

if (checkingChain === chain) continue;

try {

const address = await checkingChain.contract.ownerOf(newTokenId);

return { chain: checkingChain.name, address, tokenId: newTokenId };

} catch (e) { }

}

return { chain: '' };

};

async function print() {

for (const chain of chains) {

const owner = await ownerOf(chain);

console.log(`Token that was originally minted at ${chain.name} is at ${owner.chain}.`);

}

}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const owner = await ownerOf();

const source = chains.find((chain) => chain.name === owner.chain);

if (source === destination) throw new Error('Token is already where it should be!');

console.log('--- Initially ---');

await print();

const fee = await calculateBridgeFee(source, destination);

if (originChain === source) {

await (await source.erc721.approve(source.contract.address, owner.tokenId)).wait();

}

await (

await source.contract.sendNFT(

originChain === source ? source.erc721.address : source.contract.address,

owner.tokenId,

destination.name,

wallet.address,

{ value: fee },

)

).wait();

while (true) {

const owner = await ownerOf();

if (owner.chain === destination.name) break;

await sleep(2000);

}

console.log('--- Then ---');

await print();

}

module.exports = {

deploy,

execute,

};

This JavaScript code above is a script for deploying and interacting with smart contracts on a blockchain network, specifically for handling ERC-721 non-fungible tokens (NFTs) and linking them across different chains using Axelar network’s cross-chain communication protocol.

Additionally, it imports compiled smart contract artifacts for ERC721Demo, ExampleProxy, and NftLinker popular Ethereum library for interacting with the Ethereum blockchain. 

It also imports deployment utilities from @axelar-network/axelar-local-dev and @axelar-network/axelar-gmp-sdk-solidity. 

It also imports compiled smart contract artifacts for ERC721Demo, ExampleProxy, and NftLinker.

Now that’s complete, we’ll go ahead and populate the deploy folder, using Axelar-example’s scripts, create a runStart.js file, and paste this code below:

const { start, getWallet } = require('./libs');

// Get the wallet from the environment variables.

const wallet = getWallet();

// Fund the given addresses with aUSDC.

const fundAddresses = [wallet.address];

// Add additional addresses to fund here.

for (let j = 2; j < process.argv.length; j++) {

fundAddresses.push(process.argv[j]);

}

// Insert the chains you want to start here. Available values are:

// 'Avalanche', 'Moonbeam', 'Polygon', 'Fantom', 'Ethereum'

const chains = [];

// Start the chains.

start(fundAddresses, chains);

This Javascript file basically starts a local blockchain.

Next is runCheckBalance.js file; we can create it under the deploy folder and paste the code snippet below

'use strict';

const { ethers } = require('ethers');

const { getWallet, getBalances, getEVMChains, checkEnv } = require('./libs');

const { testnetInfo } = require('@axelar-network/axelar-local-dev');

const {

contracts: { BurnableMintableCappedERC20, AxelarGateway },

} = require('@axelar-network/axelar-local-dev');

// Get the environment from the command line. If it is not provided, use 'testnet'.

const env = process.argv[2] || 'testnet';

// Check the environment. If it is not valid, exit.

checkEnv(env);

// Get the chains for the environment.

const allTestnetChains = Object.entries(testnetInfo).map((_, chain) => chain.name);

const chains = getEVMChains(env, allTestnetChains);

// Get the wallet.

const wallet = getWallet();

// Print the balances. This will print the balances of the wallet on each chain.

console.log(`==== Print balances for ${env} =====`);

console.log('Wallet address:', wallet.address, '\n');

async function print() {

console.log('Native tokens:');

await getBalances(chains, wallet.address).then((balances) => {

for (let i = 0; i < chains.length; i++) {

console.log(`${chains[i].name}: ${ethers.utils.formatEther(balances[chains[i].name])} ${chains[i].tokenSymbol}`);

}

});

console.log('\naUSDC tokens:');

const pendingBalances = chains.map((chain) => {

const provider = new ethers.providers.JsonRpcProvider(chain.rpc);

const gateway = new ethers.Contract(chain.gateway, AxelarGateway.abi, provider);

return gateway

.tokenAddresses('aUSDC')

.then((usdc) => {

const erc20 = new ethers.Contract(usdc, BurnableMintableCappedERC20.abi, provider);

return erc20.balanceOf(wallet.address).catch(() => '0');

})

.then((balance) => {

console.log(`${chain.name}: ${ethers.utils.formatUnits(balance, 6)} aUSDC`);

});

});

await Promise.all(pendingBalances);

}

print();

This will check the balance of the wallet gotten from your connected private key.

Up next is the runDeploy.js file 

const { deploy, checkEnv, getEVMChains, getExamplePath, getWallet } = require('./libs');

const exampleName = process.argv[2];

const env = process.argv[3];

const chainsToDeploy = process.argv.slice(4);

// Check the environment. If it is not valid, exit.

checkEnv(env);

// Get the example object.

const example = require(getExamplePath(exampleName));

// Get the chains for the environment.

const chains = getEVMChains(env, chainsToDeploy);

// Get the wallet.

const wallet = getWallet();

// This will execute an example script. The example script must have a `deploy` function.

deploy(env, chains, wallet, example);

This JavaScript script is designed to deploy smart contracts across multiple Ethereum Virtual Machine (EVM) compatible blockchain networks. 

It takes command line arguments to determine the specifics of the deployment, such as which example to run, the environment to deploy to, and which chains to deploy on.

Next, we’ll create a runExecute.js file.

'use strict';

require('dotenv').config();

const { executeEVMExample, executeAptosExample, checkEnv, getExamplePath, getWallet, getEVMChains } = require('./libs');

const exampleName = process.argv[2];

const env = process.argv[3];

const args = process.argv.slice(4);

console.log('Starting execute..');

// Check the environment. If it is not valid, exit.

checkEnv(env);

console.log('Environment checked:', env);

// Get the example object.

const example = require(getExamplePath(exampleName));

console.log('Example loaded:', exampleName);

// Get the wallet.

const wallet = getWallet();

console.log('Wallet retrieved:', wallet.address);

// This will execute an example script. The example script must have an `execute` function.

if (exampleName.split('/')[0] === 'evm') {

console.log('Executing EVM example...');

// Get the chains for the environment.

let selectedChains = [];

if (args.length >= 2) {

selectedChains = [args[0], args[1]];

}

const chains = getEVMChains(env, selectedChains);

executeEVMExample(env, chains, args, wallet, example);

console.log('EVM example executed successfully.');

} else if (exampleName.split('/')[0] === 'aptos') {

const chains = getEVMChains(env);

executeAptosExample(chains, args, wallet, example);

}

This JavaScript script is designed to execute example scripts for interacting with smart contracts across different blockchain networks, specifically Ethereum Virtual Machine (EVM) compatible networks and Aptos. 

The script takes command line arguments to determine the specifics of the execution, such as which example to run, the environment to execute in, and additional arguments required by the example scripts.

Next, we have to create a libs folder inside the contract folder, and inside that folder, we have to create some really important files to aid our deployment. 

We can find a link to the libs files here, and duplicate them into your project folder.

To proceed with compilation and deployment, we need to create a .env file. 

Use the command to create it 

touch .env.

Alternatively, you can create it manually. 

In the env file, put your private key here.

EVM_PRIVATE_KEY= // Add your account private key here.

It’s simple to obtain your private account key. 

If you use MetaMask, read this article

Remember that different wallet providers may have different procedures for exporting the private key.

Next, add the following code snippet to the hardhat.config.js file to enable RPC for the Polygon and Avalanche test networks:

require("@nomicfoundation/hardhat-toolbox");

require("dotenv").config({ path: ".env" });

require("solidity-coverage");

const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY;

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);

}

});

/** @type import('hardhat/config').HardhatUserConfig */

module.exports = {

solidity: "0.8.9",

networks: {

mumbai: {

url: "https://rpc.ankr.com/polygon_mumbai",

chainId: 80001,

accounts: [PRIVATE_KEY],

},

avalancheFujiTestnet: {

url: "https://avalanche-fuji-c-chain.publicnode.com",

chainId: 43113,

accounts: [PRIVATE_KEY],

},

},

etherscan: {

url: "https://rpc.ankr.com/polygon_mumbai",

apiKey: {

polygonMumbai: "RZ2RSK3GIMZHPGF57NN42A9PZQET8K4FD6",

},

},

paths: {

sources: './contracts',

artifacts: './artifacts',

},

};

After this, we need to create a folder called chain-config in our root folder and in the chain-config folder.
We will now create a file called local.json; this file has a number of chain-rpc connections for setting up a local chain for Axelar to use; for this article, this will be the content of our local.json.

[

{

"name": "Moonbeam",

"chainId": 2500,

"gateway": "0x9E404e6ff4F2a15C99365Bd6615fCE3FB9E9Cb76",

"gasService": "0x783ce2eF32Aa74B41c8EbbbeC6F632b6Da00C1e9",

"constAddressDeployer": "0x69aeB7Dc4f2A86873Dae8D753DE89326Cf90a77a",

"create3Deployer": "0x783E7717fD4592814614aFC47ee92568a495Ce0B",

"tokens": {

"aUSDC": "aUSDC"

},

"rpc": "http://localhost:8500/0"

},

{

"name": "Avalanche",

"chainId": 2501,

"gateway": "0x54C156f087EBA12a869E0CBb49548c3F55E80edC",

"gasService": "0xD3cB555CB530C3374B122242b15d2943445c31d6",

"constAddressDeployer": "0x69aeB7Dc4f2A86873Dae8D753DE89326Cf90a77a",

"create3Deployer": "0x783E7717fD4592814614aFC47ee92568a495Ce0B",

"tokens": {

"aUSDC": "aUSDC"

},

"rpc": "http://localhost:8500/1"

},

{

"name": "Fantom",

"chainId": 2502,

"gateway": "0xcb189eB52ca573eFD633d07A3B5357e4d989D743",

"gasService": "0x85Fa9202C6Be69e9889CC0247Af72ABc70DbF542",

"constAddressDeployer": "0x69aeB7Dc4f2A86873Dae8D753DE89326Cf90a77a",

"create3Deployer": "0x783E7717fD4592814614aFC47ee92568a495Ce0B",

"tokens": {

"aUSDC": "aUSDC"

},

"rpc": "http://localhost:8500/2"

},

{

"name": "Ethereum",

"chainId": 2503,

"gateway": "0x013459EC3E8Aeced878C5C4bFfe126A366cd19E9",

"gasService": "0x28f8B50E1Be6152da35e923602a2641491E71Ed8",

"constAddressDeployer": "0x69aeB7Dc4f2A86873Dae8D753DE89326Cf90a77a",

"create3Deployer": "0x783E7717fD4592814614aFC47ee92568a495Ce0B",

"tokens": {

"aUSDC": "aUSDC"

},

"rpc": "http://localhost:8500/3"

},

{

"name": "Polygon",

"chainId": 2504,

"gateway": "0xc7B788E88BAaB770A6d4936cdcCcd5250E1bbAd8",

"gasService": "0xC573c722e21eD7fadD38A8f189818433e01Ae466",

"constAddressDeployer": "0x69aeB7Dc4f2A86873Dae8D753DE89326Cf90a77a",

"create3Deployer": "0x783E7717fD4592814614aFC47ee92568a495Ce0B",

"tokens": {

"aUSDC": "aUSDC"

},

"rpc": "http://localhost:8500/4"

}

]

Up next, we can edit our package.json file to enable easy contract deployment using basic commands.

{

"name": "hardhat-project",

"devDependencies": {

"@nomicfoundation/hardhat-toolbox": "^3.0.0",

"hardhat": "^2.18.3",

"hardhat-gas-reporter": "^1.0.8",

"mocha": "^10.0.0",

"prettier": "^2.6.2",

"prettier-plugin-solidity": "^1.0.0-beta.19",

"solhint": "^3.3.7",

"solidity-coverage": "^0.8.2"

},

"scripts": {

"test": "TEST=true mocha examples/tests",

"start": "node scripts/runStart",

"deploy": "node scripts/runDeploy",

"execute": "node scripts/runExecute",

"setup": "node scripts/runSetupEnv",

"check-balance": "node scripts/runCheckBalance",

"build": "rm -rf artifacts && npx hardhat clean && npx hardhat compile",

"build-aptos": "bash ./build-aptos.sh"

},

"dependencies": {

"@axelar-network/axelar-chains-config": "^0.1.0",

"@axelar-network/axelar-gmp-sdk-solidity": "^5.4.0",

"@axelar-network/axelar-local-dev": "^2.1.1-alpha.2",

"@axelar-network/axelarjs-sdk": "^0.12.8",

"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",

"@nomicfoundation/hardhat-ethers": "^3.0.0",

"@nomicfoundation/hardhat-verify": "^1.0.0",

"@openzeppelin/contracts": "^4.5.0",

"@typechain/ethers-v6": "^0.4.0",

"@typechain/hardhat": "^8.0.0",

"@types/chai": "^4.2.0",

"@types/mocha": ">=9.1.0",

"axios": "^0.27.2",

"bigint-buffer": "^1.1.5",

"bindings": "^1.5.0",

"bip39": "^3.0.4",

"chai": "^4.2.0",

"config": "^3.3.9",

"dotenv": "^16.3.1",

"ethers": "^5.6.5",

"ts-node": ">=8.0.0",

"typechain": "^8.2.0",

"typescript": ">=4.5.0",

"uuid": "^8.3.2"

}

}

Note the scripts; it has certain commands to easily deploy the contracts.

Now to deploy the contracts, we, first of all, have to compile them using this command:

npx hardhat compile

Once the contracts have deployed, we should run this command 

npm runStart, it should start the Axelar local development chain, and we’ll see a result similar to this:

Next, we can run 

npm run deploy contracts local

The “local” flag points the deployment script to deploy on the local chain we just started with the Aid of Axelar, pre-deployment settings.

We should see results like this:

These results show that the NFT contract has been deployed across a number of chains and can now be interlinked; you can see the respective contracts for each chain we specified in the chain-config’s local.json file.
Yay🤩, we made it.

You can find the complete code for this project on github.

Now, you can build a Frontend for this contract using Nextjs; if you wish to do this, you can check out this code repo and get some ideas.


REFERENCES

  1. Axelar documentation
  2. Axelar General Message Passing (GMP)
  3. Hardhat Docs
  4. Axelar Examples Github

Leave a Reply

Your email address will not be published. Required fields are marked *

One thought on “HOW TO BUILD A CROSS-CHAIN NFT USING HARDHAT AND AXELAR

  1. Excellent article, very detailed.