Я изучаю учебник Патрика Коллинза. Этот учебник представляет собой фантастическое введение в разработку блокчейна и web3 с использованием JavaScript, Solidity и Hardhat. Один из уроков был направлен на разработку и развертывание смарт-контракта лотереи (Raffle) с использованием hardhat-deploy, тестовой сети hardhat dev и тестовой сети Rinkeby. Патрик также реализует машинописную версию своего кода.

Эта статья призвана развить эту работу, обновив приложение до TypeScript и используя последние версии всех зависимостей. Я хотел бы использовать TypeScript сильнее. Мы также переключимся с тестовой сети Rinkeyby на более современную тестовую сеть Sepolia.

Это не полное руководство по созданию приложения в каске. Это просто расширение ультрасовременного руководства Патрика Коллинза на 2023 год, когда у нас будет версия 6 ehters.js.

Это репозиторий машинописной ветки Патрика Коллинза https://github.com/PatrickAlphaC/hardhat-smartcontract-lottery-fcc/tree/typescript

Мы внесем несколько небольших исправлений в соответствии с быстро меняющимися мирами JS и блокчейна.

Установка и подготовка

У Hardhat отличная документация https://hardhat.org/hardhat-runner/docs/getting-started#installation. Я опишу в двух словах о касках.

Установка и настройка

Вы можете установить каску глобально или локально. Я предпочитаю второй вариант, но если вы хотите стать суперквалифицированным старшим разработчиком смарт-контрактов, вам, вероятно, потребуется установить его глобально. В любом случае, мы должны как-то установить каску.

Тогда мы должны инициировать проект

hardhat
npx hardhat

Или после установки каски-сокращения вы можете использовать просто

hh

Чтобы упростить процесс, мы выбрали вариант проекта Typescript:

Согласитесь со всем и настройте его.

Нам не нужны контракты, сценарии и тесты, созданные Hardhat.

rm -rf contract/ scripts/ test/

У нас будет тот же контракт Solidity, что и у Патрика. https://github.com/shaggyrec/hardhat-smartcontract-lottery-ts/blob/main/contracts/Raffle.sol
Здесь нет никаких изменений.

Мы также должны установить пару зависимостей

npm i @nomicfoundation/hardhat-toolbox @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers dotenv hardhat-gas-reporter -D

Я обычно использую `eslint`. Патрик использовал слово «красивее». Неважно, какой инструмент вы выберете. Но было бы хорошей практикой выбрать что-то, потому что JS позволяет вам слишком много, и у вас должны быть границы, чтобы ваш код был в отличном состоянии.

Для `eslint` мне нужно сделать

npm i eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser -D

и включите его в IDE.

Мы можем заставить hh скомпилировать, чтобы иметь контрактные типизации для написания нашего кода. Но это не обязательно. Hardhat выполняет эту команду прямо перед развертыванием.

Конфигурация

Мы хотим использовать TypeScript. И мы хотим использовать это правильно. У Патрика Коллинза есть `helper-hardhat-config.ts` в его репозитории, мы не будем. Вместо него мы расширим конфиг Hardhat.

Я не хочу использовать какие-либо дополнительные файлы конфигурации. И я действительно хочу иметь все конфигурации текущей сети в опции «сеть». В этом разница между решением Патрика и текущим решением. В следующем конфигурационном файле вы можете увидеть опции `blockConfirmations`, `entranceFee`, `gasLane`, `subscriptionId`, `callbackGasLimit`, `interval` прямо в сетевом объекте.

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;

Но этого недостаточно. TypeScript/Eslint покажет ошибки

Еще одна важная вещь.
В ethers 6 вы больше не можете использовать `ethers.utils.parseEther(‘0.01’)`, функции не существует. `Utils` не выход. Мы должны использовать просто ethers.parseEther(‘0.01’)`.

Я создал файл `type-extensions.ts`, чтобы расширить конфигурацию по умолчанию.

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

Я считаю, что структура очень ясна. Объявления с суффиксом `UserConfig` предназначены для конфигурации каски. Объявления с суффиксом `Config` предназначены для демонстрации этих свойств в ваших скриптах.

Дай посмотреть. Файл `deploy/01-deploy-raffle.ts` должен быть похож на

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;

Мы импортировали «сеть» из «каски», и я могу использовать «network.config.vrfCoordinator», «network.config.subscriptionId». Потрясающий!

Еще одна замечательная вещь заключается в том, что у нас есть подсказки для нашего объекта контракта.

Мы можем получить контракт, используя дженерики TypeSctipt.

import { Raffle } from '../typechain-types';
const raffle = await ethers.getContract<Raffle>('Raffle');

И теперь мы можем использовать такую ​​магию:

Все остальное в приложении то же самое. Ссылку на репозиторий я прикреплю в конце этой статьи.

Тесты

Тесты почти одинаковые. У нас есть разница в случае «больших» взаимодействий. Но это так просто:

const contractBalance = BigInt(raffleEntranceFee) * BigInt(additionalEntrances) + BigInt(raffleEntranceFee);

Кроме того, мы должны использовать вместо

raffle.callStatic.checkUpkeep("0x")

немного другой

raffle.checkUpkeep.staticCall('0x');

И мы можем заменить этот громоздкий функционал машины времени

await network.provider.send('evm_increaseTime', [interval + 1]);
await network.provider.request({ method: 'evm_mine', params: [] });

с лаконичным

import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers';
…
await time.increase(interval + 1);

У меня есть два пропущенных теста, потому что на момент написания этой статьи срабатывание событий не работало должным образом в последней версии. Ссылка на выпуск GitHub https://github.com/NomicFoundation/hardhat/issues/4098

Сеполия

Патрик Коллинз работает с Ринкеби в своем руководстве. Использование Sepolia очень похоже.

Создание подписки https://docs.chain.link/vrf/v2/subscription/supported-networks/#sepolia-testnet

ВРФ вещи https://vrf.chain.link

Поддерживать регистрацию https://automation.chain.link/sepolia

Эфирискан https://sepolia.etherscan.io/

Заключение

Можем развернуть и войти в лотерею

hh deploy - network sepolia
hh run scripts/enterRaffle.ts - network sepolia

Мы использовали последнюю версию для всего и немного переписали приложение Freecodecamp Патрика Коллинза Raffle.

У нас проблема с подпиской на события в последней версии Hardhat. Но я верю, что это будет исправлено в ближайшее время.

Вот он, лотерейный смарт-контракт, созданный с помощью Hardhat, TypeScript и последних версий всех зависимостей и развернутый локально или в тестовой сети Sepolia.

Ссылка на репозиторий этой статьи — https://github.com/shaggyrec/hardhat-smartcontract-lottery-ts.

Подписывайтесь на меня





https://www.facebook.com/shogentle