Как обрабатывать исключения в перекрестных контрактах 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

Другие вопросы по тегам