andromeda_std/amp/
recipient.rs

1use super::{addresses::AndrAddr, messages::AMPMsg};
2use crate::{ado_contract::ADOContract, common::encode_binary, error::ContractError};
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{to_json_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, SubMsg, WasmMsg};
5use cw20::{Cw20Coin, Cw20ExecuteMsg};
6use serde::Serialize;
7
8/// A simple struct used for inter-contract communication. The struct can be used in two ways:
9///
10/// 1. Simply just providing an `AndrAddr` which will treat the communication as a transfer of any related funds
11/// 2. Providing an `AndrAddr` and a `Binary` message which will be sent to the contract at the resolved address
12///
13/// The `Binary` message can be any message that the contract at the resolved address can handle.
14#[cw_serde]
15pub struct Recipient {
16    pub address: AndrAddr,
17    pub msg: Option<Binary>,
18    pub ibc_recovery_address: Option<AndrAddr>,
19}
20
21impl Recipient {
22    pub fn new(addr: impl Into<String>, msg: Option<Binary>) -> Recipient {
23        Recipient {
24            address: AndrAddr::from_string(addr),
25            msg,
26            ibc_recovery_address: None,
27        }
28    }
29
30    /// Validates a recipient by validating its address and recovery address (if it is provided)
31    pub fn validate(&self, deps: &Deps) -> Result<(), ContractError> {
32        self.address.validate(deps.api)?;
33        self.address.get_raw_address(deps)?;
34
35        // Validate the recovery address if it is providedReci
36        if let Some(ibc_recovery_address) = self.ibc_recovery_address.clone() {
37            ibc_recovery_address.validate(deps.api)?;
38            ibc_recovery_address.get_raw_address(deps)?;
39        }
40
41        Ok(())
42    }
43
44    /// Creates a Recipient from the given string with no attached message
45    pub fn from_string(addr: impl Into<String>) -> Recipient {
46        Recipient {
47            address: AndrAddr::from_string(addr.into()),
48            msg: None,
49            ibc_recovery_address: None,
50        }
51    }
52
53    pub fn get_addr(&self) -> String {
54        self.address.to_string()
55    }
56
57    pub fn get_message(&self) -> Option<Binary> {
58        self.msg.clone()
59    }
60
61    /// Generates a direct sub message for the given recipient.
62    pub fn generate_direct_msg(
63        &self,
64        deps: &Deps,
65        funds: Vec<Coin>,
66    ) -> Result<SubMsg, ContractError> {
67        let resolved_addr = self.address.get_raw_address(deps)?;
68        Ok(match &self.msg {
69            Some(message) => SubMsg::new(WasmMsg::Execute {
70                contract_addr: resolved_addr.to_string(),
71                msg: message.clone(),
72                funds,
73            }),
74            None => SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
75                to_address: resolved_addr.to_string(),
76                amount: funds,
77            })),
78        })
79    }
80
81    // TODO: Enable ICS20 messages? Maybe send approval for Kernel address then send the message to Kernel?
82    /// Generates a message to send a CW20 token to the recipient with the attached message.
83    ///
84    /// **Assumes the attached message is a valid CW20 Hook message for the receiving address**.
85    pub fn generate_msg_cw20(
86        &self,
87        deps: &Deps,
88        cw20_coin: Cw20Coin,
89    ) -> Result<SubMsg, ContractError> {
90        let resolved_addr = self.address.get_raw_address(deps)?;
91        Ok(match &self.msg {
92            Some(msg) => SubMsg::new(WasmMsg::Execute {
93                contract_addr: cw20_coin.address,
94                msg: encode_binary(&Cw20ExecuteMsg::Send {
95                    contract: resolved_addr.to_string(),
96                    amount: cw20_coin.amount,
97                    msg: msg.clone(),
98                })?,
99                funds: vec![],
100            }),
101            None => SubMsg::new(WasmMsg::Execute {
102                contract_addr: cw20_coin.address,
103                msg: encode_binary(&Cw20ExecuteMsg::Transfer {
104                    recipient: resolved_addr.to_string(),
105                    amount: cw20_coin.amount,
106                })?,
107                funds: vec![],
108            }),
109        })
110    }
111
112    /// Generates an AMP message from the given Recipient.
113    ///
114    /// This can be attached to an AMP Packet for execution via the aOS.
115    pub fn generate_amp_msg(
116        &self,
117        deps: &Deps,
118        funds: Option<Vec<Coin>>,
119    ) -> Result<AMPMsg, ContractError> {
120        let mut address = self.address.clone();
121        if address.is_local_path() {
122            let vfs_addr = ADOContract::default().get_vfs_address(deps.storage, &deps.querier)?;
123            address = address.local_path_to_vfs_path(deps.storage, &deps.querier, vfs_addr)?;
124        }
125        Ok(AMPMsg::new(
126            address.to_string(),
127            self.msg.clone().unwrap_or_default(),
128            funds,
129        )
130        .with_ibc_recovery(self.ibc_recovery_address.clone()))
131    }
132
133    /// Adds an IBC recovery address to the recipient
134    ///
135    /// This address can be used to recover any funds on failed IBC messages
136    pub fn with_ibc_recovery(self, addr: impl Into<String>) -> Self {
137        let mut new_recip = self;
138        new_recip.ibc_recovery_address = Some(AndrAddr::from_string(addr.into()));
139        new_recip
140    }
141
142    /// Adds a message to the recipient to be sent alongside any funds
143    pub fn with_msg(self, msg: impl Serialize) -> Self {
144        let mut new_recip = self;
145        new_recip.msg = Some(to_json_binary(&msg).unwrap());
146        new_recip
147    }
148}
149
150#[cfg(test)]
151mod test {
152    use cosmwasm_std::{from_json, testing::mock_dependencies, Addr, Uint128};
153
154    use crate::testing::mock_querier::{mock_dependencies_custom, MOCK_APP_CONTRACT};
155
156    use super::*;
157
158    #[test]
159    fn test_generate_direct_msg() {
160        let deps = mock_dependencies();
161        let recipient = Recipient::from_string("test");
162        let funds = vec![Coin {
163            denom: "test".to_string(),
164            amount: Uint128::from(100u128),
165        }];
166        let msg = recipient
167            .generate_direct_msg(&deps.as_ref(), funds.clone())
168            .unwrap();
169        match msg.msg {
170            CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => {
171                assert_eq!(to_address, "test");
172                assert_eq!(amount, funds);
173            }
174            _ => panic!("Unexpected message type"),
175        }
176
177        let recipient = Recipient::new("test", Some(Binary::from(b"test".to_vec())));
178        let msg = recipient
179            .generate_direct_msg(&deps.as_ref(), funds.clone())
180            .unwrap();
181        match msg.msg {
182            CosmosMsg::Wasm(WasmMsg::Execute {
183                contract_addr,
184                msg,
185                funds: msg_funds,
186            }) => {
187                assert_eq!(contract_addr, "test");
188                assert_eq!(msg, Binary::from(b"test".to_vec()));
189                assert_eq!(msg_funds, funds);
190            }
191            _ => panic!("Unexpected message type"),
192        }
193    }
194
195    #[test]
196    fn test_generate_msg_cw20() {
197        let deps = mock_dependencies();
198        let recipient = Recipient::from_string("test");
199        let cw20_coin = Cw20Coin {
200            address: "test".to_string(),
201            amount: Uint128::from(100u128),
202        };
203        let msg = recipient
204            .generate_msg_cw20(&deps.as_ref(), cw20_coin.clone())
205            .unwrap();
206        match msg.msg {
207            CosmosMsg::Wasm(WasmMsg::Execute {
208                contract_addr,
209                msg,
210                funds,
211            }) => {
212                assert_eq!(contract_addr, "test");
213                assert_eq!(funds, vec![] as Vec<Coin>);
214                match from_json(msg).unwrap() {
215                    Cw20ExecuteMsg::Transfer { recipient, amount } => {
216                        assert_eq!(recipient, "test");
217                        assert_eq!(amount, cw20_coin.amount);
218                    }
219                    _ => panic!("Unexpected message type"),
220                }
221            }
222            _ => panic!("Unexpected message type"),
223        }
224
225        let recipient = Recipient::new("test", Some(Binary::from(b"test".to_vec())));
226        let msg = recipient
227            .generate_msg_cw20(&deps.as_ref(), cw20_coin.clone())
228            .unwrap();
229        match msg.msg {
230            CosmosMsg::Wasm(WasmMsg::Execute {
231                contract_addr,
232                msg,
233                funds,
234            }) => {
235                assert_eq!(contract_addr, "test");
236                assert_eq!(funds, vec![] as Vec<Coin>);
237                match from_json(msg).unwrap() {
238                    Cw20ExecuteMsg::Send {
239                        contract,
240                        amount,
241                        msg: send_msg,
242                    } => {
243                        assert_eq!(contract, "test");
244                        assert_eq!(amount, cw20_coin.amount);
245                        assert_eq!(send_msg, Binary::from(b"test".to_vec()));
246                    }
247                    _ => panic!("Unexpected message type"),
248                }
249            }
250            _ => panic!("Unexpected message type"),
251        }
252    }
253
254    #[test]
255    fn test_generate_amp_msg() {
256        let recipient = Recipient::from_string("test");
257        let mut deps = mock_dependencies_custom(&[]);
258        let msg = recipient.generate_amp_msg(&deps.as_ref(), None).unwrap();
259        assert_eq!(msg.recipient, "test");
260        assert_eq!(msg.message, Binary::default());
261        assert_eq!(msg.funds, vec![] as Vec<Coin>);
262
263        let recipient = Recipient::new("test", Some(Binary::from(b"test".to_vec())));
264        let msg = recipient.generate_amp_msg(&deps.as_ref(), None).unwrap();
265        assert_eq!(msg.recipient, "test");
266        assert_eq!(msg.message, Binary::from(b"test".to_vec()));
267        assert_eq!(msg.funds, vec![] as Vec<Coin>);
268
269        let funds = vec![Coin {
270            denom: "test".to_string(),
271            amount: Uint128::from(100u128),
272        }];
273        let recipient = Recipient::from_string("test");
274        let msg = recipient
275            .generate_amp_msg(&deps.as_ref(), Some(funds.clone()))
276            .unwrap();
277        assert_eq!(msg.recipient, "test");
278        assert_eq!(msg.message, Binary::default());
279        assert_eq!(msg.funds, funds);
280
281        ADOContract::default()
282            .app_contract
283            .save(deps.as_mut().storage, &Addr::unchecked(MOCK_APP_CONTRACT))
284            .unwrap();
285        let recipient = Recipient::from_string("./test");
286        let msg = recipient
287            .generate_amp_msg(&deps.as_ref(), Some(funds.clone()))
288            .unwrap();
289        assert_eq!(
290            msg.recipient.to_string(),
291            format!("~{MOCK_APP_CONTRACT}/test")
292        );
293        assert_eq!(msg.message, Binary::default());
294        assert_eq!(msg.funds, funds);
295    }
296}