Первая транзакция в Ethereum из TypeScript: 20 строк кода и три грабли по дороге

—

от автора

В прошлой статье я разбирал блокчейн как 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 секунд

Вывод скрипта выглядит буднично, но под капотом случилась красивая цепочка:

  1. Подпись — локально. viem подписал транзакцию приватным ключом прямо на вашей машине. Ключ не ушёл никуда: в сеть отправились только подписанные байты. В этом фундаментальное отличие от «пароля», который приходится отправлять серверу.

  2. Подписанные байты улетели на ноду вызовом eth_sendRawTransaction — это JSON-RPC, тот же протокол, которым ходит MetaMask и вообще весь web3.

  3. Транзакция подождала в мемпуле — очереди кандидатов, откуда валидаторы собирают блоки, приоритезируя по цене газа.

  4. Валидатор включил её в блок — новая запись в append-only логе из первой статьи. Навсегда.

  5. waitForTransactionReceipt дождался включения и вернул квитанцию: номер блока, статус, потраченный газ.

Откройте ссылку из вывода: ваша транзакция лежит в публичном логе — блок, газ, подпись, всё на витрине. Это уже не туториал, это ончейн-факт.

Кстати, про «отправить самому себе»: это не трюк, а полноценная транзакция. Сеть проверила подпись, списала газ и записала её в вечность — просто отправитель и получатель совпали. Идеальный первый шаг: промахнуться адресом невозможно.

Чеклист граблей

Симптом

Причина

Лечение

invalid private key, expected hex or 32 bytes

ключ из MetaMask без 0x

дописать 0x в начало

Cannot read properties of undefined (reading 'slice')

Node не прочитал .env

npx tsx --env-file=.env send.ts

HTTP request failed. Status: 429

дефолтная публичная нода viem

явный RPC: http('https://ethereum-sepolia-rpc.publicnode.com')

Top-level await is currently not supported

проект собирается как CommonJS

npm pkg set type=module

Что дальше

Это пятая часть серии «Web3 для JS-разработчиков», которую я веду в Telegram: два разбора в неделю плюс словарь терминов по будням, всё через аналогии из мира web2 — Postgres, Kafka, git. По ходу серии строим Копилку — учебный dApp с контрактом на Solidity и React-фронтендом, весь код открыт, на каждый пост — git-тег.

Дисклеймер: всё выше — учебный код для тестовой сети. Не отправляйте реальные деньги скриптом, который вы скопировали из статьи в интернете — даже из этой.

ссылка на оригинал статьи https://habr.com/ru/articles/1071936/