Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nft): nft abi in withdraw_nft RPC, clear_nft_db RPC #2039

Merged
merged 16 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 90 additions & 24 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ pub use rlp;
mod web3_transport;

#[path = "eth/v2_activation.rs"] pub mod v2_activation;
use crate::nft::{find_wallet_nft_amount, WithdrawNftResult};
use crate::nft::WithdrawNftResult;
use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error};

mod nonce;
use crate::nft::nft_errors::GetNftInfoError;
use crate::{PrivKeyPolicy, TransactionResult, WithdrawFrom};
use nonce::ParityNonce;

Expand Down Expand Up @@ -877,16 +878,12 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult {
pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> WithdrawNftResult {
let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?;
let (to_addr, token_addr, eth_coin) =
get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?;
let my_address = eth_coin.my_address()?;

let wallet_amount = find_wallet_nft_amount(
&ctx,
&withdraw_type.chain,
withdraw_type.token_address.to_lowercase(),
withdraw_type.token_id.clone(),
)
.await?;
get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?;
let my_address_str = eth_coin.my_address()?;

let token_id_str = &withdraw_type.token_id.to_string();
let wallet_amount = eth_coin.erc1155_balance(token_addr, token_id_str).await?;

let amount_dec = if withdraw_type.max {
wallet_amount.clone()
} else {
Expand All @@ -905,7 +902,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit
let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type {
EthCoinType::Eth => {
let function = ERC1155_CONTRACT.function("safeTransferFrom")?;
let token_id_u256 = U256::from_dec_str(&withdraw_type.token_id.to_string())
let token_id_u256 = U256::from_dec_str(token_id_str)
.map_err(|e| format!("{:?}", e))
.map_to_mm(NumConversError::new)?;
let amount_u256 = U256::from_dec_str(&amount_dec.to_string())
Expand Down Expand Up @@ -959,7 +956,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit
Ok(TransactionNftDetails {
tx_hex: BytesJson::from(signed_bytes.to_vec()),
tx_hash: format!("{:02x}", signed.tx_hash()),
from: vec![my_address],
from: vec![my_address_str],
to: vec![withdraw_type.to],
contract_type: ContractType::Erc1155,
token_address: withdraw_type.token_address,
Expand All @@ -979,8 +976,18 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit
pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> WithdrawNftResult {
let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?;
let (to_addr, token_addr, eth_coin) =
get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?;
let my_address = eth_coin.my_address()?;
get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?;
let my_address_str = eth_coin.my_address()?;

let token_id_str = &withdraw_type.token_id.to_string();
let token_owner = eth_coin.erc721_owner(token_addr, token_id_str).await?;
let my_address = eth_coin.my_address;
if token_owner != my_address {
return MmError::err(WithdrawError::MyAddressNotNftOwner {
my_address: eth_addr_to_hex(&my_address),
token_owner: eth_addr_to_hex(&token_owner),
});
}

let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type {
EthCoinType::Eth => {
Expand All @@ -989,7 +996,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd
.map_err(|e| format!("{:?}", e))
.map_to_mm(NumConversError::new)?;
let data = function.encode_input(&[
Token::Address(eth_coin.my_address),
Token::Address(my_address),
Token::Address(to_addr),
Token::Uint(token_id_u256),
])?;
Expand All @@ -1011,7 +1018,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd
)
.await?;
let _nonce_lock = eth_coin.nonce_lock.lock().await;
let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone())
let (nonce, _) = get_addr_nonce(my_address, eth_coin.web3_instances.clone())
.compat()
.timeout_secs(30.)
.await?
Expand All @@ -1034,7 +1041,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd
Ok(TransactionNftDetails {
tx_hex: BytesJson::from(signed_bytes.to_vec()),
tx_hash: format!("{:02x}", signed.tx_hash()),
from: vec![my_address],
from: vec![my_address_str],
to: vec![withdraw_type.to],
contract_type: ContractType::Erc721,
token_address: withdraw_type.token_address,
Expand Down Expand Up @@ -3943,6 +3950,61 @@ impl EthCoin {
}
}

async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult<BigDecimal, BalanceError> {
let wallet_amount_uint = match self.coin_type {
EthCoinType::Eth => {
let function = ERC1155_CONTRACT.function("balanceOf")?;
borngraced marked this conversation as resolved.
Show resolved Hide resolved
let token_id_u256 = U256::from_dec_str(token_id)
.map_err(|e| format!("{:?}", e))
.map_to_mm(NumConversError::new)?;
shamardy marked this conversation as resolved.
Show resolved Hide resolved
let data = function.encode_input(&[Token::Address(self.my_address), Token::Uint(token_id_u256)])?;
let result = self.call_request(token_addr, None, Some(data.into())).await?;
let decoded = function.decode_output(&result.0)?;
match decoded[0] {
Token::Uint(number) => number,
_ => {
let error = format!("Expected U256 as balanceOf result but got {:?}", decoded);
return MmError::err(BalanceError::InvalidResponse(error));
},
}
},
EthCoinType::Erc20 { .. } => {
return MmError::err(BalanceError::Internal(
"Erc20 coin type doesnt support Erc1155 standard".to_owned(),
))
},
};
let wallet_amount = BigDecimal::from_str(&wallet_amount_uint.to_string()).map_err(NumConversError::from)?;
onur-ozkan marked this conversation as resolved.
Show resolved Hide resolved
Ok(wallet_amount)
}

async fn erc721_owner(&self, token_addr: Address, token_id: &str) -> MmResult<Address, GetNftInfoError> {
let owner_address = match self.coin_type {
EthCoinType::Eth => {
let function = ERC721_CONTRACT.function("ownerOf")?;
borngraced marked this conversation as resolved.
Show resolved Hide resolved
let token_id_u256 = U256::from_dec_str(token_id)
.map_err(|e| format!("{:?}", e))
.map_to_mm(NumConversError::new)?;
shamardy marked this conversation as resolved.
Show resolved Hide resolved
let data = function.encode_input(&[Token::Uint(token_id_u256)])?;
let result = self.call_request(token_addr, None, Some(data.into())).await?;
let decoded = function.decode_output(&result.0)?;
match decoded[0] {
Token::Address(owner) => owner,
_ => {
let error = format!("Expected Address as ownerOf result but got {:?}", decoded);
return MmError::err(GetNftInfoError::InvalidResponse(error));
},
}
},
EthCoinType::Erc20 { .. } => {
return MmError::err(GetNftInfoError::Internal(
"Erc20 coin type doesnt support Erc721 standard".to_owned(),
))
},
};
Ok(owner_address)
}

fn estimate_gas(&self, req: CallRequest) -> Box<dyn Future<Item = U256, Error = web3::Error> + Send> {
// always using None block number as old Geth version accept only single argument in this RPC
Box::new(self.web3.eth().estimate_gas(req, None).compat())
Expand Down Expand Up @@ -5658,6 +5720,7 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256
}
}

/// Represents errors that can occur while retrieving an Ethereum address.
#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)]
pub enum GetEthAddressError {
PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed),
Expand Down Expand Up @@ -5698,21 +5761,24 @@ pub async fn get_eth_address(
})
}

/// Errors encountered while validating Ethereum addresses for NFT withdrawal.
///
/// Variants:
/// - `CoinDoesntSupportNftWithdraw`: The specified coin does not support NFT withdrawal.
/// - `InvalidAddress`: The provided address is invalid.
shamardy marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Display)]
pub enum GetValidEthWithdrawAddError {
#[display(fmt = "My address {} and from address {} mismatch", my_address, from)]
AddressMismatchError {
my_address: String,
from: String,
},
#[display(fmt = "{} coin doesn't support NFT withdrawing", coin)]
CoinDoesntSupportNftWithdraw {
coin: String,
},
InvalidAddress(String),
}

fn get_valid_nft_add_to_withdraw(
/// Validates Ethereum addresses for NFT withdrawal.
/// Returns a tuple of valid `to` address, `token` address, and `EthCoin` instance on success.
/// Errors if the coin doesn't support NFT withdrawal or if the addresses are invalid.
fn get_valid_nft_addr_to_withdraw(
coin_enum: MmCoinEnum,
to: &str,
token_add: &str,
Expand Down
17 changes: 7 additions & 10 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2315,11 +2315,6 @@ pub enum WithdrawError {
CoinDoesntSupportNftWithdraw {
coin: String,
},
#[display(fmt = "My address {} and from address {} mismatch", my_address, from)]
AddressMismatchError {
my_address: String,
from: String,
},
#[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)]
ContractTypeDoesntSupportNftWithdrawing(String),
#[display(fmt = "Action not allowed for coin: {}", _0)]
Expand All @@ -2340,6 +2335,11 @@ pub enum WithdrawError {
},
#[display(fmt = "DB error {}", _0)]
DbError(String),
#[display(fmt = "My address is {}, while current Nft owner is {}", my_address, token_owner)]
MyAddressNotNftOwner {
my_address: String,
token_owner: String,
},
}

impl HttpStatusCode for WithdrawError {
Expand All @@ -2362,10 +2362,10 @@ impl HttpStatusCode for WithdrawError {
| WithdrawError::UnsupportedError(_)
| WithdrawError::ActionNotAllowed(_)
| WithdrawError::GetNftInfoError(_)
| WithdrawError::AddressMismatchError { .. }
| WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_)
| WithdrawError::CoinDoesntSupportNftWithdraw { .. }
| WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST,
| WithdrawError::NotEnoughNftsAmount { .. }
| WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST,
WithdrawError::HwError(_) => StatusCode::GONE,
#[cfg(target_arch = "wasm32")]
WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST,
Expand Down Expand Up @@ -2409,9 +2409,6 @@ impl From<TimeoutError> for WithdrawError {
impl From<GetValidEthWithdrawAddError> 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 }
},
Expand Down
77 changes: 47 additions & 30 deletions mm2src/coins/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftLis

use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType,
EthTxFeeDetails};
use crate::nft::nft_errors::{MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError,
use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError,
UpdateSpamPhishingError};
use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, NftCommon, NftCtx, NftTransferCommon,
PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq, SpamContractReq,
SpamContractRes, TransferMeta, TransferStatus, UriMeta};
use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx,
NftTransferCommon, PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq,
SpamContractReq, SpamContractRes, TransferMeta, TransferStatus, UriMeta};
use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps};
use common::parse_rfc3339_to_timestamp;
use common::{log::error, parse_rfc3339_to_timestamp};
use crypto::StandardHDCoinAddress;
use ethereum_types::{Address, H256};
use futures::compat::Future01CompatExt;
use futures::future::try_join_all;
use mm2_err_handle::map_to_mm::MapToMmResult;
use mm2_net::transport::send_post_request_to_uri;
use mm2_number::{BigDecimal, BigUint};
use mm2_number::BigUint;
use regex::Regex;
use serde_json::Value as Json;
use std::cmp::Ordering;
Expand Down Expand Up @@ -1160,30 +1160,6 @@ async fn mark_as_spam_and_build_empty_meta<T: NftListStorageOps + NftTransferHis
}))
}

