Платежи в dApp: банк вкладов на RIDE
Зачем это нужно
Зачётка из прошлой главы данными и ограничилась. Настоящее приложение обычно работает с деньгами: принимает, хранит, возвращает. И вот тут появляется вопрос, который в Solidity решается словом payable, а здесь — иначе и удобнее.
В Waves деньги прикладывают к вызову. Не переводят отдельной транзакцией, не разрешают списание заранее — кладут прямо в ту же транзакцию, которая зовёт функцию. К моменту, когда функция начала считать, деньги уже на счёте приложения.
Эта глава — про этот механизм и про четыре проверки, без которых приложение теряет деньги. Написан и прогнан настоящий банк вкладов на локальном узле Waves 1.6.4.
Что приложено к вызову
Со стороны клиента:
const tx = invokeScript({
dApp: bank,
call: { function: 'polozhit', args: [] },
payment: [{ amount: 300000000, assetId: null }],
chainId: 'R',
}, seed);
Со стороны скрипта:
@Callable(i)
func polozhit() = {
let pay = i.payments[0]
if (pay.assetId != unit) then throw("Prinimayu tolko WAVES")
else {
let k = kluch(i.caller)
let bylo = match getInteger(this, k) { case n: Int => n case _ => 0 }
[ IntegerEntry(k, bylo + pay.amount) ]
}
}
Деньги приходят на счёт приложения до выполнения функции. Если функция упадёт, транзакция целиком не применится, и платёж тоже не состоится. Но пока она работает, на счёте уже лежит и старое, и новое — это важно помнить при расчётах баланса.
Выдача: ScriptTransfer
@Callable(i)
func zabrat(skolko: Int) = {
let k = kluch(i.caller)
let est = match getInteger(this, k) { case n: Int => n case _ => 0 }
if (skolko > est) then throw("Na schete tolko " + est.toString())
else [ IntegerEntry(k, est - skolko), ScriptTransfer(i.caller, skolko, unit) ]
}
Живой прогон:
### кладём 3 WAVES
изменения: [{"key":"vklad_3M57BFmz…","type":"integer","value":300000000}]
баланс банка: 2299900000
### забираем 1 WAVES
изменения: [{"key":"vklad_3M57BFmz…","type":"integer","value":200000000}]
переводы: [{"address":"3M57BFmz…","asset":null,"amount":100000000}]
Запись и перевод пришли одним списком и применились вместе.
Обратите внимание: в stateChanges ответа узла есть отдельное поле transfers. Там видно всё, что приложение отправило со своего счёта, — удобно при разборе «куда делись деньги».
Четыре обязательные проверки
Платёж вообще есть?
if (i.payments.size() == 0) then throw("Nuzhen platezh") else …
Без этого обращение к i.payments[0] уронит скрипт — не с вашим сообщением, а с невнятным сбоем.
Та ли монета? Самая частая пропущенная проверка. Прикладываем токен вместо WAVES:
код: 400
{"error":112,"message":"State check failed. Reason: Attempt to transfer unavailable funds:
Transaction application leads to negative asset '3fyDK2Da…' balance …"}
В этом прогоне отказала сама сеть — токенов на счёте не было. Но если бы они были, платёж прошёл бы, и приложение записало бы вклад в WAVES за приложенный токен. Проверка pay.assetId != unit стоит одну строку и спасает от этого.
Сумма разумна? Ноль и отрицательные значения отсекает сеть, но логика может требовать минимума — тогда проверять надо самому.
Хватает ли на счёте?
код: 400
{"error":306,"message":"Error while executing dApp: Na schete tolko 200000000"}
Без этой проверки приложение выдало бы чужие деньги: на его счёте лежат вклады всех.
Учёт в состоянии и деньги на счёте — разные вещи. Сеть следит только за вторым. Если в коде ошибка, сумма вкладов перестанет сходиться с балансом, и узнаете вы об этом в тот момент, когда кому-то не хватит на выдачу.
Возврат значения из функции
Вызываемая функция может вернуть не только список действий, но и значение:
@Callable(i)
func skolko() = {
let k = kluch(i.caller)
let est = match getInteger(this, k) { case n: Int => n case _ => 0 }
([], est)
}
Пара: пустой список действий и число. Такой вызов ничего не меняет, но всё равно остаётся транзакцией — за него платят комиссию и он попадает в блок.
Для чтения это плохой способ: платить 0,005 WAVES за то, что можно бесплатно прочитать запросом /addresses/data/<адрес>/<ключ>, незачем. Возврат значения нужен для другого — чтобы один dApp мог позвать другой и получить ответ.
Сложность по функциям
Узел показывает стоимость каждой функции отдельно:
curl .../addresses/scriptInfo/<адрес банка>
"callableComplexities":{"polozhit":20,"zabrat":22,"skolko":14}
Двадцать с небольшим — очень мало. Для сравнения, один sigVerify стоит 182. Работа с состоянием и деньгами почти ничего не стоит; дорого обходится криптография.
Типичные ошибки
Ошибка 1. Не проверить монету.
Разобрана выше. Одна строка кода, а без неё приложение принимает что угодно.
Ошибка 2. Считать баланс приложения вместо учёта.
let est = wavesBalance(this).available // это ВСЕ вклады, а не вклад вызвавшего
На счёте приложения лежат деньги всех. Учёт ведут в состоянии, под ключом на каждого.
Ошибка 3. Уменьшить учёт, но забыть перевод.
Или наоборот. Оба действия — в одном списке, иначе состояние и деньги разойдутся.
Ошибка 4. Переводить i.caller вместо i.originCaller, не понимая разницы.
Когда один dApp зовёт другой, i.caller — это позвавший dApp, а i.originCaller — человек, начавший всю цепочку. Для банка вкладов правильный вариант — i.caller: деньги вернутся туда, откуда пришли.
Ошибка 5. Читать состояние платным вызовом.
Комиссия 0,005 WAVES за то, что читается бесплатно обычным запросом к узлу.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Приложить деньги к вызову | payment: [{ amount, assetId: null }] |
| Прочитать платёж | let pay = i.payments[0] |
| Проверить, что платёж есть | i.payments.size() > 0 |
| Проверить, что это WAVES | pay.assetId == unit |
| Отправить деньги | ScriptTransfer(i.caller, summa, unit) |
| Отправить токен | ScriptTransfer(i.caller, summa, assetId) |
| Вернуть значение | ([], znachenie) |
| Кто начал цепочку | i.originCaller |
| Стоимость функций | поле callableComplexities в scriptInfo |
Словарь главы
| Термин | Что это |
|---|---|
| Платёж вызова | Деньги, приложенные к транзакции типа 16; до двух штук |
i.payments | Список платежей внутри функции |
unit | «Ничего» в RIDE; у платежа в WAVES assetId именно такой |
ScriptTransfer | Действие «отправить со счёта dApp» |
i.caller | Непосредственно вызвавший: человек или другой dApp |
i.originCaller | Тот, кто начал всю цепочку вызовов |
| Учёт | Записи в состоянии, по которым приложение различает вкладчиков |
transfers | Поле stateChanges, где видны все выплаты вызова |
Цель: скорость ≥ 90 зн/мин, точность ≥ 88%
let pay = i.payments[0]
Что должен уметь после главы
- приложить деньги к вызову и прочитать платёж внутри функции;
- отличить платёж в WAVES от платежа токеном;
- выдать деньги действием
ScriptTransferвместе с изменением учёта; - назвать четыре обязательные проверки и объяснить, чем грозит каждая пропущенная;
- объяснить разницу между учётом в состоянии и балансом счёта;
- отличить
i.callerотi.originCaller; - посмотреть стоимость каждой функции приложения.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Платежи в dApp — структура платежа и ограничения.
- Действия скрипта —
ScriptTransferи его поля. - Вызов dApp из dApp — зачем нужен возврат значения.
- Waves IDE — отладка вызовов с платежами.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.