defuse/tokens/
mod.rs

1#[cfg(feature = "imt")]
2pub mod imt;
3pub mod nep141;
4pub mod nep171;
5pub mod nep245;
6
7use core::{
8    fmt::{self, Debug, Display},
9    str::FromStr,
10};
11
12use defuse_core::{intents::tokens::NotifyOnTransfer, payload::multi::MultiPayload};
13use defuse_near_utils::UnwrapOrPanicError;
14use near_sdk::{AccountId, account_id::ParseAccountError, near, serde_json};
15use thiserror::Error as ThisError;
16
17#[must_use]
18#[near(serializers = [json])]
19#[derive(Debug, Clone)]
20pub struct DepositMessage {
21    pub receiver_id: AccountId,
22
23    #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
24    pub action: Option<DepositAction>,
25}
26
27impl DepositMessage {
28    #[inline]
29    pub const fn new(receiver_id: AccountId) -> Self {
30        Self {
31            receiver_id,
32            action: None,
33        }
34    }
35
36    #[inline]
37    pub fn with_action(mut self, action: impl Into<Option<DepositAction>>) -> Self {
38        self.action = action.into();
39        self
40    }
41}
42
43impl Display for DepositMessage {
44    #[inline]
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match &self.action {
47            None => f.write_str(self.receiver_id.as_str()),
48            Some(DepositAction::Execute(exec)) if exec.execute_intents.is_empty() => {
49                f.write_str(self.receiver_id.as_str())
50            }
51            Some(_) => f.write_str(&serde_json::to_string(self).unwrap_or_panic_display()),
52        }
53    }
54}
55
56impl FromStr for DepositMessage {
57    type Err = ParseDepositMessageError;
58
59    #[inline]
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        if s.starts_with('{') {
62            serde_json::from_str(s).map_err(Into::into)
63        } else {
64            s.parse().map(Self::new).map_err(Into::into)
65        }
66    }
67}
68
69#[must_use]
70#[near(serializers = [json])]
71#[serde(untagged)]
72#[derive(Debug, Clone)]
73pub enum DepositAction {
74    Execute(ExecuteIntents),
75    Notify(NotifyOnTransfer),
76}
77
78#[must_use]
79#[near(serializers = [json])]
80#[derive(Debug, Clone)]
81pub struct ExecuteIntents {
82    pub execute_intents: Vec<MultiPayload>,
83
84    #[serde(default, skip_serializing_if = "::core::ops::Not::not")]
85    pub refund_if_fails: bool,
86}
87
88#[derive(Debug, ThisError)]
89pub enum ParseDepositMessageError {
90    #[error(transparent)]
91    Account(#[from] ParseAccountError),
92    #[error("JSON: {0}")]
93    JSON(#[from] serde_json::Error),
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_deserialize_simple() {
102        // Simple format: just receiver_id
103        let json = r#"{"receiver_id": "alice.near"}"#;
104        let msg: DepositMessage = serde_json::from_str(json).unwrap();
105
106        assert_eq!(msg.receiver_id.as_str(), "alice.near");
107        assert!(msg.action.is_none());
108    }
109
110    #[test]
111    fn test_deserialize_with_notify() {
112        // With notify action (flattened untagged)
113        let json = r#"{
114            "receiver_id": "alice.near",
115            "msg": "hello world",
116            "min_gas": null
117        }"#;
118        let msg: DepositMessage = serde_json::from_str(json).unwrap();
119
120        assert_eq!(msg.receiver_id.as_str(), "alice.near");
121        match msg.action {
122            Some(DepositAction::Notify(notify)) => {
123                assert_eq!(notify.msg, "hello world");
124                assert!(notify.min_gas.is_none());
125            }
126            _ => panic!("Expected Notify action"),
127        }
128    }
129
130    #[test]
131    fn test_deserialize_with_execute() {
132        // With execute action (flattened untagged)
133        let json = r#"{
134            "receiver_id": "alice.near",
135            "execute_intents": [],
136            "refund_if_fails": true
137        }"#;
138        let msg: DepositMessage = serde_json::from_str(json).unwrap();
139
140        assert_eq!(msg.receiver_id.as_str(), "alice.near");
141        match msg.action {
142            Some(DepositAction::Execute(exec)) => {
143                assert!(exec.execute_intents.is_empty());
144                assert!(exec.refund_if_fails);
145            }
146            _ => panic!("Expected Execute action"),
147        }
148    }
149
150    #[test]
151    fn test_serialize_simple() {
152        // Simple message serialization
153        let msg = DepositMessage::new("alice.near".parse().unwrap());
154        let json = serde_json::to_string(&msg).unwrap();
155
156        // Should serialize with just receiver_id (action omitted when None)
157        assert!(json.contains("\"receiver_id\":\"alice.near\""));
158        assert!(!json.contains("action"));
159    }
160
161    #[test]
162    fn test_serialize_with_notify() {
163        // Serialization with notify action
164        let msg = DepositMessage {
165            receiver_id: "alice.near".parse().unwrap(),
166            action: Some(DepositAction::Notify(NotifyOnTransfer::new(
167                "hello".to_string(),
168            ))),
169        };
170        let json = serde_json::to_string(&msg).unwrap();
171
172        // Should serialize with flattened notify fields
173        assert!(json.contains("\"receiver_id\":\"alice.near\""));
174        assert!(json.contains("\"msg\":\"hello\""));
175    }
176
177    #[test]
178    fn test_serialize_with_execute() {
179        // Serialization with execute action
180        let msg = DepositMessage {
181            receiver_id: "alice.near".parse().unwrap(),
182            action: Some(DepositAction::Execute(ExecuteIntents {
183                execute_intents: vec![],
184                refund_if_fails: true,
185            })),
186        };
187        let json = serde_json::to_string(&msg).unwrap();
188
189        // Should serialize with flattened execute fields
190        assert!(json.contains("\"receiver_id\":\"alice.near\""));
191        assert!(json.contains("\"execute_intents\""));
192        assert!(json.contains("\"refund_if_fails\":true"));
193    }
194
195    #[test]
196    fn test_display_simple() {
197        // Display for simple message (just account ID)
198        let msg = DepositMessage::new("alice.near".parse().unwrap());
199        assert_eq!(msg.to_string(), "alice.near");
200    }
201
202    #[test]
203    fn test_display_with_action() {
204        // Display for message with action (should be JSON)
205        let msg = DepositMessage {
206            receiver_id: "alice.near".parse().unwrap(),
207            action: Some(DepositAction::Notify(NotifyOnTransfer::new(
208                "test".to_string(),
209            ))),
210        };
211        let display = msg.to_string();
212
213        assert!(display.starts_with('{'));
214        assert!(display.contains("alice.near"));
215    }
216
217    #[test]
218    fn test_from_str_simple() {
219        // Parse simple account ID
220        let msg: DepositMessage = "alice.near".parse().unwrap();
221        assert_eq!(msg.receiver_id.as_str(), "alice.near");
222        assert!(msg.action.is_none());
223    }
224
225    #[test]
226    fn test_from_str_json_with_execute() {
227        // Parse JSON with execute intents
228        let json = r#"{"receiver_id":"alice.near","execute_intents":[],"refund_if_fails":true}"#;
229        let msg: DepositMessage = json.parse().unwrap();
230
231        assert_eq!(msg.receiver_id.as_str(), "alice.near");
232        match msg.action {
233            Some(DepositAction::Execute(exec)) => {
234                assert!(exec.execute_intents.is_empty());
235                assert!(exec.refund_if_fails);
236            }
237            _ => panic!("Expected Execute action"),
238        }
239    }
240
241    #[test]
242    fn test_from_str_json_with_notify() {
243        // Parse JSON with notify action
244        let json = r#"{"receiver_id":"alice.near","msg":"test"}"#;
245        let msg: DepositMessage = json.parse().unwrap();
246
247        assert_eq!(msg.receiver_id.as_str(), "alice.near");
248        match msg.action {
249            Some(DepositAction::Notify(notify)) => {
250                assert_eq!(notify.msg, "test");
251            }
252            _ => panic!("Expected Notify action"),
253        }
254    }
255
256    #[test]
257    fn test_deserialize_execute_takes_precedence_when_both_fields_present() {
258        // When both execute_intents and msg are present, Execute variant should be matched first
259        // since it comes first in the untagged enum
260        let json = r#"{
261            "receiver_id": "alice.near",
262            "execute_intents": [],
263            "refund_if_fails": true,
264            "msg": "this should be ignored"
265        }"#;
266        let deposit_msg: DepositMessage = serde_json::from_str(json).unwrap();
267
268        assert_eq!(deposit_msg.receiver_id.as_str(), "alice.near");
269        match deposit_msg.action {
270            Some(DepositAction::Execute(exec)) => {
271                assert!(exec.execute_intents.is_empty());
272                assert!(exec.refund_if_fails);
273            }
274            Some(DepositAction::Notify(_)) => {
275                panic!("Expected Execute action, got Notify instead");
276            }
277            None => panic!("Expected Execute action, got None"),
278        }
279    }
280
281    #[test]
282    fn test_builder_methods() {
283        // Test direct construction
284        let msg = DepositMessage {
285            receiver_id: "alice.near".parse().unwrap(),
286            action: Some(DepositAction::Execute(ExecuteIntents {
287                execute_intents: vec![],
288                refund_if_fails: true,
289            })),
290        };
291
292        assert_eq!(msg.receiver_id.as_str(), "alice.near");
293        match msg.action {
294            Some(DepositAction::Execute(exec)) => {
295                assert!(exec.refund_if_fails);
296            }
297            _ => panic!("Expected Execute action"),
298        }
299    }
300
301    #[test]
302    fn test_builder_with_notify() {
303        // Test direct construction with notify
304        let msg = DepositMessage {
305            receiver_id: "alice.near".parse().unwrap(),
306            action: Some(DepositAction::Notify(NotifyOnTransfer::new(
307                "test".to_string(),
308            ))),
309        };
310
311        assert_eq!(msg.receiver_id.as_str(), "alice.near");
312        assert!(matches!(msg.action, Some(DepositAction::Notify(_))));
313    }
314}