From 737795df7ac35a25a7f4a139e80d9f40077c5a18 Mon Sep 17 00:00:00 2001 From: FroVolod <36816899+FroVolod@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:04:50 +0200 Subject: [PATCH] refactor: Updated send-ft command (#283) Updated send-ft command: - added storage_deposit action ![Screenshot 2023-12-26 at 16 12 33](https://github.com/near/near-cli-rs/assets/36816899/56e432cf-9312-46ae-a46f-569d02cecd6a) - added FT check ![example](https://github.com/near/near-cli-rs/assets/36816899/935fca08-074e-45b9-a6de-a2d1276669fe) --- src/commands/tokens/mod.rs | 33 --- src/commands/tokens/send_ft/amount_ft.rs | 307 +++++++++++++++++++++ src/commands/tokens/send_ft/mod.rs | 126 +-------- src/commands/tokens/view_ft_balance/mod.rs | 25 +- src/types/ft_properties.rs | 209 ++++++++++++++ src/types/mod.rs | 1 + 6 files changed, 527 insertions(+), 174 deletions(-) create mode 100644 src/commands/tokens/send_ft/amount_ft.rs create mode 100644 src/types/ft_properties.rs diff --git a/src/commands/tokens/mod.rs b/src/commands/tokens/mod.rs index 192ea8111..4a4d02a5c 100644 --- a/src/commands/tokens/mod.rs +++ b/src/commands/tokens/mod.rs @@ -1,9 +1,5 @@ -use color_eyre::eyre::Context; use strum::{EnumDiscriminants, EnumIter, EnumMessage}; -use crate::common::CallResultExt; -use crate::common::JsonRpcClientExt; - mod send_ft; mod send_near; mod send_nft; @@ -82,32 +78,3 @@ pub enum TokensActions { /// View the balance of NFT tokens ViewNftAssets(self::view_nft_assets::ViewNftAssets), } - -#[derive(serde::Deserialize)] -pub struct FtMetadata { - symbol: String, - decimals: u64, -} - -pub fn params_ft_metadata( - ft_contract_account_id: near_primitives::types::AccountId, - network_config: &crate::config::NetworkConfig, - block_reference: near_primitives::types::BlockReference, -) -> color_eyre::eyre::Result { - let ft_metadata: FtMetadata = network_config - .json_rpc_client() - .blocking_call_view_function( - &ft_contract_account_id, - "ft_metadata", - vec![], - block_reference, - ) - .wrap_err_with(||{ - format!("Failed to fetch query for view method: 'ft_metadata' (contract <{}> on network <{}>)", - ft_contract_account_id, - network_config.network_name - ) - })? - .parse_result_from_json()?; - Ok(ft_metadata) -} diff --git a/src/commands/tokens/send_ft/amount_ft.rs b/src/commands/tokens/send_ft/amount_ft.rs new file mode 100644 index 000000000..fb5dbfd43 --- /dev/null +++ b/src/commands/tokens/send_ft/amount_ft.rs @@ -0,0 +1,307 @@ +use std::str::FromStr; + +use color_eyre::eyre::{Context, ContextCompat}; +use inquire::{CustomType, Text}; +use serde_json::{json, Value}; + +use crate::common::CallResultExt; +use crate::common::JsonRpcClientExt; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::SendFtCommandContext)] +#[interactive_clap(output_context = AmountFtContext)] +pub struct AmountFt { + #[interactive_clap(skip_default_input_arg)] + /// Enter an amount FT to transfer: + amount_ft: crate::types::ft_properties::FungibleToken, + #[interactive_clap(named_arg)] + /// Enter gas for function call + prepaid_gas: PrepaidGas, +} + +#[derive(Debug, Clone)] +pub struct AmountFtContext { + global_context: crate::GlobalContext, + signer_account_id: near_primitives::types::AccountId, + ft_contract_account_id: near_primitives::types::AccountId, + receiver_account_id: near_primitives::types::AccountId, + amount_ft: crate::types::ft_properties::FungibleToken, +} + +impl AmountFtContext { + pub fn from_previous_context( + previous_context: super::SendFtCommandContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let network_config = crate::common::find_network_where_account_exist( + &previous_context.global_context, + previous_context.ft_contract_account_id.clone(), + ) + .wrap_err_with(|| { + format!( + "Contract <{}> does not exist in networks", + previous_context.ft_contract_account_id + ) + })?; + let ft_metadata = crate::types::ft_properties::params_ft_metadata( + previous_context.ft_contract_account_id.clone(), + &network_config, + near_primitives::types::Finality::Final.into(), + )?; + + Ok(Self { + global_context: previous_context.global_context, + signer_account_id: previous_context.signer_account_id, + ft_contract_account_id: previous_context.ft_contract_account_id, + receiver_account_id: previous_context.receiver_account_id, + amount_ft: scope.amount_ft.normalize(&ft_metadata)?, + }) + } +} + +impl AmountFt { + fn input_amount_ft( + context: &super::SendFtCommandContext, + ) -> color_eyre::eyre::Result> { + let network_config = crate::common::find_network_where_account_exist( + &context.global_context, + context.ft_contract_account_id.clone(), + ) + .wrap_err_with(|| { + format!( + "Contract <{}> does not exist in networks", + context.ft_contract_account_id + ) + })?; + + let ft_metadata = crate::types::ft_properties::params_ft_metadata( + context.ft_contract_account_id.clone(), + &network_config, + near_primitives::types::Finality::Final.into(), + )?; + eprintln!(); + + Ok(Some( + CustomType::::new(&format!( + "Enter an FT amount to transfer (example: 10 {symbol} or 0.5 {symbol}):", + symbol = ft_metadata.symbol + )) + .with_validator(move |ft: &crate::types::ft_properties::FungibleToken| { + match ft.normalize(&ft_metadata) { + Err(err) => Ok(inquire::validator::Validation::Invalid( + inquire::validator::ErrorMessage::Custom(err.to_string()), + )), + Ok(_) => Ok(inquire::validator::Validation::Valid), + } + }) + .with_formatter(&|ft| ft.to_string()) + .prompt()?, + )) + } +} + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = AmountFtContext)] +#[interactive_clap(output_context = PrepaidGasContext)] +pub struct PrepaidGas { + #[interactive_clap(skip_default_input_arg)] + /// Enter gas for function call: + gas: crate::common::NearGas, + #[interactive_clap(named_arg)] + /// Enter deposit for a function call + attached_deposit: Deposit, +} + +#[derive(Debug, Clone)] +pub struct PrepaidGasContext { + global_context: crate::GlobalContext, + signer_account_id: near_primitives::types::AccountId, + ft_contract_account_id: near_primitives::types::AccountId, + receiver_account_id: near_primitives::types::AccountId, + amount_ft: crate::types::ft_properties::FungibleToken, + gas: crate::common::NearGas, +} + +impl PrepaidGasContext { + pub fn from_previous_context( + previous_context: AmountFtContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + Ok(Self { + global_context: previous_context.global_context, + signer_account_id: previous_context.signer_account_id, + ft_contract_account_id: previous_context.ft_contract_account_id, + receiver_account_id: previous_context.receiver_account_id, + amount_ft: previous_context.amount_ft, + gas: scope.gas, + }) + } +} + +impl PrepaidGas { + fn input_gas( + _context: &AmountFtContext, + ) -> color_eyre::eyre::Result> { + eprintln!(); + let gas = loop { + match crate::common::NearGas::from_str( + &Text::new("Enter gas for function call:") + .with_initial_value("100 TeraGas") + .prompt()?, + ) { + Ok(input_gas) => { + if input_gas <= near_gas::NearGas::from_tgas(300) { + break input_gas; + } else { + eprintln!("You need to enter a value of no more than 300 TeraGas") + } + } + Err(err) => return Err(color_eyre::Report::msg(err)), + } + }; + Ok(Some(gas)) + } +} + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = PrepaidGasContext)] +#[interactive_clap(output_context = DepositContext)] +pub struct Deposit { + #[interactive_clap(skip_default_input_arg)] + /// Enter deposit for a function call: + deposit: crate::types::near_token::NearToken, + #[interactive_clap(named_arg)] + /// Select network + network_config: crate::network_for_transaction::NetworkForTransactionArgs, +} + +#[derive(Clone)] +pub struct DepositContext(crate::commands::ActionContext); + +impl DepositContext { + pub fn from_previous_context( + previous_context: PrepaidGasContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let on_after_getting_network_callback: crate::commands::OnAfterGettingNetworkCallback = + std::sync::Arc::new({ + let signer_account_id = previous_context.signer_account_id.clone(); + let ft_contract_account_id = previous_context.ft_contract_account_id.clone(); + let receiver_account_id = previous_context.receiver_account_id.clone(); + let deposit = scope.deposit; + let amount_ft = previous_context.amount_ft.clone(); + let action_ft_transfer = near_primitives::transaction::Action::FunctionCall( + Box::new(near_primitives::transaction::FunctionCallAction { + method_name: "ft_transfer".to_string(), + args: serde_json::to_vec(&json!({ + "receiver_id": receiver_account_id.to_string(), + "amount": amount_ft.amount().to_string() + }))?, + gas: previous_context.gas.as_gas(), + deposit: deposit.as_yoctonear(), + }), + ); + + move |network_config| { + let args = serde_json::to_vec(&json!({ + "account_id": receiver_account_id.to_string(), + }))?; + let call_result = network_config + .json_rpc_client() + .blocking_call_view_function( + &ft_contract_account_id, + "storage_balance_of", + args.clone(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err_with(||{ + format!("Failed to fetch query for view method: 'storage_balance_of' (contract <{}> on network <{}>)", + ft_contract_account_id, + network_config.network_name + ) + })?; + + if call_result.parse_result_from_json::()?.is_null() { + let action_storage_deposit = + near_primitives::transaction::Action::FunctionCall(Box::new( + near_primitives::transaction::FunctionCallAction { + method_name: "storage_deposit".to_string(), + args, + gas: previous_context.gas.as_gas(), + deposit: near_token::NearToken::from_millinear(100) + .as_yoctonear(), + }, + )); + return Ok(crate::commands::PrepopulatedTransaction { + signer_id: signer_account_id.clone(), + receiver_id: ft_contract_account_id.clone(), + actions: vec![action_storage_deposit, action_ft_transfer.clone()], + }); + } + + Ok(crate::commands::PrepopulatedTransaction { + signer_id: signer_account_id.clone(), + receiver_id: ft_contract_account_id.clone(), + actions: vec![action_ft_transfer.clone()], + }) + } + }); + + let on_after_sending_transaction_callback: crate::transaction_signature_options::OnAfterSendingTransactionCallback = std::sync::Arc::new({ + let signer_account_id = previous_context.signer_account_id.clone(); + let amount_ft = previous_context.amount_ft.clone(); + let ft_contract_account_id = previous_context.ft_contract_account_id.clone(); + let receiver_account_id = previous_context.receiver_account_id.clone(); + + move |outcome_view, _network_config| { + if let near_primitives::views::FinalExecutionStatus::SuccessValue(_) = outcome_view.status { + eprintln!( + "<{signer_account_id}> has successfully transferred {amount_ft} (FT-contract: {ft_contract_account_id}) to <{receiver_account_id}>.", + ); + } + Ok(()) + } + }); + + Ok(Self(crate::commands::ActionContext { + global_context: previous_context.global_context, + interacting_with_account_ids: vec![ + previous_context.ft_contract_account_id, + previous_context.signer_account_id, + previous_context.receiver_account_id, + ], + on_after_getting_network_callback, + on_before_signing_callback: std::sync::Arc::new( + |_prepolulated_unsinged_transaction, _network_config| Ok(()), + ), + on_before_sending_transaction_callback: std::sync::Arc::new( + |_signed_transaction, _network_config, _message| Ok(()), + ), + on_after_sending_transaction_callback, + })) + } +} + +impl From for crate::commands::ActionContext { + fn from(item: DepositContext) -> Self { + item.0 + } +} + +impl Deposit { + fn input_deposit( + _context: &PrepaidGasContext, + ) -> color_eyre::eyre::Result> { + eprintln!(); + match crate::types::near_token::NearToken::from_str( + &Text::new( + "Enter deposit for a function call (example: 10 NEAR or 0.5 near or 10000 yoctonear):", + ) + .with_initial_value("1 yoctoNEAR") + .prompt()?, + ) { + Ok(deposit) => Ok(Some(deposit)), + Err(err) => Err(color_eyre::Report::msg(err)), + } + } +} diff --git a/src/commands/tokens/send_ft/mod.rs b/src/commands/tokens/send_ft/mod.rs index 7655b45c0..560dc40e7 100644 --- a/src/commands/tokens/send_ft/mod.rs +++ b/src/commands/tokens/send_ft/mod.rs @@ -1,7 +1,4 @@ -use std::str::FromStr; - -use inquire::Text; -use serde_json::json; +mod amount_ft; #[derive(Debug, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = super::TokensCommandsContext)] @@ -13,19 +10,9 @@ pub struct SendFtCommand { #[interactive_clap(skip_default_input_arg)] /// What is the receiver account ID? receiver_account_id: crate::types::account_id::AccountId, - /// Enter an amount FT to transfer: - amount: u128, - #[interactive_clap(long = "prepaid-gas")] - #[interactive_clap(skip_default_input_arg)] - /// Enter gas for function call: - gas: crate::common::NearGas, - #[interactive_clap(long = "attached-deposit")] - #[interactive_clap(skip_default_input_arg)] - /// Enter deposit for a function call: - deposit: crate::types::near_token::NearToken, #[interactive_clap(named_arg)] - /// Select network - network_config: crate::network_for_transaction::NetworkForTransactionArgs, + /// Specify amount FT + amount_ft: self::amount_ft::AmountFt, } #[derive(Debug, Clone)] @@ -34,9 +21,6 @@ pub struct SendFtCommandContext { signer_account_id: near_primitives::types::AccountId, ft_contract_account_id: near_primitives::types::AccountId, receiver_account_id: near_primitives::types::AccountId, - amount: u128, - gas: crate::common::NearGas, - deposit: crate::types::near_token::NearToken, } impl SendFtCommandContext { @@ -49,75 +33,10 @@ impl SendFtCommandContext { signer_account_id: previous_context.owner_account_id, ft_contract_account_id: scope.ft_contract_account_id.clone().into(), receiver_account_id: scope.receiver_account_id.clone().into(), - amount: scope.amount, - gas: scope.gas, - deposit: scope.deposit, }) } } -impl From for crate::commands::ActionContext { - fn from(item: SendFtCommandContext) -> Self { - let on_after_getting_network_callback: crate::commands::OnAfterGettingNetworkCallback = - std::sync::Arc::new({ - let signer_account_id = item.signer_account_id.clone(); - let ft_contract_account_id = item.ft_contract_account_id.clone(); - let receiver_account_id = item.receiver_account_id.clone(); - - move |_network_config| { - Ok(crate::commands::PrepopulatedTransaction { - signer_id: signer_account_id.clone(), - receiver_id: ft_contract_account_id.clone(), - actions: vec![near_primitives::transaction::Action::FunctionCall( - Box::new(near_primitives::transaction::FunctionCallAction { - method_name: "ft_transfer".to_string(), - args: serde_json::to_vec(&json!({ - "receiver_id": receiver_account_id.to_string(), - "amount": item.amount.to_string() - }))?, - gas: item.gas.as_gas(), - deposit: item.deposit.as_yoctonear(), - }), - )], - }) - } - }); - - let on_after_sending_transaction_callback: crate::transaction_signature_options::OnAfterSendingTransactionCallback = std::sync::Arc::new({ - let signer_account_id = item.signer_account_id.clone(); - let amount = item.amount; - let ft_contract_account_id = item.ft_contract_account_id.clone(); - let receiver_account_id = item.receiver_account_id.clone(); - - move |outcome_view, _network_config| { - if let near_primitives::views::FinalExecutionStatus::SuccessValue(_) = outcome_view.status { - eprintln!( - "<{signer_account_id}> has successfully transferred {amount} FT ({ft_contract_account_id}) to <{receiver_account_id}>.", - ); - } - Ok(()) - } - }); - - Self { - global_context: item.global_context, - interacting_with_account_ids: vec![ - item.ft_contract_account_id, - item.signer_account_id, - item.receiver_account_id, - ], - on_after_getting_network_callback, - on_before_signing_callback: std::sync::Arc::new( - |_prepolulated_unsinged_transaction, _network_config| Ok(()), - ), - on_before_sending_transaction_callback: std::sync::Arc::new( - |_signed_transaction, _network_config, _message| Ok(()), - ), - on_after_sending_transaction_callback, - } - } -} - impl SendFtCommand { pub fn input_ft_contract_account_id( context: &super::TokensCommandsContext, @@ -136,43 +55,4 @@ impl SendFtCommand { "What is the receiver account ID?", ) } - - fn input_gas( - _context: &super::TokensCommandsContext, - ) -> color_eyre::eyre::Result> { - eprintln!(); - let gas = loop { - match crate::common::NearGas::from_str( - &Text::new("Enter gas for function call:") - .with_initial_value("100 TeraGas") - .prompt()?, - ) { - Ok(input_gas) => { - if input_gas <= near_gas::NearGas::from_tgas(300) { - break input_gas; - } else { - eprintln!("You need to enter a value of no more than 300 TERAGAS") - } - } - Err(err) => return Err(color_eyre::Report::msg(err)), - } - }; - Ok(Some(gas)) - } - - fn input_deposit( - _context: &super::TokensCommandsContext, - ) -> color_eyre::eyre::Result> { - eprintln!(); - match crate::types::near_token::NearToken::from_str( - &Text::new( - "Enter deposit for a function call (example: 10NEAR or 0.5near or 10000yoctonear):", - ) - .with_initial_value("1 yoctoNEAR") - .prompt()?, - ) { - Ok(deposit) => Ok(Some(deposit)), - Err(err) => Err(color_eyre::Report::msg(err)), - } - } } diff --git a/src/commands/tokens/view_ft_balance/mod.rs b/src/commands/tokens/view_ft_balance/mod.rs index 02998cd6e..34e287776 100644 --- a/src/commands/tokens/view_ft_balance/mod.rs +++ b/src/commands/tokens/view_ft_balance/mod.rs @@ -30,7 +30,7 @@ impl ViewFtBalanceContext { scope.ft_contract_account_id.clone().into(); move |network_config, block_reference| { - let super::FtMetadata { decimals, symbol } = super::params_ft_metadata( + let crate::types::ft_properties::FtMetadata { decimals, symbol } = crate::types::ft_properties::params_ft_metadata( ft_contract_account_id.clone(), network_config, block_reference.clone(), @@ -54,25 +54,14 @@ impl ViewFtBalanceContext { })?; call_result.print_logs(); let amount: String = call_result.parse_result_from_json()?; - let amount = amount.parse::().unwrap(); - let amount_fmt = { - if amount == 0 { - format!("0 {}", symbol) - } else if (amount % 10u128.pow(decimals as u32)) == 0 { - format!("{} {}", amount / 10u128.pow(decimals as u32), symbol,) - } else { - format!( - "{}.{} {}", - amount / 10u128.pow(decimals as u32), - format!("{:0>decimals$}", amount % 10u128.pow(decimals as u32), decimals=decimals.try_into().unwrap()).trim_end_matches('0'), - symbol - ) - } - }; + let fungible_token = crate::types::ft_properties::FungibleToken::from_params_ft( + amount.parse::()?, + decimals, + symbol + ); eprintln!( - "\n<{}> account has {} (FT-contract: {})", - owner_account_id, amount_fmt, ft_contract_account_id + "\n<{owner_account_id}> account has {fungible_token} (FT-contract: {ft_contract_account_id})" ); Ok(()) } diff --git a/src/types/ft_properties.rs b/src/types/ft_properties.rs new file mode 100644 index 000000000..a8ac84eac --- /dev/null +++ b/src/types/ft_properties.rs @@ -0,0 +1,209 @@ +use color_eyre::eyre::{Context, ContextCompat}; + +use crate::common::CallResultExt; +use crate::common::JsonRpcClientExt; + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd)] +pub struct FungibleToken { + amount: u128, + decimals: u8, + symbol: String, +} + +impl FungibleToken { + pub fn from_params_ft(amount: u128, decimals: u8, symbol: String) -> Self { + Self { + amount, + decimals, + symbol, + } + } + + pub fn normalize(&self, ft_metadata: &FtMetadata) -> color_eyre::eyre::Result { + if ft_metadata.symbol.to_uppercase() != self.symbol.to_uppercase() { + color_eyre::eyre::bail!("Invalid currency symbol") + } else if let Some(decimals_diff) = ft_metadata.decimals.checked_sub(self.decimals) { + let amount = if decimals_diff == 0 { + self.amount + } else { + self.amount + .checked_mul( + 10u128 + .checked_pow(decimals_diff.into()) + .wrap_err("Overflow in decimal normalization")?, + ) + .wrap_err("Overflow in decimal normalization")? + }; + Ok(Self { + symbol: ft_metadata.symbol.clone(), + decimals: ft_metadata.decimals, + amount, + }) + } else { + color_eyre::eyre::bail!( + "Invalid decimal places. Your FT amount exceeds {} decimal places.", + ft_metadata.decimals + ) + } + } + + pub fn amount(&self) -> u128 { + self.amount + } + + pub fn decimals(&self) -> u8 { + self.decimals + } + + pub fn symbol(&self) -> &str { + &self.symbol + } +} + +impl std::fmt::Display for FungibleToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let one_ft: u128 = 10u128 + .checked_pow(self.decimals.into()) + .wrap_err("Overflow in FungibleToken normalization") + .unwrap(); + if self.amount == 0 { + write!(f, "0 {}", self.symbol) + } else if self.amount % one_ft == 0 { + write!(f, "{} {}", self.amount / one_ft, self.symbol) + } else { + write!( + f, + "{}.{} {}", + self.amount / one_ft, + format!( + "{:0>decimals$}", + (self.amount % one_ft), + decimals = self.decimals.into() + ) + .trim_end_matches('0'), + self.symbol + ) + } + } +} + +impl std::str::FromStr for FungibleToken { + type Err = String; + + fn from_str(s: &str) -> Result { + let num = s.trim().trim_end_matches(char::is_alphabetic).trim(); + let currency = s.trim().trim_start_matches(num).trim().to_string(); + let res_split: Vec<&str> = num.split('.').collect(); + match res_split.len() { + 2 => { + let num_int_part = res_split[0] + .parse::() + .map_err(|err| format!("FungibleToken: {}", err))?; + let len_fract: u8 = res_split[1] + .trim_end_matches('0') + .len() + .try_into() + .map_err(|_| "Error converting len_fract to u8")?; + let num_fract_part = res_split[1] + .trim_end_matches('0') + .parse::() + .map_err(|err| format!("FungibleToken: {}", err))?; + let amount = num_int_part + .checked_mul( + 10u128 + .checked_pow(len_fract.into()) + .ok_or("FT Balance: overflow happens")?, + ) + .ok_or("FungibleToken: overflow happens")? + .checked_add(num_fract_part) + .ok_or("FungibleToken: overflow happens")?; + Ok(Self { + amount, + decimals: len_fract, + symbol: currency, + }) + } + 1 => { + if res_split[0].starts_with('0') && res_split[0] != "0" { + return Err("FungibleToken: incorrect number entered".to_string()); + }; + let amount = res_split[0] + .parse::() + .map_err(|err| format!("FungibleToken: {}", err))?; + Ok(Self { + amount, + decimals: 0, + symbol: currency, + }) + } + _ => Err("FungibleToken: incorrect number entered".to_string()), + } + } +} + +impl interactive_clap::ToCli for FungibleToken { + type CliVariant = FungibleToken; +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, serde::Deserialize)] +pub struct FtMetadata { + pub symbol: String, + pub decimals: u8, +} + +pub fn params_ft_metadata( + ft_contract_account_id: near_primitives::types::AccountId, + network_config: &crate::config::NetworkConfig, + block_reference: near_primitives::types::BlockReference, +) -> color_eyre::eyre::Result { + let ft_metadata: FtMetadata = network_config + .json_rpc_client() + .blocking_call_view_function( + &ft_contract_account_id, + "ft_metadata", + vec![], + block_reference, + ) + .wrap_err_with(||{ + format!("Failed to fetch query for view method: 'ft_metadata' (contract <{}> on network <{}>)", + ft_contract_account_id, + network_config.network_name + ) + })? + .parse_result_from_json()?; + Ok(ft_metadata) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn ft_token_to_string_0_wnear() { + let ft_token = FungibleToken::from_str("0 wNEAR").unwrap(); + assert_eq!(ft_token.to_string(), "0 wNEAR".to_string()); + assert_eq!(ft_token.symbol, "wNEAR".to_string()); + assert_eq!(ft_token.decimals, 0) + } + #[test] + fn ft_token_to_string_10_wnear() { + let ft_token = FungibleToken::from_str("10 wNEAR").unwrap(); + assert_eq!(ft_token.to_string(), "10 wNEAR".to_string()); + assert_eq!(ft_token.symbol, "wNEAR".to_string()); + assert_eq!(ft_token.decimals, 0) + } + #[test] + fn ft_token_to_string_0dot0200_wnear() { + let ft_token = FungibleToken::from_str("0.0200 wNEAR").unwrap(); + assert_eq!(ft_token.to_string(), "0.02 wNEAR".to_string()); + assert_eq!(ft_token.symbol, "wNEAR".to_string()); + assert_eq!(ft_token.decimals, 2) + } + #[test] + fn ft_token_to_string_0dot123456_usdc() { + let ft_token = FungibleToken::from_str("0.123456 USDC").unwrap(); + assert_eq!(ft_token.to_string(), "0.123456 USDC".to_string()); + assert_eq!(ft_token.symbol, "USDC".to_string()); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index bbea49f80..bff6b93d9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,6 +3,7 @@ pub mod api_key; pub mod base64_bytes; pub mod crypto_hash; pub mod file_bytes; +pub mod ft_properties; pub mod json; pub mod near_token; pub mod path_buf;