diff --git a/Cargo.lock b/Cargo.lock index 87e5032b8b..475508ffe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,6 +1129,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "url", ] [[package]] diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 26396617bb..6c0d88b452 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -35,6 +35,8 @@ pub enum ValidatePaymentError { WatcherRewardError(String), /// Input payment timelock overflows the type used by specific coin. TimelockOverflow(TryFromIntError), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for ValidatePaymentError { @@ -79,6 +81,7 @@ impl From for ValidatePaymentError { Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { ValidatePaymentError::InternalError(internal) }, + Web3RpcError::NftProtocolNotSupported => ValidatePaymentError::NftProtocolNotSupported, } } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 27f1c10c3f..83a7f9d77f 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -24,7 +24,8 @@ use super::eth::Action::{Call, Create}; use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; use crate::lp_price::get_base_price_in_rel; -use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; +use crate::nft::nft_structs::{ContractType, ConvertChain, NftInfo, TransactionNftDetails, WithdrawErc1155, + WithdrawErc721}; use crate::{DexFee, RpcCommonOps, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; @@ -224,6 +225,8 @@ pub enum Web3RpcError { Timeout(String), #[display(fmt = "Internal: {}", _0)] Internal(String), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for Web3RpcError { @@ -266,6 +269,9 @@ impl From for RawTransactionError { Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { RawTransactionError::InternalError(internal) }, + Web3RpcError::NftProtocolNotSupported => { + RawTransactionError::InternalError("Nft Protocol is not supported yet!".to_string()) + }, } } } @@ -307,6 +313,7 @@ impl From for WithdrawError { Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { WithdrawError::InternalError(internal) }, + Web3RpcError::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported, } } } @@ -322,6 +329,7 @@ impl From for TradePreimageError { Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { TradePreimageError::InternalError(internal) }, + Web3RpcError::NftProtocolNotSupported => TradePreimageError::NftProtocolNotSupported, } } } @@ -351,6 +359,9 @@ impl From for BalanceError { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => BalanceError::Transport(tr), Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => BalanceError::Internal(internal), + Web3RpcError::NftProtocolNotSupported => { + BalanceError::Internal("Nft Protocol is not supported yet!".to_string()) + }, } } } @@ -381,7 +392,13 @@ pub enum EthCoinType { Eth, /// ERC20 token with smart contract address /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md - Erc20 { platform: String, token_addr: Address }, + Erc20 { + platform: String, + token_addr: Address, + }, + Nft { + platform: String, + }, } /// An alternative to `crate::PrivKeyBuildPolicy`, typical only for ETH coin. @@ -447,6 +464,10 @@ pub struct EthCoinImpl { logs_block_range: u64, nonce_lock: Arc>, erc20_tokens_infos: Arc>>, + /// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key + /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets + /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. + pub nfts_infos: Arc>>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, @@ -595,7 +616,7 @@ impl EthCoinImpl { pub fn erc20_token_address(&self) -> Option
{ match self.coin_type { EthCoinType::Erc20 { token_addr, .. } => Some(token_addr), - EthCoinType::Eth => None, + EthCoinType::Eth | EthCoinType::Nft { .. } => None, } } @@ -680,6 +701,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; (0.into(), data, *token_addr, platform.as_str()) }, + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; @@ -848,6 +870,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit "Erc20 coin type doesnt support withdraw nft".to_owned(), )) }, + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; let (gas, gas_price) = get_eth_gas_details( ð_coin, @@ -934,6 +957,8 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd "Erc20 coin type doesnt support withdraw nft".to_owned(), )) }, + // TODO: start to use NFT GLOBAL TOKEN for withdraw + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; let (gas, gas_price) = get_eth_gas_details( ð_coin, @@ -1645,6 +1670,7 @@ impl WatcherOps for EthCoin { ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -1885,6 +1911,7 @@ impl WatcherOps for EthCoin { ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -1977,6 +2004,11 @@ impl WatcherOps for EthCoin { })? } }, + EthCoinType::Nft { .. } => { + return MmError::err(WatcherRewardError::InternalError( + "Nft Protocol is not supported yet!".to_string(), + )) + }, } }, }; @@ -2074,7 +2106,7 @@ impl MarketCoinOps for EthCoin { fn platform_ticker(&self) -> &str { match &self.coin_type { EthCoinType::Eth => self.ticker(), - EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => platform, } } @@ -2227,6 +2259,11 @@ impl MarketCoinOps for EthCoin { let func_name = match self.coin_type { EthCoinType::Eth => get_function_name("ethPayment", args.watcher_reward), EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", args.watcher_reward), + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, }; let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&func_name)); @@ -2910,6 +2947,14 @@ impl EthCoin { let fee_coin = match &self.coin_type { EthCoinType::Eth => self.ticker(), EthCoinType::Erc20 { platform, .. } => platform.as_str(), + EthCoinType::Nft { .. } => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting fee coin: Nft Protocol is not supported yet!"), + ); + continue; + }, }; let fee_details: Option = match receipt { Some(r) => { @@ -3282,6 +3327,14 @@ impl EthCoin { let fee_coin = match &self.coin_type { EthCoinType::Eth => self.ticker(), EthCoinType::Erc20 { platform, .. } => platform.as_str(), + EthCoinType::Nft { .. } => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting fee coin: Nft Protocol is not supported yet!"), + ); + continue; + }, }; let fee_details = match receipt { Some(r) => { @@ -3399,6 +3452,11 @@ impl EthCoin { let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); self.sign_and_send_transaction(0.into(), Action::Call(*token_addr), data, U256::from(210_000)) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -3559,6 +3617,11 @@ impl EthCoin { } })) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -3675,6 +3738,11 @@ impl EthCoin { }), ) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -3795,6 +3863,11 @@ impl EthCoin { }), ) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -3912,6 +3985,11 @@ impl EthCoin { }), ) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -4030,6 +4108,11 @@ impl EthCoin { }), ) }, + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, } } @@ -4052,6 +4135,9 @@ impl EthCoin { }, } }, + EthCoinType::Nft { .. } => { + MmError::err(BalanceError::Internal("Nft Protocol is not supported yet!".to_string())) + }, } }; Box::new(fut.boxed().compat()) @@ -4093,7 +4179,7 @@ impl EthCoin { async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult { let wallet_amount_uint = match self.coin_type { - EthCoinType::Eth => { + EthCoinType::Eth | EthCoinType::Nft { .. } => { let function = ERC1155_CONTRACT.function("balanceOf")?; let token_id_u256 = U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; @@ -4120,7 +4206,7 @@ impl EthCoin { async fn erc721_owner(&self, token_addr: Address, token_id: &str) -> MmResult { let owner_address = match self.coin_type { - EthCoinType::Eth => { + EthCoinType::Eth | EthCoinType::Nft { .. } => { let function = ERC721_CONTRACT.function("ownerOf")?; let token_id_u256 = U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; @@ -4226,6 +4312,7 @@ impl EthCoin { }, } }, + EthCoinType::Nft { .. } => MmError::err(Web3RpcError::NftProtocolNotSupported), } }; Box::new(fut.boxed().compat()) @@ -4271,6 +4358,11 @@ impl EthCoin { let token_addr = match coin.coin_type { EthCoinType::Eth => return TX_PLAIN_ERR!("'approve' is expected to be call for ERC20 coins only"), EthCoinType::Erc20 { token_addr, .. } => token_addr, + EthCoinType::Nft { .. } => { + return Err(TransactionErr::NftProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + ))) + }, }; let function = try_tx_s!(ERC20_CONTRACT.function("approve")); let data = try_tx_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); @@ -4608,6 +4700,7 @@ impl EthCoin { ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -4653,6 +4746,7 @@ impl EthCoin { let func_name = match self.coin_type { EthCoinType::Eth => get_function_name("ethPayment", watcher_reward), EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", watcher_reward), + EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), }; let payment_func = try_s!(SWAP_CONTRACT.function(&func_name)); @@ -5103,6 +5197,7 @@ impl MmCoin for EthCoin { match coin.coin_type { EthCoinType::Eth => coin.process_eth_history(&ctx).await, EthCoinType::Erc20 { ref token_addr, .. } => coin.process_erc20_history(*token_addr, &ctx).await, + EthCoinType::Nft {..} => return Err(()) } Ok(()) }; @@ -5122,6 +5217,7 @@ impl MmCoin for EthCoin { let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), }; Ok(TradeFee { coin: fee_coin.into(), @@ -5170,6 +5266,7 @@ impl MmCoin for EthCoin { U256::from(300_000) } }, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; let total_fee = gas_limit * gas_price; @@ -5177,6 +5274,7 @@ impl MmCoin for EthCoin { let fee_coin = match &self.coin_type { EthCoinType::Eth => &self.ticker, EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; Ok(TradeFee { coin: fee_coin.into(), @@ -5195,6 +5293,7 @@ impl MmCoin for EthCoin { let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; Ok(TradeFee { coin: fee_coin.into(), @@ -5222,6 +5321,7 @@ impl MmCoin for EthCoin { let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)])?; (0.into(), data, token_addr, platform) }, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; let gas_price = self.get_gas_price().compat().await?; @@ -5461,6 +5561,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - }, } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -5826,6 +5927,7 @@ pub async fn eth_coin_from_conf_and_request( let key_lock = match &coin_type { EthCoinType::Eth => String::from(ticker), EthCoinType::Erc20 { ref platform, .. } => String::from(platform), + EthCoinType::Nft { .. } => return ERR!("Does not support NFT protocol"), }; let nonce_lock = { @@ -5858,6 +5960,7 @@ pub async fn eth_coin_from_conf_and_request( logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), nonce_lock, erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system, }; @@ -6015,6 +6118,8 @@ pub enum EthGasDetailsErr { Internal(String), #[display(fmt = "Transport: {}", _0)] Transport(String), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for EthGasDetailsErr { @@ -6026,6 +6131,7 @@ impl From for EthGasDetailsErr { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => EthGasDetailsErr::Transport(tr), Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => EthGasDetailsErr::Internal(internal), + Web3RpcError::NftProtocolNotSupported => EthGasDetailsErr::NftProtocolNotSupported, } } } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 1684d40faa..c7e32a78ce 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -131,6 +131,7 @@ fn eth_coin_from_keypair( let ticker = match coin_type { EthCoinType::Eth => "ETH".to_string(), EthCoinType::Erc20 { .. } => "JST".to_string(), + EthCoinType::Nft { ref platform } => platform.to_string(), }; let eth_coin = EthCoin(Arc::new(EthCoinImpl { @@ -154,6 +155,7 @@ fn eth_coin_from_keypair( logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); (ctx, eth_coin) @@ -364,6 +366,7 @@ fn test_nonce_several_urls() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); @@ -415,6 +418,7 @@ fn test_wait_for_payment_spend_timeout() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), }; @@ -1126,6 +1130,7 @@ fn test_message_hash() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); @@ -1173,6 +1178,7 @@ fn test_sign_verify_message() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); @@ -1229,6 +1235,7 @@ fn test_eth_extract_secret() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index f0c94aadfd..ea2dace0cd 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -49,6 +49,7 @@ async fn test_send() { logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, nonce_lock: new_nonce_lock(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system: AbortableQueue::default(), })); let maker_payment_args = SendPaymentArgs { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index f340dec7f6..1573a17cf7 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,4 +1,7 @@ use super::*; +use crate::nft::get_nfts_for_activation; +use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; +use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; use crypto::{CryptoCtxError, StandardHDCoinAddress}; @@ -7,6 +10,8 @@ use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; +use std::sync::atomic::Ordering; +use url::Url; use web3_transport::websocket_transport::WebsocketTransport; #[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] @@ -39,6 +44,7 @@ pub enum EthActivationV2Error { #[from_trait(WithInternal::internal)] #[display(fmt = "Internal: {}", _0)] InternalError(String), + Transport(String), } impl From for EthActivationV2Error { @@ -57,11 +63,28 @@ impl From for EthActivationV2Error { fn from(e: UnexpectedDerivationMethod) -> Self { EthActivationV2Error::InternalError(e.to_string()) } } +impl From for EthActivationV2Error { + fn from(e: EthTokenActivationError) -> Self { + match e { + EthTokenActivationError::InternalError(err) => EthActivationV2Error::InternalError(err), + EthTokenActivationError::CouldNotFetchBalance(err) => EthActivationV2Error::CouldNotFetchBalance(err), + EthTokenActivationError::InvalidPayload(err) => EthActivationV2Error::InvalidPayload(err), + EthTokenActivationError::Transport(err) | EthTokenActivationError::ClientConnectionFailed(err) => { + EthActivationV2Error::Transport(err) + }, + } + } +} + #[cfg(target_arch = "wasm32")] impl From for EthActivationV2Error { fn from(e: MetamaskError) -> Self { from_metamask_error(e) } } +impl From for EthActivationV2Error { + fn from(e: ParseChainTypeError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + /// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. #[derive(Clone, Deserialize)] pub enum EthPrivKeyActivationPolicy { @@ -116,30 +139,99 @@ pub struct EthNode { #[derive(Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] -pub enum Erc20TokenActivationError { +pub enum EthTokenActivationError { InternalError(String), ClientConnectionFailed(String), CouldNotFetchBalance(String), + InvalidPayload(String), + Transport(String), } -impl From for Erc20TokenActivationError { - fn from(e: AbortedError) -> Self { Erc20TokenActivationError::InternalError(e.to_string()) } +impl From for EthTokenActivationError { + fn from(e: AbortedError) -> Self { EthTokenActivationError::InternalError(e.to_string()) } } -impl From for Erc20TokenActivationError { +impl From for EthTokenActivationError { fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } } +impl From for EthTokenActivationError { + fn from(e: GetNftInfoError) -> Self { + match e { + GetNftInfoError::InvalidRequest(err) => EthTokenActivationError::InvalidPayload(err), + GetNftInfoError::ContractTypeIsNull => EthTokenActivationError::InvalidPayload( + "The contract type is required and should not be null.".to_string(), + ), + GetNftInfoError::Transport(err) | GetNftInfoError::InvalidResponse(err) => { + EthTokenActivationError::Transport(err) + }, + GetNftInfoError::Internal(err) | GetNftInfoError::DbError(err) | GetNftInfoError::NumConversError(err) => { + EthTokenActivationError::InternalError(err) + }, + GetNftInfoError::GetEthAddressError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::ParseRfc3339Err(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::ProtectFromSpamError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::TransferConfirmationsError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::TokenNotFoundInWallet { + token_address, + token_id, + } => EthTokenActivationError::InternalError(format!( + "Token not found in wallet: {}, {}", + token_address, token_id + )), + } + } +} + +impl From for EthTokenActivationError { + fn from(e: ParseChainTypeError) -> Self { EthTokenActivationError::InternalError(e.to_string()) } +} + +/// Represents the parameters required for activating either an ERC-20 token or an NFT on the Ethereum platform. +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum EthTokenActivationParams { + Nft(NftActivationRequest), + Erc20(Erc20TokenActivationRequest), +} + +/// Holds ERC-20 token-specific activation parameters, including optional confirmation requirements. #[derive(Clone, Deserialize)] pub struct Erc20TokenActivationRequest { pub required_confirmations: Option, } +/// Encapsulates the request parameters for NFT activation, specifying the provider to be used. +#[derive(Clone, Deserialize)] +pub struct NftActivationRequest { + pub provider: NftProviderEnum, +} + +/// Defines available NFT providers and their configuration. +#[derive(Clone, Deserialize)] +#[serde(tag = "type", content = "info")] +pub enum NftProviderEnum { + Moralis { url: Url }, +} + +/// Represents the protocol type for an Ethereum-based token, distinguishing between ERC-20 tokens and NFTs. +pub enum EthTokenProtocol { + Erc20(Erc20Protocol), + Nft(NftProtocol), +} + +/// Details for an ERC-20 token protocol. pub struct Erc20Protocol { pub platform: String, pub token_addr: Address, } +/// Details for an NFT protocol. +#[derive(Debug)] +pub struct NftProtocol { + pub platform: String, +} + #[cfg_attr(test, mockable)] impl EthCoin { pub async fn initialize_erc20_token( @@ -147,13 +239,13 @@ impl EthCoin { activation_params: Erc20TokenActivationRequest, protocol: Erc20Protocol, ticker: String, - ) -> MmResult { + ) -> MmResult { // TODO // Check if ctx is required. // Remove it to avoid circular references if possible let ctx = MmArc::from_weak(&self.ctx) .ok_or_else(|| String::from("No context")) - .map_err(Erc20TokenActivationError::InternalError)?; + .map_err(EthTokenActivationError::InternalError)?; let conf = coin_conf(&ctx, &ticker); @@ -162,11 +254,11 @@ impl EthCoin { &self .web3() .await - .map_err(|e| Erc20TokenActivationError::ClientConnectionFailed(e.to_string()))?, + .map_err(|e| EthTokenActivationError::ClientConnectionFailed(e.to_string()))?, protocol.token_addr, ) .await - .map_err(Erc20TokenActivationError::InternalError)?, + .map_err(EthTokenActivationError::InternalError)?, Some(d) => d as u8, }; @@ -221,11 +313,59 @@ impl EthCoin { logs_block_range: self.logs_block_range, nonce_lock: self.nonce_lock.clone(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system, }; Ok(EthCoin(Arc::new(token))) } + + /// Initializes a Global NFT instance for a specific blockchain platform (e.g., Ethereum, Polygon). + /// + /// A "Global NFT" consolidates information about all NFTs owned by a user into a single `EthCoin` instance, + /// avoiding the need for separate instances for each NFT. + /// The function configures the necessary settings for the Global NFT, including web3 connections and confirmation requirements. + /// It fetches NFT details from a given URL to populate the `nfts_infos` field, which stores information about the user's NFTs. + /// + /// This setup allows the Global NFT to function like a coin, supporting swap operations and providing easy access to NFT details via `nfts_infos`. + pub async fn global_nft_from_platform_coin(&self, url: &Url) -> MmResult { + let chain = Chain::from_ticker(self.ticker())?; + let ticker = chain.to_nft_ticker().to_string(); + + // Create an abortable system linked to the `platform_coin` (which is self) so if the platform coin is disabled, + // all spawned futures related to global Non-Fungible Token will be aborted as well. + let abortable_system = self.abortable_system.create_subsystem()?; + + let nft_infos = get_nfts_for_activation(&chain, &self.my_address, url).await?; + + let global_nft = EthCoinImpl { + ticker, + coin_type: EthCoinType::Nft { + platform: self.ticker.clone(), + }, + priv_key_policy: self.priv_key_policy.clone(), + my_address: self.my_address, + sign_message_prefix: self.sign_message_prefix.clone(), + swap_contract_address: self.swap_contract_address, + fallback_swap_contract: self.fallback_swap_contract, + contract_supports_watchers: self.contract_supports_watchers, + web3_instances: self.web3_instances.lock().await.clone().into(), + decimals: self.decimals, + gas_station_url: self.gas_station_url.clone(), + gas_station_decimals: self.gas_station_decimals, + gas_station_policy: self.gas_station_policy.clone(), + history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + required_confirmations: AtomicU64::new(self.required_confirmations.load(Ordering::Relaxed)), + ctx: self.ctx.clone(), + chain_id: self.chain_id, + logs_block_range: self.logs_block_range, + nonce_lock: self.nonce_lock.clone(), + erc20_tokens_infos: Default::default(), + nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), + abortable_system, + }; + Ok(EthCoin(Arc::new(global_nft))) + } } pub async fn eth_coin_from_conf_and_request_v2( @@ -329,6 +469,7 @@ pub async fn eth_coin_from_conf_and_request_v2( logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), nonce_lock, erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), abortable_system, }; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 4545e70718..5179a5db80 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -371,9 +371,9 @@ pub enum RawTransactionError { impl HttpStatusCode for RawTransactionError { fn status_code(&self) -> StatusCode { match self { - RawTransactionError::Transport(_) - | RawTransactionError::InternalError(_) - | RawTransactionError::SigningError(_) => StatusCode::INTERNAL_SERVER_ERROR, + RawTransactionError::InternalError(_) | RawTransactionError::SigningError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, RawTransactionError::NoSuchCoin { .. } | RawTransactionError::InvalidHashError(_) | RawTransactionError::HashNotExist(_) @@ -382,6 +382,7 @@ impl HttpStatusCode for RawTransactionError { | RawTransactionError::NonExistentPrevOutputError(_) | RawTransactionError::TransactionError(_) => StatusCode::BAD_REQUEST, RawTransactionError::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, + RawTransactionError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -632,13 +633,14 @@ pub enum TxMarshalingErr { Internal(String), } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] #[allow(clippy::large_enum_variant)] pub enum TransactionErr { /// Keeps transactions while throwing errors. TxRecoverable(TransactionEnum, String), /// Simply for plain error messages. Plain(String), + NftProtocolNotSupported(String), } impl TransactionErr { @@ -657,6 +659,7 @@ impl TransactionErr { match self { TransactionErr::TxRecoverable(_, err) => err.to_string(), TransactionErr::Plain(err) => err.to_string(), + TransactionErr::NftProtocolNotSupported(err) => err.to_string(), } } } @@ -2235,6 +2238,8 @@ pub enum TradePreimageError { Transport(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for TradePreimageError { @@ -2412,7 +2417,8 @@ impl HttpStatusCode for StakingInfosError { StakingInfosError::NoSuchCoin { .. } | StakingInfosError::CoinDoesntSupportStakingInfos { .. } | StakingInfosError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, - StakingInfosError::Transport(_) | StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StakingInfosError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -2532,7 +2538,8 @@ impl From for DelegationError { impl HttpStatusCode for DelegationError { fn status_code(&self) -> StatusCode { match self { - DelegationError::Transport(_) | DelegationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + DelegationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + DelegationError::Transport(_) => StatusCode::BAD_GATEWAY, _ => StatusCode::BAD_REQUEST, } } @@ -2686,6 +2693,8 @@ pub enum WithdrawError { my_address: String, token_owner: String, }, + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl HttpStatusCode for WithdrawError { @@ -2715,9 +2724,10 @@ impl HttpStatusCode for WithdrawError { WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, - WithdrawError::Transport(_) | WithdrawError::InternalError(_) | WithdrawError::DbError(_) => { + WithdrawError::InternalError(_) | WithdrawError::DbError(_) | WithdrawError::NftProtocolNotSupported => { StatusCode::INTERNAL_SERVER_ERROR }, + WithdrawError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -2769,6 +2779,7 @@ impl From for WithdrawError { EthGasDetailsErr::InvalidFeePolicy(e) => WithdrawError::InvalidFeePolicy(e), EthGasDetailsErr::Internal(e) => WithdrawError::InternalError(e), EthGasDetailsErr::Transport(e) => WithdrawError::Transport(e), + EthGasDetailsErr::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported, } } } @@ -3430,10 +3441,17 @@ impl CoinsContext { self.add_token(coin).await } + /// Adds a platform coin and its associated tokens to the CoinsContext. + /// + /// Registers a platform coin alongside its associated ERC-20 tokens and optionally a global NFT. + /// Regular tokens are added to the context without overwriting existing entries, preserving any previously activated tokens. + /// In contrast, the global NFT, if provided, replaces any previously stored NFT data for the platform, ensuring the NFT info is up-to-date. + /// An error is returned if the platform coin is already activated within the context, enforcing a single active instance for each platform. pub async fn add_platform_with_tokens( &self, platform: MmCoinEnum, tokens: Vec, + global_nft: Option, ) -> Result<(), MmError> { let mut coins = self.coins.lock().await; let mut platform_coin_tokens = self.platform_coin_tokens.lock(); @@ -3464,6 +3482,11 @@ impl CoinsContext { .entry(token.ticker().into()) .or_insert_with(|| MmCoinStruct::new(token)); } + if let Some(nft) = global_nft { + token_tickers.insert(nft.ticker().to_string()); + // For NFT overwrite existing data + coins.insert(nft.ticker().into(), MmCoinStruct::new(nft)); + } platform_coin_tokens .entry(platform_ticker) @@ -3746,6 +3769,9 @@ pub enum CoinProtocol { decimals: u8, }, ZHTLC(ZcoinProtocolInfo), + Nft { + platform: String, + }, } pub type RpcTransportEventHandlerShared = Arc; @@ -3998,6 +4024,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TENDERMINT protocol is not supported by lp_coininit"), CoinProtocol::TENDERMINTTOKEN(_) => return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), + CoinProtocol::Nft { .. } => return ERR!("NFT protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] @@ -4541,7 +4568,7 @@ pub fn address_by_coin_conf_and_pubkey_str( ) -> Result { let protocol: CoinProtocol = try_s!(json::from_value(conf["protocol"].clone())); match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH => eth::addr_from_pubkey_str(pubkey), + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::Nft { .. } => eth::addr_from_pubkey_str(pubkey), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, @@ -4881,7 +4908,7 @@ mod tests { let coin = MmCoinEnum::Test(TestCoin::new(RICK)); // Add test coin to coins context - common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap(); + common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); // Try to find RICK from coins context that was added above let _found = common::block_on(lp_coinfind(&ctx, RICK)).unwrap(); @@ -4906,7 +4933,7 @@ mod tests { let coin = MmCoinEnum::Test(TestCoin::new(RICK)); // Add test coin to coins context - common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap(); + common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); // Try to find RICK from coins context that was added above let _found = common::block_on(lp_coinfind_any(&ctx, RICK)).unwrap(); diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 5dd6ede8ca..8e73e2b272 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -3,12 +3,13 @@ use mm2_err_handle::prelude::{MmError, MmResult}; use url::Url; pub(crate) mod nft_errors; -pub(crate) mod nft_structs; +pub mod nft_structs; pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::{coin_conf, get_my_address, lp_coinfind_or_err, MarketCoinOps, MmCoinEnum, MyAddressReq, WithdrawError}; +use crate::{coin_conf, get_my_address, lp_coinfind_or_err, CoinsContext, MarketCoinOps, MmCoinEnum, MmCoinStruct, + MyAddressReq, WithdrawError}; use nft_errors::{GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, @@ -18,7 +19,7 @@ use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_er EthTxFeeDetails}; use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, UpdateSpamPhishingError}; -use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, +use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, NftTransferCommon, PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq, SpamContractReq, SpamContractRes, TransferMeta, TransferStatus, UriMeta}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; @@ -31,6 +32,7 @@ use mm2_err_handle::map_to_mm::MapToMmResult; use mm2_net::transport::send_post_request_to_uri; use mm2_number::BigUint; use regex::Regex; +use serde::Deserialize; use serde_json::Value as Json; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -94,7 +96,6 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult MmResult MmResult { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; @@ -133,7 +121,6 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult MmResult`: A result indicating success or an error. pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNftError> { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; @@ -306,6 +283,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft &req.url_antispam, ) .await?; + update_nft_global_in_coins_ctx(&ctx, &storage, *chain).await?; update_transfers_with_empty_meta(&storage, chain, &req.url, &req.url_antispam).await?; update_spam(&storage, *chain, &req.url_antispam).await?; update_phishing(&storage, chain, &req.url_antispam).await?; @@ -313,6 +291,63 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft Ok(()) } +/// Updates the global NFT information in the coins context. +/// +/// This function uses the up-to-date NFT list for a given chain and updates the +/// corresponding global NFT information in the coins context. +async fn update_nft_global_in_coins_ctx(ctx: &MmArc, storage: &T, chain: Chain) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let coins_ctx = CoinsContext::from_ctx(ctx).map_to_mm(UpdateNftError::Internal)?; + let mut coins = coins_ctx.coins.lock().await; + let ticker = chain.to_nft_ticker(); + + if let Some(MmCoinStruct { + inner: MmCoinEnum::EthCoin(nft_global), + .. + }) = coins.get_mut(ticker) + { + let nft_list = storage.get_nft_list(vec![chain], true, 1, None, None).await?; + update_nft_infos(nft_global, nft_list.nfts).await; + return Ok(()); + } + // if global NFT is None in CoinsContext, then it's just not activated + Ok(()) +} + +/// Updates the global NFT information with the latest NFT list. +/// +/// This function replaces the existing NFT information (`nfts_infos`) in the global NFT with the new data provided by `nft_list`. +/// The `nft_list` must be current, accurately reflecting the NFTs presently owned by the user. +/// This includes accounting for any changes such as NFTs that have been transferred away, so user is not owner anymore, +/// or changes in the amounts of ERC1155 tokens. +/// Ensuring the data's accuracy is vital for maintaining a correct representation of ownership in the global NFT. +/// +/// # Warning +/// Using an outdated `nft_list` for this operation may result in incorrect NFT information persisting in the global NFT, +/// potentially leading to inconsistencies with the actual state of NFT ownership. +async fn update_nft_infos(nft_global: &mut EthCoin, nft_list: Vec) { + let new_nft_infos: HashMap = nft_list + .into_iter() + .map(|nft| { + let key = format!("{},{}", nft.common.token_address, nft.token_id); + let nft_info = NftInfo { + token_address: nft.common.token_address, + token_id: nft.token_id, + chain: nft.chain, + contract_type: nft.contract_type, + amount: nft.common.amount, + }; + (key, nft_info) + }) + .collect(); + + let mut global_nft_infos = nft_global.nfts_infos.lock().await; + // we can do this as some `global_nft_infos` keys may not present in `new_nft_infos`, so we will have to remove them anyway + *global_nft_infos = new_nft_infos; +} + /// `update_spam` function updates spam contracts info in NFT list and NFT transfers. async fn update_spam(storage: &T, chain: Chain, url_antispam: &Url) -> MmResult<(), UpdateSpamPhishingError> where @@ -418,15 +453,6 @@ fn prepare_uri_for_blocklist_endpoint( /// phishing domains using the provided `url_antispam`. If the fetched metadata or its domain /// is identified as spam or matches with any phishing domains, the NFT's `possible_spam` and/or /// `possible_phishing` flags are set to true. -/// -/// # Arguments -/// -/// * `ctx`: Context required for handling internal operations. -/// * `req`: A request containing details about the NFT whose metadata needs to be refreshed. -/// -/// # Returns -/// -/// * `MmResult<(), UpdateNftError>`: A result indicating success or an error. pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResult<(), UpdateNftError> { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; @@ -585,19 +611,7 @@ async fn get_moralis_nft_list( let ticker = chain.to_ticker(); let conf = coin_conf(ctx, ticker); let my_address = get_eth_address(ctx, &conf, ticker, &StandardHDCoinAddress::default()).await?; - - let mut uri_without_cursor = url.clone(); - uri_without_cursor.set_path(MORALIS_API_ENDPOINT); - uri_without_cursor - .path_segments_mut() - .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? - .push(&my_address.wallet_address) - .push("nft"); - uri_without_cursor - .query_pairs_mut() - .append_pair("chain", &chain.to_string()) - .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); - drop_mutability!(uri_without_cursor); + let uri_without_cursor = construct_moralis_uri_for_nft(url, &my_address.wallet_address, chain)?; // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); @@ -606,7 +620,7 @@ async fn get_moralis_nft_list( let response = send_request_to_uri(uri.as_str()).await?; if let Some(nfts_list) = response["result"].as_array() { for nft_json in nfts_list { - let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string())?; + let nft_moralis = NftFromMoralis::deserialize(nft_json)?; let contract_type = match nft_moralis.contract_type { Some(contract_type) => contract_type, None => continue, @@ -624,12 +638,68 @@ async fn get_moralis_nft_list( } else { break; } + } else { + break; } } - drop_mutability!(res_list); Ok(res_list) } +pub(crate) async fn get_nfts_for_activation( + chain: &Chain, + my_address: &Address, + url: &Url, +) -> MmResult, GetNftInfoError> { + let mut nfts_map = HashMap::new(); + let uri_without_cursor = construct_moralis_uri_for_nft(url, ð_addr_to_hex(my_address), chain)?; + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_request_to_uri(uri.as_str()).await?; + if let Some(nfts_list) = response["result"].as_array() { + process_nft_list_for_activation(nfts_list, chain, &mut nfts_map)?; + // if cursor is not null, there are other NFTs on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } else { + break; + } + } + Ok(nfts_map) +} + +fn process_nft_list_for_activation( + nfts_list: &[Json], + chain: &Chain, + nfts_map: &mut HashMap, +) -> MmResult<(), GetNftInfoError> { + for nft_json in nfts_list { + let nft_moralis = NftFromMoralis::deserialize(nft_json)?; + let contract_type = match nft_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let token_address_str = eth_addr_to_hex(&nft_moralis.common.token_address); + let nft_info = NftInfo { + token_address: nft_moralis.common.token_address, + token_id: nft_moralis.token_id.0.clone(), + chain: *chain, + contract_type, + amount: nft_moralis.common.amount, + }; + let key = format!("{},{}", token_address_str, nft_moralis.token_id.0); + nfts_map.insert(key, nft_info); + } + Ok(()) +} + async fn get_moralis_nft_transfers( ctx: &MmArc, chain: &Chain, @@ -668,51 +738,7 @@ async fn get_moralis_nft_transfers( let uri = format!("{}{}", uri_without_cursor, cursor); let response = send_request_to_uri(uri.as_str()).await?; if let Some(transfer_list) = response["result"].as_array() { - for transfer in transfer_list { - let transfer_moralis: NftTransferHistoryFromMoralis = serde_json::from_str(&transfer.to_string())?; - let contract_type = match transfer_moralis.contract_type { - Some(contract_type) => contract_type, - None => continue, - }; - let status = - get_transfer_status(&wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); - let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; - let fee_details = get_fee_details(ð_coin, &transfer_moralis.common.transaction_hash).await; - let transfer_history = NftTransferHistory { - common: NftTransferCommon { - block_hash: transfer_moralis.common.block_hash, - transaction_hash: transfer_moralis.common.transaction_hash, - transaction_index: transfer_moralis.common.transaction_index, - log_index: transfer_moralis.common.log_index, - value: transfer_moralis.common.value, - transaction_type: transfer_moralis.common.transaction_type, - token_address: transfer_moralis.common.token_address, - from_address: transfer_moralis.common.from_address, - to_address: transfer_moralis.common.to_address, - amount: transfer_moralis.common.amount, - verified: transfer_moralis.common.verified, - operator: transfer_moralis.common.operator, - possible_spam: transfer_moralis.common.possible_spam, - }, - chain: *chain, - token_id: transfer_moralis.token_id.0, - block_number: *transfer_moralis.block_number, - block_timestamp, - contract_type, - token_uri: None, - token_domain: None, - collection_name: None, - image_url: None, - image_domain: None, - token_name: None, - status, - possible_phishing: false, - fee_details, - confirmations: 0, - }; - // collect NFTs transfers from the page - res_list.push(transfer_history); - } + process_transfer_list(transfer_list, chain, wallet_address.as_str(), ð_coin, &mut res_list).await?; // if the cursor is not null, there are other NFTs transfers on next page, // and we need to send new request with cursor to get info from the next page. if let Some(cursor_res) = response["cursor"].as_str() { @@ -721,18 +747,74 @@ async fn get_moralis_nft_transfers( } else { break; } + } else { + break; } } - drop_mutability!(res_list); Ok(res_list) } +async fn process_transfer_list( + transfer_list: &[Json], + chain: &Chain, + wallet_address: &str, + eth_coin: &EthCoin, + res_list: &mut Vec, +) -> MmResult<(), GetNftInfoError> { + for transfer in transfer_list { + let transfer_moralis = NftTransferHistoryFromMoralis::deserialize(transfer)?; + let contract_type = match transfer_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let status = get_transfer_status(wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); + let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; + let fee_details = get_fee_details(eth_coin, &transfer_moralis.common.transaction_hash).await; + let transfer_history = NftTransferHistory { + common: NftTransferCommon { + block_hash: transfer_moralis.common.block_hash, + transaction_hash: transfer_moralis.common.transaction_hash, + transaction_index: transfer_moralis.common.transaction_index, + log_index: transfer_moralis.common.log_index, + value: transfer_moralis.common.value, + transaction_type: transfer_moralis.common.transaction_type, + token_address: transfer_moralis.common.token_address, + from_address: transfer_moralis.common.from_address, + to_address: transfer_moralis.common.to_address, + amount: transfer_moralis.common.amount, + verified: transfer_moralis.common.verified, + operator: transfer_moralis.common.operator, + possible_spam: transfer_moralis.common.possible_spam, + }, + chain: *chain, + token_id: transfer_moralis.token_id.0, + block_number: *transfer_moralis.block_number, + block_timestamp, + contract_type, + token_uri: None, + token_domain: None, + collection_name: None, + image_url: None, + image_domain: None, + token_name: None, + status, + possible_phishing: false, + fee_details, + confirmations: 0, + }; + // collect NFTs transfers from the page + res_list.push(transfer_history); + } + Ok(()) +} + +// TODO: get fee details from non fungible token instead of eth coin? async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let hash = H256::from_str(transaction_hash).ok()?; let receipt = eth_coin.web3().await.ok()?.eth().transaction_receipt(hash).await.ok()?; let fee_coin = match eth_coin.coin_type { EthCoinType::Eth => eth_coin.ticker(), - EthCoinType::Erc20 { .. } => return None, + EthCoinType::Erc20 { .. } | EthCoinType::Nft { .. } => return None, }; match receipt { @@ -844,7 +926,6 @@ async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>, url_antis } } update_uri_moralis_ipfs_fields(&mut uri_meta); - drop_mutability!(uri_meta); uri_meta } @@ -1200,8 +1281,8 @@ async fn update_transfers_with_empty_meta( where T: NftListStorageOps + NftTransferHistoryStorageOps, { - let nft_token_addr_id = storage.get_transfers_with_empty_meta(*chain).await?; - for addr_id_pair in nft_token_addr_id.into_iter() { + let token_addr_id = storage.get_transfers_with_empty_meta(*chain).await?; + for addr_id_pair in token_addr_id.into_iter() { let mut nft_meta = match get_moralis_metadata( addr_id_pair.token_address.clone(), addr_id_pair.token_id, @@ -1422,3 +1503,16 @@ where } Ok(()) } + +fn construct_moralis_uri_for_nft(base_url: &Url, address: &str, chain: &Chain) -> MmResult { + let mut uri = base_url.clone(); + uri.set_path(MORALIS_API_ENDPOINT); + uri.path_segments_mut() + .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(address) + .push("nft"); + uri.query_pairs_mut() + .append_pair("chain", &chain.to_string()) + .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); + Ok(uri) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index 96e520e5cd..ba1c31c0d2 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -213,6 +213,10 @@ pub enum UpdateNftError { CoinDoesntSupportNft { coin: String, }, + #[display(fmt = "Global NFT type mismatch for token '{}'", token)] + GlobalNftTypeMismatch { + token: String, + }, } impl From for UpdateNftError { @@ -269,7 +273,8 @@ impl HttpStatusCode for UpdateNftError { | UpdateNftError::SerdeError(_) | UpdateNftError::ProtectFromSpamError(_) | UpdateNftError::NoSuchCoin { .. } - | UpdateNftError::CoinDoesntSupportNft { .. } => StatusCode::INTERNAL_SERVER_ERROR, + | UpdateNftError::CoinDoesntSupportNft { .. } + | UpdateNftError::GlobalNftTypeMismatch { .. } => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -329,7 +334,7 @@ impl From for UpdateSpamPhishingError { /// Errors encountered when parsing a `Chain` from a string. #[derive(Debug, Display)] pub enum ParseChainTypeError { - /// The provided string does not correspond to any of the supported blockchain types. + #[display(fmt = "The provided string does not correspond to any of the supported blockchain types.")] UnsupportedChainType, } @@ -419,3 +424,10 @@ impl HttpStatusCode for ClearNftDbError { } } } + +/// An error type for issues encountered while parsing contract type. +#[derive(Debug, Display)] +pub enum ParseContractTypeError { + /// Indicates that the contract type being parsed is not supported or recognized. + UnsupportedContractType, +} diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 4a4163f633..baf6c0c65d 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -17,7 +17,7 @@ use url::Url; use crate::eth::EthTxFeeDetails; use crate::nft::eth_addr_to_hex; -use crate::nft::nft_errors::{LockDBError, ParseChainTypeError}; +use crate::nft::nft_errors::{LockDBError, ParseChainTypeError, ParseContractTypeError}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; use crate::{TransactionType, TxFeeDetails, WithdrawFee}; @@ -112,11 +112,15 @@ pub enum Chain { Polygon, } -pub(crate) trait ConvertChain { +pub trait ConvertChain { fn to_ticker(&self) -> &'static str; + fn from_ticker(s: &str) -> MmResult; + fn to_nft_ticker(&self) -> &'static str; + fn from_nft_ticker(s: &str) -> MmResult; } impl ConvertChain for Chain { + #[inline(always)] fn to_ticker(&self) -> &'static str { match self { Chain::Avalanche => "AVAX", @@ -126,6 +130,43 @@ impl ConvertChain for Chain { Chain::Polygon => "MATIC", } } + + /// Converts a coin ticker string to a `Chain` enum. + #[inline(always)] + fn from_ticker(s: &str) -> MmResult { + match s { + "AVAX" | "avax" => Ok(Chain::Avalanche), + "BNB" | "bnb" => Ok(Chain::Bsc), + "ETH" | "eth" => Ok(Chain::Eth), + "FTM" | "ftm" => Ok(Chain::Fantom), + "MATIC" | "matic" => Ok(Chain::Polygon), + _ => MmError::err(ParseChainTypeError::UnsupportedChainType), + } + } + + #[inline(always)] + fn to_nft_ticker(&self) -> &'static str { + match self { + Chain::Avalanche => "NFT_AVAX", + Chain::Bsc => "NFT_BNB", + Chain::Eth => "NFT_ETH", + Chain::Fantom => "NFT_FTM", + Chain::Polygon => "NFT_MATIC", + } + } + + /// Converts a NFT ticker string to a `Chain` enum. + #[inline(always)] + fn from_nft_ticker(s: &str) -> MmResult { + match s.to_uppercase().as_str() { + "NFT_AVAX" => Ok(Chain::Avalanche), + "NFT_BNB" => Ok(Chain::Bsc), + "NFT_ETH" => Ok(Chain::Eth), + "NFT_FTM" => Ok(Chain::Fantom), + "NFT_MATIC" => Ok(Chain::Polygon), + _ => MmError::err(ParseChainTypeError::UnsupportedChainType), + } + } } impl fmt::Display for Chain { @@ -143,19 +184,16 @@ impl fmt::Display for Chain { impl FromStr for Chain { type Err = ParseChainTypeError; - #[inline] + /// Converts a string slice to a `Chain` enum. + /// This implementation is primarily used in the context of deserialization with Serde. + #[inline(always)] fn from_str(s: &str) -> Result { match s { - "AVALANCHE" => Ok(Chain::Avalanche), - "avalanche" => Ok(Chain::Avalanche), - "BSC" => Ok(Chain::Bsc), - "bsc" => Ok(Chain::Bsc), - "ETH" => Ok(Chain::Eth), - "eth" => Ok(Chain::Eth), - "FANTOM" => Ok(Chain::Fantom), - "fantom" => Ok(Chain::Fantom), - "POLYGON" => Ok(Chain::Polygon), - "polygon" => Ok(Chain::Polygon), + "AVALANCHE" | "avalanche" => Ok(Chain::Avalanche), + "BSC" | "bsc" => Ok(Chain::Bsc), + "ETH" | "eth" => Ok(Chain::Eth), + "FANTOM" | "fantom" => Ok(Chain::Fantom), + "POLYGON" | "polygon" => Ok(Chain::Polygon), _ => Err(ParseChainTypeError::UnsupportedChainType), } } @@ -172,15 +210,16 @@ impl<'de> Deserialize<'de> for Chain { } } -#[derive(Debug, Display)] -pub(crate) enum ParseContractTypeError { - UnsupportedContractType, -} - +/// Represents the type of smart contract used for NFTs. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "UPPERCASE")] -pub(crate) enum ContractType { +pub enum ContractType { + /// Represents an ERC-1155 contract, which allows a single contract to manage + /// multiple types of NFTs. This means ERC-1155 represents both fungible and non-fungible assets within a single contract. + /// ERC-1155 provides a way for each token ID to represent multiple assets. Erc1155, + /// Represents an ERC-721 contract, a standard for non-fungible tokens on EVM based chains, + /// where each token is unique and owned individually. Erc721, } @@ -772,3 +811,21 @@ pub struct ClearNftDbReq { #[serde(default)] pub(crate) clear_all: bool, } + +/// Represents detailed information about a Non-Fungible Token (NFT). +/// This struct is used to keep info about NFTs owned by user in global Non-Fungible Token. +#[derive(Clone, Debug, Serialize)] +pub struct NftInfo { + /// The address of the NFT token. + pub(crate) token_address: Address, + /// The ID of the NFT token. + #[serde(serialize_with = "serialize_token_id")] + pub(crate) token_id: BigUint, + /// The blockchain where the NFT exists. + pub(crate) chain: Chain, + /// The type of smart contract that governs this NFT. + pub(crate) contract_type: ContractType, + /// The quantity of this type of NFT owned. Particularly relevant for ERC-1155 tokens, + /// where a single token ID can represent multiple assets. + pub(crate) amount: BigDecimal, +} diff --git a/mm2src/coins/nft/storage/wasm/mod.rs b/mm2src/coins/nft/storage/wasm/mod.rs index ab8f69af68..05c7bac5d9 100644 --- a/mm2src/coins/nft/storage/wasm/mod.rs +++ b/mm2src/coins/nft/storage/wasm/mod.rs @@ -19,6 +19,9 @@ pub enum WasmNftCacheError { NotSupported(String), InternalError(String), GetLastNftBlockError(String), + GetItemError(String), + CursorBuilderError(String), + OpenCursorError(String), } impl From for WasmNftCacheError { diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index aabe945d8c..0578e6e82c 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -502,20 +502,21 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { ) -> MmResult, Self::Error> { let db_transaction = self.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let items = table + let mut cursor_iter = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? .bound("block_number", BeBigUint::from(from_block), BeBigUint::from(u64::MAX)) .open_cursor(CHAIN_BLOCK_NUMBER_INDEX) .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() - .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))?; let mut res = Vec::new(); - for (_item_id, item) in items.into_iter() { + while let Some((_item_id, item)) = cursor_iter + .next() + .await + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))? + { let transfer = transfer_details_from_item(item)?; res.push(transfer); } @@ -613,19 +614,20 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { async fn get_transfers_with_empty_meta(&self, chain: Chain) -> MmResult, Self::Error> { let db_transaction = self.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let items = table + let mut cursor_iter = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? .open_cursor("chain") .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() - .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))?; let mut res = HashSet::new(); - for (_item_id, item) in items.into_iter() { + while let Some((_item_id, item)) = cursor_iter + .next() + .await + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))? + { if item.token_uri.is_none() && item.collection_name.is_none() && item.image_url.is_none() @@ -813,25 +815,26 @@ async fn get_last_block_from_table( table: DbTable<'_, impl TableSignature + BlockNumberTable>, cursor: &str, ) -> MmResult, WasmNftCacheError> { - let items = table + let maybe_item = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? // Sets lower and upper bounds for block_number field .bound("block_number", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) + // Cursor returns values from the lowest to highest key indexes. + // But we need to get the highest block_number, so reverse the cursor direction. + .reverse() + .where_first() // Opens a cursor by the specified index. // In get_last_block_from_table case it is CHAIN_BLOCK_NUMBER_INDEX, as we need to search block_number for specific chain. - // Cursor returns values from the lowest to highest key indexes. .open_cursor(cursor) .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))? + .next() .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))?; - let maybe_item = items - .into_iter() - .last() + let maybe_item = maybe_item .map(|(_item_id, item)| { item.get_block_number() .to_u64() diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 4d44945d28..2b5087e0e0 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -2172,6 +2172,7 @@ mod slp_tests { let err = match tx_err.clone() { TransactionErr::TxRecoverable(_tx, err) => err, TransactionErr::Plain(err) => err, + TransactionErr::NftProtocolNotSupported(err) => err, }; println!("{:?}", err); diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index 09fd4adf8e..e9fb215c1b 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -32,6 +32,7 @@ ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1.0" serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +url = { version = "2.2.2", features = ["serde"] } [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_metamask = { path = "../mm2_metamask" } diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index b99b49235a..4d55bb5168 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -212,7 +212,7 @@ impl PlatformWithTokensActivationOps for BchCoin { async fn enable_platform_coin( ctx: MmArc, ticker: String, - platform_conf: Json, + platform_conf: &Json, activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { @@ -229,7 +229,7 @@ impl PlatformWithTokensActivationOps for BchCoin { let platform_coin = bch_coin_with_policy( &ctx, &ticker, - &platform_conf, + platform_conf, activation_request.platform_request, slp_prefix, priv_key_policy, @@ -239,6 +239,13 @@ impl PlatformWithTokensActivationOps for BchCoin { Ok(platform_coin) } + async fn enable_global_nft( + &self, + _activation_request: &Self::ActivationRequest, + ) -> Result, MmError> { + Ok(None) + } + fn try_from_mm_coin(coin: MmCoinEnum) -> Option where Self: Sized, @@ -260,6 +267,7 @@ impl PlatformWithTokensActivationOps for BchCoin { async fn get_activation_result( &self, activation_request: &Self::ActivationRequest, + _nft_global: &Option, ) -> Result> { let current_block = self.as_ref().rpc_client.get_block_count().compat().await?; diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 851d188267..027f767539 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -1,7 +1,9 @@ use crate::{prelude::{TryFromCoinProtocol, TryPlatformCoinFromMmCoinEnum}, token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}}; use async_trait::async_trait; -use coins::{eth::{v2_activation::{Erc20Protocol, Erc20TokenActivationError, Erc20TokenActivationRequest}, +use coins::eth::v2_activation::{EthTokenActivationParams, EthTokenProtocol, NftProtocol, NftProviderEnum}; +use coins::nft::nft_structs::NftInfo; +use coins::{eth::{v2_activation::{Erc20Protocol, EthTokenActivationError}, valid_addr_from_str, EthCoin}, CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; use common::Future01CompatExt; @@ -9,6 +11,13 @@ use mm2_err_handle::prelude::MmError; use serde::Serialize; use std::collections::HashMap; +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum EthTokenInitResult { + Erc20(Erc20InitResult), + Nft(NftInitResult), +} + #[derive(Debug, Serialize)] pub struct Erc20InitResult { balances: HashMap, @@ -17,12 +26,20 @@ pub struct Erc20InitResult { required_confirmations: u64, } -impl From for EnableTokenError { - fn from(err: Erc20TokenActivationError) -> Self { +#[derive(Debug, Serialize)] +pub struct NftInitResult { + nfts: HashMap, + platform_coin: String, +} + +impl From for EnableTokenError { + fn from(err: EthTokenActivationError) -> Self { match err { - Erc20TokenActivationError::InternalError(e) => EnableTokenError::Internal(e), - Erc20TokenActivationError::CouldNotFetchBalance(e) - | Erc20TokenActivationError::ClientConnectionFailed(e) => EnableTokenError::Transport(e), + EthTokenActivationError::InternalError(e) => EnableTokenError::Internal(e), + EthTokenActivationError::CouldNotFetchBalance(e) + | EthTokenActivationError::Transport(e) + | EthTokenActivationError::ClientConnectionFailed(e) => EnableTokenError::Transport(e), + EthTokenActivationError::InvalidPayload(e) => EnableTokenError::InvalidPayload(e), } } } @@ -61,16 +78,41 @@ impl TryFromCoinProtocol for Erc20Protocol { } } +impl TryFromCoinProtocol for EthTokenProtocol { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> + where + Self: Sized, + { + match proto { + CoinProtocol::ERC20 { .. } => { + let erc20_protocol = Erc20Protocol::try_from_coin_protocol(proto)?; + Ok(EthTokenProtocol::Erc20(erc20_protocol)) + }, + CoinProtocol::Nft { platform } => Ok(EthTokenProtocol::Nft(NftProtocol { platform })), + proto => MmError::err(proto), + } + } +} + impl TokenProtocolParams for Erc20Protocol { fn platform_coin_ticker(&self) -> &str { &self.platform } } +impl TokenProtocolParams for EthTokenProtocol { + fn platform_coin_ticker(&self) -> &str { + match self { + EthTokenProtocol::Erc20(erc20_protocol) => erc20_protocol.platform_coin_ticker(), + EthTokenProtocol::Nft(nft_protocol) => &nft_protocol.platform, + } + } +} + #[async_trait] impl TokenActivationOps for EthCoin { - type ActivationParams = Erc20TokenActivationRequest; - type ProtocolInfo = Erc20Protocol; - type ActivationResult = Erc20InitResult; - type ActivationError = Erc20TokenActivationError; + type ActivationParams = EthTokenActivationParams; + type ProtocolInfo = EthTokenProtocol; + type ActivationResult = EthTokenInitResult; + type ActivationError = EthTokenActivationError; async fn enable_token( ticker: String, @@ -78,30 +120,60 @@ impl TokenActivationOps for EthCoin { activation_params: Self::ActivationParams, protocol_conf: Self::ProtocolInfo, ) -> Result<(Self, Self::ActivationResult), MmError> { - let token = platform_coin - .initialize_erc20_token(activation_params, protocol_conf, ticker) - .await?; - - let address = token.my_address()?; - let token_contract_address = token - .erc20_token_address() - .ok_or_else(|| Erc20TokenActivationError::InternalError("Token contract address is missing".to_string()))?; - - let balance = token - .my_balance() - .compat() - .await - .map_err(|e| Erc20TokenActivationError::CouldNotFetchBalance(e.to_string()))?; - - let balances = HashMap::from([(address, balance)]); - - let init_result = Erc20InitResult { - balances, - platform_coin: token.platform_ticker().to_owned(), - required_confirmations: token.required_confirmations(), - token_contract_address: format!("{:#02x}", token_contract_address), - }; - - Ok((token, init_result)) + match activation_params { + EthTokenActivationParams::Erc20(erc20_init_params) => match protocol_conf { + EthTokenProtocol::Erc20(erc20_protocol) => { + let token = platform_coin + .initialize_erc20_token(erc20_init_params, erc20_protocol, ticker) + .await?; + + let address = token.my_address()?; + let token_contract_address = token.erc20_token_address().ok_or_else(|| { + EthTokenActivationError::InternalError("Token contract address is missing".to_string()) + })?; + + let balance = token + .my_balance() + .compat() + .await + .map_err(|e| EthTokenActivationError::CouldNotFetchBalance(e.to_string()))?; + + let balances = HashMap::from([(address, balance)]); + + let init_result = EthTokenInitResult::Erc20(Erc20InitResult { + balances, + platform_coin: token.platform_ticker().to_owned(), + required_confirmations: token.required_confirmations(), + token_contract_address: format!("{:#02x}", token_contract_address), + }); + + Ok((token, init_result)) + }, + _ => Err(MmError::new(EthTokenActivationError::InternalError( + "Mismatched protocol info for ERC-20".to_string(), + ))), + }, + EthTokenActivationParams::Nft(nft_init_params) => match protocol_conf { + EthTokenProtocol::Nft(nft_protocol) => { + if nft_protocol.platform != platform_coin.ticker() { + return MmError::err(EthTokenActivationError::InternalError( + "NFT platform coin ticker does not match the expected platform".to_string(), + )); + } + let nft_global = match &nft_init_params.provider { + NftProviderEnum::Moralis { url } => platform_coin.global_nft_from_platform_coin(url).await?, + }; + let nfts = nft_global.nfts_infos.lock().await.clone(); + let init_result = EthTokenInitResult::Nft(NftInitResult { + nfts, + platform_coin: platform_coin.ticker().to_owned(), + }); + Ok((nft_global, init_result)) + }, + _ => Err(MmError::new(EthTokenActivationError::InternalError( + "Mismatched protocol info for NFT".to_string(), + ))), + }, + } } } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index abbc17caa0..9e0b75ebb4 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -4,10 +4,12 @@ use crate::{platform_coin_with_tokens::{EnablePlatformCoinWithTokensError, GetPl TokenInitializer, TokenOf}, prelude::*}; use async_trait::async_trait; +use coins::eth::v2_activation::{NftActivationRequest, NftProviderEnum}; use coins::eth::EthPrivKeyBuildPolicy; +use coins::nft::nft_structs::NftInfo; use coins::{eth::v2_activation::EthPrivKeyActivationPolicy, MmCoinEnum}; -use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationError, - Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request}, +use coins::{eth::{v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, + EthActivationV2Error, EthActivationV2Request, EthTokenActivationError}, Erc20TokenInfo, EthCoin, EthCoinType}, my_tx_history_v2::TxHistoryStorage, CoinBalance, CoinProtocol, MarketCoinOps, MmCoin}; @@ -56,6 +58,7 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) }, EthActivationV2Error::InternalError(e) => EnablePlatformCoinWithTokensError::Internal(e), + EthActivationV2Error::Transport(e) => EnablePlatformCoinWithTokensError::Transport(e), } } } @@ -76,12 +79,15 @@ pub struct Erc20Initializer { platform_coin: EthCoin, } -impl From for InitTokensAsMmCoinsError { - fn from(error: Erc20TokenActivationError) -> Self { +impl From for InitTokensAsMmCoinsError { + fn from(error: EthTokenActivationError) -> Self { match error { - Erc20TokenActivationError::InternalError(e) => InitTokensAsMmCoinsError::Internal(e), - Erc20TokenActivationError::CouldNotFetchBalance(e) - | Erc20TokenActivationError::ClientConnectionFailed(e) => InitTokensAsMmCoinsError::CouldNotFetchBalance(e), + EthTokenActivationError::InternalError(e) => InitTokensAsMmCoinsError::Internal(e), + EthTokenActivationError::CouldNotFetchBalance(e) | EthTokenActivationError::ClientConnectionFailed(e) => { + InitTokensAsMmCoinsError::CouldNotFetchBalance(e) + }, + EthTokenActivationError::InvalidPayload(e) => InitTokensAsMmCoinsError::InvalidPayload(e), + EthTokenActivationError::Transport(e) => InitTokensAsMmCoinsError::Transport(e), } } } @@ -91,7 +97,7 @@ impl TokenInitializer for Erc20Initializer { type Token = EthCoin; type TokenActivationRequest = Erc20TokenActivationRequest; type TokenProtocol = Erc20Protocol; - type InitTokensError = Erc20TokenActivationError; + type InitTokensError = EthTokenActivationError; fn tokens_requests_from_platform_request( platform_params: &EthWithTokensActivationRequest, @@ -102,7 +108,7 @@ impl TokenInitializer for Erc20Initializer { async fn enable_tokens( &self, activation_params: Vec>, - ) -> Result, MmError> { + ) -> Result, MmError> { let mut tokens = vec![]; for param in activation_params { let token: EthCoin = self @@ -125,6 +131,7 @@ pub struct EthWithTokensActivationRequest { erc20_tokens_requests: Vec>, #[serde(default = "true_f")] pub get_balances: bool, + nft_req: Option, } impl TxHistory for EthWithTokensActivationRequest { @@ -137,6 +144,11 @@ impl TokenOf for EthCoin { impl RegisterTokenInfo for EthCoin { fn register_token_info(&self, token: &EthCoin) { + // Dont register Nft in platform coin. + if matches!(token.coin_type, EthCoinType::Nft { .. }) { + return; + } + self.add_erc_token_info(token.ticker().to_string(), Erc20TokenInfo { token_address: token.erc20_token_address().unwrap(), decimals: token.decimals(), @@ -144,11 +156,16 @@ impl RegisterTokenInfo for EthCoin { } } +/// Represents the result of activating an Ethereum-based coin along with its associated tokens (ERC20 and NFTs). +/// +/// This structure provides a snapshot of the relevant activation data, including the current blockchain block, +/// information about Ethereum addresses and their balances, ERC-20 token balances, and a summary of NFT ownership. #[derive(Serialize)] pub struct EthWithTokensActivationResult { current_block: u64, eth_addresses_infos: HashMap>, erc20_addresses_infos: HashMap>, + nfts_infos: HashMap, } impl GetPlatformBalance for EthWithTokensActivationResult { @@ -175,7 +192,7 @@ impl PlatformWithTokensActivationOps for EthCoin { async fn enable_platform_coin( ctx: MmArc, ticker: String, - platform_conf: Json, + platform_conf: &Json, activation_request: Self::ActivationRequest, _protocol: Self::PlatformProtocolInfo, ) -> Result> { @@ -184,7 +201,7 @@ impl PlatformWithTokensActivationOps for EthCoin { let platform_coin = eth_coin_from_conf_and_request_v2( &ctx, &ticker, - &platform_conf, + platform_conf, activation_request.platform_request, priv_key_policy, ) @@ -193,6 +210,20 @@ impl PlatformWithTokensActivationOps for EthCoin { Ok(platform_coin) } + async fn enable_global_nft( + &self, + activation_request: &Self::ActivationRequest, + ) -> Result, MmError> { + let url = match &activation_request.nft_req { + Some(nft_req) => match &nft_req.provider { + NftProviderEnum::Moralis { url } => url, + }, + None => return Ok(None), + }; + let nft_global = self.global_nft_from_platform_coin(url).await?; + Ok(Some(MmCoinEnum::EthCoin(nft_global))) + } + fn try_from_mm_coin(coin: MmCoinEnum) -> Option where Self: Sized, @@ -214,6 +245,7 @@ impl PlatformWithTokensActivationOps for EthCoin { async fn get_activation_result( &self, activation_request: &Self::ActivationRequest, + nft_global: &Option, ) -> Result> { let current_block = self .current_block() @@ -238,6 +270,12 @@ impl PlatformWithTokensActivationOps for EthCoin { tickers: None, }; + let nfts_map = if let Some(MmCoinEnum::EthCoin(nft_global)) = nft_global { + nft_global.nfts_infos.lock().await.clone() + } else { + Default::default() + }; + if !activation_request.get_balances { drop_mutability!(eth_address_info); let tickers: HashSet<_> = self.get_erc_tokens_infos().into_keys().collect(); @@ -248,6 +286,7 @@ impl PlatformWithTokensActivationOps for EthCoin { current_block, eth_addresses_infos: HashMap::from([(my_address.clone(), eth_address_info)]), erc20_addresses_infos: HashMap::from([(my_address, erc20_address_info)]), + nfts_infos: nfts_map, }); } @@ -270,6 +309,7 @@ impl PlatformWithTokensActivationOps for EthCoin { current_block, eth_addresses_infos: HashMap::from([(my_address.clone(), eth_address_info)]), erc20_addresses_infos: HashMap::from([(my_address, erc20_address_info)]), + nfts_infos: nfts_map, }) } diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 2d2f914446..c20dccf302 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -68,6 +68,8 @@ pub enum InitTokensAsMmCoinsError { Internal(String), TokenProtocolParseError { ticker: String, error: String }, UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + Transport(String), + InvalidPayload(String), } impl From for InitTokensAsMmCoinsError { @@ -142,11 +144,16 @@ pub trait PlatformWithTokensActivationOps: Into { async fn enable_platform_coin( ctx: MmArc, ticker: String, - coin_conf: Json, + coin_conf: &Json, activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, ) -> Result>; + async fn enable_global_nft( + &self, + activation_request: &Self::ActivationRequest, + ) -> Result, MmError>; + fn try_from_mm_coin(coin: MmCoinEnum) -> Option where Self: Sized; @@ -158,6 +165,7 @@ pub trait PlatformWithTokensActivationOps: Into { async fn get_activation_result( &self, activation_request: &Self::ActivationRequest, + nft_global: &Option, ) -> Result>; fn start_history_background_fetching( @@ -257,7 +265,10 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { ticker, protocol } }, InitTokensAsMmCoinsError::Internal(e) => EnablePlatformCoinWithTokensError::Internal(e), - InitTokensAsMmCoinsError::CouldNotFetchBalance(e) => EnablePlatformCoinWithTokensError::Transport(e), + InitTokensAsMmCoinsError::CouldNotFetchBalance(e) | InitTokensAsMmCoinsError::Transport(e) => { + EnablePlatformCoinWithTokensError::Transport(e) + }, + InitTokensAsMmCoinsError::InvalidPayload(e) => EnablePlatformCoinWithTokensError::InvalidPayload(e), } } } @@ -282,7 +293,6 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PlatformCoinCreationError { .. } | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) - | EnablePlatformCoinWithTokensError::Transport(_) | EnablePlatformCoinWithTokensError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(_) | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) @@ -292,6 +302,7 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::AtLeastOneNodeRequired(_) | EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(_) | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, + EnablePlatformCoinWithTokensError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -312,12 +323,14 @@ where mm_tokens.extend(tokens); } - let activation_result = platform_coin.get_activation_result(&req.request).await?; + let nft_global = platform_coin.enable_global_nft(&req.request).await?; + + let activation_result = platform_coin.get_activation_result(&req.request, &nft_global).await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx - .add_platform_with_tokens(platform_coin.clone().into(), mm_tokens) + .add_platform_with_tokens(platform_coin.clone().into(), mm_tokens, nft_global) .await .mm_err(|e| EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(e.ticker))?; @@ -350,7 +363,7 @@ where let platform_coin = Platform::enable_platform_coin( ctx.clone(), req.ticker.clone(), - platform_conf, + &platform_conf, req.request.clone(), platform_protocol, ) @@ -362,7 +375,9 @@ where mm_tokens.extend(tokens); } - let activation_result = platform_coin.get_activation_result(&req.request).await?; + let nft_global = platform_coin.enable_global_nft(&req.request).await?; + + let activation_result = platform_coin.get_activation_result(&req.request, &nft_global).await?; log::info!("{} current block {}", req.ticker, activation_result.current_block()); if req.request.tx_history() { @@ -379,7 +394,7 @@ where let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx - .add_platform_with_tokens(platform_coin.into(), mm_tokens) + .add_platform_with_tokens(platform_coin.into(), mm_tokens, nft_global) .await .mm_err(|e| EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(e.ticker))?; diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 2061509ee4..21a04ed8ad 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,3 +1,4 @@ +use coins::nft::nft_structs::{Chain, ConvertChain}; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, MmCoinEnum}; @@ -70,27 +71,42 @@ pub enum CoinConfWithProtocolError { UnexpectedProtocol { ticker: String, protocol: CoinProtocol }, } +/// Determines the coin configuration and protocol information for a given coin or NFT ticker. +/// In the case of NFT ticker, it's platform coin config will be returned. #[allow(clippy::result_large_err)] pub fn coin_conf_with_protocol( ctx: &MmArc, coin: &str, ) -> Result<(Json, T), MmError> { - let conf = coin_conf(ctx, coin); + let (conf, coin_protocol) = match Chain::from_nft_ticker(coin) { + Ok(chain) => { + let platform = chain.to_ticker(); + let platform_conf = coin_conf(ctx, platform); + let nft_protocol = CoinProtocol::Nft { + platform: platform.to_string(), + }; + (platform_conf, nft_protocol) + }, + Err(_) => { + let conf = coin_conf(ctx, coin); + let coin_protocol: CoinProtocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { + CoinConfWithProtocolError::CoinProtocolParseError { + ticker: coin.into(), + err, + } + })?; + (conf, coin_protocol) + }, + }; + if conf.is_null() { return MmError::err(CoinConfWithProtocolError::ConfigIsNotFound(coin.into())); } - let coin_protocol: CoinProtocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { - CoinConfWithProtocolError::CoinProtocolParseError { - ticker: coin.into(), - err, - } - })?; - - let coin_protocol = + let protocol = T::try_from_coin_protocol(coin_protocol).mm_err(|protocol| CoinConfWithProtocolError::UnexpectedProtocol { ticker: coin.into(), protocol, })?; - Ok((conf, coin_protocol)) + Ok((conf, protocol)) } diff --git a/mm2src/coins_activation/src/solana_with_tokens_activation.rs b/mm2src/coins_activation/src/solana_with_tokens_activation.rs index aa049d0867..2ba63839da 100644 --- a/mm2src/coins_activation/src/solana_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/solana_with_tokens_activation.rs @@ -186,7 +186,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { async fn enable_platform_coin( ctx: MmArc, ticker: String, - platform_conf: Json, + platform_conf: &Json, activation_request: Self::ActivationRequest, _protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { @@ -194,7 +194,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { solana_coin_with_policy( &ctx, &ticker, - &platform_conf, + platform_conf, activation_request.platform_request, priv_key_policy, ) @@ -202,6 +202,13 @@ impl PlatformWithTokensActivationOps for SolanaCoin { .map_to_mm(|error| SolanaWithTokensActivationError::PlatformCoinCreationError { ticker, error }) } + async fn enable_global_nft( + &self, + _activation_request: &Self::ActivationRequest, + ) -> Result, MmError> { + Ok(None) + } + fn try_from_mm_coin(coin: MmCoinEnum) -> Option where Self: Sized, @@ -223,6 +230,7 @@ impl PlatformWithTokensActivationOps for SolanaCoin { async fn get_activation_result( &self, activation_request: &Self::ActivationRequest, + _nft_global: &Option, ) -> Result> { let current_block = self .current_block() diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index e2e8fda8f0..01b944a0b8 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -168,11 +168,11 @@ impl PlatformWithTokensActivationOps for TendermintCoin { async fn enable_platform_coin( ctx: MmArc, ticker: String, - coin_conf: Json, + coin_conf: &Json, activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { - let conf = TendermintConf::try_from_json(&ticker, &coin_conf)?; + let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; let priv_key_build_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { @@ -199,6 +199,13 @@ impl PlatformWithTokensActivationOps for TendermintCoin { .await } + async fn enable_global_nft( + &self, + _activation_request: &Self::ActivationRequest, + ) -> Result, MmError> { + Ok(None) + } + fn try_from_mm_coin(coin: MmCoinEnum) -> Option where Self: Sized, @@ -220,6 +227,7 @@ impl PlatformWithTokensActivationOps for TendermintCoin { async fn get_activation_result( &self, activation_request: &Self::ActivationRequest, + _nft_global: &Option, ) -> Result> { let current_block = self.current_block().compat().await.map_to_mm(|e| TendermintInitError { ticker: self.ticker().to_owned(), diff --git a/mm2src/coins_activation/src/token.rs b/mm2src/coins_activation/src/token.rs index 70751b59cc..d1449dea8d 100644 --- a/mm2src/coins_activation/src/token.rs +++ b/mm2src/coins_activation/src/token.rs @@ -62,6 +62,7 @@ pub enum EnableTokenError { InvalidConfig(String), Transport(String), Internal(String), + InvalidPayload(String), } impl From for EnableTokenError { @@ -161,7 +162,8 @@ impl HttpStatusCode for EnableTokenError { EnableTokenError::TokenIsAlreadyActivated(_) | EnableTokenError::PlatformCoinIsNotActivated(_) | EnableTokenError::TokenConfigIsNotFound { .. } - | EnableTokenError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, + | EnableTokenError::UnexpectedTokenProtocol { .. } + | EnableTokenError::InvalidPayload(_) => StatusCode::BAD_REQUEST, EnableTokenError::TokenProtocolParseError { .. } | EnableTokenError::UnsupportedPlatformCoin { .. } | EnableTokenError::UnexpectedDerivationMethod(_) diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 9e820445de..a1393bf71e 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -5802,9 +5802,11 @@ fn orderbook_address( ) -> Result> { let protocol: CoinProtocol = json::from_value(conf["protocol"].clone())?; match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH => coins::eth::addr_from_pubkey_str(pubkey) - .map(OrderbookAddress::Transparent) - .map_to_mm(OrderbookAddrErr::AddrFromPubkeyError), + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::Nft { .. } => { + coins::eth::addr_from_pubkey_str(pubkey) + .map(OrderbookAddress::Transparent) + .map_to_mm(OrderbookAddrErr::AddrFromPubkeyError) + }, CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { coins::utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) .map(OrderbookAddress::Transparent) diff --git a/mm2src/mm2_main/src/lp_swap/check_balance.rs b/mm2src/mm2_main/src/lp_swap/check_balance.rs index 5590679f6f..178a3433fe 100644 --- a/mm2src/mm2_main/src/lp_swap/check_balance.rs +++ b/mm2src/mm2_main/src/lp_swap/check_balance.rs @@ -270,6 +270,9 @@ impl CheckBalanceError { }, TradePreimageError::Transport(transport) => CheckBalanceError::Transport(transport), TradePreimageError::InternalError(internal) => CheckBalanceError::InternalError(internal), + TradePreimageError::NftProtocolNotSupported => { + CheckBalanceError::InternalError("Nft Protocol is not supported yet!".to_string()) + }, } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 4924003cc0..1307807c13 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -163,6 +163,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_token::).await, "enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_erc20" => handle_mmrpc(ctx, request, enable_token::).await, + "enable_nft" => handle_mmrpc(ctx, request, enable_token::).await, "enable_tendermint_with_assets" => { handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await },