/// `find_wallet_nft_amount` function returns NFT amount of cached NFT.
/// Note: in db **token_address** is kept in **lowercase**, because Moralis returns all addresses in lowercase.
pub(crate) async fn find_wallet_nft_amount(
ctx: &MmArc,
chain: &Chain,
token_address: String,
token_id: BigUint,
) -> MmResult<BigDecimal, GetNftInfoError> {
let nft_ctx = NftCtx::from_ctx(ctx).map_to_mm(GetNftInfoError::Internal)?;

let storage = nft_ctx.lock_db().await?;
if !NftListStorageOps::is_initialized(&storage, chain).await? {
NftListStorageOps::init(&storage, chain).await?;
}
let nft_meta = storage
.get_nft(chain, token_address.to_lowercase(), token_id.clone())
.await?
.ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet {
token_address,
token_id: token_id.to_string(),
})?;
Ok(nft_meta.common.amount)
}

async fn cache_nfts_from_moralis<T: NftListStorageOps + NftTransferHistoryStorageOps>(
ctx: &MmArc,
storage: &T,
Expand Down Expand Up @@ -1396,3 +1372,44 @@ pub(crate) fn get_domain_from_url(url: Option<&str>) -> Option<String> {
url.and_then(|uri| Url::parse(uri).ok())
.and_then(|url| url.domain().map(String::from))
}

/// Clears NFT data from the database for specified chains.
pub async fn clear_nft_db(ctx: MmArc, req: ClearNftDbReq) -> MmResult<(), ClearNftDbError> {
if req.chains.is_none() && !req.clear_all {
return MmError::err(ClearNftDbError::InvalidRequest(
"Nothing to clear was specified".to_string(),
));
}
let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(ClearNftDbError::Internal)?;
let storage = nft_ctx.lock_db().await?;
onur-ozkan marked this conversation as resolved.
Show resolved Hide resolved

if req.clear_all {
storage.clear_all_nft_data().await?;
storage.clear_all_history_data().await?;
} else if let Some(chains) = req.chains {
shamardy marked this conversation as resolved.
Show resolved Hide resolved
for chain in chains.iter() {
if let Err(e) = clear_data_for_chain(&storage, chain).await {
error!("Failed to clear data for chain {}: {}", chain, e);
}
}
}

Ok(())
}

async fn clear_data_for_chain<T>(storage: &T, chain: &Chain) -> MmResult<(), ClearNftDbError>
where
T: NftListStorageOps + NftTransferHistoryStorageOps,
{
let (is_nft_list_init, is_history_init) = (
NftListStorageOps::is_initialized(storage, chain).await?,
NftTransferHistoryStorageOps::is_initialized(storage, chain).await?,
);
if is_nft_list_init {
storage.clear_nft_data(chain).await?;
}
if is_history_init {
storage.clear_history_data(chain).await?;
}
Ok(())
}
Loading
Loading