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