Перейти к основному содержимому
Блокчейн71ГЛАВА 71Платежи в dApp: банк вкладов на RIDEвкладчик3 WAVES1 WAVESбанкvklad_3M57BFmz…200000000учёт в состоянииденьги едут с вызовом

Платежи в dApp: банк вкладов на RIDE

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Зачётка из прошлой главы данными и ограничилась. Настоящее приложение обычно работает с деньгами: принимает, хранит, возвращает. И вот тут появляется вопрос, который в Solidity решается словом payable, а здесь — иначе и удобнее.

В Waves деньги прикладывают к вызову. Не переводят отдельной транзакцией, не разрешают списание заранее — кладут прямо в ту же транзакцию, которая зовёт функцию. К моменту, когда функция начала считать, деньги уже на счёте приложения.

Эта глава — про этот механизм и про четыре проверки, без которых приложение теряет деньги. Написан и прогнан настоящий банк вкладов на локальном узле Waves 1.6.4.

Что приложено к вызову​

ДЕНЬГИ ПРИКЛАДЫВАЮТ К ВЫЗОВУ, А НЕ ПЕРЕВОДЯТ ОТДЕЛЬНОТранзакция вызоваdApp: 3MPa7P5SDzhdqE3gh1nAbRyc…call: { function: "polozhit" }payment: [{ amount: 300000000, assetId: null }]Внутри функцииi.payments[0].amounti.payments[0].assetIdassetId равен unit, если это WAVESдо двух платежей в одном вызоведеньги уже на счёте приложения к моменту, когда функция начала считатьотдельного перевода не нужно — и отдельной транзакции, которую можно забыть, тоже
Платёж едет в той же транзакции, что и вызов

Со стороны клиента:

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) ]
}
Нажми на часть выражения
ПОЛОЖИТЬ И ЗАБРАТЬ — ЖИВОЙ ПРОГОНpolozhit() с платежом 3 WAVESIntegerEntry("vklad_3M57BFmz…", 300000000)баланс банка стал 2 299 900 000 — деньги физически на его счётеzabrat(100000000)IntegerEntry("vklad_3M57BFmz…", 200000000)ScriptTransfer(3M57BFmz…, 100000000, null)запись и перевод — два действия одного списка, применяются вместеучёт в состоянии и деньги на счёте — разные вещи: расходятся при первой же ошибке в коде
Живой прогон: положили три, забрали один

Живой прогон:

### кладём 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. Там видно всё, что приложение отправило со своего счёта, — удобно при разборе «куда делись деньги».

Отвечено вопросов: 0 из 3
Как деньги попадают в dApp?
Что означает assetId равный unit?
Что делает ScriptTransfer?
Отвечено верно: 0 из 3

Четыре обязательные проверки​

ЧЕТЫРЕ ПРОВЕРКИ, БЕЗ КОТОРЫХ ПРИЛОЖЕНИЕ ТЕРЯЕТ ДЕНЬГИплатёж вообще есть?i.payments.size() > 0иначе обращение к [0] уронит скриптта ли монета?pay.assetId == unitиначе за токен запишут вклад в WAVESсумма разумна?pay.amount > 0ноль и отрицательные отсекает сеть, но не логикухватает на счёте?skolko > estиначе выдадут чужие деньгипропущенная проверка монеты — самая частая: токен приложили, вклад записали в WAVESсеть проверяет только балансы; смысл операции проверяет ваш код и больше никто
Сеть проверяет только балансы; смысл операции проверяет ваш код

Платёж вообще есть?

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. Работа с состоянием и деньгами почти ничего не стоит; дорого обходится криптография.

Отвечено вопросов: 0 из 3
Почему обязательно проверять i.payments.size()?
Чем опасна пропущенная проверка монеты?
Функция вернула ([], est). Что это значит?
Отвечено верно: 0 из 3

Типичные ошибки​

Ошибка 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 за то, что читается бесплатно обычным запросом к узлу.

Отвечено вопросов: 0 из 3
Почему нельзя считать вклад по балансу приложения?
Один dApp позвал другой. Кто такой i.caller во втором?
Сколько стоит функция polozhit в живом прогоне?
Отвечено верно: 0 из 3

Шпаргалка по главе​

Что нужноКак
Приложить деньги к вызовуpayment: [{ amount, assetId: null }]
Прочитать платёжlet pay = i.payments[0]
Проверить, что платёж естьi.payments.size() > 0
Проверить, что это WAVESpay.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 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоWaves Smart Contracts: Escrow ExampleWaves Tech
ВидеоWaves Platform - Ride, Smart Assets and NFT | Odyssey ConnectOdyssey
ВидеоHow to create a smart contract on Waves Platform - Part 1: Creating a smart assetCryptin.eu
ВидеоHow to create a smart contract on Waves Platform - Part 2: Setting a smart accountCryptin.eu
ВидеоCODE SPRINT: Creating the smart contract on WavesVladimir Zhuravlev

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.