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}