near_api/storage.rs
1use std::sync::Arc;
2
3use near_api_types::{AccountId, Data, NearToken, StorageBalance, StorageBalanceInternal};
4use serde_json::json;
5
6use crate::{
7 common::query::{CallResultHandler, PostprocessHandler, RequestBuilder},
8 contract::ContractTransactBuilder,
9 transactions::ConstructTransaction,
10 Signer,
11};
12
13///A wrapper struct that simplifies interactions with the [Storage Management](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) standard
14///
15/// Contracts on NEAR Protocol often implement a [NEP-145](https://github.com/near/NEPs/blob/master/neps/nep-0145.md) for managing storage deposits,
16/// which are required for storing data on the blockchain. This struct provides convenient methods
17/// to interact with these storage-related functions on the contract.
18///
19/// # Example
20/// ```
21/// use near_api::*;
22///
23/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24/// let storage = StorageDeposit::on_contract("contract.testnet".parse()?);
25///
26/// // Check storage balance
27/// let balance = storage.view_account_storage("alice.testnet".parse()?).fetch_from_testnet().await?;
28/// println!("Storage balance: {:?}", balance);
29///
30/// // Bob pays for Alice's storage on the contract contract.testnet
31/// let deposit_tx = storage.deposit("alice.testnet".parse()?, NearToken::from_near(1))
32/// .with_signer("bob.testnet".parse()?, Signer::from_ledger()?)
33/// .send_to_testnet()
34/// .await
35/// .unwrap();
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Clone, Debug)]
40pub struct StorageDeposit(crate::Contract);
41
42impl StorageDeposit {
43 pub const fn on_contract(contract_id: AccountId) -> Self {
44 Self(crate::Contract(contract_id))
45 }
46
47 /// Returns the underlying contract account ID for this storage deposit wrapper.
48 ///
49 /// # Example
50 /// ```rust,no_run
51 /// use near_api::*;
52 ///
53 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
54 /// let storage = StorageDeposit::on_contract("contract.testnet".parse()?);
55 /// let contract_id = storage.contract_id();
56 /// println!("Contract ID: {}", contract_id);
57 /// # Ok(())
58 /// # }
59 /// ```
60 pub const fn contract_id(&self) -> &AccountId {
61 self.0.account_id()
62 }
63
64 /// Converts this storage deposit wrapper to a Contract for other contract operations.
65 ///
66 /// # Example
67 /// ```rust,no_run
68 /// use near_api::*;
69 ///
70 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
71 /// let storage = StorageDeposit::on_contract("usdt.tether-token.near".parse()?);
72 /// let contract = storage.as_contract();
73 ///
74 /// // Now you can call other contract methods
75 /// let metadata: serde_json::Value = contract.call_function("ft_metadata", ()).read_only().fetch_from_mainnet().await?.data;
76 /// println!("Token metadata: {:?}", metadata);
77 /// # Ok(())
78 /// # }
79 /// ```
80 pub fn as_contract(&self) -> crate::contract::Contract {
81 self.0.clone()
82 }
83
84 /// Prepares a new contract query (`storage_balance_of`) for fetching the storage balance (Option<[StorageBalance]>) of the account on the contract.
85 ///
86 /// ## Example
87 /// ```rust,no_run
88 /// use near_api::*;
89 ///
90 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
91 /// let balance = StorageDeposit::on_contract("contract.testnet".parse()?)
92 /// .view_account_storage("alice.testnet".parse()?)
93 /// .fetch_from_testnet()
94 /// .await?;
95 /// println!("Storage balance: {:?}", balance);
96 /// # Ok(())
97 /// # }
98 /// ```
99 #[allow(clippy::type_complexity)]
100 pub fn view_account_storage(
101 &self,
102 account_id: AccountId,
103 ) -> RequestBuilder<
104 PostprocessHandler<
105 Data<Option<StorageBalance>>,
106 CallResultHandler<Option<StorageBalanceInternal>>,
107 >,
108 > {
109 self.0
110 .call_function(
111 "storage_balance_of",
112 json!({
113 "account_id": account_id,
114 }),
115 )
116 .read_only()
117 .map(|storage: Data<Option<StorageBalanceInternal>>| {
118 storage.map(|option_storage| {
119 option_storage.map(|data| StorageBalance {
120 available: data.available,
121 total: data.total,
122 locked: NearToken::from_yoctonear(
123 data.total.as_yoctonear() - data.available.as_yoctonear(),
124 ),
125 })
126 })
127 })
128 }
129
130 /// Prepares a new transaction contract call (`storage_deposit`) for depositing storage on the contract.
131 ///
132 /// Returns a [`StorageDepositBuilder`] that allows configuring the deposit behavior
133 /// with [`registration_only()`](StorageDepositBuilder::registration_only).
134 ///
135 /// ## Example
136 /// ```rust,no_run
137 /// use near_api::*;
138 ///
139 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
140 /// // Basic deposit for another account
141 /// let tx = StorageDeposit::on_contract("contract.testnet".parse()?)
142 /// .deposit("alice.testnet".parse()?, NearToken::from_near(1))
143 /// .with_signer("bob.testnet".parse()?, Signer::from_ledger()?)
144 /// .send_to_testnet()
145 /// .await?;
146 ///
147 /// // Registration-only deposit (refunds excess above minimum)
148 /// let tx = StorageDeposit::on_contract("contract.testnet".parse()?)
149 /// .deposit("alice.testnet".parse()?, NearToken::from_near(1))
150 /// .registration_only()
151 /// .with_signer("bob.testnet".parse()?, Signer::from_ledger()?)
152 /// .send_to_testnet()
153 /// .await?;
154 /// # Ok(())
155 /// # }
156 /// ```
157 pub fn deposit(
158 &self,
159 receiver_account_id: AccountId,
160 amount: NearToken,
161 ) -> StorageDepositBuilder {
162 StorageDepositBuilder {
163 contract: self.0.clone(),
164 account_id: receiver_account_id,
165 amount,
166 registration_only: false,
167 }
168 }
169
170 /// Prepares a new transaction contract call (`storage_withdraw`) for withdrawing storage from the contract.
171 ///
172 /// ## Example
173 /// ```rust,no_run
174 /// use near_api::*;
175 ///
176 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
177 /// let tx = StorageDeposit::on_contract("contract.testnet".parse()?)
178 /// .withdraw("alice.testnet".parse()?, NearToken::from_near(1))
179 /// .with_signer(Signer::from_ledger()?)
180 /// .send_to_testnet()
181 /// .await?;
182 /// # Ok(())
183 /// # }
184 /// ```
185 pub fn withdraw(&self, account_id: AccountId, amount: NearToken) -> ConstructTransaction {
186 self.0
187 .call_function("storage_withdraw", json!({ "amount": amount }))
188 .transaction()
189 .deposit(NearToken::from_yoctonear(1))
190 .with_signer_account(account_id)
191 }
192
193 /// Prepares a new transaction contract call (`storage_unregister`) for unregistering
194 /// the predecessor account and returning the storage NEAR deposit.
195 ///
196 /// If the predecessor account is not registered, the function returns `false` without panic.
197 ///
198 /// By default, the contract will panic if the caller has existing account data (such as
199 /// a positive token balance). Use [`force()`](StorageUnregisterBuilder::force) to ignore
200 /// existing account data and force unregistering (which may burn token balances).
201 ///
202 /// **Note:** Requires exactly 1 yoctoNEAR attached for security purposes.
203 ///
204 /// ## Example
205 /// ```rust,no_run
206 /// use near_api::*;
207 ///
208 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
209 /// // Normal unregister (fails if account has data like token balance)
210 /// let tx = StorageDeposit::on_contract("contract.testnet".parse()?)
211 /// .unregister()
212 /// .with_signer("alice.testnet".parse()?, Signer::from_ledger()?)
213 /// .send_to_testnet()
214 /// .await?;
215 ///
216 /// // Force unregister (burns any remaining token balance)
217 /// let tx = StorageDeposit::on_contract("contract.testnet".parse()?)
218 /// .unregister()
219 /// .force()
220 /// .with_signer("alice.testnet".parse()?, Signer::from_ledger()?)
221 /// .send_to_testnet()
222 /// .await?;
223 /// # Ok(())
224 /// # }
225 /// ```
226 pub fn unregister(&self) -> StorageUnregisterBuilder {
227 StorageUnregisterBuilder {
228 contract: self.0.clone(),
229 force: false,
230 }
231 }
232}
233
234/// Builder for configuring a `storage_deposit` transaction.
235///
236/// Created by [`StorageDeposit::deposit`].
237#[derive(Clone, Debug)]
238pub struct StorageDepositBuilder {
239 contract: crate::Contract,
240 account_id: AccountId,
241 amount: NearToken,
242 registration_only: bool,
243}
244
245impl StorageDepositBuilder {
246 /// Sets `registration_only=true` for the deposit.
247 ///
248 /// When enabled, the contract will refund any deposit above the minimum balance
249 /// if the account wasn't registered, and refund the full deposit if already registered.
250 pub const fn registration_only(mut self) -> Self {
251 self.registration_only = true;
252 self
253 }
254
255 /// Builds and returns the transaction builder for this storage deposit.
256 pub fn into_transaction(self) -> ContractTransactBuilder {
257 let args = if self.registration_only {
258 json!({
259 "account_id": self.account_id.to_string(),
260 "registration_only": true,
261 })
262 } else {
263 json!({
264 "account_id": self.account_id.to_string(),
265 })
266 };
267
268 self.contract
269 .call_function("storage_deposit", args)
270 .transaction()
271 .deposit(self.amount)
272 }
273
274 /// Adds a signer to the transaction.
275 ///
276 /// This is a convenience method that calls `into_transaction()` and then `with_signer()`.
277 pub fn with_signer(
278 self,
279 signer_id: AccountId,
280 signer: Arc<Signer>,
281 ) -> crate::common::send::ExecuteSignedTransaction {
282 self.into_transaction().with_signer(signer_id, signer)
283 }
284}
285
286/// Builder for configuring a `storage_unregister` transaction.
287///
288/// Created by [`StorageDeposit::unregister`].
289#[derive(Clone, Debug)]
290pub struct StorageUnregisterBuilder {
291 contract: crate::Contract,
292 force: bool,
293}
294
295impl StorageUnregisterBuilder {
296 /// Sets `force=true` for the unregistering.
297 ///
298 /// When enabled, the contract will ignore existing account data (such as non-zero
299 /// token balances) and close the account anyway, potentially burning those balances.
300 ///
301 /// **Warning:** This may result in permanent loss of tokens or other account data.
302 pub const fn force(mut self) -> Self {
303 self.force = true;
304 self
305 }
306
307 /// Builds and returns the transaction builder for this storage unregister.
308 pub fn into_transaction(self) -> ContractTransactBuilder {
309 let args = if self.force {
310 json!({ "force": true })
311 } else {
312 json!({})
313 };
314
315 self.contract
316 .call_function("storage_unregister", args)
317 .transaction()
318 .deposit(NearToken::from_yoctonear(1))
319 }
320
321 /// Adds a signer to the transaction.
322 ///
323 /// This is a convenience method that calls `into_transaction()` and then `with_signer()`.
324 pub fn with_signer(
325 self,
326 signer_id: AccountId,
327 signer: Arc<Signer>,
328 ) -> crate::common::send::ExecuteSignedTransaction {
329 self.into_transaction().with_signer(signer_id, signer)
330 }
331}