andromeda_std/amp/
messages.rs

1use crate::ado_contract::ADOContract;
2use crate::common::encode_binary;
3use crate::error::ContractError;
4use crate::os::aos_querier::AOSQuerier;
5use crate::os::{kernel::ExecuteMsg as KernelExecuteMsg, kernel::QueryMsg as KernelQueryMsg};
6use cosmwasm_schema::cw_serde;
7use cosmwasm_std::{
8    to_json_binary, wasm_execute, Addr, Binary, Coin, ContractInfoResponse, CosmosMsg, Deps, Empty,
9    MessageInfo, QueryRequest, ReplyOn, SubMsg, WasmMsg, WasmQuery,
10};
11
12use super::addresses::AndrAddr;
13use super::ADO_DB_KEY;
14
15/// Exposed for ease of serialisation.
16#[cw_serde]
17pub enum ExecuteMsg {
18    /// The common message enum to receive an AMP message within a contract.
19    #[serde(rename = "amp_receive")]
20    AMPReceive(AMPPkt),
21}
22
23#[cw_serde]
24#[derive(Default)]
25pub struct IBCConfig {
26    pub recovery_addr: Option<AndrAddr>,
27}
28
29impl IBCConfig {
30    #[inline]
31    pub fn new(recovery_addr: Option<AndrAddr>) -> IBCConfig {
32        IBCConfig { recovery_addr }
33    }
34}
35
36/// The configuration of the message to be sent.
37///
38/// Used when a sub message is generated for the given AMP Msg (only used in the case of Wasm Messages).
39#[cw_serde]
40pub struct AMPMsgConfig {
41    /// When the message should reply, defaults to Always
42    pub reply_on: ReplyOn,
43    /// Determines whether the operation should terminate or proceed upon a failed message
44    pub exit_at_error: bool,
45    /// An optional imposed gas limit for the message
46    pub gas_limit: Option<u64>,
47    /// Whether to send the message directly to the given recipient
48    pub direct: bool,
49    pub ibc_config: Option<IBCConfig>,
50}
51
52impl AMPMsgConfig {
53    #[inline]
54    pub fn new(
55        reply_on: Option<ReplyOn>,
56        exit_at_error: Option<bool>,
57        gas_limit: Option<u64>,
58        ibc_config: Option<IBCConfig>,
59    ) -> AMPMsgConfig {
60        AMPMsgConfig {
61            reply_on: reply_on.unwrap_or(ReplyOn::Always),
62            exit_at_error: exit_at_error.unwrap_or(true),
63            gas_limit,
64            direct: false,
65            ibc_config,
66        }
67    }
68
69    /// Converts the current AMP message to be a direct message to the given contract
70    pub fn as_direct_msg(self) -> AMPMsgConfig {
71        AMPMsgConfig {
72            reply_on: self.reply_on,
73            exit_at_error: self.exit_at_error,
74            gas_limit: self.gas_limit,
75            direct: true,
76            ibc_config: self.ibc_config,
77        }
78    }
79}
80
81impl Default for AMPMsgConfig {
82    #[inline]
83    fn default() -> AMPMsgConfig {
84        AMPMsgConfig {
85            reply_on: ReplyOn::Always,
86            exit_at_error: true,
87            gas_limit: None,
88            direct: false,
89            ibc_config: None,
90        }
91    }
92}
93
94#[cw_serde]
95/// This struct defines how the kernel parses and relays messages between ADOs
96/// If the desired recipient is via IBC then namespacing must be employed
97/// The attached message must be a binary encoded execute message for the receiving ADO
98/// Funds can be attached for an individual message and will be attached accordingly
99pub struct AMPMsg {
100    /// The message recipient, can be a contract/wallet address or a namespaced URI
101    pub recipient: AndrAddr,
102    /// The message to be sent to the recipient
103    pub message: Binary,
104    /// Any funds to be attached to the message, defaults to an empty vector
105    pub funds: Vec<Coin>,
106    /// When the message should reply, defaults to Always
107    pub config: AMPMsgConfig,
108}
109
110impl AMPMsg {
111    /// Creates a new AMPMsg
112    pub fn new(recipient: impl Into<String>, message: Binary, funds: Option<Vec<Coin>>) -> AMPMsg {
113        AMPMsg {
114            recipient: AndrAddr::from_string(recipient),
115            message,
116            funds: funds.unwrap_or_default(),
117            config: AMPMsgConfig::default(),
118        }
119    }
120
121    pub fn with_config(&self, config: AMPMsgConfig) -> AMPMsg {
122        AMPMsg {
123            recipient: self.recipient.clone(),
124            message: self.message.clone(),
125            funds: self.funds.clone(),
126            config,
127        }
128    }
129
130    /// Generates an AMPPkt containing the given AMPMsg
131    pub fn generate_amp_pkt(
132        &self,
133        deps: &Deps,
134        origin: impl Into<String>,
135        previous_sender: impl Into<String>,
136        id: u64,
137    ) -> Result<SubMsg, ContractError> {
138        let contract_addr = self.recipient.get_raw_address(deps)?;
139        let pkt = AMPPkt::new(origin, previous_sender, vec![self.clone()]);
140        let msg = to_json_binary(&ExecuteMsg::AMPReceive(pkt))?;
141        Ok(SubMsg {
142            id,
143            reply_on: self.config.reply_on.clone(),
144            gas_limit: self.config.gas_limit,
145            msg: CosmosMsg::Wasm(WasmMsg::Execute {
146                contract_addr: contract_addr.into(),
147                msg,
148                funds: self.funds.to_vec(),
149            }),
150        })
151    }
152
153    pub fn generate_sub_msg_direct(&self, addr: Addr, id: u64) -> SubMsg<Empty> {
154        SubMsg {
155            id,
156            reply_on: self.config.reply_on.clone(),
157            gas_limit: self.config.gas_limit,
158            msg: CosmosMsg::Wasm(wasm_execute(addr, &self.message, self.funds.to_vec()).unwrap()),
159        }
160    }
161
162    pub fn to_ibc_hooks_memo(&self, contract_addr: String, callback_addr: String) -> String {
163        #[derive(::serde::Serialize)]
164        struct IbcHooksWasmMsg<T: ::serde::Serialize> {
165            contract: String,
166            msg: T,
167        }
168        #[derive(::serde::Serialize)]
169        struct IbcHooksMsg<T: ::serde::Serialize> {
170            wasm: IbcHooksWasmMsg<T>,
171            ibc_callback: String,
172        }
173        let wasm_msg = IbcHooksWasmMsg {
174            contract: contract_addr,
175            msg: KernelExecuteMsg::Send {
176                message: self.clone(),
177            },
178        };
179        let msg = IbcHooksMsg {
180            wasm: wasm_msg,
181            ibc_callback: callback_addr,
182        };
183
184        serde_json_wasm::to_string(&msg).unwrap()
185    }
186
187    /// Adds an IBC recovery address to the message
188    pub fn with_ibc_recovery(&self, recovery_addr: Option<AndrAddr>) -> AMPMsg {
189        if let Some(ibc_config) = self.config.ibc_config.clone() {
190            let mut ibc_config = ibc_config;
191            ibc_config.recovery_addr = recovery_addr;
192            let mut msg = self.clone();
193            msg.config.ibc_config = Some(ibc_config);
194            msg
195        } else if let Some(recovery_addr) = recovery_addr {
196            let ibc_config = Some(IBCConfig {
197                recovery_addr: Some(recovery_addr),
198            });
199            let mut msg = self.clone();
200            msg.config.ibc_config = ibc_config;
201            msg
202        } else {
203            self.clone()
204        }
205    }
206}
207
208#[cw_serde]
209pub struct AMPCtx {
210    origin: String,
211    origin_username: Option<AndrAddr>,
212    pub previous_sender: String,
213    pub id: u64,
214}
215
216impl AMPCtx {
217    #[inline]
218    pub fn new(
219        origin: impl Into<String>,
220        previous_sender: impl Into<String>,
221        id: u64,
222        origin_username: Option<AndrAddr>,
223    ) -> AMPCtx {
224        AMPCtx {
225            origin: origin.into(),
226            origin_username,
227            previous_sender: previous_sender.into(),
228            id,
229        }
230    }
231
232    /// Gets the original sender of a message
233    pub fn get_origin(&self) -> String {
234        self.origin.clone()
235    }
236
237    /// Gets the previous sender of a message
238    pub fn get_previous_sender(&self) -> String {
239        self.previous_sender.clone()
240    }
241}
242
243#[cw_serde]
244/// An Andromeda packet contains all message protocol related data, this is what is sent between ADOs when communicating
245/// It contains an original sender, if used for authorisation the sender must be authorised
246/// The previous sender is the one who sent the message
247/// A packet may contain several messages which allows for message batching
248
249pub struct AMPPkt {
250    /// Any messages associated with the packet
251    pub messages: Vec<AMPMsg>,
252    pub ctx: AMPCtx,
253}
254
255impl AMPPkt {
256    /// Creates a new AMP Packet
257    pub fn new(
258        origin: impl Into<String>,
259        previous_sender: impl Into<String>,
260        messages: Vec<AMPMsg>,
261    ) -> AMPPkt {
262        AMPPkt {
263            messages,
264            ctx: AMPCtx::new(origin, previous_sender, 0, None),
265        }
266    }
267
268    /// Adds a message to the current AMP Packet
269    pub fn add_message(mut self, message: AMPMsg) -> Self {
270        self.messages.push(message);
271        self
272    }
273
274    /// Gets all unique recipients for messages
275    pub fn get_unique_recipients(&self) -> Vec<String> {
276        let mut recipients: Vec<String> = self
277            .messages
278            .iter()
279            .cloned()
280            .map(|msg| msg.recipient.to_string())
281            .collect();
282        recipients.sort_unstable();
283        recipients.dedup();
284        recipients
285    }
286
287    /// Gets all messages for a given recipient
288    pub fn get_messages_for_recipient(&self, recipient: String) -> Vec<AMPMsg> {
289        self.messages
290            .iter()
291            .cloned()
292            .filter(|msg| msg.recipient == recipient.clone())
293            .collect()
294    }
295
296    /// Used to verify that the sender of the AMPPkt is authorised to attach the given origin field.
297    /// A sender is valid if:
298    ///
299    /// 1. The origin matches the sender
300    /// 2. The sender is the kernel
301    /// 3. The sender has a code ID stored within the ADODB (and as such is a valid ADO)
302    ///
303    /// If the sender is not valid, an error is returned
304    pub fn verify_origin(&self, info: &MessageInfo, deps: &Deps) -> Result<(), ContractError> {
305        let kernel_address = ADOContract::default().get_kernel_address(deps.storage)?;
306        if info.sender == self.ctx.origin || info.sender == kernel_address {
307            Ok(())
308        } else {
309            let adodb_address: Addr =
310                deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
311                    contract_addr: kernel_address.to_string(),
312                    msg: to_json_binary(&KernelQueryMsg::KeyAddress {
313                        key: ADO_DB_KEY.to_string(),
314                    })?,
315                }))?;
316
317            // Get the sender's Code ID
318            let contract_info: ContractInfoResponse =
319                deps.querier
320                    .query(&QueryRequest::Wasm(WasmQuery::ContractInfo {
321                        contract_addr: info.sender.to_string(),
322                    }))?;
323
324            let sender_code_id = contract_info.code_id;
325
326            // We query the ADO type in the adodb, it will return an error if the sender's Code ID doesn't exist.
327            AOSQuerier::verify_code_id(&deps.querier, &adodb_address, sender_code_id)
328        }
329    }
330
331    ///Verifies the origin of the AMPPkt and returns the origin if it is valid
332    pub fn get_verified_origin(
333        &self,
334        info: &MessageInfo,
335        deps: &Deps,
336    ) -> Result<String, ContractError> {
337        let origin = self.ctx.get_origin();
338        let res = self.verify_origin(info, deps);
339        match res {
340            Ok(_) => Ok(origin),
341            Err(err) => Err(err),
342        }
343    }
344
345    /// Generates a SubMsg to send the AMPPkt to the kernel
346
347    pub fn to_sub_msg(
348        &self,
349        address: impl Into<String>,
350        funds: Option<Vec<Coin>>,
351        id: u64,
352    ) -> Result<SubMsg, ContractError> {
353        let sub_msg = SubMsg::reply_always(
354            WasmMsg::Execute {
355                contract_addr: address.into(),
356                msg: encode_binary(&KernelExecuteMsg::AMPReceive(self.clone()))?,
357                funds: funds.unwrap_or_default(),
358            },
359            id,
360        );
361        Ok(sub_msg)
362    }
363
364    ///  Attaches an ID to the current packet
365    pub fn with_id(&self, id: u64) -> AMPPkt {
366        let mut new = self.clone();
367        new.ctx.id = id;
368        new
369    }
370
371    /// Converts a given AMP Packet to an IBC Hook memo for use with Osmosis' IBC Hooks module
372    pub fn to_ibc_hooks_memo(&self, contract_addr: String, callback_addr: String) -> String {
373        #[derive(::serde::Serialize)]
374        struct IbcHooksWasmMsg<T: ::serde::Serialize> {
375            contract: String,
376            msg: T,
377        }
378        #[derive(::serde::Serialize)]
379        struct IbcHooksMsg<T: ::serde::Serialize> {
380            wasm: IbcHooksWasmMsg<T>,
381            ibc_callback: String,
382        }
383        let wasm_msg = IbcHooksWasmMsg {
384            contract: contract_addr,
385            msg: KernelExecuteMsg::AMPReceive(self.clone()),
386        };
387        let msg = IbcHooksMsg {
388            wasm: wasm_msg,
389            ibc_callback: callback_addr,
390        };
391
392        serde_json_wasm::to_string(&msg).unwrap()
393    }
394
395    /// Serializes the given AMP Packet to a JSON string
396    pub fn to_json(&self) -> String {
397        serde_json_wasm::to_string(&self).unwrap()
398    }
399
400    /// Generates an AMP Packet from context
401    pub fn from_ctx(ctx: Option<AMPPkt>, current_address: String) -> Self {
402        let mut ctx = if let Some(pkt) = ctx {
403            pkt.ctx
404        } else {
405            AMPCtx::new(current_address.clone(), current_address.clone(), 0, None)
406        };
407        ctx.previous_sender = current_address;
408
409        Self {
410            messages: vec![],
411            ctx,
412        }
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use cosmwasm_std::testing::{mock_dependencies, mock_info};
419
420    use crate::testing::mock_querier::{mock_dependencies_custom, INVALID_CONTRACT};
421
422    use super::*;
423
424    #[test]
425    fn test_generate_amp_pkt() {
426        let deps = mock_dependencies();
427        let msg = AMPMsg::new("test", Binary::default(), None);
428
429        let sub_msg = msg
430            .generate_amp_pkt(&deps.as_ref(), "origin", "previoussender", 1)
431            .unwrap();
432
433        let expected_msg = ExecuteMsg::AMPReceive(AMPPkt::new(
434            "origin",
435            "previoussender",
436            vec![AMPMsg::new("test", Binary::default(), None)],
437        ));
438        assert_eq!(sub_msg.id, 1);
439        assert_eq!(sub_msg.reply_on, ReplyOn::Always);
440        assert_eq!(sub_msg.gas_limit, None);
441        assert_eq!(
442            sub_msg.msg,
443            CosmosMsg::Wasm(WasmMsg::Execute {
444                contract_addr: "test".to_string(),
445                msg: to_json_binary(&expected_msg).unwrap(),
446                funds: vec![],
447            })
448        );
449    }
450
451    #[test]
452    fn test_get_unique_recipients() {
453        let msg = AMPMsg::new("test", Binary::default(), None);
454        let msg2 = AMPMsg::new("test2", Binary::default(), None);
455
456        let mut pkt = AMPPkt::new("origin", "previoussender", vec![msg, msg2]);
457
458        let recipients = pkt.get_unique_recipients();
459        assert_eq!(recipients.len(), 2);
460        assert_eq!(recipients[0], "test".to_string());
461        assert_eq!(recipients[1], "test2".to_string());
462
463        pkt = pkt.add_message(AMPMsg::new("test", Binary::default(), None));
464        let recipients = pkt.get_unique_recipients();
465        assert_eq!(recipients.len(), 2);
466        assert_eq!(recipients[0], "test".to_string());
467        assert_eq!(recipients[1], "test2".to_string());
468    }
469
470    #[test]
471    fn test_get_messages_for_recipient() {
472        let msg = AMPMsg::new("test", Binary::default(), None);
473        let msg2 = AMPMsg::new("test2", Binary::default(), None);
474
475        let mut pkt = AMPPkt::new("origin", "previoussender", vec![msg, msg2]);
476
477        let messages = pkt.get_messages_for_recipient("test".to_string());
478        assert_eq!(messages.len(), 1);
479        assert_eq!(messages[0].recipient.to_string(), "test".to_string());
480
481        let messages = pkt.get_messages_for_recipient("test2".to_string());
482        assert_eq!(messages.len(), 1);
483        assert_eq!(messages[0].recipient.to_string(), "test2".to_string());
484
485        pkt = pkt.add_message(AMPMsg::new("test", Binary::default(), None));
486        let messages = pkt.get_messages_for_recipient("test".to_string());
487        assert_eq!(messages.len(), 2);
488        assert_eq!(messages[0].recipient.to_string(), "test".to_string());
489        assert_eq!(messages[1].recipient.to_string(), "test".to_string());
490    }
491
492    #[test]
493    fn test_verify_origin() {
494        let deps = mock_dependencies_custom(&[]);
495        let msg = AMPMsg::new("test", Binary::default(), None);
496
497        let pkt = AMPPkt::new("origin", "previoussender", vec![msg.clone()]);
498
499        let info = mock_info("validaddress", &[]);
500        let res = pkt.verify_origin(&info, &deps.as_ref());
501        assert!(res.is_ok());
502
503        let info = mock_info(INVALID_CONTRACT, &[]);
504        let res = pkt.verify_origin(&info, &deps.as_ref());
505        assert!(res.is_err());
506
507        let offchain_pkt = AMPPkt::new(INVALID_CONTRACT, INVALID_CONTRACT, vec![msg]);
508        let res = offchain_pkt.verify_origin(&info, &deps.as_ref());
509        assert!(res.is_ok());
510    }
511
512    #[test]
513    fn test_to_sub_msg() {
514        let msg = AMPMsg::new("test", Binary::default(), None);
515
516        let pkt = AMPPkt::new("origin", "previoussender", vec![msg.clone()]);
517
518        let sub_msg = pkt.to_sub_msg("kernel", None, 1).unwrap();
519
520        let expected_msg =
521            ExecuteMsg::AMPReceive(AMPPkt::new("origin", "previoussender", vec![msg]));
522        assert_eq!(sub_msg.id, 1);
523        assert_eq!(sub_msg.reply_on, ReplyOn::Always);
524        assert_eq!(sub_msg.gas_limit, None);
525        assert_eq!(
526            sub_msg.msg,
527            CosmosMsg::Wasm(WasmMsg::Execute {
528                contract_addr: "kernel".to_string(),
529                msg: to_json_binary(&expected_msg).unwrap(),
530                funds: vec![],
531            })
532        );
533    }
534    #[test]
535    fn test_to_json() {
536        let msg = AMPPkt::new("origin", "previoussender", vec![]);
537
538        let memo = msg.to_json();
539        assert_eq!(memo, "{\"messages\":[],\"ctx\":{\"origin\":\"origin\",\"origin_username\":null,\"previous_sender\":\"previoussender\",\"id\":0}}".to_string());
540    }
541
542    #[test]
543    fn test_to_ibc_hooks_memo() {
544        let msg = AMPPkt::new("origin", "previoussender", vec![]);
545        let contract_addr = "contractaddr";
546        let memo = msg.to_ibc_hooks_memo(contract_addr.to_string(), "callback".to_string());
547        assert_eq!(memo, "{\"wasm\":{\"contract\":\"contractaddr\",\"msg\":{\"amp_receive\":{\"messages\":[],\"ctx\":{\"origin\":\"origin\",\"origin_username\":null,\"previous_sender\":\"previoussender\",\"id\":0}}}},\"ibc_callback\":\"callback\"}".to_string());
548    }
549}