BigInt в Web3-фронтенде: форматирование больших чисел
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 6 из 30.
EVM использует целые числа, размер которых часто превышает безопасный диапазон JavaScript Number. Даже если номер нашего курса равен всего 17, библиотека viem возвращает многие сетевые значения как bigint. В этом уроке зададим правила преобразования: вычисление остаётся точным, а строка для человека появляется только на границе отображения.
Предполагается снимок реестра из предыдущего урока. Результат — номер курса, блок и денежные единицы можно показать и сохранить без потери младших разрядов. Учебный профиль viem 2.37.9 не меняется. Листинги подготовлены без запуска; примерные числа ниже являются заданными входами, а не измеренной стоимостью сети.
Номер и количество имеют разные задачи
Номер курса — идентификатор. Для него обычно не нужны дробная часть, валютный знак или округление. Баланс и комиссия — количества, у которых есть единица измерения. Один и тот же способ форматирования этих значений создаст путаницу: номер курса с разделителем тысяч может быть удобен читателю, но строка для вызова контракта должна однозначно превращаться обратно в исходное целое число.
Number безопасно представляет целые значения только до определённой границы. Потеря точности происходит до того, как приложение рисует результат: если преобразование уже отбросило разряд, последующее toString не восстановит его. Поэтому нельзя сначала преобразовать большой баланс в Number, а затем надеяться на библиотеку красивого форматирования.
bigint хранит целое без дробной части. При делении целых результат также целый; оставшаяся часть отбрасывается. Это важно для отображения денежной суммы: арифметическая операция должна явно сохранять масштаб. Если величина выражена в wei, деление на число единиц даст только целую часть ETH, если отдельно не обработать остаток.
Преобразование входного номера
Форма получает строку. Допустим только положительную десятичную запись без экспоненты, знака и пробелов внутри. Удаление пробелов по краям выполняется явно; пустая строка не должна превращаться в ноль и маскировать отсутствие ввода.
const MAX_UINT256 = (1n << 256n) - 1n;
export function parseCourseId(raw) {
const value = raw.trim();
if (!/^[1-9][0-9]*$/.test(value)) {
throw new Error('Нужен положительный номер курса');
}
const id = BigInt(value);
if (id > MAX_UINT256) throw new Error('Номер не помещается в uint256');
return id;
}
export function showInteger(value) {
if (typeof value !== 'bigint') throw new Error('Ожидается bigint');
return value.toString(10);
}
Ожидается, что строка 17 даст 17n, а 1e3, -17 и пустое значение будут отклонены. Ограничение верхней границы относится к ABI uint256, а не к возможностям JavaScript. BigInt способен представить большее число, но контракт такой аргумент не примет. Проверка формата помогает дать понятное сообщение до кодирования вызова.
Начальные нули в нашем интерфейсе запрещены как политика формы. Это не универсальное правило Ethereum: значение 00017 численно равно 17. Но одна каноническая запись уменьшает число разных URL и ключей кеша для одного курса. При выборе другой политики следует всё равно нормализовать идентификатор перед сравнением.
Масштаб денежной единицы
Для величины в wei используем formatUnits, а для обратного преобразования — parseUnits. В базовом реестре публикация не принимает ETH, поэтому эти функции сейчас нужны для отображения предполагаемой комиссии, а не для добавления оплаты в publish.
import { formatUnits, parseUnits } from 'viem';
const exampleWei = 1_250_000_000_000_000n;
const text = formatUnits(exampleWei, 18);
const restored = parseUnits(text, 18);
// Ожидаемый текст: '0.00125'; restored соответствует exampleWei.
Число 18 здесь связано с базовой единицей ETH в выбранном профиле сети. Для произвольного токена нельзя автоматически использовать тот же масштаб. Значение decimals описывает представление количества, а не цену токена и не право на доступ. Уроки о токенах рассматривают эти вопросы отдельно.
Округлённая строка для карточки не годится как источник следующей транзакции. Если интерфейс показывает только четыре знака после запятой, исходное количество всё равно сохраняется целиком. Пользователь должен иметь возможность увидеть точное значение и единицу. Для операции с ограничением стоимости округление вниз может скрыть превышение бюджета, поэтому направление округления задаётся осознанно.
JSON и сохранённая история
Обычный JSON.stringify не сериализует bigint автоматически. Не следует исправлять это глобальной модификацией прототипа для всего приложения. На границе хранения создаём небольшой объект с десятичными строками и понятными названиями полей. При чтении восстанавливаем только те поля, чей тип известен схеме.
export function saveSnapshot(snapshot) {
return JSON.stringify({
version: 1,
courseId: snapshot.courseId.toString(),
blockNumber: snapshot.blockNumber.toString(),
blockHash: snapshot.blockHash,
contentHash: snapshot.contentHash,
active: snapshot.active,
});
}
Строка не становится числом только потому, что похожа на десятичную запись. В сохранённом документе version определяет схему. При восстановлении проверяем допустимые поля, длины и типы; подпись или сумма из непроверенного JSON не отправляется в кошелёк. Сохранённый снимок остаётся кешем наблюдения, который нужно связать с экземпляром и сетью.
Не путайте сетевые шестнадцатеричные количества с десятичными строками приложения. eth_chainId обычно возвращает строку вроде 0x7a69, а наш сериализатор номера курса сохраняет 17. Оба формата полезны, но преобразование должно знать, откуда пришло значение. Универсальная функция, удаляющая любые буквы из строки, уничтожила бы смысл и скрыла ошибочный вход.
Проверка по расчёту
Возьмите число больше безопасной границы Number, сохраните его десятичной строкой и восстановите как BigInt. Ожидается точное совпадение. Затем рассмотрите, почему преобразование через Number могло бы дать соседнее значение. Это упражнение можно выполнить вручную по разрядам; показанные программы в нашей подготовке не запускались.
Следующий результат относится к отпечатку выпуска: он тоже выглядит как большое шестнадцатеричное значение, но не является количеством и не нуждается в денежном форматировании. Различие идентификатора, суммы и байтов станет частью модели карточки. Утилиты единиц описаны в formatUnits и parseUnits.
Разделитель и локаль
Русский интерфейс может показывать запятую как дробный разделитель. Но строка аргумента должна проходить отдельный разбор по объявленной политике. Если поле принимает и точку, и запятую, сначала нормализуем единственный допустимый разделитель и отвергаем неоднозначные записи. Строка 1,234 в разных локалях может означать разное, поэтому универсальное удаление запятых создаёт ошибку количества.
Для номера курса дробный разделитель вообще не нужен. Отпечаток также не форматируется с разделителями групп. Поля внешне содержат цифры, но имеют разные семантические типы; отдельные функции парсинга делают эту разницу явной. Не стоит создавать один parseBlockchainValue, который пытается угадать назначение по набору символов.
В UI можно применить локальное представление к копии значения, сохранив исходное целое рядом. Кнопка копирования и программный аргумент используют каноническую строку, а карточка — удобный для человека текст. Так форматирование не становится скрытым преобразованием будущей транзакции.