diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa68968c4..2fdca57fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ **Features:** - NFT integration `WIP` [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) - NFT integration PoC added. Includes ERC721 support for ETH and BSC [#1652](https://github.com/KomodoPlatform/atomicDEX-API/pull/1652) + - Withdraw ERC1155 and EVM based chains support added for NFT PoC [#1704](https://github.com/KomodoPlatform/atomicDEX-API/pull/1704) - Swap watcher nodes [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) - Watcher rewards for ETH swaps were added [#1658](https://github.com/KomodoPlatform/atomicDEX-API/pull/1658) - Cosmos integration `WIP` [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 70060976a8..8c55c5c366 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -22,7 +22,8 @@ // use super::eth::Action::{Call, Create}; #[cfg(feature = "enable-nft-integration")] -use crate::nft::nft_structs::{Chain, ContractType, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; +use crate::nft::nft_structs::{ContractType, ConvertChain, NftListReq, TransactionNftDetails, WithdrawErc1155, + WithdrawErc721}; use async_trait::async_trait; use bitcrypto::{keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry}; @@ -34,6 +35,7 @@ use common::{get_utc_timestamp, now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; +use enum_from::EnumFromStringify; use ethabi::{Contract, Function, Token}; pub use ethcore_transaction::SignedTransaction as SignedEthTx; use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; @@ -99,7 +101,7 @@ mod web3_transport; #[path = "eth/v2_activation.rs"] pub mod v2_activation; #[cfg(feature = "enable-nft-integration")] -use crate::nft::WithdrawNftResult; +use crate::nft::{find_wallet_amount, WithdrawNftResult}; #[cfg(feature = "enable-nft-integration")] use crate::{lp_coinfind_or_err, MmCoinEnum, TransactionType}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; @@ -116,6 +118,8 @@ const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md const ERC721_ABI: &str = include_str!("eth/erc721_abi.json"); +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md +const ERC1155_ABI: &str = include_str!("eth/erc1155_abi.json"); /// Payment states from etomic swap smart contract: https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol#L5 pub const PAYMENT_STATE_UNINITIALIZED: u8 = 0; pub const PAYMENT_STATE_SENT: u8 = 1; @@ -155,11 +159,13 @@ lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); pub static ref ERC721_CONTRACT: Contract = Contract::load(ERC721_ABI.as_bytes()).unwrap(); + pub static ref ERC1155_CONTRACT: Contract = Contract::load(ERC1155_ABI.as_bytes()).unwrap(); } pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; pub type GasStationResult = Result>; +type GasDetails = (U256, U256); #[derive(Debug, Display)] pub enum GasStationReqErr { @@ -750,40 +756,8 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }; let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; - let (gas, gas_price) = match req.fee { - Some(WithdrawFee::EthGas { gas_price, gas }) => { - let gas_price = wei_from_big_decimal(&gas_price, 9)?; - (gas.into(), gas_price) - }, - Some(fee_policy) => { - let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); - return MmError::err(WithdrawError::InvalidFeePolicy(error)); - }, - None => { - let gas_price = coin.get_gas_price().compat().await?; - // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH - let eth_value_for_estimate = if req.max && coin.coin_type == EthCoinType::Eth { - eth_value - gas_price * U256::from(21000) - } else { - eth_value - }; - let estimate_gas_req = CallRequest { - value: Some(eth_value_for_estimate), - data: Some(data.clone().into()), - from: Some(coin.my_address), - to: Some(call_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = coin.estimate_gas(estimate_gas_req).compat().await?; - (gas_limit, gas_price) - }, - }; + let (gas, gas_price) = + get_eth_gas_details(&coin, req.fee, eth_value, data.clone().into(), call_addr, req.max).await?; let total_fee = gas * gas_price; let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; @@ -894,49 +868,50 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { } #[cfg(feature = "enable-nft-integration")] +/// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, +/// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftResult { - let ticker = match req.chain { - Chain::Bsc => "BNB", - Chain::Eth => "ETH", + let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; + let my_address = eth_coin.my_address()?; + + // todo check amount in nft cache, instead of sending new moralis req + // dont use `get_nft_metadata` for erc1155, it can return info related to other owner. + let nft_req = NftListReq { + chains: vec![req.chain], }; - let _coin = lp_coinfind_or_err(&ctx, ticker).await?; - unimplemented!() -} + let wallet_amount = find_wallet_amount(ctx, nft_req, req.token_address.clone(), req.token_id.clone()).await?; -#[cfg(feature = "enable-nft-integration")] -pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResult { - let ticker = match req.chain { - Chain::Bsc => "BNB", - Chain::Eth => "ETH", - }; - let coin = lp_coinfind_or_err(&ctx, ticker).await?; - let eth_coin = match coin { - MmCoinEnum::EthCoin(eth_coin) => eth_coin, - _ => { - return MmError::err(WithdrawError::CoinDoesntSupportNftWithdraw { - coin: coin.ticker().to_owned(), - }) - }, + let amount_dec = if req.max { + wallet_amount.clone() + } else { + req.amount.unwrap_or_else(|| 1.into()) }; - let from_addr = valid_addr_from_str(&req.from).map_to_mm(WithdrawError::InvalidAddress)?; - if eth_coin.my_address != from_addr { - return MmError::err(WithdrawError::AddressMismatchError { - my_address: eth_coin.my_address.to_string(), - from: req.from, + + if amount_dec > wallet_amount { + return MmError::err(WithdrawError::NotEnoughNftsAmount { + token_address: req.token_address, + token_id: req.token_id.to_string(), + available: wallet_amount, + required: amount_dec, }); } - let to_addr = valid_addr_from_str(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; - let token_addr = addr_from_str(&req.token_address).map_to_mm(WithdrawError::InvalidAddress)?; + let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { - let function = ERC721_CONTRACT.function("safeTransferFrom")?; + let function = ERC1155_CONTRACT.function("safeTransferFrom")?; let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) .map_err(|e| format!("{:?}", e)) .map_to_mm(NumConversError::new)?; + let amount_u256 = U256::from_dec_str(&amount_dec.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; let data = function.encode_input(&[ - Token::Address(from_addr), + Token::Address(eth_coin.my_address), Token::Address(to_addr), Token::Uint(token_id_u256), + Token::Uint(amount_u256), + Token::Bytes("0x".into()), ])?; (0.into(), data, token_addr, eth_coin.ticker()) }, @@ -946,34 +921,76 @@ pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResu )) }, }; - let (gas, gas_price) = match req.fee { - Some(WithdrawFee::EthGas { gas_price, gas }) => { - let gas_price = wei_from_big_decimal(&gas_price, 9)?; - (gas.into(), gas_price) - }, - Some(fee_policy) => { - let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); - return MmError::err(WithdrawError::InvalidFeePolicy(error)); + let (gas, gas_price) = + get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; + let _nonce_lock = eth_coin.nonce_lock.lock().await; + let nonce = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data, + gas, + gas_price, + }; + + let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let signed = tx.sign(secret, eth_coin.chain_id); + let signed_bytes = rlp::encode(&signed); + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + + Ok(TransactionNftDetails { + tx_hex: BytesJson::from(signed_bytes.to_vec()), + tx_hash: format!("{:02x}", signed.tx_hash()), + from: vec![my_address], + to: vec![req.to], + contract_type: ContractType::Erc1155, + token_address: req.token_address, + token_id: req.token_id, + amount: amount_dec, + fee_details: Some(fee_details.into()), + coin: eth_coin.ticker.clone(), + block_height: 0, + timestamp: now_ms() / 1000, + internal_id: 0, + transaction_type: TransactionType::NftTransfer, + }) +} + +#[cfg(feature = "enable-nft-integration")] +/// `withdraw_erc721` function returns details of `ERC-721` transaction including tx hex, +/// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. +pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResult { + let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; + let my_address = eth_coin.my_address()?; + + let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { + EthCoinType::Eth => { + let function = ERC721_CONTRACT.function("safeTransferFrom")?; + let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; + let data = function.encode_input(&[ + Token::Address(eth_coin.my_address), + Token::Address(to_addr), + Token::Uint(token_id_u256), + ])?; + (0.into(), data, token_addr, eth_coin.ticker()) }, - None => { - let gas_price = eth_coin.get_gas_price().compat().await?; - let estimate_gas_req = CallRequest { - value: Some(eth_value), - data: Some(data.clone().into()), - from: Some(eth_coin.my_address), - to: Some(call_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - // Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; - (gas_limit, gas_price) + EthCoinType::Erc20 { .. } => { + return MmError::err(WithdrawError::InternalError( + "Erc20 coin type doesnt support withdraw nft".to_owned(), + )) }, }; + let (gas, gas_price) = + get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; let nonce = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) .compat() @@ -989,14 +1006,16 @@ pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResu gas, gas_price, }; + let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); let signed = tx.sign(secret, eth_coin.chain_id); let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash()), - from: vec![req.from], + from: vec![my_address], to: vec![req.to], contract_type: ContractType::Erc721, token_address: req.token_address, @@ -5076,7 +5095,7 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 } } -#[derive(Debug, Deserialize, Serialize, Display)] +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum GetEthAddressError { PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), EthActivationV2Error(EthActivationV2Error), @@ -5109,3 +5128,105 @@ pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult MmResult<(Address, Address, EthCoin), GetValidEthWithdrawAddError> { + let eth_coin = match coin_enum { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => { + return MmError::err(GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { + coin: coin_enum.ticker().to_owned(), + }) + }, + }; + let to_addr = valid_addr_from_str(to).map_err(GetValidEthWithdrawAddError::InvalidAddress)?; + let token_addr = addr_from_str(token_add).map_err(GetValidEthWithdrawAddError::InvalidAddress)?; + Ok((to_addr, token_addr, eth_coin)) +} + +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] +pub enum EthGasDetailsErr { + #[display(fmt = "Invalid fee policy: {}", _0)] + InvalidFeePolicy(String), + #[from_stringify("NumConversError")] + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), +} + +impl From for EthGasDetailsErr { + fn from(e: web3::Error) -> Self { EthGasDetailsErr::from(Web3RpcError::from(e)) } +} + +impl From for EthGasDetailsErr { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => EthGasDetailsErr::Transport(tr), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => EthGasDetailsErr::Internal(internal), + } + } +} + +async fn get_eth_gas_details( + eth_coin: &EthCoin, + fee: Option, + eth_value: U256, + data: Bytes, + call_addr: Address, + fungible_max: bool, +) -> MmResult { + match fee { + Some(WithdrawFee::EthGas { gas_price, gas }) => { + let gas_price = wei_from_big_decimal(&gas_price, 9)?; + Ok((gas.into(), gas_price)) + }, + Some(fee_policy) => { + let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); + MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)) + }, + None => { + let gas_price = eth_coin.get_gas_price().compat().await?; + // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH + let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { + eth_value - gas_price * U256::from(21000) + } else { + eth_value + }; + let estimate_gas_req = CallRequest { + value: Some(eth_value_for_estimate), + data: Some(data), + from: Some(eth_coin.my_address), + to: Some(call_addr), + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price: Some(gas_price), + ..CallRequest::default() + }; + // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. + let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; + Ok((gas_limit, gas_price)) + }, + } +} diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index d2251a6686..f3eda4d4ff 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -6,7 +6,7 @@ use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; -#[derive(Debug, Deserialize, Display, EnumFromTrait, Serialize, SerializeErrorType)] +#[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 3546277283..78118f54b7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -208,7 +208,10 @@ use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; pub mod coins_tests; pub mod eth; -use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthTxFeeDetails, GetEthAddressError, SignedEthTx}; +#[cfg(feature = "enable-nft-integration")] +use eth::GetValidEthWithdrawAddError; +use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, + GetEthAddressError, SignedEthTx}; pub mod hd_confirm_address; pub mod hd_pubkey; @@ -279,6 +282,8 @@ use utxo::UtxoActivationParams; use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; #[cfg(feature = "enable-nft-integration")] pub mod nft; +#[cfg(feature = "enable-nft-integration")] +use nft::nft_errors::GetNftInfoError; #[cfg(not(target_arch = "wasm32"))] pub mod z_coin; #[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; @@ -427,7 +432,7 @@ pub enum TxHistoryError { InternalError(String), } -#[derive(Clone, Debug, Display, Deserialize)] +#[derive(Clone, Debug, Deserialize, Display, PartialEq)] pub enum PrivKeyPolicyNotAllowed { #[display(fmt = "Hardware Wallet is not supported")] HardwareWalletNotSupported, @@ -1770,7 +1775,7 @@ impl DelegationError { } } -#[derive(Clone, Debug, Display, EnumFromStringify, EnumFromTrait, Serialize, SerializeErrorType, PartialEq)] +#[derive(Clone, Debug, Display, EnumFromStringify, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum WithdrawError { #[display( @@ -1833,14 +1838,38 @@ pub enum WithdrawError { #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[cfg(feature = "enable-nft-integration")] #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] CoinDoesntSupportNftWithdraw { coin: String }, + #[cfg(feature = "enable-nft-integration")] #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] AddressMismatchError { my_address: String, from: String }, + #[cfg(feature = "enable-nft-integration")] #[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)] ContractTypeDoesntSupportNftWithdrawing(String), #[display(fmt = "Action not allowed for coin: {}", _0)] ActionNotAllowed(String), + #[cfg(feature = "enable-nft-integration")] + GetNftInfoError(GetNftInfoError), + #[cfg(feature = "enable-nft-integration")] + #[display( + fmt = "Not enough NFTs amount with token_address: {} and token_id {}. Available {}, required {}", + token_address, + token_id, + available, + required + )] + NotEnoughNftsAmount { + token_address: String, + token_id: String, + available: BigDecimal, + required: BigDecimal, + }, +} + +#[cfg(feature = "enable-nft-integration")] +impl From for WithdrawError { + fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } } impl HttpStatusCode for WithdrawError { @@ -1860,14 +1889,17 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } | WithdrawError::UnexpectedUserAction { .. } - | WithdrawError::CoinDoesntSupportNftWithdraw { .. } - | WithdrawError::AddressMismatchError { .. } - | WithdrawError::ActionNotAllowed(_) - | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) => StatusCode::BAD_REQUEST, + | WithdrawError::ActionNotAllowed(_) => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + #[cfg(feature = "enable-nft-integration")] + WithdrawError::GetNftInfoError(_) + | WithdrawError::AddressMismatchError { .. } + | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) + | WithdrawError::CoinDoesntSupportNftWithdraw { .. } + | WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST, } } } @@ -1902,6 +1934,31 @@ impl From for WithdrawError { fn from(e: TimeoutError) -> Self { WithdrawError::Timeout(e.duration) } } +#[cfg(feature = "enable-nft-integration")] +impl From for WithdrawError { + fn from(e: GetValidEthWithdrawAddError) -> Self { + match e { + GetValidEthWithdrawAddError::AddressMismatchError { my_address, from } => { + WithdrawError::AddressMismatchError { my_address, from } + }, + GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { coin } => { + WithdrawError::CoinDoesntSupportNftWithdraw { coin } + }, + GetValidEthWithdrawAddError::InvalidAddress(e) => WithdrawError::InvalidAddress(e), + } + } +} + +impl From for WithdrawError { + fn from(e: EthGasDetailsErr) -> Self { + match e { + EthGasDetailsErr::InvalidFeePolicy(e) => WithdrawError::InvalidFeePolicy(e), + EthGasDetailsErr::Internal(e) => WithdrawError::InternalError(e), + EthGasDetailsErr::Transport(e) => WithdrawError::Transport(e), + } + } +} + impl WithdrawError { /// Construct [`WithdrawError`] from [`GenerateTxError`] using additional `coin` and `decimals`. pub fn from_generate_tx_error(gen_tx_err: GenerateTxError, coin: String, decimals: u8) -> WithdrawError { diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 0dc9a0aef3..2b768738bd 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -6,12 +6,14 @@ pub(crate) mod nft_structs; use crate::WithdrawError; use nft_errors::GetNftInfoError; -use nft_structs::{Chain, Nft, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryWrapper, - NftTransfersReq, NftWrapper, NftsTransferHistoryList, TransactionNftDetails, WithdrawNftReq}; +use nft_structs::{Chain, ConvertChain, Nft, NftList, NftListReq, NftMetadataReq, NftTransferHistory, + NftTransferHistoryWrapper, NftTransfersReq, NftWrapper, NftsTransferHistoryList, + TransactionNftDetails, WithdrawNftReq}; -use crate::eth::{get_eth_address, withdraw_erc721}; +use crate::eth::{get_eth_address, withdraw_erc1155, withdraw_erc721}; use common::{APPLICATION_JSON, X_API_KEY}; use http::header::ACCEPT; +use mm2_number::BigDecimal; use serde_json::Value as Json; /// url for moralis requests @@ -23,7 +25,7 @@ const DIRECTION_BOTH_MORALIS: &str = "direction=both"; pub type WithdrawNftResult = Result>; -/// `get_nft_list` function returns list of NFTs on ETH or/and BNB chains owned by user. +/// `get_nft_list` function returns list of NFTs on requested chains owned by user. pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult { let api_key = ctx.conf["api_key"] .as_str() @@ -32,11 +34,8 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult ("BNB", "bsc"), - Chain::Eth => ("ETH", "eth"), - }; - let my_address = get_eth_address(&ctx, coin_str).await?; + let (coin_str, chain_str) = chain.to_ticker_chain(); + let my_address = get_eth_address(&ctx, &coin_str).await?; let uri_without_cursor = format!( "{}{}/nft?chain={}&{}", URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS @@ -83,10 +82,7 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult MmResult MmResult { let api_key = ctx.conf["api_key"] .as_str() .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; let chain_str = match req.chain { + Chain::Avalanche => "avalanche", Chain::Bsc => "bsc", Chain::Eth => "eth", + Chain::Fantom => "fantom", + Chain::Polygon => "polygon", }; let uri = format!( "{}nft/{}/{}?chain={}&{}", @@ -129,7 +132,7 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult MmResult { let api_key = ctx.conf["api_key"] @@ -140,8 +143,11 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult ("AVAX", "avalanche"), Chain::Bsc => ("BNB", "bsc"), Chain::Eth => ("ETH", "eth"), + Chain::Fantom => ("FTM", "fantom"), + Chain::Polygon => ("MATIC", "polygon"), }; let my_address = get_eth_address(&ctx, coin_str).await?; let uri_without_cursor = format!( @@ -192,7 +198,6 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult MmResult WithdrawNftResult { match req_type { - WithdrawNftReq::WithdrawErc1155(_) => MmError::err(WithdrawError::ContractTypeDoesntSupportNftWithdrawing( - "ERC1155".to_owned(), - )), + WithdrawNftReq::WithdrawErc1155(erc1155_req) => withdraw_erc1155(ctx, erc1155_req).await, WithdrawNftReq::WithdrawErc721(erc721_req) => withdraw_erc721(ctx, erc721_req).await, } } @@ -265,3 +267,21 @@ async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult MmResult { + let nft_list = get_nft_list(ctx, nft_list).await?.nfts; + let nft = nft_list + .into_iter() + .find(|nft| nft.token_address == token_address_req && nft.token_id == token_id_req) + .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { + token_address: token_address_req, + token_id: token_id_req.to_string(), + })?; + Ok(nft.amount) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index d48753266b..bd510108ab 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -7,7 +7,7 @@ use mm2_net::transport::SlurpError; use serde::{Deserialize, Serialize}; use web3::Error; -#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum GetNftInfoError { /// `http::Error` can appear on an HTTP request [`http::Builder::build`] building. @@ -24,6 +24,15 @@ pub enum GetNftInfoError { GetEthAddressError(GetEthAddressError), #[display(fmt = "X-API-Key is missing")] ApiKeyError, + #[display( + fmt = "Token: token_address {}, token_id {} was not find in wallet", + token_address, + token_id + )] + TokenNotFoundInWallet { + token_address: String, + token_id: String, + }, } impl From for GetNftInfoError { @@ -60,9 +69,10 @@ impl HttpStatusCode for GetNftInfoError { GetNftInfoError::InvalidRequest(_) => StatusCode::BAD_REQUEST, GetNftInfoError::InvalidResponse(_) => StatusCode::FAILED_DEPENDENCY, GetNftInfoError::ApiKeyError => StatusCode::FORBIDDEN, - GetNftInfoError::Transport(_) | GetNftInfoError::Internal(_) | GetNftInfoError::GetEthAddressError(_) => { - StatusCode::INTERNAL_SERVER_ERROR - }, + GetNftInfoError::Transport(_) + | GetNftInfoError::Internal(_) + | GetNftInfoError::GetEthAddressError(_) + | GetNftInfoError::TokenNotFoundInWallet { .. } => StatusCode::INTERNAL_SERVER_ERROR, } } } diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index efe89cfab4..09e247967c 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -19,8 +19,39 @@ pub struct NftMetadataReq { #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "UPPERCASE")] pub(crate) enum Chain { + Avalanche, Bsc, Eth, + Fantom, + Polygon, +} + +pub(crate) trait ConvertChain { + fn to_ticker(&self) -> String; + + fn to_ticker_chain(&self) -> (String, String); +} + +impl ConvertChain for Chain { + fn to_ticker(&self) -> String { + match self { + Chain::Avalanche => "AVAX".to_owned(), + Chain::Bsc => "BNB".to_owned(), + Chain::Eth => "ETH".to_owned(), + Chain::Fantom => "FTM".to_owned(), + Chain::Polygon => "MATIC".to_owned(), + } + } + + fn to_ticker_chain(&self) -> (String, String) { + match self { + Chain::Avalanche => ("AVAX".to_owned(), "avalanche".to_owned()), + Chain::Bsc => ("BNB".to_owned(), "bsc".to_owned()), + Chain::Eth => ("ETH".to_owned(), "eth".to_owned()), + Chain::Fantom => ("FTM".to_owned(), "fantom".to_owned()), + Chain::Polygon => ("MATIC".to_owned(), "polygon".to_owned()), + } + } } #[derive(Debug, Display)] @@ -114,28 +145,24 @@ impl std::ops::Deref for SerdeStringWrap { #[derive(Debug, Serialize)] pub struct NftList { - pub(crate) count: u64, pub(crate) nfts: Vec, } -#[allow(dead_code)] #[derive(Clone, Deserialize)] pub struct WithdrawErc1155 { pub(crate) chain: Chain, - from: String, - to: String, - token_address: String, - token_id: BigDecimal, - amount: BigDecimal, + pub(crate) to: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: Option, #[serde(default)] - max: bool, - fee: Option, + pub(crate) max: bool, + pub(crate) fee: Option, } #[derive(Clone, Deserialize)] pub struct WithdrawErc721 { pub(crate) chain: Chain, - pub(crate) from: String, pub(crate) to: String, pub(crate) token_address: String, pub(crate) token_id: BigDecimal, @@ -151,7 +178,7 @@ pub enum WithdrawNftReq { #[derive(Debug, Deserialize, Serialize)] pub struct TransactionNftDetails { - /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction` RPC to broadcast the transaction pub(crate) tx_hex: BytesJson, pub(crate) tx_hash: String, /// NFTs are sent from these addresses @@ -226,6 +253,5 @@ pub(crate) struct NftTransferHistoryWrapper { #[derive(Debug, Serialize)] pub struct NftsTransferHistoryList { - pub(crate) count: u64, pub(crate) transfer_history: Vec, }