near_api/tokens.rs
1use near_api_types::{
2 AccountId, Action, Data, NearToken, Reference,
3 ft::FungibleTokenMetadata,
4 json::U128,
5 nft::{NFTContractMetadata, Token},
6 tokens::{FTBalance, STORAGE_COST_PER_BYTE, UserBalance},
7 transaction::PrepopulateTransaction,
8 transaction::actions::TransferAction,
9};
10use serde_json::json;
11
12use crate::{
13 NetworkConfig, StorageDeposit,
14 advanced::{query_request::QueryRequest, query_rpc::SimpleQueryRpc},
15 common::{
16 query::{
17 AccountViewHandler, CallResultHandler, MultiQueryHandler, MultiRequestBuilder,
18 PostprocessHandler, RequestBuilder,
19 },
20 send::Transactionable,
21 },
22 contract::Contract,
23 errors::{BuilderError, FTValidatorError, ValidationError},
24 transactions::{ConstructTransaction, TransactionWithSign},
25};
26
27type Result<T> = core::result::Result<T, BuilderError>;
28
29// This is not too long as most of the size is a links to the docs
30#[allow(clippy::too_long_first_doc_paragraph)]
31/// A wrapper struct that simplifies interactions with
32/// [NEAR](https://docs.near.org/concepts/basics/tokens),
33/// [FT](https://docs.near.org/build/primitives/ft),
34/// [NFT](https://docs.near.org/build/primitives/nft)
35///
36/// This struct provides convenient methods to interact with different types of tokens on NEAR Protocol:
37/// - [Native NEAR](https://docs.near.org/concepts/basics/tokens) token operations
38/// - Fungible Token - [Documentation and examples](https://docs.near.org/build/primitives/ft), [NEP-141](https://github.com/near/NEPs/blob/master/neps/nep-0141.md)
39/// - Non-Fungible Token - [Documentation and examples](https://docs.near.org/build/primitives/nft), [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md)
40///
41/// ## Examples
42///
43/// ### Fungible Token Operations
44/// ```
45/// use near_api::*;
46///
47/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
48/// let bob_tokens = Tokens::account("bob.testnet".parse()?);
49///
50/// // Check FT balance
51/// let balance = bob_tokens.ft_balance("usdt.tether-token.near".parse()?)?.fetch_from_mainnet().await?;
52/// println!("Bob balance: {}", balance);
53///
54/// // Transfer FT tokens
55/// bob_tokens.send_to("alice.testnet".parse()?)
56/// .ft(
57/// "usdt.tether-token.near".parse()?,
58/// USDT_BALANCE.with_whole_amount(100)
59/// )?
60/// .with_signer(Signer::new(Signer::from_ledger())?)
61/// .send_to_mainnet()
62/// .await?;
63/// # Ok(())
64/// # }
65/// ```
66///
67/// ### NFT Operations
68/// ```
69/// use near_api::*;
70///
71/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
72/// let alice_tokens = Tokens::account("alice.testnet".parse()?);
73///
74/// // Check NFT assets
75/// let tokens = alice_tokens.nft_assets("nft-contract.testnet".parse()?)?.fetch_from_testnet().await?;
76/// println!("NFT count: {}", tokens.data.len());
77///
78/// // Transfer NFT
79/// alice_tokens.send_to("bob.testnet".parse()?)
80/// .nft("nft-contract.testnet".parse()?, "token-id".to_string())?
81/// .with_signer(Signer::new(Signer::from_ledger())?)
82/// .send_to_testnet()
83/// .await?;
84/// # Ok(())
85/// # }
86/// ```
87///
88/// ### NEAR Token Operations
89/// ```
90/// use near_api::*;
91///
92/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
93/// let alice_account = Tokens::account("alice.testnet".parse()?);
94///
95/// // Check NEAR balance
96/// let balance = alice_account.near_balance().fetch_from_testnet().await?;
97/// println!("NEAR balance: {}", balance.total);
98///
99/// // Send NEAR
100/// alice_account.send_to("bob.testnet".parse()?)
101/// .near(NearToken::from_near(1))
102/// .with_signer(Signer::new(Signer::from_ledger())?)
103/// .send_to_testnet()
104/// .await?;
105/// # Ok(())
106/// # }
107/// ```
108#[derive(Debug, Clone)]
109pub struct Tokens {
110 account_id: AccountId,
111}
112
113impl Tokens {
114 pub const fn account(account_id: AccountId) -> Self {
115 Self { account_id }
116 }
117
118 /// Fetches the total NEAR balance ([UserBalance]) of the account.
119 ///
120 /// ## Example
121 /// ```rust,no_run
122 /// use near_api::*;
123 ///
124 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
125 /// let alice_tokens = Tokens::account("alice.testnet".parse()?);
126 /// let balance = alice_tokens.near_balance().fetch_from_testnet().await?;
127 /// println!("Alice's NEAR balance: {:?}", balance);
128 /// # Ok(())
129 /// # }
130 /// ```
131 pub fn near_balance(
132 &self,
133 ) -> RequestBuilder<PostprocessHandler<UserBalance, AccountViewHandler>> {
134 let request = QueryRequest::ViewAccount {
135 account_id: self.account_id.clone(),
136 };
137
138 RequestBuilder::new(
139 SimpleQueryRpc { request },
140 Reference::Optimistic,
141 AccountViewHandler,
142 )
143 .map(|account| {
144 let account = account.data;
145 let storage_locked = NearToken::from_yoctonear(
146 account.storage_usage as u128 * STORAGE_COST_PER_BYTE.as_yoctonear(),
147 );
148 UserBalance {
149 total: account.amount,
150 storage_locked,
151 storage_usage: account.storage_usage,
152 locked: account.locked,
153 }
154 })
155 }
156
157 /// Prepares a new contract query (`nft_metadata`) for fetching the NFT metadata ([NFTContractMetadata]).
158 ///
159 /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
160 ///
161 /// ## Example
162 /// ```rust,no_run
163 /// use near_api::*;
164 ///
165 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
166 /// let metadata = Tokens::nft_metadata("nft-contract.testnet".parse()?)?
167 /// .fetch_from_testnet()
168 /// .await?;
169 /// println!("NFT metadata: {:?}", metadata);
170 /// # Ok(())
171 /// # }
172 /// ```
173 pub fn nft_metadata(
174 contract_id: AccountId,
175 ) -> Result<RequestBuilder<CallResultHandler<NFTContractMetadata>>> {
176 Ok(Contract(contract_id)
177 .call_function("nft_metadata", ())?
178 .read_only())
179 }
180
181 /// Prepares a new contract query (`nft_tokens_for_owner`) for fetching the NFT assets of the account ([Vec]<[Token]>).
182 ///
183 /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
184 ///
185 /// ## Example
186 /// ```rust,no_run
187 /// use near_api::*;
188 ///
189 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
190 /// let alice_tokens = Tokens::account("alice.testnet".parse()?);
191 /// let alice_assets = alice_tokens.nft_assets("nft-contract.testnet".parse()?)?
192 /// .fetch_from_testnet()
193 /// .await?;
194 /// println!("Alice's NFT assets: {:?}", alice_assets);
195 /// # Ok(())
196 /// # }
197 /// ```
198 pub fn nft_assets(
199 &self,
200 nft_contract: AccountId,
201 ) -> Result<RequestBuilder<CallResultHandler<Vec<Token>>>> {
202 Ok(Contract(nft_contract)
203 .call_function(
204 "nft_tokens_for_owner",
205 json!({
206 "account_id": self.account_id.to_string(),
207 }),
208 )?
209 .read_only())
210 }
211
212 /// Prepares a new contract query (`ft_metadata`) for fetching the FT metadata ([FungibleTokenMetadata]).
213 ///
214 /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
215 ///
216 /// ## Example
217 /// ```rust,no_run
218 /// use near_api::*;
219 ///
220 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
221 /// let metadata = Tokens::ft_metadata("usdt.tether-token.near".parse()?)?
222 /// .fetch_from_testnet()
223 /// .await?
224 /// .data;
225 /// println!("FT metadata: {} {}", metadata.name, metadata.symbol);
226 /// # Ok(())
227 /// # }
228 /// ```
229 pub fn ft_metadata(
230 contract_id: AccountId,
231 ) -> Result<RequestBuilder<CallResultHandler<FungibleTokenMetadata>>> {
232 Ok(Contract(contract_id)
233 .call_function("ft_metadata", ())?
234 .read_only())
235 }
236
237 /// Prepares a new contract query (`ft_balance_of`, `ft_metadata`) for fetching the [FTBalance] of the account.
238 ///
239 /// This query is a multi-query, meaning it will fetch the FT metadata and the FT balance of the account.
240 /// The result is then postprocessed to create a `FTBalance` instance.
241 ///
242 /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
243 ///
244 /// # Example
245 /// ```rust,no_run
246 /// use near_api::*;
247 ///
248 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
249 /// let alice_usdt_balance = Tokens::account("alice.near".parse()?)
250 /// .ft_balance("usdt.tether-token.near".parse()?)?
251 /// .fetch_from_mainnet()
252 /// .await?;
253 /// println!("Alice's USDT balance: {}", alice_usdt_balance);
254 /// # Ok(())
255 /// # }
256 /// ```
257 #[allow(clippy::type_complexity)]
258 pub fn ft_balance(
259 &self,
260 ft_contract: AccountId,
261 ) -> Result<
262 MultiRequestBuilder<
263 PostprocessHandler<
264 FTBalance,
265 MultiQueryHandler<(
266 CallResultHandler<FungibleTokenMetadata>,
267 CallResultHandler<U128>,
268 )>,
269 >,
270 >,
271 > {
272 let handler = MultiQueryHandler::new((
273 CallResultHandler::<FungibleTokenMetadata>::new(),
274 CallResultHandler::<U128>::new(),
275 ));
276 let multiquery = MultiRequestBuilder::new(handler, Reference::Optimistic)
277 .add_query_builder(Self::ft_metadata(ft_contract.clone())?)
278 .add_query_builder(
279 Contract(ft_contract)
280 .call_function(
281 "ft_balance_of",
282 json!({
283 "account_id": self.account_id.clone()
284 }),
285 )?
286 .read_only::<()>(),
287 )
288 .map(
289 |(metadata, amount): (Data<FungibleTokenMetadata>, Data<U128>)| {
290 FTBalance::with_decimals(metadata.data.decimals).with_amount(amount.data.0)
291 },
292 );
293 Ok(multiquery)
294 }
295
296 /// Prepares a new transaction builder for sending tokens to another account.
297 ///
298 /// This builder is used to construct transactions for sending NEAR, FT, and NFT tokens.
299 ///
300 /// ## Sending NEAR
301 /// ```rust,no_run
302 /// use near_api::*;
303 ///
304 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
305 /// let alice_tokens = Tokens::account("alice.near".parse()?);
306 ///
307 /// let result = alice_tokens.send_to("bob.near".parse()?)
308 /// .near(NearToken::from_near(1))
309 /// .with_signer(Signer::new(Signer::from_ledger())?)
310 /// .send_to_mainnet()
311 /// .await?;
312 /// # Ok(())
313 /// # }
314 /// ```
315 ///
316 /// ## Sending FT
317 /// ```rust,no_run
318 /// use near_api::*;
319 ///
320 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
321 /// let alice_tokens = Tokens::account("alice.near".parse()?);
322 ///
323 /// let result = alice_tokens.send_to("bob.near".parse()?)
324 /// .ft("usdt.tether-token.near".parse()?, USDT_BALANCE.with_whole_amount(100))?
325 /// .with_signer(Signer::new(Signer::from_ledger())?)
326 /// .send_to_mainnet()
327 /// .await?;
328 /// # Ok(())
329 /// # }
330 /// ```
331 ///
332 /// ## Sending NFT
333 /// ```rust,no_run
334 /// use near_api::*;
335 ///
336 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
337 /// let alice_tokens = Tokens::account("alice.near".parse()?);
338 ///
339 /// let result = alice_tokens.send_to("bob.near".parse()?)
340 /// .nft("nft-contract.testnet".parse()?, "token-id".to_string())?
341 /// .with_signer(Signer::new(Signer::from_ledger())?)
342 /// .send_to_testnet()
343 /// .await?;
344 /// # Ok(())
345 /// # }
346 /// ```
347 pub fn send_to(&self, receiver_id: AccountId) -> SendToBuilder {
348 SendToBuilder {
349 from: self.account_id.clone(),
350 receiver_id,
351 }
352 }
353}
354
355#[derive(Debug, Clone)]
356pub struct SendToBuilder {
357 from: AccountId,
358 receiver_id: AccountId,
359}
360
361impl SendToBuilder {
362 /// Prepares a new transaction for sending NEAR tokens to another account.
363 pub fn near(self, amount: NearToken) -> ConstructTransaction {
364 ConstructTransaction::new(self.from, self.receiver_id)
365 .add_action(Action::Transfer(TransferAction { deposit: amount }))
366 }
367
368 /// Prepares a new transaction contract call (`ft_transfer`, `ft_metadata`, `storage_balance_of`, `storage_deposit`) for sending FT tokens to another account.
369 ///
370 /// Please note that if the receiver does not have enough storage, we will automatically deposit 100 milliNEAR for storage from
371 /// the sender.
372 ///
373 /// The provided function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
374 pub fn ft(
375 self,
376 ft_contract: AccountId,
377 amount: FTBalance,
378 ) -> Result<TransactionWithSign<FTTransactionable>> {
379 let tr = Contract(ft_contract)
380 .call_function(
381 "ft_transfer",
382 json!({
383 "receiver_id": self.receiver_id,
384 "amount": amount.amount().to_string(),
385 }),
386 )?
387 .transaction()
388 .deposit(NearToken::from_yoctonear(1))
389 .with_signer_account(self.from);
390
391 Ok(TransactionWithSign {
392 tx: FTTransactionable {
393 receiver: self.receiver_id,
394 prepopulated: tr.tr,
395 decimals: amount.decimals(),
396 },
397 })
398 }
399
400 /// Prepares a new transaction contract call (`nft_transfer`) for sending NFT tokens to another account.
401 ///
402 /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
403 pub fn nft(self, nft_contract: AccountId, token_id: String) -> Result<ConstructTransaction> {
404 Ok(Contract(nft_contract)
405 .call_function(
406 "nft_transfer",
407 json!({
408 "receiver_id": self.receiver_id,
409 "token_id": token_id
410 }),
411 )?
412 .transaction()
413 .deposit(NearToken::from_yoctonear(1))
414 .with_signer_account(self.from))
415 }
416}
417
418/// The structs validates the decimals correctness on runtime level before
419/// sending the ft tokens as well as deposits 100 milliNear of the deposit if
420/// the receiver doesn't have any allocated storage in the provided FT contract
421#[derive(Clone, Debug)]
422pub struct FTTransactionable {
423 prepopulated: PrepopulateTransaction,
424 receiver: AccountId,
425 decimals: u8,
426}
427
428impl FTTransactionable {
429 pub async fn check_decimals(
430 &self,
431 network: &NetworkConfig,
432 ) -> core::result::Result<(), ValidationError> {
433 let metadata = Tokens::ft_metadata(self.prepopulated.receiver_id.clone())?;
434
435 let metadata = metadata
436 .fetch_from(network)
437 .await
438 .map_err(|_| FTValidatorError::NoMetadata)?;
439 if metadata.data.decimals != self.decimals {
440 Err(FTValidatorError::DecimalsMismatch {
441 expected: metadata.data.decimals,
442 got: self.decimals,
443 })?;
444 }
445 Ok(())
446 }
447}
448
449#[async_trait::async_trait]
450impl Transactionable for FTTransactionable {
451 fn prepopulated(&self) -> PrepopulateTransaction {
452 self.prepopulated.clone()
453 }
454
455 async fn validate_with_network(
456 &self,
457 network: &NetworkConfig,
458 ) -> core::result::Result<(), ValidationError> {
459 self.check_decimals(network).await?;
460
461 let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
462 .view_account_storage(self.receiver.clone())?
463 .fetch_from(network)
464 .await
465 .map_err(ValidationError::QueryError)?;
466
467 if storage_balance.data.is_none() {
468 Err(FTValidatorError::StorageDepositNeeded)?;
469 }
470
471 Ok(())
472 }
473
474 async fn edit_with_network(
475 &mut self,
476 network: &NetworkConfig,
477 ) -> core::result::Result<(), ValidationError> {
478 self.check_decimals(network).await?;
479
480 let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
481 .view_account_storage(self.receiver.clone())?
482 .fetch_from(network)
483 .await
484 .map_err(ValidationError::QueryError)?;
485
486 if storage_balance.data.is_none() {
487 let mut action = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
488 .deposit(self.receiver.clone(), NearToken::from_millinear(100))?
489 .with_signer_account(self.prepopulated.signer_id.clone())
490 .tr
491 .actions;
492 action.append(&mut self.prepopulated.actions);
493 self.prepopulated.actions = action;
494 }
495 Ok(())
496 }
497}