Test and Deploy with Hardhat¶
Introduction¶
After creating a smart contract, the next crucial steps are testing and deployment. Proper testing ensures your contract behaves as expected, while deployment makes your contract available on the blockchain. This tutorial will guide you through using Hardhat, a popular development environment, to test and deploy the Storage.sol
contract you created in the Create a Smart Contract tutorial. For more information about Hardhat usage, check the Hardhat guide.
Prerequisites¶
Before starting, make sure you have:
- The
Storage.sol
contract created in the previous tutorial - Node.js (v16.0.0 or later) and npm installed
- Basic understanding of JavaScript for writing tests
- Some WND test tokens to cover transaction fees (obtained from the Polkadot faucet)
Setting Up the Development Environment¶
Let's start by setting up Hardhat for your Storage contract project:
-
Create a new directory for your project and navigate into it:
-
Initialize a new npm project:
-
Install Hardhat and the required plugins:
-
Install the Hardhat revive specific plugins:
-
Initialize a Hardhat project:
Select Create an empty hardhat.config.js when prompted.
-
Configure Hardhat by updating the
hardhat.config.js
file:hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('hardhat-resolc'); require('hardhat-revive-node'); require('dotenv').config(); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', resolc: { compilerSource: 'binary', settings: { optimizer: { enabled: true, runs: 400, }, evmVersion: 'istanbul', compilerPath: 'INSERT_PATH_TO_RESOLC_COMPILER', standardJson: true, }, }, networks: { hardhat: { polkavm: true, nodeConfig: { nodeBinaryPath: 'INSERT_PATH_TO_SUBSTRATE_NODE', rpcPort: 8000, dev: true, }, adapterConfig: { adapterBinaryPath: 'INSERT_PATH_TO_ETH_RPC_ADAPTER', dev: true, }, }, localNode: { polkavm: true, url: `http://127.0.0.1:8545`, }, westendAssetHub: { polkavm: true, url: 'https://westend-asset-hub-eth-rpc.polkadot.io', accounts: [process.env.PRIVATE_KEY], }, }, };
To configure the binary, replace
INSERT_PATH_TO_RESOLC_COMPILER
with the correct path to the compiler binary. Detailed installation instructions can be found in the installation section of thepallet-revive
repository. Also, ensure thatINSERT_PATH_TO_SUBSTRATE_NODE
andINSERT_PATH_TO_ETH_RPC_ADAPTER
are replaced with the proper paths to the compiled binaries.For more information about these compiled binaries, see the Deploying with a local node section in the Hardhat documentation.
This setup loads essential plugins, including
hardhat-toolbox
,hardhat-resolc
, andhardhat-revive-node
, while utilizing environment variables throughdotenv
. The Solidity compiler is set to version 0.8.19 with optimization enabled for improved gas efficiency. The resolc plugin is configured to use the Remix compiler with Istanbul compatibility.The configuration also defines two network settings:
localNode
- runs a PolkaVM instance onhttp://127.0.0.1:8545
for local development and testingwestendAssetHub
- connects to the Westend Asset Hub network using a predefined RPC URL and a private key stored in environment variables
-
Create a
.env
file in your project root to store your private key:Replace
INSERT_PRIVATE_KEY
with your actual private key.For further details on private key exportation, refer to the article How to export an account's private key.
Warning
Keep your private key safe, and never share it with anyone. If it is compromised, your funds can be stolen.
Adding the Smart Contract¶
-
Create a new folder called
contracts
and create aStorage.sol
file. Add the contract code from the previous tutorial:Storage.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.28; contract Storage { // State variable to store our number uint256 private number; // Event to notify when the number changes event NumberChanged(uint256 newNumber); // Function to store a new number function store(uint256 newNumber) public { number = newNumber; emit NumberChanged(newNumber); } // Function to retrieve the stored number function retrieve() public view returns (uint256) { return number; } }
-
Compile the contract:
-
If successful, you will see the following output in your terminal:
npx hardhat compile Compiling 1 Solidity file Successfully compiled 1 Solidity file
After compilation, the artifacts-pvm
and cache-pvm
folders, containing the metadata and binary files of your compiled contract, will be created in the root of your project.
Writing Tests¶
Testing is a critical part of smart contract development. Hardhat makes it easy to write tests in JavaScript using frameworks like Mocha and Chai.
-
Create a folder for testing called
test
. Inside that directory, create a file namedStorage.js
and add the following code:Storage.jsconst { ethers } = require('hardhat'); describe('Storage', function () { let storage; let owner; let addr1; beforeEach(async function () { // Get signers [owner, addr1] = await ethers.getSigners(); // Deploy the Storage contract const Storage = await ethers.getContractFactory('Storage'); storage = await Storage.deploy(); await storage.waitForDeployment(); }); describe('Basic functionality', function () { // Add your logic here }); });
The
beforeEach
hook ensures stateless contract execution by redeploying a fresh instance of the Storage contract before each test case. This approach guarantees that each test starts with a clean and independent contract state by usingethers.getSigners()
to obtain test accounts andethers.getContractFactory('Storage').deploy()
to create a new contract instance.Now, you can add custom unit tests to check your contract functionality. Some example tests are available below:
a. Initial state verification - ensures that the contract starts with a default value of zero, which is a fundamental expectation for the
Storage.sol
contractStorage.jsit('Should return 0 initially', async function () { expect(await storage.retrieve()).to.equal(0); });
Explanation:
- Checks the initial state of the contract
- Verifies that a newly deployed contract has a default value of 0
- Confirms the
retrieve()
method works correctly for a new contract
b. Value storage test - validate the core functionality of storing and retrieving a value in the contract
Storage.jsit('Should update when store is called', async function () { const testValue = 42; // Store a value await storage.store(testValue); // Check if the value was updated expect(await storage.retrieve()).to.equal(testValue); });
Explanation:
- Demonstrates the ability to store a specific value
- Checks that the stored value can be retrieved correctly
- Verifies the basic write and read functionality of the contract
c. Event emission verification - confirm that the contract emits the correct event when storing a value, which is crucial for off-chain tracking
Storage.jsit('Should emit an event when storing a value', async function () { const testValue = 100; // Check if the NumberChanged event is emitted with the correct value await expect(storage.store(testValue)) .to.emit(storage, 'NumberChanged') .withArgs(testValue); });
Explanation:
- Ensures the
NumberChanged
event is emitted during storage - Verifies that the event contains the correct stored value
- Validates the contract's event logging mechanism
d. Sequential value storage test - check the contract's ability to store multiple values sequentially and maintain the most recent value
Storage.jsit('Should allow storing sequentially increasing values', async function () { const values = [10, 20, 30, 40]; for (const value of values) { await storage.store(value); expect(await storage.retrieve()).to.equal(value); } });
Explanation:
- Verifies that multiple values can be stored in sequence
- Confirms that each new store operation updates the contract's state
- Demonstrates the contract's ability always to reflect the most recently stored value
The complete
test/Storage.js
should look like this:View complete script
Storage.jsconst { expect } = require('chai'); const { ethers } = require('hardhat'); describe('Storage', function () { let storage; let owner; let addr1; beforeEach(async function () { // Get signers [owner, addr1] = await ethers.getSigners(); // Deploy the Storage contract const Storage = await ethers.getContractFactory('Storage'); storage = await Storage.deploy(); await storage.waitForDeployment(); }); describe('Basic functionality', function () { it('Should return 0 initially', async function () { expect(await storage.retrieve()).to.equal(0); }); it('Should update when store is called', async function () { const testValue = 42; // Store a value await storage.store(testValue); // Check if the value was updated expect(await storage.retrieve()).to.equal(testValue); }); it('Should emit an event when storing a value', async function () { const testValue = 100; // Check if the NumberChanged event is emitted with the correct value await expect(storage.store(testValue)) .to.emit(storage, 'NumberChanged') .withArgs(testValue); }); it('Should allow storing sequentially increasing values', async function () { const values = [10, 20, 30, 40]; for (const value of values) { await storage.store(value); expect(await storage.retrieve()).to.equal(value); } }); }); });
-
Run the tests:
-
After running the above command, you will see the output showing that all tests have passed:
npx hardhat test Storage Basic functionality ✔ Should return 0 initially ✔ Should update when store is called (1126ms) ✔ Should emit an event when storing a value (1131ms) ✔ Should allow storing sequentially increasing values (12477ms) 4 passing (31s)
Deploying with Ignition¶
Hardhat's Ignition is a deployment system designed to make deployments predictable and manageable. Let's create a deployment script:
-
Create a new folder called
ignition/modules
. Add a new file namedStorageModule.js
with the following logic: -
Deploy to the local network:
a. First, start a local node:
b. Then, in a new terminal window, deploy the contract:
c. If successful, output similar to the following will display in your terminal:
npx hardhat ignition deploy ./ignition/modules/Storage.js --network localNode ✔ Confirm deploy to network localNode (420420420)? … yes Hardhat Ignition 🚀 Deploying [ StorageModule ] Batch #1 Executed StorageModule#Storage [ StorageModule ] successfully deployed 🚀 Deployed Addresses StorageModule#Storage - 0xc01Ee7f10EA4aF4673cFff62710E1D7792aBa8f3 -
Deploy to Westend Asset Hub:
a. Make sure your account has enough WND tokens for gas fees, then run:
b. After deployment, you'll see the contract address in the console output. Save this address for future interactions.
npx hardhat ignition deploy ./ignition/modules/Storage.js --network westendAssetHub ✔ Confirm deploy to network localNode (420420420)? … yes Hardhat Ignition 🚀 Deploying [ StorageModule ] Batch #1 Executed StorageModule#Storage [ StorageModule ] successfully deployed 🚀 Deployed Addresses StorageModule#Storage - 0x5BCE10D9e89ffc067B6C0Da04eD0D44E37df7224
Interacting with Your Deployed Contract¶
To interact with your deployed contract:
-
Create a new folder named
scripts
and add theinteract.js
with the following content:interact.jsconst hre = require('hardhat'); async function main() { // Replace with your deployed contract address const contractAddress = 'INSERT_DEPLOYED_CONTRACT_ADDRESS'; // Get the contract instance const Storage = await hre.ethers.getContractFactory('Storage'); const storage = await Storage.attach(contractAddress); // Get current value const currentValue = await storage.retrieve(); console.log('Current stored value:', currentValue.toString()); // Store a new value const newValue = 42; console.log(`Storing new value: ${newValue}...`); const tx = await storage.store(newValue); // Wait for transaction to be mined await tx.wait(); console.log('Transaction confirmed'); // Get updated value const updatedValue = await storage.retrieve(); console.log('Updated stored value:', updatedValue.toString()); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
Ensure that
INSERT_DEPLOYED_CONTRACT_ADDRESS
is replaced with the value obtained in the previous step. -
Run the interaction script:
-
If successful, the terminal will show the following output:
npx hardhat run scripts/interact.js --network westendAssetHub Current stored value: 0 Storing new value: 42... Transaction confirmed Updated stored value: 42
Conclusion¶
Congratulations! You've successfully set up a Hardhat development environment, written comprehensive tests for your Storage contract, and deployed it to local and Westend Asset Hub networks. This tutorial covered essential steps in smart contract development, including configuration, testing, deployment, and interaction.
| Created: April 9, 2025