Hardhat Smartcontract Lottery (Raffle) Using TypeScript and latest versions of everything

Hardhat Smartcontract Lottery (Raffle) Using TypeScript and latest versions of everything Patrick Collings freecodecamp

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.

Installation & preparations

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:

Hardhat Smartcontract Lottery (Raffle) Using TypeScript and latest versions of everything

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.

Config

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

Hardhat Smartcontract Lottery (Raffle) Using TypeScript and latest versions of everything

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:

Image description

All other things of the application are the same. I will attach the link to the repository in the end of this article.

Tests

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

Sepolia

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/

Conclusion

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