Как обрабатывать исключения в перекрестных контрактах NEAR?
Как я могу поймать и обработать исключение в цепочке асинхронных вызовов между контрактами?
Предположим, что моя транзакция инициирует следующие вызовы:
contractA.run()
-> do changes in contractA
-> calls contractB.run()
-> do changes in contractB
-> then calls another method on contractA: contractA.callback()
* callback() crashes
После исключения в обещании NEAR не откатывает изменения, внесенные в прошлые обещания. Я также не вижу никакого метода обработки исключений в near-sdk.
Одна из идей - возвращать ошибки вместо генерации исключений и создавать кучу частных функций для обновления состояния после значения ошибки и добавления / освобождения мьютексов. Однако это не решит проблему, иногда мы не можем это контролировать, например, во внешних смарт-контрактах (например, еслиcontractB.do
вызовет панику в приведенном выше примере).
1 ответ
Единственный способ поймать исключение - выполнить обратный вызов обещания, которое сгенерировало исключение.
В объясненном сценарии contractA.callback()
не должно вылетать. Вам нужно создать контракт достаточно осторожно, чтобы избежать сбоя при обратном вызове. В большинстве случаев это возможно, поскольку вы управляете вводом в обратный вызов и количеством присоединенного газа. Если обратный вызов не работает, это похоже на исключение в коде обработки исключений.
Также обратите внимание, что вы можете убедиться, что callback
запланировано правильно с достаточным количеством газа, подключенным в contractA.run()
. Если это не так и, например, у вас недостаточно газа, подключенного кrun
, планирование обратного вызова и другого обещания не удастся, и все состояние из run
изменения откатываются. Но однаждыrun
завершается, состояние меняется с run
привержены и callback
должен быть тщательно обработан.
У нас есть несколько мест в lockup
контракт, в котором обратный вызов может завершиться ошибкой: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/owner_callbacks.rs
А также большинство мест, где обратный вызов не дает сбоев: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/owner_callbacks.rs
Отметим, что есть некоторые ситуации, когда контракт не хочет полагаться на стабильность других контрактов, например, когда поток A --> B --> A --> B
. В этом случаеB
не может прикрепить обратный вызов к ресурсу, предоставленному A
. Для этих сценариев мы обсуждали возможность добавления конкретной конструкции, которая является атомарной и имеет разрешающий обратный вызов после удаления. Мы назвали этоSafe
: https://github.com/nearprotocol/NEPs/pull/26
РЕДАКТИРОВАТЬ
Что, если
contractB.run
не удается, и я хочу обновить состояние вcontractA
откатить изменения сcontractA.run
?
В этом случае contractA.callback()
все еще называется, но у него есть PromiseResult::Failed
для его зависимости contractB.run
.
Так callback()
может изменить состояние contractA
чтобы отменить изменения.
Например, обратный вызов из реализации контракта блокировки для обработки вывода из контракта пула ставок: https://github.com/near/core-contracts/blob/6fb13584d5c9eb1b372cfd80cd18f4a4ba8d15b6/lockup/src/foundation_callbacks.rs
Если мы адаптируем имена для соответствия примеру:
Контракт о блокировке (contractA
) пытается вывести средства (run()
) из пула ставок (contractB
), но средства все еще могут быть заблокированы из-за недавнего снятия ставок, поэтому вывод не удастся (contractB.run()
не удается). Обратный вызов называется (contractA.callback()
) и проверяет успешность обещания (из contractB.run
). Поскольку снятие средств не удалось, обратный вызов возвращает состояние обратно к исходному (восстанавливает состояние).
На самом деле это немного сложнее, потому что фактическая последовательность A.withdraw_all -> B.get_amount -> A.on_amount_for_withdraw -> B.withdraw(amount) -> A.on_withdraw