Skip to main content

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