near_api/tokens.rs
1use near_api_types::{
2 ft::FungibleTokenMetadata,
3 json::U128,
4 nft::{NFTContractMetadata, Token},
5 tokens::{FTBalance, UserBalance, STORAGE_COST_PER_BYTE},
6 transaction::actions::TransferAction,
7 transaction::PrepopulateTransaction,
8 AccountId, Action, Data, NearToken, Reference,
9};
10use serde_json::json;
11
12use crate::{
13 advanced::{query_request::QueryRequest, query_rpc::SimpleQueryRpc},
14 common::{
15 query::{
16 AccountViewHandler, CallResultHandler, MultiQueryHandler, MultiRequestBuilder,
17 PostprocessHandler, RequestBuilder,
18 },
19 send::Transactionable,
20 },
21 contract::Contract,
22 errors::{ArgumentValidationError, FTValidatorError, ValidationError},
23 transactions::{ConstructTransaction, TransactionWithSign},
24 NetworkConfig, StorageDeposit,
25};
26
27// This is not too long as most of the size is a links to the docs
28#[allow(clippy::too_long_first_doc_paragraph)]
29/// A wrapper struct that simplifies interactions with
30/// [NEAR](https://docs.near.org/concepts/basics/tokens),
31/// [FT](https://docs.near.org/build/primitives/ft),
32/// [NFT](https://docs.near.org/build/primitives/nft)
33///
34/// This struct provides convenient methods to interact with different types of tokens on NEAR Protocol:
35/// - [Native NEAR](https://docs.near.org/concepts/basics/tokens) token operations
36/// - 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)
37/// - 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)
38///
39/// ## Examples
40///
41/// ### Fungible Token Operations
42/// ```
43/// use near_api::*;
44///
45/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
46/// let bob_tokens = Tokens::account("bob.testnet".parse()?);
47///
48/// // Check FT balance
49/// let balance = bob_tokens.ft_balance("usdt.tether-token.near".parse()?).fetch_from_mainnet().await?;
50/// println!("Bob balance: {}", balance);
51///
52/// // Transfer FT tokens
53/// bob_tokens.send_to("alice.testnet".parse()?)
54/// .ft(
55/// "usdt.tether-token.near".parse()?,
56/// USDT_BALANCE.with_whole_amount(100)
57/// )
58/// .with_signer(Signer::from_ledger()?)
59/// .send_to_mainnet()
60/// .await?;
61/// # Ok(())
62/// # }
63/// ```
64///
65/// ### NFT Operations
66/// ```
67/// use near_api::*;
68///
69/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
70/// let alice_tokens = Tokens::account("alice.testnet".parse()?);
71///
72/// // Check NFT assets
73/// let tokens = alice_tokens.nft_assets("nft-contract.testnet".parse()?).fetch_from_testnet().await?;
74/// println!("NFT count: {}", tokens.data.len());
75///
76/// // Transfer NFT
77/// alice_tokens.send_to("bob.testnet".parse()?)
78/// .nft("nft-contract.testnet".parse()?, "token-id".to_string())
79/// .with_signer(Signer::from_ledger()?)
80/// .send_to_testnet()
81/// .await?;
82/// # Ok(())
83/// # }
84/// ```
85///
86/// ### NEAR Token Operations
87/// ```
88/// use near_api::*;
89///
90/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
91/// let alice_account = Tokens::account("alice.testnet".parse()?);
92///
93/// // Check NEAR balance
94/// let balance = alice_account.near_balance().fetch_from_testnet().await?;
95/// println!("NEAR balance: {}", balance.total);
96///
97/// // Send NEAR
98/// alice_account.send_to("bob.testnet".parse()?)
99/// .near(NearToken::from_near(1))
100/// .with_signer(Signer::from_ledger()?)
101/// .send_to_testnet()
102/// .await?;
103/// # Ok(())
104/// # }
105/// ```
106#[derive(Debug, Clone)]
107pub struct Tokens {
108 account_id: AccountId,
109}
110
111impl Tokens {
112 pub const fn account(account_id: AccountId) -> Self {
113 Self { account_id }
114 }
115
116 /// Returns the underlying account ID for this tokens wrapper.
117 ///
118 /// # Example
119 /// ```rust,no_run
120 /// use near_api::*;
121 ///
122 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
123 /// let tokens = Tokens::account("alice.testnet".parse()?);
124 /// let account_id = tokens.account_id();
125 /// println!("Account ID: {}", account_id);
126 /// # Ok(())
127 /// # }
128 /// ```
129 pub const fn account_id(&self) -> &AccountId {
130 &self.account_id
131 }
132
133 /// Converts this tokens wrapper to an Account for account-related operations.
134 ///
135 /// # Example
136 /// ```rust,no_run
137 /// use near_api::*;
138 ///
139 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
140 /// let tokens = Tokens::account("alice.testnet".parse()?);
141 /// let account = tokens.as_account();
142 /// let account_info = account.view().fetch_from_testnet().await?;
143 /// println!("Account info: {:?}", account_info);
144 /// # Ok(())
145 /// # }
146 /// ```
147 pub fn as_account(&self) -> crate::account::Account {
148 crate::account::Account(self.account_id.clone())
149 }
150
151 /// Fetches the total NEAR balance ([UserBalance]) of the account.
152 ///
153 /// ## Example
154 /// ```rust,no_run
155 /// use near_api::*;
156 ///
157 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
158 /// let alice_tokens = Tokens::account("alice.testnet".parse()?);
159 /// let balance = alice_tokens.near_balance().fetch_from_testnet().await?;
160 /// println!("Alice's NEAR balance: {:?}", balance);
161 /// # Ok(())
162 /// # }
163 /// ```
164 pub fn near_balance(
165 &self,
166 ) -> RequestBuilder<PostprocessHandler<UserBalance, AccountViewHandler>> {
167 let request = QueryRequest::ViewAccount {
168 account_id: self.account_id.clone(),
169 };
170
171 RequestBuilder::new(
172 SimpleQueryRpc { request },
173 Reference::Optimistic,
174 AccountViewHandler,
175 )
176 .map(|account| {
177 let account = account.data;
178 let storage_locked = NearToken::from_yoctonear(
179 account.storage_usage as u128 * STORAGE_COST_PER_BYTE.as_yoctonear(),
180 );
181 UserBalance {
182 total: account.amount,
183 storage_locked,
184 storage_usage: account.storage_usage,
185 locked: account.locked,
186 }
187 })
188 }
189
190 /// Prepares a new contract query (`nft_metadata`) for fetching the NFT metadata ([NFTContractMetadata]).
191 ///
192 /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
193 ///
194 /// ## Example
195 /// ```rust,no_run
196 /// use near_api::*;
197 ///
198 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
199 /// let metadata = Tokens::nft_metadata("nft-contract.testnet".parse()?)
200 /// .fetch_from_testnet()
201 /// .await?;
202 /// println!("NFT metadata: {:?}", metadata);
203 /// # Ok(())
204 /// # }
205 /// ```
206 pub fn nft_metadata(
207 contract_id: AccountId,
208 ) -> RequestBuilder<CallResultHandler<NFTContractMetadata>> {
209 Contract(contract_id)
210 .call_function("nft_metadata", ())
211 .read_only()
212 }
213
214 /// Prepares a new contract query (`nft_tokens_for_owner`) for fetching the NFT assets of the account ([Vec]<[Token]>).
215 ///
216 /// The function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
217 ///
218 /// ## Example
219 /// ```rust,no_run
220 /// use near_api::*;
221 ///
222 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
223 /// let alice_tokens = Tokens::account("alice.testnet".parse()?);
224 /// let alice_assets = alice_tokens.nft_assets("nft-contract.testnet".parse()?)
225 /// .fetch_from_testnet()
226 /// .await?;
227 /// println!("Alice's NFT assets: {:?}", alice_assets);
228 /// # Ok(())
229 /// # }
230 /// ```
231 pub fn nft_assets(
232 &self,
233 nft_contract: AccountId,
234 ) -> RequestBuilder<CallResultHandler<Vec<Token>>> {
235 Contract(nft_contract)
236 .call_function(
237 "nft_tokens_for_owner",
238 json!({
239 "account_id": self.account_id.to_string(),
240 }),
241 )
242 .read_only()
243 }
244
245 /// Prepares a new contract query (`ft_metadata`) for fetching the FT metadata ([FungibleTokenMetadata]).
246 ///
247 /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
248 ///
249 /// ## Example
250 /// ```rust,no_run
251 /// use near_api::*;
252 ///
253 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
254 /// let metadata = Tokens::ft_metadata("usdt.tether-token.near".parse()?)
255 /// .fetch_from_testnet()
256 /// .await?
257 /// .data;
258 /// println!("FT metadata: {} {}", metadata.name, metadata.symbol);
259 /// # Ok(())
260 /// # }
261 /// ```
262 pub fn ft_metadata(
263 contract_id: AccountId,
264 ) -> RequestBuilder<CallResultHandler<FungibleTokenMetadata>> {
265 Contract(contract_id)
266 .call_function("ft_metadata", ())
267 .read_only()
268 }
269
270 /// Prepares a new contract query (`ft_balance_of`, `ft_metadata`) for fetching the [FTBalance] of the account.
271 ///
272 /// This query is a multi-query, meaning it will fetch the FT metadata and the FT balance of the account.
273 /// The result is then postprocessed to create a `FTBalance` instance.
274 ///
275 /// The function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
276 ///
277 /// # Example
278 /// ```rust,no_run
279 /// use near_api::*;
280 ///
281 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
282 /// let alice_usdt_balance = Tokens::account("alice.near".parse()?)
283 /// .ft_balance("usdt.tether-token.near".parse()?)
284 /// .fetch_from_mainnet()
285 /// .await?;
286 /// println!("Alice's USDT balance: {}", alice_usdt_balance);
287 /// # Ok(())
288 /// # }
289 /// ```
290 #[allow(clippy::type_complexity)]
291 pub fn ft_balance(
292 &self,
293 ft_contract: AccountId,
294 ) -> MultiRequestBuilder<
295 PostprocessHandler<
296 FTBalance,
297 MultiQueryHandler<(
298 CallResultHandler<FungibleTokenMetadata>,
299 CallResultHandler<U128>,
300 )>,
301 >,
302 > {
303 let handler = MultiQueryHandler::new((
304 CallResultHandler::<FungibleTokenMetadata>::new(),
305 CallResultHandler::<U128>::new(),
306 ));
307
308 MultiRequestBuilder::new(handler, Reference::Optimistic)
309 .add_query_builder(Self::ft_metadata(ft_contract.clone()))
310 .add_query_builder(
311 Contract(ft_contract)
312 .call_function(
313 "ft_balance_of",
314 json!({
315 "account_id": self.account_id.clone()
316 }),
317 )
318 .read_only::<()>(),
319 )
320 .map(
321 |(metadata, amount): (Data<FungibleTokenMetadata>, Data<U128>)| {
322 FTBalance::with_decimals(metadata.data.decimals).with_amount(amount.data.0)
323 },
324 )
325 }
326
327 /// Prepares a new transaction builder for sending tokens to another account.
328 ///
329 /// This builder is used to construct transactions for sending NEAR, FT, and NFT tokens.
330 ///
331 /// ## Sending NEAR
332 /// ```rust,no_run
333 /// use near_api::*;
334 ///
335 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
336 /// let alice_tokens = Tokens::account("alice.near".parse()?);
337 ///
338 /// let result = alice_tokens.send_to("bob.near".parse()?)
339 /// .near(NearToken::from_near(1))
340 /// .with_signer(Signer::from_ledger()?)
341 /// .send_to_mainnet()
342 /// .await?;
343 /// # Ok(())
344 /// # }
345 /// ```
346 ///
347 /// ## Sending FT
348 /// ```rust,no_run
349 /// use near_api::*;
350 ///
351 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
352 /// let alice_tokens = Tokens::account("alice.near".parse()?);
353 ///
354 /// let result = alice_tokens.send_to("bob.near".parse()?)
355 /// .ft("usdt.tether-token.near".parse()?, USDT_BALANCE.with_whole_amount(100))
356 /// .with_signer(Signer::from_ledger()?)
357 /// .send_to_mainnet()
358 /// .await?;
359 /// # Ok(())
360 /// # }
361 /// ```
362 ///
363 /// ## Sending NFT
364 /// ```rust,no_run
365 /// use near_api::*;
366 ///
367 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
368 /// let alice_tokens = Tokens::account("alice.near".parse()?);
369 ///
370 /// let result = alice_tokens.send_to("bob.near".parse()?)
371 /// .nft("nft-contract.testnet".parse()?, "token-id".to_string())
372 /// .with_signer(Signer::from_ledger()?)
373 /// .send_to_testnet()
374 /// .await?;
375 /// # Ok(())
376 /// # }
377 /// ```
378 pub fn send_to(&self, receiver_id: AccountId) -> SendToBuilder {
379 SendToBuilder {
380 from: self.account_id.clone(),
381 receiver_id,
382 }
383 }
384}
385
386#[derive(Debug, Clone)]
387pub struct SendToBuilder {
388 from: AccountId,
389 receiver_id: AccountId,
390}
391
392impl SendToBuilder {
393 /// Prepares a new transaction for sending NEAR tokens to another account.
394 pub fn near(self, amount: NearToken) -> ConstructTransaction {
395 ConstructTransaction::new(self.from, self.receiver_id)
396 .add_action(Action::Transfer(TransferAction { deposit: amount }))
397 }
398
399 /// Prepares a new transaction contract call (`ft_transfer`, `ft_metadata`, `storage_balance_of`, `storage_deposit`) for sending FT tokens to another account.
400 ///
401 /// Please note that if the receiver does not have enough storage, we will automatically deposit 100 milliNEAR for storage from
402 /// the sender.
403 ///
404 /// The provided function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
405 ///
406 /// For transferring tokens and calling a receiver contract method in a single transaction, see [`ft_call`](Self::ft_call).
407 pub fn ft(
408 self,
409 ft_contract: AccountId,
410 amount: FTBalance,
411 ) -> TransactionWithSign<FTTransactionable> {
412 let transaction = Contract(ft_contract)
413 .call_function(
414 "ft_transfer",
415 json!({
416 "receiver_id": self.receiver_id,
417 "amount": amount.amount().to_string(),
418 }),
419 )
420 .transaction()
421 .deposit(NearToken::from_yoctonear(1))
422 .with_signer_account(self.from);
423
424 TransactionWithSign {
425 tx: FTTransactionable {
426 receiver: self.receiver_id,
427 transaction: transaction.transaction,
428 decimals: amount.decimals(),
429 },
430 }
431 }
432
433 /// Prepares a new transaction contract call (`nft_transfer`) for sending NFT tokens to another account.
434 ///
435 /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
436 ///
437 /// For transferring an NFT and calling a receiver contract method in a single transaction, see [`nft_call`](Self::nft_call).
438 pub fn nft(self, nft_contract: AccountId, token_id: String) -> ConstructTransaction {
439 Contract(nft_contract)
440 .call_function(
441 "nft_transfer",
442 json!({
443 "receiver_id": self.receiver_id,
444 "token_id": token_id
445 }),
446 )
447 .transaction()
448 .deposit(NearToken::from_yoctonear(1))
449 .with_signer_account(self.from)
450 }
451
452 /// Prepares a new transaction contract call (`ft_transfer_call`, `ft_metadata`, `storage_balance_of`, `storage_deposit`) for transferring FT tokens and calling a receiver contract method.
453 ///
454 /// This method enables transferring tokens and invoking a receiver contract method in a single transaction.
455 /// The receiver contract must implement `ft_on_transfer` according to NEP-141.
456 ///
457 /// Please note that if the receiver does not have enough storage, we will automatically deposit 100 milliNEAR for storage from
458 /// the sender.
459 ///
460 /// The provided function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
461 ///
462 /// ## Example
463 /// ```rust,no_run
464 /// use near_api::*;
465 ///
466 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
467 /// let alice_tokens = Tokens::account("alice.near".parse()?);
468 ///
469 /// let result = alice_tokens.send_to("contract.near".parse()?)
470 /// .ft_call(
471 /// "usdt.tether-token.near".parse()?,
472 /// USDT_BALANCE.with_whole_amount(100),
473 /// "deposit".to_string(),
474 /// )
475 /// .with_signer(Signer::from_ledger()?)
476 /// .send_to_mainnet()
477 /// .await?;
478 /// # Ok(())
479 /// # }
480 /// ```
481 pub fn ft_call(
482 self,
483 ft_contract: AccountId,
484 amount: FTBalance,
485 msg: String,
486 ) -> TransactionWithSign<FTTransactionable> {
487 let transaction = Contract(ft_contract)
488 .call_function(
489 "ft_transfer_call",
490 json!({
491 "receiver_id": self.receiver_id,
492 "amount": amount.amount().to_string(),
493 "msg": msg,
494 }),
495 )
496 .transaction()
497 .deposit(NearToken::from_yoctonear(1))
498 .with_signer_account(self.from);
499
500 TransactionWithSign {
501 tx: FTTransactionable {
502 receiver: self.receiver_id,
503 transaction: transaction.transaction,
504 decimals: amount.decimals(),
505 },
506 }
507 }
508
509 /// Prepares a new transaction contract call (`nft_transfer_call`) for transferring an NFT and calling a receiver contract method.
510 ///
511 /// This method enables "transfer and call" functionality, allowing a user to attach an NFT to a function call
512 /// on a separate contract in a single transaction. The receiver contract must implement `nft_on_transfer` according to NEP-171.
513 ///
514 /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
515 ///
516 /// ## Example
517 /// ```rust,no_run
518 /// use near_api::*;
519 ///
520 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
521 /// let alice_tokens = Tokens::account("alice.near".parse()?);
522 ///
523 /// let result = alice_tokens.send_to("marketplace.near".parse()?)
524 /// .nft_call(
525 /// "nft-contract.testnet".parse()?,
526 /// "token-123".to_string(),
527 /// "list_for_sale".to_string(),
528 /// )
529 /// .with_signer(Signer::from_ledger()?)
530 /// .send_to_testnet()
531 /// .await?;
532 /// # Ok(())
533 /// # }
534 /// ```
535 pub fn nft_call(
536 self,
537 nft_contract: AccountId,
538 token_id: String,
539 msg: String,
540 ) -> ConstructTransaction {
541 Contract(nft_contract)
542 .call_function(
543 "nft_transfer_call",
544 json!({
545 "receiver_id": self.receiver_id,
546 "token_id": token_id,
547 "msg": msg,
548 }),
549 )
550 .transaction()
551 .deposit(NearToken::from_yoctonear(1))
552 .with_signer_account(self.from)
553 }
554}
555
556/// The structs validates the decimals correctness on runtime level before
557/// sending the ft tokens as well as deposits 100 milliNear of the deposit if
558/// the receiver doesn't have any allocated storage in the provided FT contract
559#[derive(Clone, Debug)]
560pub struct FTTransactionable {
561 transaction: Result<PrepopulateTransaction, ArgumentValidationError>,
562 receiver: AccountId,
563 decimals: u8,
564}
565
566impl FTTransactionable {
567 pub async fn check_decimals(&self, network: &NetworkConfig) -> Result<(), ValidationError> {
568 let transaction = match &self.transaction {
569 Ok(transaction) => transaction,
570 Err(e) => return Err(e.to_owned().into()),
571 };
572
573 let metadata = Tokens::ft_metadata(transaction.receiver_id.clone());
574
575 let Ok(metadata) = metadata.fetch_from(network).await else {
576 // If there is no metadata, than we can't check it
577 return Ok(());
578 };
579
580 if metadata.data.decimals != self.decimals {
581 Err(FTValidatorError::DecimalsMismatch {
582 expected: metadata.data.decimals,
583 got: self.decimals,
584 })?;
585 }
586 Ok(())
587 }
588}
589
590#[async_trait::async_trait]
591impl Transactionable for FTTransactionable {
592 fn prepopulated(&self) -> Result<PrepopulateTransaction, ArgumentValidationError> {
593 self.transaction.clone()
594 }
595
596 async fn validate_with_network(
597 &self,
598 network: &NetworkConfig,
599 ) -> core::result::Result<(), ValidationError> {
600 self.check_decimals(network).await?;
601
602 let transaction = match &self.transaction {
603 Ok(transaction) => transaction,
604 Err(_) => return Ok(()),
605 };
606
607 let storage_balance = StorageDeposit::on_contract(transaction.receiver_id.clone())
608 .view_account_storage(self.receiver.clone())
609 .fetch_from(network)
610 .await
611 .map_err(ValidationError::QueryError)?;
612
613 if storage_balance.data.is_none() {
614 Err(FTValidatorError::StorageDepositNeeded)?;
615 }
616
617 Ok(())
618 }
619
620 async fn edit_with_network(
621 &mut self,
622 network: &NetworkConfig,
623 ) -> core::result::Result<(), ValidationError> {
624 self.check_decimals(network).await?;
625
626 let transaction = match &mut self.transaction {
627 Ok(transaction) => transaction,
628 Err(_) => return Ok(()),
629 };
630
631 let storage_balance = StorageDeposit::on_contract(transaction.receiver_id.clone())
632 .view_account_storage(self.receiver.clone())
633 .fetch_from(network)
634 .await
635 .map_err(ValidationError::QueryError)?;
636
637 if storage_balance.data.is_none() {
638 let mut action = StorageDeposit::on_contract(transaction.receiver_id.clone())
639 .deposit(self.receiver.clone(), NearToken::from_millinear(100))
640 .into_transaction()
641 .with_signer_account(transaction.signer_id.clone())
642 .prepopulated()?
643 .actions;
644 action.append(&mut transaction.actions);
645 transaction.actions = action;
646 }
647 Ok(())
648 }
649}