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