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}