defuse/contract/tokens/nep171/
withdraw.rs1use crate::{
2 contract::{Contract, ContractExt, Role, tokens::STORAGE_DEPOSIT_GAS},
3 tokens::nep171::{
4 NonFungibleTokenForceWithdrawer, NonFungibleTokenWithdrawResolver,
5 NonFungibleTokenWithdrawer,
6 },
7};
8use defuse_core::{
9 DefuseError, Result,
10 engine::StateView,
11 intents::tokens::NftWithdraw,
12 token_id::{nep141::Nep141TokenId, nep171::Nep171TokenId},
13};
14use defuse_near_utils::{REFUND_MEMO, UnwrapOrPanic};
15use defuse_wnear::{NEAR_WITHDRAW_GAS, ext_wnear};
16use near_contract_standards::{
17 non_fungible_token::{self, core::ext_nft_core},
18 storage_management::ext_storage_management,
19};
20use near_plugins::{AccessControllable, Pausable, access_control_any, pause};
21use near_sdk::{
22 AccountId, Gas, NearToken, Promise, PromiseOrValue, assert_one_yocto, env, json_types::U128,
23 near, require, serde_json,
24};
25use std::iter;
26
27#[near]
28impl NonFungibleTokenWithdrawer for Contract {
29 #[pause]
30 #[payable]
31 fn nft_withdraw(
32 &mut self,
33 token: AccountId,
34 receiver_id: AccountId,
35 token_id: non_fungible_token::TokenId,
36 memo: Option<String>,
37 msg: Option<String>,
38 ) -> PromiseOrValue<bool> {
39 assert_one_yocto();
40 self.internal_nft_withdraw(
41 self.ensure_auth_predecessor_id(),
42 NftWithdraw {
43 token,
44 receiver_id,
45 token_id,
46 memo,
47 msg,
48 storage_deposit: None,
49 min_gas: None,
50 },
51 false,
52 )
53 .unwrap_or_panic()
54 }
55}
56
57impl Contract {
58 pub(crate) fn internal_nft_withdraw(
59 &mut self,
60 owner_id: AccountId,
61 withdraw: NftWithdraw,
62 force: bool,
63 ) -> Result<PromiseOrValue<bool>> {
64 self.withdraw(
65 &owner_id,
66 iter::once((
67 Nep171TokenId::new(withdraw.token.clone(), withdraw.token_id.clone()).into(),
68 1,
69 ))
70 .chain(withdraw.storage_deposit.map(|amount| {
71 (
72 Nep141TokenId::new(self.wnear_id().into_owned()).into(),
73 amount.as_yoctonear(),
74 )
75 })),
76 Some("withdraw"),
77 force,
78 )?;
79
80 let is_call = withdraw.is_call();
81 Ok(if let Some(storage_deposit) = withdraw.storage_deposit {
82 ext_wnear::ext(self.wnear_id.clone())
83 .with_attached_deposit(NearToken::from_yoctonear(1))
84 .with_static_gas(NEAR_WITHDRAW_GAS)
85 .with_unused_gas_weight(0)
87 .near_withdraw(U128(storage_deposit.as_yoctonear()))
88 .then(
89 Self::ext(env::current_account_id())
91 .with_static_gas(
92 Self::DO_NFT_WITHDRAW_GAS
93 .checked_add(withdraw.min_gas())
94 .ok_or(DefuseError::GasOverflow)
95 .unwrap_or_panic(),
96 )
97 .do_nft_withdraw(withdraw.clone()),
98 )
99 } else {
100 Self::do_nft_withdraw(withdraw.clone())
101 }
102 .then(
103 Self::ext(env::current_account_id())
104 .with_static_gas(Self::NFT_RESOLVE_WITHDRAW_GAS)
105 .with_unused_gas_weight(0)
107 .nft_resolve_withdraw(withdraw.token, owner_id, withdraw.token_id, is_call),
108 )
109 .into())
110 }
111}
112
113#[near]
114impl Contract {
115 const NFT_RESOLVE_WITHDRAW_GAS: Gas = Gas::from_tgas(5);
116 const DO_NFT_WITHDRAW_GAS: Gas = Gas::from_tgas(5)
117 .saturating_add(STORAGE_DEPOSIT_GAS);
120
121 #[private]
122 pub fn do_nft_withdraw(withdraw: NftWithdraw) -> Promise {
123 let min_gas = withdraw.min_gas();
124 let p = if let Some(storage_deposit) = withdraw.storage_deposit {
125 require!(
126 matches!(env::promise_result_checked(0, 0), Ok(data) if data.is_empty()),
127 "near_withdraw failed",
128 );
129
130 ext_storage_management::ext(withdraw.token)
131 .with_attached_deposit(storage_deposit)
132 .with_static_gas(STORAGE_DEPOSIT_GAS)
133 .with_unused_gas_weight(0)
135 .storage_deposit(Some(withdraw.receiver_id.clone()), None)
136 } else {
137 Promise::new(withdraw.token)
138 };
139
140 let p = ext_nft_core::ext_on(p)
141 .with_attached_deposit(NearToken::from_yoctonear(1))
142 .with_static_gas(min_gas)
143 .with_unused_gas_weight(1);
145 if let Some(msg) = withdraw.msg {
146 p.nft_transfer_call(
147 withdraw.receiver_id,
148 withdraw.token_id,
149 None,
150 withdraw.memo,
151 msg,
152 )
153 } else {
154 p.nft_transfer(withdraw.receiver_id, withdraw.token_id, None, withdraw.memo)
155 }
156 }
157}
158
159#[near]
160impl NonFungibleTokenWithdrawResolver for Contract {
161 #[private]
162 fn nft_resolve_withdraw(
163 &mut self,
164 token: AccountId,
165 sender_id: AccountId,
166 token_id: non_fungible_token::TokenId,
167 is_call: bool,
168 ) -> bool {
169 const MAX_RESULT_LENGTH: usize = "false".len(); let used = env::promise_result_checked(0, MAX_RESULT_LENGTH).map_or(
171 is_call,
175 |value| {
176 if is_call {
177 serde_json::from_slice::<bool>(&value).unwrap_or_default()
179 } else {
180 value.is_empty()
182 }
183 },
184 );
185
186 if !used {
187 self.deposit(
188 sender_id,
189 [(Nep171TokenId::new(token, token_id).into(), 1)],
190 Some(REFUND_MEMO),
191 )
192 .unwrap_or_panic();
193 }
194
195 used
196 }
197}
198
199#[near]
200impl NonFungibleTokenForceWithdrawer for Contract {
201 #[access_control_any(roles(Role::DAO, Role::UnrestrictedWithdrawer))]
202 #[payable]
203 fn nft_force_withdraw(
204 &mut self,
205 owner_id: AccountId,
206 token: AccountId,
207 receiver_id: AccountId,
208 token_id: non_fungible_token::TokenId,
209 memo: Option<String>,
210 msg: Option<String>,
211 ) -> PromiseOrValue<bool> {
212 assert_one_yocto();
213 self.internal_nft_withdraw(
214 owner_id,
215 NftWithdraw {
216 token,
217 receiver_id,
218 token_id,
219 memo,
220 msg,
221 storage_deposit: None,
222 min_gas: None,
223 },
224 true,
225 )
226 .unwrap_or_panic()
227 }
228}