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