polytone_evm/
callbacks.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{
3    to_json_binary, Addr, Api, Binary, CosmosMsg, HexBinary, IbcPacketAckMsg, IbcPacketTimeoutMsg,
4    StdResult, Storage, Uint64, WasmMsg,
5};
6use cw_storage_plus::Map;
7
8use crate::{
9    ack::Ack,
10    ibc::{ExecuteResponsePacket, ExecuteResult},
11};
12
13/// A request for a callback.
14#[cw_serde]
15pub struct CallbackRequest {
16    pub receiver: String,
17    pub msg: Binary,
18}
19
20/// Executed on the callback receiver upon message completion. When
21/// being executed, the message will be tagged with "callback":
22///
23/// ```json
24/// {"callback": {
25///       "initiator": ...,
26///       "initiator_msg": ...,
27///       "result": ...,
28/// }}
29/// ```
30#[cw_serde]
31pub struct CallbackMessage {
32    /// Initaitor on the note chain.
33    pub initiator: Addr,
34    /// Message sent by the initaitor. This _must_ be base64 encoded
35    /// or execution will fail.
36    pub initiator_msg: Binary,
37    /// Data from the host chain.
38    pub result: Callback,
39}
40
41#[cw_serde]
42pub enum Callback {
43    /// Result of executing the requested messages, or an error.
44    ///
45    /// 14/04/23: if a submessage errors the reply handler can see
46    /// `codespace: wasm, code: 5`, but not the actual error. as a
47    /// result, we can't return good errors for Execution and this
48    /// error string will only tell you the error's codespace. for
49    /// example, an out-of-gas error is code 11 and looks like
50    /// `codespace: sdk, code: 11`.
51    Execute(Result<ExecutionResponse, String>),
52
53    /// An error occured that could not be recovered from. The only
54    /// known way that this can occur is message handling running out
55    /// of gas, in which case the error will be `codespace: sdk, code:
56    /// 11`.
57    ///
58    /// This error is not named because it could also occur due to a
59    /// panic or unhandled error during message processing. We don't
60    /// expect this to happen and have carefully written the code to
61    /// avoid it.
62    FatalError(String),
63}
64
65#[cw_serde]
66pub struct ExecutionResponse {
67    /// The address on the remote chain that executed the messages.
68    pub executed_by: String,
69    /// Index `i` corresponds to the result of executing the `i`th
70    /// message.
71    pub result: Vec<ExecuteResult>,
72}
73
74#[cw_serde]
75pub struct ErrorResponse {
76    /// The index of the first message who's execution failed.
77    pub message_index: Uint64,
78    /// The error that occured executing the message.
79    pub error: String,
80}
81
82/// Disembiguates between a callback for remote message execution and
83/// queries.
84#[cw_serde]
85pub enum CallbackRequestType {
86    Execute,
87    // Query,
88}
89
90/// Requests that a callback be returned for the IBC message
91/// identified by `(channel_id, sequence_number)`.
92pub fn request_callback(
93    storage: &mut dyn Storage,
94    api: &dyn Api,
95    channel_id: String,
96    sequence_number: u64,
97    initiator: Addr,
98    request: Option<CallbackRequest>,
99    request_type: CallbackRequestType,
100) -> StdResult<()> {
101    if let Some(request) = request {
102        let receiver = api.addr_validate(&request.receiver)?;
103        let initiator_msg = request.msg;
104
105        CALLBACKS.save(
106            storage,
107            (channel_id, sequence_number),
108            &PendingCallback {
109                initiator,
110                initiator_msg,
111                receiver,
112                request_type,
113            },
114        )?;
115    }
116
117    Ok(())
118}
119
120const ACK_SUCCESS: u8 = 0;
121#[allow(unused)]
122const ACK_FAILURE: u8 = 1;
123
124/// Call on every packet ACK. Returns a callback message to execute,
125/// if any, and the address that executed the request on the remote
126/// chain (the message initiator's remote account), if any.
127///
128/// (storage, ack) -> (callback, executed_by)
129pub fn on_ack(
130    storage: &mut dyn Storage,
131    // TODO: Update with enum response type that supports queries here in future
132    IbcPacketAckMsg {
133        acknowledgement,
134        original_packet,
135        ..
136    }: &IbcPacketAckMsg,
137) -> (Option<CosmosMsg>, Option<String>, Ack) {
138    let (ack_res, ack_res_data) = acknowledgement.data.split_at(1);
139
140    let mut executed_by = None;
141
142    let maybe_response = ExecuteResponsePacket::decode_bytes(ack_res_data);
143    let result: Ack = match maybe_response {
144        Ok(decoded) => {
145            executed_by = match &decoded {
146                ExecuteResponsePacket { executed_by, .. } => Some(executed_by),
147            }
148            .cloned();
149            Ack::Execute(Ok(ExecutionResponse {
150                // TODO: fix unwrap
151                executed_by: executed_by.clone().unwrap(),
152                result: decoded.result,
153            }))
154        }
155        Err(err) => {
156            if ack_res[0] != ACK_SUCCESS {
157                Ack::FatalError(HexBinary::from(ack_res_data).to_string())
158            } else {
159                Ack::Execute(Err(err.to_string()))
160            }
161        }
162    };
163
164    let callback_message = dequeue_callback(
165        storage,
166        original_packet.src.channel_id.clone(),
167        original_packet.sequence,
168    )
169    .map(|request| callback_message(request, result.clone()));
170
171    (callback_message, executed_by, result)
172}
173
174/// Call on every packet timeout. Returns a callback message to execute,
175/// if any.
176pub fn on_timeout(
177    storage: &mut dyn Storage,
178    IbcPacketTimeoutMsg { packet, .. }: &IbcPacketTimeoutMsg,
179) -> Option<CosmosMsg> {
180    let request = dequeue_callback(storage, packet.src.channel_id.clone(), packet.sequence)?;
181    let timeout = "timeout".to_string();
182    let result = match request.request_type {
183        CallbackRequestType::Execute => Callback::Execute(Err(timeout)),
184        // CallbackRequestType::Query => Callback::Query(Err(ErrorResponse {
185        //     message_index: Uint64::zero(),
186        //     error: timeout,
187        // })),
188    };
189    Some(callback_message(request, result))
190}
191
192fn callback_message(request: PendingCallback, result: Callback) -> CosmosMsg {
193    /// Gives the executed message a "callback" tag:
194    /// `{ "callback": CallbackMsg }`.
195    #[cw_serde]
196    enum C {
197        Callback(CallbackMessage),
198    }
199    WasmMsg::Execute {
200        contract_addr: request.receiver.into_string(),
201        msg: to_json_binary(&C::Callback(CallbackMessage {
202            initiator: request.initiator,
203            initiator_msg: request.initiator_msg,
204            result,
205        }))
206        .expect("fields are known to be serializable"),
207        funds: vec![],
208    }
209    .into()
210}
211
212fn dequeue_callback(
213    storage: &mut dyn Storage,
214    channel_id: String,
215    sequence_number: u64,
216) -> Option<PendingCallback> {
217    let request = CALLBACKS
218        .may_load(storage, (channel_id.clone(), sequence_number))
219        .unwrap()?;
220    CALLBACKS.remove(storage, (channel_id, sequence_number));
221    Some(request)
222}
223
224#[cw_serde]
225struct PendingCallback {
226    initiator: Addr,
227    initiator_msg: Binary,
228    /// The address that will receive the callback on completion.
229    receiver: Addr,
230    /// Used to return the appropriate callback type during timeouts.
231    request_type: CallbackRequestType,
232}
233
234/// (channel_id, sequence_number) -> callback
235const CALLBACKS: Map<(String, u64), PendingCallback> = Map::new("polytone-callbacks");