I am in the progress of learning Patrick Collins’ tutorial. This tutorial provided a fantastic introduction to blockchain and web3 development using JavaScript, Solidity, and Hardhat. One of the lessons aimed to develop and deploy a lottery(Raffle) smart contract using hardhat-deploy, hardhat dev testnet, and the Rinkeby testnet. Patrick also implement a typescript version of his code.
This article aims to build upon that work by updating the application to TypeScript and using the latest versions of all dependencies. I would like to use TypeScript in a stronger way. We’ll also switch from the Rinkeyby testnet to the Sepolia testnet, which is more up-to-date.
It’s not a complete tutorial of creating a hardhat application. It’s just an extension to Patrick Collins’ ultra-ultimate tutorial for year 2023 when we have ehters.js version 6.
This is the typescript branch Patric Collins’ repository https://github.com/PatrickAlphaC/hardhat-smartcontract-lottery-fcc/tree/typescript
We will be making a few small corrections in line with the rapidly changing worlds of JS and blockchain.
Hardhat has great documentation https://hardhat.org/hardhat-runner/docs/getting-started#installation. I will describe hardhat things in a nutshell.
Installation and setting up
You can install hardhat globally or locally. I prefer the second option but if you desire to be a super-skilled senior smart-contract developer you probably need to have it installed globally. Anyway, we have to install hardhat somehow.
Then we should initiate the project
hardhat
npx hardhat
Or after installing the hardhat-shorthand you can use just
hh
To simplify the process we chose the Typescript project option:
Agree with everything and set it up.
We don’t need contracts, scripts, and tests generated by Hardhat.
rm -rf contract/ scripts/ test/
We will have the same Solidity contract as Patrik. https://github.com/shaggyrec/hardhat-smartcontract-lottery-ts/blob/main/contracts/Raffle.sol There are no changes here.
We also have to install a couple of dependencies
npm i @nomicfoundation/hardhat-toolbox @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers dotenv hardhat-gas-reporter -D
I usually use eslint
. Patrick used prettier
. It doesn’t matter which tool you choose. But it would be good practice to choose something because JS allows you too much and you should have borders to have your code in great condition.
For eslint
I need to make
npm i eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
and turn it on in the IDE.
We can make ‘hh compile to have contract typings to write our code. But it is not necessary. The Hardhat executes this command right before the deploy.
We want to use TypeScript. And we want to use it in the right way. Patrik Collins has helper-hardhat-config.ts
in his repo we won’t. Instead of it, we will extend the Hardhat config.
I don’t want to use any additional config files. And I really want to have all configurations of the current network in the network
option. This is a difference between Patrics’ solution and the current solution. In the following config file you can see blockConfirmations
, entranceFee
, gasLane
, subscriptionId
, callbackGasLimit
, interval
option right in the network object.
import '@typechain/hardhat';
import '@nomicfoundation/hardhat-toolbox';
import '@nomiclabs/hardhat-ethers';
import '@nomicfoundation/hardhat-ethers';
import 'hardhat-deploy';
import 'hardhat-gas-reporter';
import './type-extensions';
import 'dotenv/config';
import { HardhatUserConfig } from 'hardhat/config';
import { ethers } from 'ethers';
const config: HardhatUserConfig = {
solidity: '0.8.18',
defaultNetwork: 'hardhat',
networks: {
hardhat: {
chainId: 31337,
blockConfirmations: 1,
entranceFee: ethers.parseEther('0.01'),
gasLane: '0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c',
subscriptionId: '',
callbackGasLimit: '500000',
interval: 30
},
sepolia: {
chainId: 11155111,
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.SEPOLIA_PRIVATE_KEY as string],
blockConfirmations: 6,
vrfCoordinator: '0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625',
entranceFee: ethers.parseEther('0.01'),
gasLane: '0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c',
subscriptionId: '3092',
callbackGasLimit: '500000',
interval: 30
}
},
etherscan: {
apiKey: {
sepolia: process.env.ETHERSCAN_API_KEY || ''
},
},
namedAccounts: {
deployer: {
default: 0
},
player: {
default: 1
}
},
developmentChains: ['hardhat', 'localhost'],
organizerFee: 10,
mocha: {
timeout: 40000
}
};
export default config;
But it’s not enough. TypeScript/Eslint will show the errors
One more important thing.
In ethers 6 you can no longer use ethers.utils.parseEther('0.01')
the function doesn’t exist. Utils
isn’t exit. We should use just ethers.parseEther('0.01')
.
I created type-extensions.ts
file to extend the default config.
import 'hardhat/types/config';
declare module 'hardhat/types/config' {
export interface HttpNetworkUserConfig {
blockConfirmations?: number;
vrfCoordinator?: string;
entranceFee: bigint;
gasLane: string;
subscriptionId?: string;
callbackGasLimit: string;
interval: number;
}
export interface HardhatNetworkUserConfig {
blockConfirmations?: number;
entranceFee: bigint;
gasLane: string;
subscriptionId?: string;
callbackGasLimit?: string;
interval: number;
}
export interface HardhatUserConfig {
developmentChains: string[];
organizerFee: number;
}
export interface HttpNetworkConfig {
blockConfirmations: number;
vrfCoordinator: string;
entranceFee: bigint;
gasLane: string;
subscriptionId: string;
callbackGasLimit: string;
interval: number;
}
export interface HardhatNetworkConfig {
blockConfirmations: number;
vrfCoordinator?: string;
entranceFee: bigint;
gasLane: string;
subscriptionId: string;
callbackGasLimit: string;
interval: number;
}
export interface HardhatConfig {
developmentChains: string[];
organizerFee: number;
}
}
I believe that structure is very clear. Declarations with UserConfig
suffix are for hardhat config. Declarations with Config
suffix are for exposing these properties in your scripts.
Let see it. The deploy/01-deploy-raffle.ts
should be similar to
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { config, network } from 'hardhat';
import { DeployFunction } from 'hardhat-deploy/dist/types';
import verify from '../utils/verify';
import { VRFCoordinatorV2Mock } from '../typechain-types';
import { EventLog } from 'ethers';
const deployRaffle: DeployFunction = async function ({ getNamedAccounts, deployments, ethers }: HardhatRuntimeEnvironment) {
const { deploy, log } = deployments;
const { deployer } = await getNamedAccounts();
let vrfCoordinatorV2Address = network.config.vrfCoordinator;
let subscriptionId = network.config.subscriptionId;
let vrfCoordinatorV2Mock: VRFCoordinatorV2Mock;
if (config.developmentChains.includes(network.name)) {
vrfCoordinatorV2Mock = await ethers.getContract('VRFCoordinatorV2Mock');
vrfCoordinatorV2Address = await vrfCoordinatorV2Mock.getAddress();
const transactionReceipt = await (await vrfCoordinatorV2Mock.createSubscription())
.wait(1);
subscriptionId = (transactionReceipt?.logs[0] as EventLog).args.subId;
await vrfCoordinatorV2Mock.fundSubscription(subscriptionId, ethers.parseEther('2'));
}
const args = [
vrfCoordinatorV2Address,
network.config.entranceFee,
network.config.gasLane,
subscriptionId,
network.config.callbackGasLimit,
network.config.interval,
config.organizerFee
];
const raffle = await deploy('Raffle', {
from: deployer,
args,
log: true,
waitConfirmations: network.config.blockConfirmations || 1
});
if (config.developmentChains.includes(network.name)) {
await vrfCoordinatorV2Mock!.addConsumer(subscriptionId, raffle.address);
}
if (!config.developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
await verify(raffle.address, args, log);
}
log('----------------------------');
};
deployRaffle.tags = ['all', 'raffle'];
export default deployRaffle;
We’ve imported the network
from the hardhat
and I can use network.config.vrfCoordinator
, network.config.subscriptionId
. Awesome!
Another awesome thing is that we have hintings for our contract object.
We can get contract using TypeSctipt generics thing
import { Raffle } from '../typechain-types';
const raffle = await ethers.getContract<Raffle>('Raffle');
And now we can use this kind of magic:
All other things of the application are the same. I will attach the link to the repository in the end of this article.
Tests are almost the same. We have the difference in case of bigint
interactions. But it is so easy:
const contractBalance = BigInt(raffleEntranceFee) * BigInt(additionalEntrances) + BigInt(raffleEntranceFee);
Also, we should use instead of
raffle.callStatic.checkUpkeep("0x")
a bit different
raffle.checkUpkeep.staticCall('0x');
And we can replace this bulky time-machine functionality
await network.provider.send('evm_increaseTime', [interval + 1]);
await network.provider.request({ method: 'evm_mine', params: [] });
with laconic
import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers';
...
await time.increase(interval + 1);
I have two skipped tests because at the time of writing this article events firing doesn’t work properly in the latest version. Link to the github issue
Patric Collins works with Rinkeby in his tutorial. Using of Sepolia is quite similar.
Creating subscription https://docs.chain.link/vrf/v2/subscription/supported-networks/#sepolia-testnet
VRF things https://vrf.chain.link/
Upkeep registering https://automation.chain.link/sepolia
Etherscan https://sepolia.etherscan.io/
We can deploy and enter the lottery
hh deploy --network sepolia
hh run scripts/enterRaffle.ts --network sepolia
We used the latest version on everything and bit rewrite Freecodecamp Patrick Collins’ Raffle application
We have a problem with a subscription to the events in the latest version of hardhat. But I believe that it will be fixed soon.
There you have it, a lottery smart contract built with Hardhat, TypeScript, and the latest versions of all dependencies, and deployed locally or on the Sepolia testnet.
The link to the repository of this article is
https://github.com/shaggyrec/hardhat-smartcontract-lottery-ts