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}