Числа в Web3: wei, gwei, ether и bigint
Почему в Web3 нельзя хранить деньги во float, что такое wei/gwei/ether и как ими безопасно оперировать.
wei — минимальная неделимая единица эфира. 1 ether = 10^18 wei. Все суммы on-chain — целые числа в минимальных единицах, без дробей.
Это урок, который спасает от потери денег. В блокчейне нет вещественных чисел: всё хранится в целых минимальных единицах. Попытка считать балансы во float рано или поздно даёт ошибку округления — и в деньгах это катастрофа.
Почему не float
Числа с плавающей точкой (тип number в JS) не представляют десятичные дроби точно. Классика:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // falseВывод:
0.30000000000000004 false
Для денег это недопустимо. Поэтому суммы держат в целых wei и оперируют типом bigint, который точен для сколь угодно больших целых.
Единицы
| Единица | В wei | Где встречается |
| wei | 1 | внутреннее хранение |
| gwei | 1 000 000 000 (10^9) | цена газа |
| ether | 1 000 000 000 000 000 000 (10^18) | суммы для людей |
parseEther и formatEther
ethers даёт две функции-моста: parseEther переводит строку «1.5» в wei (bigint), formatEther — обратно wei в человекочитаемую строку:
import { parseEther, formatEther } from "ethers";
const wei = parseEther("1.5"); // 1500000000000000000n (bigint)
const eth = formatEther(wei); // "1.5" (строка!)
// важно: parseEther принимает СТРОКУ, а не number, чтобы не потерять точностьАрифметика на bigint — чистая логика
Покажем «ручную» конвертацию wei↔ether на bigint, чтобы понять, что делает ethers внутри. Делим на 10^18, дробную часть собираем отдельно:
const DECIMALS = 18n;
const ONE = 10n ** DECIMALS;
function formatEth(wei) {
const whole = wei / ONE; // целая часть
const frac = wei % ONE; // остаток в wei
// дробную часть дополняем нулями слева до 18 знаков и убираем хвостовые нули
let fracStr = frac.toString().padStart(18, "0").replace(/0+$/, "");
return fracStr ? whole + "." + fracStr : whole.toString();
}
console.log(formatEth(1500000000000000000n)); // 1.5
console.log(formatEth(1000000000000000000n)); // 1
console.log(formatEth(1234500000000000000n)); // 1.2345
console.log(formatEth(1n)); // 0.000000000000000001Вывод:
1.5 1 1.2345 0.000000000000000001
Как работает под капотом
Внутри ethers parseEther("1.5") не использует float: он разбирает строку, отделяет целую и дробную части, дополняет дробную нулями до 18 знаков и склеивает в одно большое целое — ровно как мы сделали вручную, только наоборот. Именно поэтому вход — строка: число 1.5 уже было бы неточным float ещё до парсинга.
Частые ошибки
- parseEther(1.5) с числом. Передавайте строку
"1.5"; число потеряет точность. - Складывать bigint с number.
1n + 1бросит ошибку; приводите типы явно. - Хранить баланс во float и потом парсить. Ошибка округления уже произошла; держите суммы в bigint/строке.
Итоги
- On-chain все суммы — целые wei; считайте на bigint, не на float.
parseEther/formatEther— мост между wei и человекочитаемым видом; вход parseEther — строка.- 1 ether = 10^18 wei, 1 gwei = 10^9 wei.