В прошлой статье я разбирал блокчейн как append-only базу данных — сплошная теория и одна read-only команда. Долг перед практикой остался, и сегодня я его закрываю: отправим в Ethereum настоящую транзакцию из TypeScript-скрипта.
Сам код — 20 строк и 10 минут. Но по дороге вас ждут три грабли. Я на них наступил сам, когда только заходил в web3, а теперь регулярно наблюдаю, как на те же самые грабли наступают мои менти — с завидным постоянством, в одном и том же порядке. Разберу все три заранее — сэкономлю вам час отладки.
Всё происходит в тестовой сети Sepolia: эфир там бесплатный, ошибки безболезненны, а механика — байт в байт как в мейннете. Тестовый эфир раздают фосеты-«краники», и они имеют привычку умирать — поэтому живые я собираю в шпаргалке в репозитории Копилки: это учебный dApp, который я разрабатываю в открытую по ходу серии «Web3 для JS-разработчиков» в Telegram. Этот туториал — её пятая часть, дальше серия уходит в Solidity.
Что понадобится
-
Node.js 20+ (нужна поддержка
--env-file) -
Тестовый кошелёк — отдельный, не тот, где лежат реальные деньги. Подойдёт свежесозданный аккаунт MetaMask
-
Немного Sepolia ETH — из любого фосета по шпаргалке выше; если баланса нет вообще ни на одном адресе, берите PoW-фосет pk910 — он не требует ничего, кроме открытой вкладки
Шаг 1. Проект
mkdir first-tx && cd first-txnpm init -y && npm pkg set type=modulenpm i viem && npm i -D tsx typescript @types/node
type=module обязателен: мы используем top-level await, а в CommonJS его нет — без этой строки esbuild упадёт с «Top-level await is currently not supported with the «cjs» output format».
viem — современный TypeScript-клиент Ethereum, наследник ethers.js: строгие типы и минимум магии.
Шаг 2. Ключ — и грабля №1
Экспортируйте приватный ключ тестового аккаунта (MetaMask: селектор аккаунтов → три точки у аккаунта → Account details → Private keys) и положите его в .env в корне проекта:
SEPOLIA_PRIVATE_KEY=0x<64 hex-символа>
Грабля №1: MetaMask отдаёт ключ без префикса 0x — просто 64 hex-символа. Библиотеки же ждут ключ с префиксом. Не допишете 0x сами — получите загадочное «invalid private key, expected hex or 32 bytes, got string» из глубин криптографии.
Сразу же — правила безопасности, оба навсегда: ключ живёт только в .env, который лежит в .gitignore; и это ключ отдельного тестового кошелька, НИКОГДА — основного.
Грабля №2: Node не читает .env сам по себе. Файл может лежать в корне сколько угодно — process.env.SEPOLIA_PRIVATE_KEY останется undefined, и вы получите не менее загадочное «Cannot read properties of undefined (reading ‘slice’)». Файл нужно явно скормить рантайму флагом --env-file (появился в Node 20.6) — команда запуска будет ниже.
Шаг 3. Код целиком
// send.tsimport { createWalletClient, createPublicClient, http, parseEther, formatEther } from 'viem';import { privateKeyToAccount } from 'viem/accounts';import { sepolia } from 'viem/chains';const pk = process.env.SEPOLIA_PRIVATE_KEY;if (!pk) throw new Error('SEPOLIA_PRIVATE_KEY не задан. Запуск: npx tsx --env-file=.env send.ts');const account = privateKeyToAccount(pk as `0x${string}`);// грабля №3 живёт в этой константе — о ней нижеconst RPC = 'https://ethereum-sepolia-rpc.publicnode.com';const publicClient = createPublicClient({ chain: sepolia, transport: http(RPC) });const wallet = createWalletClient({ account, chain: sepolia, transport: http(RPC) });const balance = await publicClient.getBalance({ address: account.address });console.log('Баланс:', formatEther(balance), 'ETH');// отправляем 0.001 тестового ETH... самому себе — для первого раза идеальноconst hash = await wallet.sendTransaction({ to: account.address, value: parseEther('0.001'),});console.log('Отправлена:', hash);console.log('Смотреть: https://sepolia.etherscan.io/tx/' + hash);const receipt = await publicClient.waitForTransactionReceipt({ hash });console.log('В блоке:', receipt.blockNumber, '· статус:', receipt.status);
Запуск:
npx tsx --env-file=.env send.ts
Грабля №3: дефолтная нода. Если создать транспорт как http() без аргумента, viem пойдёт на публичный RPC по умолчанию — и у того такой rate-limit, что чтение баланса ещё проходит, а eth_sendRawTransaction встречает ошибку 429. Мораль вполне архитектурная: не выбрал ноду сам — за тебя выбрали, и плохо. В коде нода задана явно (publicnode — без регистрации и ключей); список живых публичных эндпоинтов — в той же шпаргалке.
Что произошло за эти 12 секунд
Вывод скрипта выглядит буднично, но под капотом случилась красивая цепочка:
-
Подпись — локально. viem подписал транзакцию приватным ключом прямо на вашей машине. Ключ не ушёл никуда: в сеть отправились только подписанные байты. В этом фундаментальное отличие от «пароля», который приходится отправлять серверу.
-
Подписанные байты улетели на ноду вызовом
eth_sendRawTransaction— это JSON-RPC, тот же протокол, которым ходит MetaMask и вообще весь web3. -
Транзакция подождала в мемпуле — очереди кандидатов, откуда валидаторы собирают блоки, приоритезируя по цене газа.
-
Валидатор включил её в блок — новая запись в append-only логе из первой статьи. Навсегда.
-
waitForTransactionReceiptдождался включения и вернул квитанцию: номер блока, статус, потраченный газ.
Откройте ссылку из вывода: ваша транзакция лежит в публичном логе — блок, газ, подпись, всё на витрине. Это уже не туториал, это ончейн-факт.
Кстати, про «отправить самому себе»: это не трюк, а полноценная транзакция. Сеть проверила подпись, списала газ и записала её в вечность — просто отправитель и получатель совпали. Идеальный первый шаг: промахнуться адресом невозможно.
Чеклист граблей
|
Симптом |
Причина |
Лечение |
|---|---|---|
|
|
ключ из MetaMask без |
дописать |
|
|
Node не прочитал |
|
|
|
дефолтная публичная нода viem |
явный RPC: |
|
|
проект собирается как CommonJS |
|
Что дальше
Это пятая часть серии «Web3 для JS-разработчиков», которую я веду в Telegram: два разбора в неделю плюс словарь терминов по будням, всё через аналогии из мира web2 — Postgres, Kafka, git. По ходу серии строим Копилку — учебный dApp с контрактом на Solidity и React-фронтендом, весь код открыт, на каждый пост — git-тег.
-
Серия: t.me/blockwright_dev
-
Код этого туториала: github.com/blockwright/kopilka, тег
post-05 -
Дальше по плану — Solidity: пишем первый контракт
Дисклеймер: всё выше — учебный код для тестовой сети. Не отправляйте реальные деньги скриптом, который вы скопировали из статьи в интернете — даже из этой.
ссылка на оригинал статьи https://habr.com/ru/articles/1071936/