Транзакции Waves: тип, комиссия, подпись
Зачем это нужно
В Ethereum транзакция одна: «отправить данные по адресу». Всё остальное — перевод, выпуск токена, вызов контракта — это уже толкование этих данных кодом контракта.
В Waves сделано наоборот. Каждому действию соответствует свой тип транзакции со своим набором полей и своей ценой. Перевод — тип 4, запись данных — 12, вызов dApp — 16. Это заметно проще для чтения и заметно жёстче: нельзя придумать своё действие, можно только собрать его из имеющихся.
Эта глава — про то, как устроен этот набор, откуда берётся комиссия и как транзакция проходит путь от объекта в памяти до блока. Все ответы узла ниже сняты на локальном узле Waves 1.6.4.
Типы и цены
Типов около двадцати. В повседневной работе нужны четыре:
| Номер | Название | Что делает | Комиссия |
|---|---|---|---|
| 3 | Issue | выпустить токен | 1 WAVES |
| 4 | Transfer | перевести | 0,001 WAVES |
| 12 | Data | записать в состояние аккаунта | 0,001 WAVES |
| 16 | InvokeScript | вызвать функцию dApp | 0,005 WAVES |
Комиссия в Waves фиксированная, а не аукцион. В Ethereum вы назначаете цену газа и соревнуетесь за место в блоке; здесь цена типа записана в правилах сети, и заплатить больше нет смысла. Это важно для расчётов: стоимость работы приложения считается заранее и точно.
Четыре шага транзакции
const { transfer } = require('@waves/waves-transactions');
const tx = transfer({
amount: 500000000,
recipient: '3MDKRTnd5H6zoscVfvSBiazwQaNmwDouJDx',
chainId: 'R',
}, seed);
console.log('тип:', tx.type, '| комиссия:', tx.fee, '| id:', tx.id);
console.log('подписей:', tx.proofs.length);
тип: 4 | комиссия: 100000 | id: 4v5T8XJxrEMGRKBCsjs8htjWnm31UMfZsjkPWVn39AMm
подписей (proofs): 1
Отправляем:
const r = await fetch(NODE + '/transactions/broadcast', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(tx),
});
ответ узла: 200
И ждём попадания в блок:
const info = await fetch(NODE + '/transactions/info/' + tx.id);
в блоке: 18 | применена: succeeded
200 на отправку значит «принято в очередь и предварительно проверено», а не «записано». Пока /transactions/info/<id> отвечает 404, транзакции в цепочке нет. Ожидание — это цикл с паузой, и писать его приходится почти всегда.
Проверяем, что деньги дошли
баланс до перевода: {"address":"3MDKRTnd5H6z…","balance":0}
баланс после перевода: {"address":"3MDKRTnd5H6z…","balance":500000000}
Ровно пять WAVES. Комиссию заплатил отправитель, и на его счету стало на 500000000 + 100000 меньше.
Когда узел отказывает
Не хватает денег. Переводим больше, чем есть:
код: 400
{"error":112,"message":"State check failed. Reason: Attempt to transfer unavailable funds:
Transaction application leads to negative waves balance to (at least) temporary negative state,
current balance equals 500000000, …"}
Слишком маленькая комиссия. Ставим 1000 вместо 100000:
код: 400
{"error":112,"message":"State check failed. Reason: Fee for TransferTransaction
(1000 in WAVES) does not exceed minimal value of 100000 WAVES."}
Сообщение прямо называет и то, что прислали, и то, что нужно. Такой отказ виден сразу, до блока.
error: 112 — «не прошла проверка состояния». Этот же код выдаётся и при нехватке денег, и при маленькой комиссии, и при попытке распорядиться чужим активом. Смотреть надо не на код, а на message: там причина словами.
Подпись и поле proofs
В объекте транзакции подпись лежит в массиве proofs, а не в одном поле. Массив — не прихоть: мест в нём до восьми.
Обычный аккаунт кладёт одну подпись в нулевую позицию. Но если на аккаунт навешен скрипт, он сам решает, что считать достаточным доказательством: две подписи из трёх, подпись плюс пароль, подпись другого аккаунта. Отсюда и множественное число в названии поля.
tx.proofs // [ '4v5T8XJxrEMG…' ] — одна подпись
tx.proofs.length // 1
К этому вернёмся в главе про смарт-аккаунты. Пока достаточно помнить: подпись — это элемент массива, и обращаться к ней надо по индексу.
Читаем транзакцию из цепочки
curl http://127.0.0.1:6869/transactions/info/<id>
Полезные поля ответа:
| Поле | Что означает |
|---|---|
type | номер типа: 4 перевод, 12 данные, 16 вызов dApp |
height | номер блока, в котором транзакция оказалась |
applicationStatus | succeeded или script_execution_failed |
fee | сколько списано комиссии |
sender | адрес отправителя |
stateChanges | для типа 16: что именно изменил вызов |
applicationStatus стоит проверять всегда. Транзакция может попасть в блок и не примениться — если скрипт dApp бросил ошибку после того, как комиссия уже была списана. Такое поведение похоже на Fabric: в цепочке запись есть, состояние не изменилось.
Типичные ошибки
Ошибка 1. Считать 200 подтверждением.
Разобрано выше. Нужен цикл ожидания по /transactions/info/<id>.
Ошибка 2. Забыть chainId.
Библиотека по умолчанию подпишет для основной сети. Локальный узел такую транзакцию не примет, а сообщение будет про адрес, а не про сеть — и разбираться придётся долго.
Ошибка 3. Считать комиссию частью суммы.
transfer({ amount: balance }) // не пройдёт: на комиссию не осталось
transfer({ amount: balance - fee })
Узел откажет с «temporary negative state»: даже временный минус запрещён.
Ошибка 4. Дробная сумма.
amount: 1.5 * 100000000 // иногда 150000000.00000001
amount: 150000000 // так правильно
Ошибка 5. Повторная отправка того же объекта.
id считается из содержимого. Отправили дважды — второй раз узел ответит, что такая транзакция уже есть. Чтобы сделать перевод ещё раз, нужен новый объект: у него будет другая метка времени и другой id.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Перевод | transfer({ amount, recipient, chainId }, seed) |
| Запись данных | data({ data: [...], chainId }, seed) |
| Вызов dApp | invokeScript({ dApp, call, chainId }, seed) |
| Отправить | POST /transactions/broadcast с объектом транзакции |
| Проверить попадание | GET /transactions/info/<id> — 404 значит «ещё нет» |
| Узнать комиссию заранее | POST /transactions/calculateFee |
| Список транзакций адреса | GET /transactions/address/<адрес>/limit/10 |
Словарь главы
| Термин | Что это |
|---|---|
| Тип транзакции | Номер, задающий и смысл действия, и набор полей, и цену |
| Комиссия | Фиксированная плата по типу; в Waves не аукцион |
proofs | Массив подписей, до восьми мест |
id транзакции | Считается из содержимого; повторить тот же объект нельзя |
broadcast | Отправка в сеть; ответ 200 — «принято в очередь» |
applicationStatus | Применилась ли транзакция после попадания в блок |
error: 112 | Не прошла проверка состояния |
| «Копейки» | Целое число, в котором хранится сумма |
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
const { transfer } = require('@waves/waves-transactions');
Что должен уметь после главы
- назвать четыре нужных типа транзакций и их цену;
- объяснить, чем фиксированная комиссия отличается от аукциона газа;
- собрать, подписать, отправить транзакцию и дождаться её попадания в блок;
- прочитать отказ узла по трём полям и понять причину;
- объяснить, зачем подпись лежит в массиве;
- проверить
applicationStatusи объяснить, зачем это нужно.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Типы транзакций Waves — все типы с полями и ценами.
- Комиссии — таблица и правила расчёта.
- @waves/waves-transactions — библиотека сборки и подписи.
- REST API узла — все запросы, включая broadcast и info.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.