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}