hardhat.config.ts: настройки и сборка контрактов
- Зачем это нужно
- Файл целиком
- plugins: то, чего без них не будет
- solidity: версия компилятора и профили сборки
- Оптимизатор: вдвое короче при том же поведении
- Что делает сборка
- Артефакт: что внутри
- ABI: единственный способ поговорить с контрактом
- build-info: слепок сборки целиком
- networks: симулятор и настоящий узел
- Секреты: почему ключ не пишут в файл настроек
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Файл настроек — единственное, что отличает папку с установленным пакетом от проекта. Без него любая команда упирается в HHE3; с ним работает всё остальное.
При этом файл маленький — сорок строк, — и каждая из них на что-то влияет напрямую. Эта глава разбирает его по одной строке и показывает прогонами, что именно отваливается, если строку убрать или написать неверно.
Вторая половина — про сборку: что происходит между .sol и адресом в сети, какие файлы появляются на диске и зачем они нужны.
Файл целиком
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatToolboxMochaEthersPlugin],
solidity: {
profiles: {
default: { version: "0.8.34" },
production: {
version: "0.8.34",
settings: { optimizer: { enabled: true, runs: 200 } },
},
},
},
networks: {
hardhatMainnet: { type: "edr-simulated", chainType: "l1" },
local: {
type: "http",
chainType: "l1",
url: "http://127.0.0.1:8545",
accounts: ["0x59c6…8690d"],
},
sepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
},
});
Четыре верхних ключа и есть весь файл. Разберём по одному.
plugins: то, чего без них не будет
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
plugins: [hardhatToolboxMochaEthersPlugin],
Подключаемая часть (plugin) — пакет, который добавляет Hardhat новые возможности. Взятый нами набор приносит сразу несколько: работу с сетью через ethers, запуск тестов через mocha, проверку утверждений, сбор статистики по газу.
Коварство в том, что без него ничего не ломается сразу. Прогон убирает строку plugins из файла и собирает проект:
── в конфиге забыли подключить набор инструментов ──
сборка: Compiled 2 Solidity files with solc 0.8.34 (evm target: osaka)
а теперь попросим у него ethers в скрипте:
ethers есть? false
Сборка прошла как ни в чём не бывало. А в скрипте объекта ethers просто нет — и падение случится далеко от причины, на строке ethers.deployContract, где искать будут что угодно, кроме файла настроек.
Правило поиска: если в скрипте «не существует» что-то, чего вы не писали сами, — сначала откройте файл настроек и проверьте список подключаемых частей. Это дешевле, чем читать стек вызовов.
solidity: версия компилятора и профили сборки
solidity: {
profiles: {
default: { version: "0.8.34" },
production: {
version: "0.8.34",
settings: { optimizer: { enabled: true, runs: 200 } },
},
},
},
Версия компилятора и pragma в контракте — две разные вещи, которые обязаны договориться. pragma solidity ^0.8.28 означает «собери меня любым компилятором 0.8.x не ниже 0.8.28». Если в настройках стоит 0.8.20, договориться не получится:
Downloading solc 0.8.20
Error HHE909: Failed to create compilation job for file ".../contracts/Counter.sol"
using the build profile "default".
No solc version enabled in this profile is compatible with this file.
Обратите внимание: компилятор он сначала скачал и только потом обнаружил несовместимость. Это нормально — проверка делается уже установленной версией.
А если версии не существует вовсе:
Error HHE903: Solidity version 0.8.99 is invalid or hasn't been released yet.
If you are certain it has been released, run "npx hardhat clean --global" and try again
Оптимизатор: вдвое короче при том же поведении
Прогон собирает один и тот же контракт двумя профилями и меряет результат:
обычная сборка : deployedBytecode 2253 байт
production-сборка: deployedBytecode 1115 байт
Вдвое. Поведение при этом не изменилось ни на шаг: оптимизатор переписывает машинные команды, а не логику.
Зачем это нужно, если места вроде бы хватает:
- Предел на контракт в сети — 24 576 байт. Пока контракт учебный, до предела далеко. Как только появляется наследование от нескольких библиотек, предел оказывается неожиданно близко, и первое, что делают, — включают оптимизатор.
- Дешевле развёртывание. За каждый байт кода при развёртывании платят газом.
- Дешевле вызовы. Короткий код — меньше работы на каждый вызов.
Собирать и тестировать обычно удобнее без оптимизатора: сборка быстрее, а места в сообщениях об ошибках понятнее. Разворачивать — с оптимизатором. Ровно для этого в файле и лежат два профиля, а не один.
Что делает сборка
npx hardhat build
Downloading solc 0.8.34
Downloading solc 0.8.34 (WASM build)
Compiled 2 Solidity files with solc 0.8.34 (evm target: osaka)
Первый запуск качает компилятор — две загрузки, потому что Hardhat берёт и родную сборку, и запасную. Дальше он лежит на диске и не качается больше никогда.
evm target: osaka — набор машинных команд, на который рассчитан результат. Он должен быть не новее того, что понимает сеть; для нашего geth 1.17 это так.
artifacts/artifacts.d.ts
artifacts/build-info/solc-0_8_34-94ca57….json
artifacts/build-info/solc-0_8_34-94ca57….output.json
artifacts/contracts/Vault.sol/Vault.json
artifacts/contracts/Vault.sol/artifacts.d.ts
Артефакт: что внутри
── ключи файла артефакта ──
_format, contractName, sourceName, abi, bytecode, deployedBytecode,
linkReferences, deployedLinkReferences, immutableReferences, buildInfoId
── байт-код ──
bytecode (для развёртывания) : 4954 символов = 2476 байт
deployedBytecode (что в сети) : 4508 символов = 2253 байт
начало bytecode : 0x6080604052348015600e575f5ffd5b50604051 …
предел на контракт в сети : 24576 байт
Два байт-кода, а не один. Первый — то, что отправляют в транзакции развёртывания; он длиннее, потому что содержит ещё и конструктор с его работой. Второй — то, что останется лежать по адресу навсегда. Конструктор выполняется один раз и в сеть не попадает — отсюда разница в 223 байта.
В байт-коде нет имён. Ни deposits, ни withdraw, ни комментариев. Всё это существует только в исходнике; сеть видит только команды. Именно поэтому в главе про прокси слоты нумеровались, а не назывались.
ABI: единственный способ поговорить с контрактом
── что в ABI ──
constructor: (uint256 _rate) [nonpayable]
error: NotOwner()
error: NothingToWithdraw()
event: Deposited(address who, uint256 amount)
event: Withdrawn(address who, uint256 amount)
function: deposit() [payable]
function: deposits(address ) → uint256 [view]
function: interestOf(address who) → uint256 [view]
function: owner() → address [view]
function: rate() → uint256 [view]
function: setRate(uint256 _rate) [nonpayable]
function: totalDeposits() → uint256 [view]
function: withdraw() [nonpayable]
ABI (application binary interface) — список всего, что у контракта есть снаружи: функции с типами аргументов и результатов, события, свои ошибки, конструктор.
Три наблюдения по этому списку.
Публичные поля стали функциями. deposits, totalDeposits, rate, owner мы объявляли как поля — в ABI они превратились в функции чтения. Так работает public у полей: компилятор дописывает функцию за вас.
Свои ошибки попали в ABI. Поэтому библиотека и умеет показать NothingToWithdraw вместо непонятного набора байтов: она находит совпадение по списку.
Ничего лишнего. Приватных полей в ABI нет — но это не значит, что их не видно в сети: читать хранилище по номеру слота может кто угодно. ABI прячет их от удобного доступа, а не от любого.
ABI — это всё, что нужно, чтобы говорить с контрактом. Исходники для этого не требуются: по адресу и ABI библиотека соберёт любой вызов. Отсюда и работают обозреватели сети с чужими контрактами.
build-info: слепок сборки целиком
── файл со всеми подробностями сборки ──
размер build-info output : 93 КБ
что в нём хранится : _format, id, solcVersion, solcLongVersion,
compilerType, userSourceNameMap, input
версия solc : 0.8.34
настройки, с которыми собирали : {"evmVersion":"osaka","outputSelection":…}
93 килобайта на два маленьких контракта. Внутри — исходники целиком, точная версия компилятора и все настройки, с которыми собирали.
Зачем такой объём: чтобы сборку можно было повторить в точности. Подтверждение исходников в обозревателе сети устроено именно так — обозреватель собирает ваш код теми же настройками и сравнивает байт-код. Совпал — подпись «исходники подтверждены».
Заодно это объясняет, почему после сборки двумя профилями в папке оказалось два набора: настройки разные — значит, и слепки разные.
networks: симулятор и настоящий узел
networks: {
hardhatMainnet: { type: "edr-simulated", chainType: "l1" },
local: {
type: "http",
chainType: "l1",
url: "http://127.0.0.1:8545",
accounts: ["0x59c6…8690d"],
},
},
Прогон запускает один и тот же скрипт в обеих сетях:
hardhatMainnet chainId 31337 блок 0 счетов 20 первый 0xf39Fd6e5…b92266
баланс первого: 10000.0 ETH
local chainId 1337 блок 306 счетов 1 первый 0x70997970…dc79C8
баланс первого: 496.999983918662483 ETH
Каждая строка тут о чём-то говорит.
Счетов 20 против 1. Симулятор раздаёт двадцать счетов с круглыми балансами. В своей сети счета — ровно те ключи, которые вы вписали в accounts. Отсюда знаменитое «почему getSigners вернул один адрес вместо двадцати»: вы уже не в симуляторе.
Баланс 496,999983918662483. Это не случайное число: столько осталось у счёта B после переводов из прошлой главы. Сеть — та же самая, папка та же, история на месте.
Блок 306. Узел с прошлой главы так и остался работать с блоками по таймеру и за это время напечатал триста пустых блоков. В симуляторе блок всегда 0: он только что родился.
Секреты: почему ключ не пишут в файл настроек
Заготовка проекта пришла с третьей сетью, и написана она иначе:
sepolia: {
type: "http",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
configVariable — это «значение возьмёшь потом, снаружи». Попытка подключиться, не задав его, даёт понятный отказ:
Error HHE7: Configuration Variable "SEPOLIA_RPC_URL" not found.
You can define it using a plugin like hardhat-keystore, or set it as an
environment variable.
Значение берётся либо из переменной окружения, либо из встроенного зашифрованного хранилища:
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
Зачем весь этот огород, если можно вписать ключ строкой: файл настроек лежит в репозитории. Ключ, попавший туда однажды, остаётся в истории навсегда — его находят автоматические поисковики, и на настоящих сетях счёт опустошают за минуты.
В нашей local ключ вписан строкой сознательно: это публичный учебный ключ от приватной сети на своей машине. Правило простое — строкой можно только то, что не жалко опубликовать.
Типичные ошибки
HHE3: No Hardhat config file found. Запустили не из папки проекта.
HHE909: No solc version enabled in this profile is compatible with this file. Версия в настройках не подходит под pragma контракта. Смотреть надо на оба места сразу: иногда проще поправить pragma, иногда — настройки.
HHE903: Solidity version … is invalid or hasn't been released yet. Опечатка в номере версии — или версия действительно ещё не вышла.
HHE7: Configuration Variable … not found. Сеть требует значение снаружи, а его не задали.
«ethers is not defined» в скрипте. Не подключён набор инструментов. Сборка при этом проходит — в том и подвох.
Собрали без оптимизатора и упёрлись в предел размера. Сообщение приходит не от компилятора, а от сети при развёртывании, и выглядит непонятно. Первое действие — собрать профилем production и сравнить размер.
Слова главы
- Файл настроек — единственное, что отличает проект от папки с пакетом.
- Подключаемая часть — пакет с новыми возможностями; без неё сборка идёт, а скрипты падают.
- Профиль сборки — набор настроек компилятора; выбирается флагом, по умолчанию
default. - Оптимизатор — переписывает байт-код короче и дешевле, не трогая поведение.
- Артефакт — результат сборки одного контракта: ABI и два байт-кода.
- ABI — список всего, что у контракта есть снаружи; по нему с ним и говорят.
- Слепок сборки — исходники и настройки целиком; нужен для повторяемости и подтверждения.
- Значение снаружи — способ не писать ключи и адреса в файл, который лежит в репозитории.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
export default defineConfig({ plugins: [] });
На чемпионате
Файл настроек — первое, что открывает проверяющий, и первое, что стоит привести в порядок.
Своя сеть должна быть в networks под понятным именем. Не hardhatMainnet, не localhost по умолчанию, а именно та, которую вы подняли: адрес, номер сети, ключи. Тогда любая команда с --network local работает без объяснений.
Ключи от приватной сети — строкой, всё остальное — снаружи. Это читается как осознанное решение, а не как небрежность. Одна строка комментария рядом снимает вопрос совсем.
Профиль production — не украшение. Если контракт большой, разворачивать его придётся именно им, и узнать об этом лучше заранее.
Сборку запустите первой командой. Компилятор качается один раз, и это единственное место, где без интернета всё встанет. Если он не скачан — узнавать об этом надо на первой минуте.
Куда углубиться
runsв оптимизаторе — как выбор числа влияет на размер кода и цену вызовов.- Несколько версий компилятора в одном проекте — когда контракты требуют разных.
- Подтверждение исходников — как обозреватель сети сверяет байт-код со слепком сборки.
- Свои задачи Hardhat — добавить в набор команд собственную.
npx hardhat flatten— собрать контракт со всеми зависимостями в один файл; иногда этого просят при сдаче.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Hardhat: конфигурация — значения снаружи и встроенное хранилище.
- Hardhat: компиляция — артефакты и слепки сборки.
- Solidity: оптимизатор — что именно он переписывает.
- Solidity: ABI — как аргументы превращаются в байты.
- EIP-170: предел размера контракта — откуда взялись 24 576 байт.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.