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 /// 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 ///
375 /// For transferring tokens and calling a receiver contract method in a single transaction, see [`ft_call`](Self::ft_call).
376 pub fn ft(
377 self,
378 ft_contract: AccountId,
379 amount: FTBalance,
380 ) -> Result<TransactionWithSign<FTTransactionable>> {
381 let tr = Contract(ft_contract)
382 .call_function(
383 "ft_transfer",
384 json!({
385 "receiver_id": self.receiver_id,
386 "amount": amount.amount().to_string(),
387 }),
388 )?
389 .transaction()
390 .deposit(NearToken::from_yoctonear(1))
391 .with_signer_account(self.from);
392
393 Ok(TransactionWithSign {
394 tx: FTTransactionable {
395 receiver: self.receiver_id,
396 prepopulated: tr.tr,
397 decimals: amount.decimals(),
398 },
399 })
400 }
401
402 /// Prepares a new transaction contract call (`nft_transfer`) for sending NFT tokens to another account.
403 ///
404 /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
405 ///
406 /// For transferring an NFT and calling a receiver contract method in a single transaction, see [`nft_call`](Self::nft_call).
407 pub fn nft(self, nft_contract: AccountId, token_id: String) -> Result<ConstructTransaction> {
408 Ok(Contract(nft_contract)
409 .call_function(
410 "nft_transfer",
411 json!({
412 "receiver_id": self.receiver_id,
413 "token_id": token_id
414 }),
415 )?
416 .transaction()
417 .deposit(NearToken::from_yoctonear(1))
418 .with_signer_account(self.from))
419 }
420
421 /// 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.
422 ///
423 /// This method enables transferring tokens and invoking a receiver contract method in a single transaction.
424 /// The receiver contract must implement `ft_on_transfer` according to NEP-141.
425 ///
426 /// Please note that if the receiver does not have enough storage, we will automatically deposit 100 milliNEAR for storage from
427 /// the sender.
428 ///
429 /// The provided function depends that the contract implements [`NEP-141`](https://nomicon.io/Standards/Tokens/FungibleToken/Core#nep-141)
430 ///
431 /// ## Example
432 /// ```rust,no_run
433 /// use near_api::*;
434 ///
435 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
436 /// let alice_tokens = Tokens::account("alice.near".parse()?);
437 ///
438 /// let result = alice_tokens.send_to("contract.near".parse()?)
439 /// .ft_call(
440 /// "usdt.tether-token.near".parse()?,
441 /// USDT_BALANCE.with_whole_amount(100),
442 /// "deposit".to_string(),
443 /// )?
444 /// .with_signer(Signer::new(Signer::from_ledger())?)
445 /// .send_to_mainnet()
446 /// .await?;
447 /// # Ok(())
448 /// # }
449 /// ```
450 pub fn ft_call(
451 self,
452 ft_contract: AccountId,
453 amount: FTBalance,
454 msg: String,
455 ) -> Result<TransactionWithSign<FTTransactionable>> {
456 let tr = Contract(ft_contract)
457 .call_function(
458 "ft_transfer_call",
459 json!({
460 "receiver_id": self.receiver_id,
461 "amount": amount.amount().to_string(),
462 "msg": msg,
463 }),
464 )?
465 .transaction()
466 .deposit(NearToken::from_yoctonear(1))
467 .with_signer_account(self.from);
468
469 Ok(TransactionWithSign {
470 tx: FTTransactionable {
471 receiver: self.receiver_id,
472 prepopulated: tr.tr,
473 decimals: amount.decimals(),
474 },
475 })
476 }
477
478 /// Prepares a new transaction contract call (`nft_transfer_call`) for transferring an NFT and calling a receiver contract method.
479 ///
480 /// This method enables "transfer and call" functionality, allowing a user to attach an NFT to a function call
481 /// on a separate contract in a single transaction. The receiver contract must implement `nft_on_transfer` according to NEP-171.
482 ///
483 /// The provided function depends that the contract implements [`NEP-171`](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core#nep-171)
484 ///
485 /// ## Example
486 /// ```rust,no_run
487 /// use near_api::*;
488 ///
489 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
490 /// let alice_tokens = Tokens::account("alice.near".parse()?);
491 ///
492 /// let result = alice_tokens.send_to("marketplace.near".parse()?)
493 /// .nft_call(
494 /// "nft-contract.testnet".parse()?,
495 /// "token-123".to_string(),
496 /// "list_for_sale".to_string(),
497 /// )?
498 /// .with_signer(Signer::new(Signer::from_ledger())?)
499 /// .send_to_testnet()
500 /// .await?;
501 /// # Ok(())
502 /// # }
503 /// ```
504 pub fn nft_call(
505 self,
506 nft_contract: AccountId,
507 token_id: String,
508 msg: String,
509 ) -> Result<ConstructTransaction> {
510 Ok(Contract(nft_contract)
511 .call_function(
512 "nft_transfer_call",
513 json!({
514 "receiver_id": self.receiver_id,
515 "token_id": token_id,
516 "msg": msg,
517 }),
518 )?
519 .transaction()
520 .deposit(NearToken::from_yoctonear(1))
521 .with_signer_account(self.from))
522 }
523}
524
525/// The structs validates the decimals correctness on runtime level before
526/// sending the ft tokens as well as deposits 100 milliNear of the deposit if
527/// the receiver doesn't have any allocated storage in the provided FT contract
528#[derive(Clone, Debug)]
529pub struct FTTransactionable {
530 prepopulated: PrepopulateTransaction,
531 receiver: AccountId,
532 decimals: u8,
533}
534
535impl FTTransactionable {
536 pub async fn check_decimals(
537 &self,
538 network: &NetworkConfig,
539 ) -> core::result::Result<(), ValidationError> {
540 let metadata = Tokens::ft_metadata(self.prepopulated.receiver_id.clone())?;
541
542 let Ok(metadata) = metadata.fetch_from(network).await else {
543 // If there is no metadata, than we can't check it
544 return Ok(());
545 };
546
547 if metadata.data.decimals != self.decimals {
548 Err(FTValidatorError::DecimalsMismatch {
549 expected: metadata.data.decimals,
550 got: self.decimals,
551 })?;
552 }
553 Ok(())
554 }
555}
556
557#[async_trait::async_trait]
558impl Transactionable for FTTransactionable {
559 fn prepopulated(&self) -> PrepopulateTransaction {
560 self.prepopulated.clone()
561 }
562
563 async fn validate_with_network(
564 &self,
565 network: &NetworkConfig,
566 ) -> core::result::Result<(), ValidationError> {
567 self.check_decimals(network).await?;
568
569 let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
570 .view_account_storage(self.receiver.clone())?
571 .fetch_from(network)
572 .await
573 .map_err(ValidationError::QueryError)?;
574
575 if storage_balance.data.is_none() {
576 Err(FTValidatorError::StorageDepositNeeded)?;
577 }
578
579 Ok(())
580 }
581
582 async fn edit_with_network(
583 &mut self,
584 network: &NetworkConfig,
585 ) -> core::result::Result<(), ValidationError> {
586 self.check_decimals(network).await?;
587
588 let storage_balance = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
589 .view_account_storage(self.receiver.clone())?
590 .fetch_from(network)
591 .await
592 .map_err(ValidationError::QueryError)?;
593
594 if storage_balance.data.is_none() {
595 let mut action = StorageDeposit::on_contract(self.prepopulated.receiver_id.clone())
596 .deposit(self.receiver.clone(), NearToken::from_millinear(100))?
597 .with_signer_account(self.prepopulated.signer_id.clone())
598 .tr
599 .actions;
600 action.append(&mut self.prepopulated.actions);
601 self.prepopulated.actions = action;
602 }
603 Ok(())
604 }
605}