1use std::{borrow::Cow, collections::BTreeMap};
2
3use near_contract_standards::non_fungible_token;
4use near_sdk::{AccountId, AccountIdRef, CryptoHash, Gas, NearToken, json_types::U128, near};
5use serde_with::{DisplayFromStr, serde_as};
6
7use crate::{
8 DefuseError, Result,
9 accounts::AccountEvent,
10 amounts::Amounts,
11 engine::{Engine, Inspector, State},
12 events::DefuseEvent,
13};
14
15use super::{ExecutableIntent, IntentEvent};
16
17#[cfg_attr(
18 all(feature = "abi", not(target_arch = "wasm32")),
19 serde_as(schemars = true)
20)]
21#[cfg_attr(
22 not(all(feature = "abi", not(target_arch = "wasm32"))),
23 serde_as(schemars = false)
24)]
25#[near(serializers = [borsh, json])]
26#[derive(Debug, Clone)]
27pub struct Transfer {
29 pub receiver_id: AccountId,
30
31 #[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
32 pub tokens: Amounts,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub memo: Option<String>,
36}
37
38impl ExecutableIntent for Transfer {
39 fn execute_intent<S, I>(
40 self,
41 sender_id: &AccountIdRef,
42 engine: &mut Engine<S, I>,
43 intent_hash: CryptoHash,
44 ) -> Result<()>
45 where
46 S: State,
47 I: Inspector,
48 {
49 if sender_id == self.receiver_id || self.tokens.is_empty() {
50 return Err(DefuseError::InvalidIntent);
51 }
52
53 engine
54 .inspector
55 .on_event(DefuseEvent::Transfer(Cow::Borrowed(
56 [IntentEvent::new(
57 AccountEvent::new(sender_id, Cow::Borrowed(&self)),
58 intent_hash,
59 )]
60 .as_slice(),
61 )));
62
63 engine
64 .state
65 .internal_sub_balance(sender_id, self.tokens.clone())?;
66 engine
67 .state
68 .internal_add_balance(self.receiver_id, self.tokens)?;
69 Ok(())
70 }
71}
72
73#[near(serializers = [borsh, json])]
74#[derive(Debug, Clone)]
75pub struct FtWithdraw {
77 pub token: AccountId,
78 pub receiver_id: AccountId,
79 pub amount: U128,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub memo: Option<String>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub msg: Option<String>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub storage_deposit: Option<NearToken>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub min_gas: Option<Gas>,
103}
104
105impl FtWithdraw {
106 const FT_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(15);
107 const FT_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(15);
108
109 const FT_TRANSFER_CALL_GAS_MIN: Gas = Gas::from_tgas(30);
111 const FT_TRANSFER_CALL_GAS_DEFAULT: Gas = Gas::from_tgas(50);
112
113 #[inline]
115 pub const fn is_call(&self) -> bool {
116 self.msg.is_some()
117 }
118
119 #[inline]
121 pub fn min_gas(&self) -> Gas {
122 let (min, default) = if self.is_call() {
123 (
124 Self::FT_TRANSFER_CALL_GAS_MIN,
125 Self::FT_TRANSFER_CALL_GAS_DEFAULT,
126 )
127 } else {
128 (Self::FT_TRANSFER_GAS_MIN, Self::FT_TRANSFER_GAS_DEFAULT)
129 };
130
131 self.min_gas
132 .unwrap_or(default)
133 .max(min)
139 }
140}
141
142impl ExecutableIntent for FtWithdraw {
143 #[inline]
144 fn execute_intent<S, I>(
145 self,
146 owner_id: &AccountIdRef,
147 engine: &mut Engine<S, I>,
148 intent_hash: CryptoHash,
149 ) -> Result<()>
150 where
151 S: State,
152 I: Inspector,
153 {
154 engine
155 .inspector
156 .on_event(DefuseEvent::FtWithdraw(Cow::Borrowed(
157 [IntentEvent::new(
158 AccountEvent::new(owner_id, Cow::Borrowed(&self)),
159 intent_hash,
160 )]
161 .as_slice(),
162 )));
163
164 engine.state.ft_withdraw(owner_id, self)
165 }
166}
167
168#[near(serializers = [borsh, json])]
169#[derive(Debug, Clone)]
170pub struct NftWithdraw {
172 pub token: AccountId,
173 pub receiver_id: AccountId,
174 pub token_id: non_fungible_token::TokenId,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub memo: Option<String>,
177
178 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub msg: Option<String>,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub storage_deposit: Option<NearToken>,
189
190 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub min_gas: Option<Gas>,
198}
199
200impl NftWithdraw {
201 const NFT_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(15);
202 const NFT_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(15);
203
204 const NFT_TRANSFER_CALL_GAS_MIN: Gas = Gas::from_tgas(30);
206 const NFT_TRANSFER_CALL_GAS_DEFAULT: Gas = Gas::from_tgas(50);
207
208 #[inline]
210 pub const fn is_call(&self) -> bool {
211 self.msg.is_some()
212 }
213
214 #[inline]
216 pub fn min_gas(&self) -> Gas {
217 let (min, default) = if self.is_call() {
218 (
219 Self::NFT_TRANSFER_CALL_GAS_MIN,
220 Self::NFT_TRANSFER_CALL_GAS_DEFAULT,
221 )
222 } else {
223 (Self::NFT_TRANSFER_GAS_MIN, Self::NFT_TRANSFER_GAS_DEFAULT)
224 };
225
226 self.min_gas
227 .unwrap_or(default)
228 .max(min)
234 }
235}
236
237impl ExecutableIntent for NftWithdraw {
238 #[inline]
239 fn execute_intent<S, I>(
240 self,
241 owner_id: &AccountIdRef,
242 engine: &mut Engine<S, I>,
243 intent_hash: CryptoHash,
244 ) -> Result<()>
245 where
246 S: State,
247 I: Inspector,
248 {
249 engine
250 .inspector
251 .on_event(DefuseEvent::NftWithdraw(Cow::Borrowed(
252 [IntentEvent::new(
253 AccountEvent::new(owner_id, Cow::Borrowed(&self)),
254 intent_hash,
255 )]
256 .as_slice(),
257 )));
258
259 engine.state.nft_withdraw(owner_id, self)
260 }
261}
262
263#[near(serializers = [borsh, json])]
264#[derive(Debug, Clone)]
265pub struct MtWithdraw {
270 pub token: AccountId,
271 pub receiver_id: AccountId,
272 pub token_ids: Vec<defuse_nep245::TokenId>,
273 pub amounts: Vec<U128>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub memo: Option<String>,
276
277 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub msg: Option<String>,
282
283 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub storage_deposit: Option<NearToken>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub min_gas: Option<Gas>,
297}
298
299impl MtWithdraw {
300 const MT_BATCH_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(20);
302 const MT_BATCH_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(20);
303
304 const MT_BATCH_TRANSFER_CALL_GAS_MIN: Gas = Gas::from_tgas(35);
305 const MT_BATCH_TRANSFER_CALL_GAS_DEFAULT: Gas = Gas::from_tgas(50);
306
307 #[inline]
309 pub const fn is_call(&self) -> bool {
310 self.msg.is_some()
311 }
312
313 #[inline]
315 pub fn min_gas(&self) -> Gas {
316 let (min, default) = if self.is_call() {
317 (
318 Self::MT_BATCH_TRANSFER_CALL_GAS_MIN,
319 Self::MT_BATCH_TRANSFER_CALL_GAS_DEFAULT,
320 )
321 } else {
322 (
323 Self::MT_BATCH_TRANSFER_GAS_MIN,
324 Self::MT_BATCH_TRANSFER_GAS_DEFAULT,
325 )
326 };
327
328 self.min_gas
329 .unwrap_or(default)
330 .max(min)
336 }
337}
338
339impl ExecutableIntent for MtWithdraw {
340 #[inline]
341 fn execute_intent<S, I>(
342 self,
343 owner_id: &AccountIdRef,
344 engine: &mut Engine<S, I>,
345 intent_hash: CryptoHash,
346 ) -> Result<()>
347 where
348 S: State,
349 I: Inspector,
350 {
351 engine
352 .inspector
353 .on_event(DefuseEvent::MtWithdraw(Cow::Borrowed(
354 [IntentEvent::new(
355 AccountEvent::new(owner_id, Cow::Borrowed(&self)),
356 intent_hash,
357 )]
358 .as_slice(),
359 )));
360
361 engine.state.mt_withdraw(owner_id, self)
362 }
363}
364
365#[near(serializers = [borsh, json])]
366#[derive(Debug, Clone)]
367pub struct NativeWithdraw {
372 pub receiver_id: AccountId,
373 pub amount: NearToken,
374}
375
376impl ExecutableIntent for NativeWithdraw {
377 #[inline]
378 fn execute_intent<S, I>(
379 self,
380 owner_id: &AccountIdRef,
381 engine: &mut Engine<S, I>,
382 intent_hash: CryptoHash,
383 ) -> Result<()>
384 where
385 S: State,
386 I: Inspector,
387 {
388 engine
389 .inspector
390 .on_event(DefuseEvent::NativeWithdraw(Cow::Borrowed(
391 [IntentEvent::new(
392 AccountEvent::new(owner_id, Cow::Borrowed(&self)),
393 intent_hash,
394 )]
395 .as_slice(),
396 )));
397
398 engine.state.native_withdraw(owner_id, self)
399 }
400}
401
402#[near(serializers = [borsh, json])]
414#[derive(Debug, Clone)]
415pub struct StorageDeposit {
416 pub contract_id: AccountId,
417 #[serde(
418 alias = "account_id",
421 )]
422 pub deposit_for_account_id: AccountId,
423 pub amount: NearToken,
424}
425
426impl ExecutableIntent for StorageDeposit {
427 #[inline]
428 fn execute_intent<S, I>(
429 self,
430 owner_id: &AccountIdRef,
431 engine: &mut Engine<S, I>,
432 intent_hash: CryptoHash,
433 ) -> Result<()>
434 where
435 S: State,
436 I: Inspector,
437 {
438 engine
439 .inspector
440 .on_event(DefuseEvent::StorageDeposit(Cow::Borrowed(
441 [IntentEvent::new(
442 AccountEvent::new(owner_id, Cow::Borrowed(&self)),
443 intent_hash,
444 )]
445 .as_slice(),
446 )));
447
448 engine.state.storage_deposit(owner_id, self)
449 }
450}