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}