Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions mm2src/coins/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5293,6 +5293,10 @@ fn checksum_address(addr: &str) -> String {
result
}

/// `eth_add_to_hex` converts Address to hex format.
/// Note: the result will be in lowercase.
pub(crate) fn eth_add_to_hex(address: &Address) -> String { format!("{:#02x}", address) }
Comment thread
shamardy marked this conversation as resolved.
Outdated

/// Checks that input is valid mixed-case checksum form address
/// The input must be 0x prefixed hex string
fn is_valid_checksum_addr(addr: &str) -> bool { addr == checksum_address(addr) }
Expand Down Expand Up @@ -5408,6 +5412,7 @@ impl From<CryptoCtxError> for GetEthAddressError {
}

/// `get_eth_address` returns wallet address for coin with `ETH` protocol type.
/// Note: result address has mixed-case checksum form.
pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult<MyWalletAddress, GetEthAddressError> {
let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?;
// Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible.
Expand Down
103 changes: 80 additions & 23 deletions mm2src/coins/nft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,36 @@ use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftLis
NftTransferHistory, NftTransfersReq, NftTxHistoryFromMoralis, NftsTransferHistoryList,
TransactionNftDetails, UpdateNftReq, WithdrawNftReq};

use crate::eth::{get_eth_address, withdraw_erc1155, withdraw_erc721};
use crate::eth::{eth_add_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721};
use crate::nft::nft_errors::ProtectFromSpamError;
use crate::nft::nft_structs::{NftCommon, NftTransferCommon, RefreshMetadataReq, TransferStatus, TxMeta, UriMeta};
use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetadataReq, TransferStatus, TxMeta,
UriMeta};
use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTxHistoryStorageOps};
use common::{parse_rfc3339_to_timestamp, APPLICATION_JSON};
use ethereum_types::Address;
use http::header::ACCEPT;
use mm2_err_handle::map_to_mm::MapToMmResult;
use mm2_number::BigDecimal;
use regex::Regex;
use serde_json::Value as Json;
use std::cmp::Ordering;
use std::str::FromStr;

const MORALIS_API_ENDPOINT: &str = "api/v2";
/// query parameters for moralis request: The format of the token ID
const MORALIS_FORMAT_QUERY_NAME: &str = "format";
const MORALIS_FORMAT_QUERY_VALUE: &str = "decimal";
/// query parameters for moralis request: The transfer direction
const MORALIS_DIRECTION_QUERY_NAME: &str = "direction";
const MORALIS_DIRECTION_QUERY_VALUE: &str = "both";
/// The minimum block number from which to get the transfers
const MORALIS_FROM_BLOCK_QUERY_NAME: &str = "from_block";

pub type WithdrawNftResult = Result<TransactionNftDetails, MmError<WithdrawError>>;

/// `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<NftList, GetNftInfoError> {
let nft_ctx = NftCtx::from_ctx(&ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
Comment thread
shamardy marked this conversation as resolved.
Outdated
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(&ctx).build()?;
for chain in req.chains.iter() {
if !NftListStorageOps::is_initialized(&storage, chain).await? {
Expand All @@ -60,6 +64,10 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult<NftList, GetN

/// `get_nft_metadata` function returns info of one specific NFT.
pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult<Nft, GetNftInfoError> {
let nft_ctx = NftCtx::from_ctx(&ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(&ctx).build()?;
if !NftListStorageOps::is_initialized(&storage, &req.chain).await? {
NftListStorageOps::init(&storage, &req.chain).await?;
Expand All @@ -80,6 +88,10 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult<Nft,

/// `get_nft_transfers` function returns a transfer history of NFTs on requested chains owned by user.
pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult<NftsTransferHistoryList, GetNftInfoError> {
let nft_ctx = NftCtx::from_ctx(&ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(&ctx).build()?;
for chain in req.chains.iter() {
if !NftTxHistoryStorageOps::is_initialized(&storage, chain).await? {
Expand All @@ -100,6 +112,10 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult<Nft

/// `update_nft` function updates cache of nft transfer history and nft list.
pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNftError> {
let nft_ctx = NftCtx::from_ctx(&ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(&ctx).build()?;
for chain in req.chains.iter() {
let tx_history_initialized = NftTxHistoryStorageOps::is_initialized(&storage, chain).await?;
Expand Down Expand Up @@ -160,6 +176,10 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft
}

pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResult<(), UpdateNftError> {
let nft_ctx = NftCtx::from_ctx(&ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(&ctx).build()?;
let moralis_meta = get_moralis_metadata(
format!("{:#02x}", req.token_address),
Expand All @@ -174,7 +194,7 @@ pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResu
chain: req.chain,
protect_from_spam: false,
};
let mut nft_db = get_nft_metadata(ctx, req).await?;
let mut nft_db = get_nft_metadata(ctx.clone(), req).await?;
let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref());
let uri_meta = get_uri_meta(token_uri.as_deref(), moralis_meta.common.metadata.as_deref()).await;
nft_db.common.collection_name = moralis_meta.common.collection_name;
Expand Down Expand Up @@ -266,7 +286,6 @@ async fn get_moralis_nft_transfers(
.query_pairs_mut()
.append_pair("chain", &chain.to_string())
.append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE)
.append_pair(MORALIS_DIRECTION_QUERY_NAME, MORALIS_DIRECTION_QUERY_VALUE)
.append_pair(MORALIS_FROM_BLOCK_QUERY_NAME, &from_block);
drop_mutability!(uri_without_cursor);

Expand All @@ -283,7 +302,7 @@ async fn get_moralis_nft_transfers(
Some(contract_type) => contract_type,
None => continue,
};
let status = get_tx_status(&wallet_address, &transfer_moralis.common.to_address);
let status = get_tx_status(&wallet_address, &eth_add_to_hex(&transfer_moralis.common.to_address));
let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?;
let transfer_history = NftTransferHistory {
common: NftTransferCommon {
Expand Down Expand Up @@ -517,16 +536,25 @@ async fn handle_send_erc721<T: NftListStorageOps + NftTxHistoryStorageOps>(
tx: NftTransferHistory,
) -> MmResult<(), UpdateNftError> {
let nft_db = storage
.get_nft(chain, tx.common.token_address.clone(), tx.common.token_id.clone())
.get_nft(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id.clone(),
)
.await?
.ok_or_else(|| UpdateNftError::TokenNotFoundInWallet {
token_address: tx.common.token_address.clone(),
token_address: eth_add_to_hex(&tx.common.token_address),
token_id: tx.common.token_id.to_string(),
})?;
let tx_meta = TxMeta::from(nft_db);
storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?;
storage
.remove_nft_from_list(chain, tx.common.token_address, tx.common.token_id, tx.block_number)
.remove_nft_from_list(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id,
tx.block_number,
)
.await?;
Ok(())
}
Expand All @@ -539,13 +567,17 @@ async fn handle_receive_erc721<T: NftListStorageOps + NftTxHistoryStorageOps>(
my_address: &str,
) -> MmResult<(), UpdateNftError> {
let nft = match storage
.get_nft(chain, tx.common.token_address.clone(), tx.common.token_id.clone())
.get_nft(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id.clone(),
)
.await?
{
Some(mut nft_db) => {
// An error is raised if user tries to receive an identical ERC-721 token they already own
// and if owner address != from address
if my_address != tx.common.from_address {
if my_address != eth_add_to_hex(&tx.common.from_address) {
return MmError::err(UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 {
tx_hash: tx.common.transaction_hash,
});
Expand All @@ -559,10 +591,12 @@ async fn handle_receive_erc721<T: NftListStorageOps + NftTxHistoryStorageOps>(
},
// If token isn't in NFT LIST table then add nft to the table.
None => {
let mut nft = get_moralis_metadata(tx.common.token_address, tx.common.token_id, chain, url).await?;
let mut nft =
get_moralis_metadata(eth_add_to_hex(&tx.common.token_address), tx.common.token_id, chain, url).await?;
// sometimes moralis updates Get All NFTs (which also affects Get Metadata) later
// than History by Wallet update
nft.common.owner_of = my_address.to_string();
nft.common.owner_of =
Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?;
nft.block_number = tx.block_number;
drop_mutability!(nft);
storage
Expand All @@ -582,16 +616,25 @@ async fn handle_send_erc1155<T: NftListStorageOps + NftTxHistoryStorageOps>(
tx: NftTransferHistory,
) -> MmResult<(), UpdateNftError> {
let mut nft_db = storage
.get_nft(chain, tx.common.token_address.clone(), tx.common.token_id.clone())
.get_nft(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id.clone(),
)
.await?
.ok_or_else(|| UpdateNftError::TokenNotFoundInWallet {
token_address: tx.common.token_address.clone(),
token_address: eth_add_to_hex(&tx.common.token_address),
token_id: tx.common.token_id.to_string(),
})?;
match nft_db.common.amount.cmp(&tx.common.amount) {
Ordering::Equal => {
storage
.remove_nft_from_list(chain, tx.common.token_address, tx.common.token_id, tx.block_number)
.remove_nft_from_list(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id,
tx.block_number,
)
.await?;
},
Ordering::Greater => {
Expand Down Expand Up @@ -620,13 +663,17 @@ async fn handle_receive_erc1155<T: NftListStorageOps + NftTxHistoryStorageOps>(
my_address: &str,
) -> MmResult<(), UpdateNftError> {
let nft = match storage
.get_nft(chain, tx.common.token_address.clone(), tx.common.token_id.clone())
.get_nft(
chain,
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id.clone(),
)
.await?
{
Some(mut nft_db) => {
// if owner address == from address, then owner sent tokens to themself,
// which means that the amount will not change.
if my_address != tx.common.from_address {
if my_address != eth_add_to_hex(&tx.common.from_address) {
nft_db.common.amount += tx.common.amount;
}
nft_db.block_number = tx.block_number;
Expand All @@ -638,16 +685,22 @@ async fn handle_receive_erc1155<T: NftListStorageOps + NftTxHistoryStorageOps>(
},
// If token isn't in NFT LIST table then add nft to the table.
None => {
let moralis_meta =
get_moralis_metadata(tx.common.token_address, tx.common.token_id.clone(), chain, url).await?;
let moralis_meta = get_moralis_metadata(
eth_add_to_hex(&tx.common.token_address),
tx.common.token_id.clone(),
chain,
url,
)
.await?;
let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref());
let uri_meta = get_uri_meta(token_uri.as_deref(), moralis_meta.common.metadata.as_deref()).await;
let nft = Nft {
common: NftCommon {
token_address: moralis_meta.common.token_address,
token_id: moralis_meta.common.token_id,
amount: tx.common.amount,
owner_of: my_address.to_string(),
owner_of: Address::from_str(my_address)
.map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?,
token_hash: moralis_meta.common.token_hash,
collection_name: moralis_meta.common.collection_name,
symbol: moralis_meta.common.symbol,
Expand Down Expand Up @@ -681,6 +734,10 @@ pub(crate) async fn find_wallet_nft_amount(
token_address: String,
token_id: BigDecimal,
) -> MmResult<BigDecimal, GetNftInfoError> {
let nft_ctx = NftCtx::from_ctx(ctx).map_err(GetNftInfoError::Internal)?;
let guard = nft_ctx.guard.clone();
let _lock = guard.lock().await;

let storage = NftStorageBuilder::new(ctx).build()?;
if !NftListStorageOps::is_initialized(&storage, chain).await? {
NftListStorageOps::init(&storage, chain).await?;
Expand Down
5 changes: 4 additions & 1 deletion mm2src/coins/nft/nft_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub enum UpdateNftError {
AttemptToReceiveAlreadyOwnedErc721 {
tx_hash: String,
},
#[display(fmt = "Invalid hex string: {}", _0)]
InvalidHexString(String),
}

impl From<CreateNftStorageError> for UpdateNftError {
Expand Down Expand Up @@ -199,7 +201,8 @@ impl HttpStatusCode for UpdateNftError {
| UpdateNftError::InsufficientAmountInCache { .. }
| UpdateNftError::InvalidBlockOrder { .. }
| UpdateNftError::LastScannedBlockNotFound { .. }
| UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { .. } => StatusCode::INTERNAL_SERVER_ERROR,
| UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { .. }
| UpdateNftError::InvalidHexString(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Expand Down
34 changes: 26 additions & 8 deletions mm2src/coins/nft/nft_structs.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use crate::nft::eth_add_to_hex;
use crate::{TransactionType, TxFeeDetails, WithdrawFee};
use common::ten;
use ethereum_types::Address;
use futures::lock::Mutex as AsyncMutex;
use mm2_core::mm_ctx::{from_ctx, MmArc};
use mm2_number::BigDecimal;
use rpc::v1::types::Bytes as BytesJson;
use serde::Deserialize;
use serde_json::Value as Json;
use std::fmt;
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::Arc;
use url::Url;

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -41,7 +45,7 @@ pub struct RefreshMetadataReq {

#[derive(Debug, Display)]
pub enum ParseChainTypeError {
UnsupportedCainType,
UnsupportedChainType,
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -93,7 +97,7 @@ impl FromStr for Chain {
"ETH" => Ok(Chain::Eth),
"FANTOM" => Ok(Chain::Fantom),
"POLYGON" => Ok(Chain::Polygon),
_ => Err(ParseChainTypeError::UnsupportedCainType),
_ => Err(ParseChainTypeError::UnsupportedChainType),
}
}
}
Expand Down Expand Up @@ -188,10 +192,10 @@ impl UriMeta {
/// [`NftCommon`] structure contains common fields from [`Nft`] and [`NftFromMoralis`]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NftCommon {
pub(crate) token_address: String,
pub(crate) token_address: Address,
pub(crate) token_id: BigDecimal,
pub(crate) amount: BigDecimal,
pub(crate) owner_of: String,
pub(crate) owner_of: Address,
pub(crate) token_hash: Option<String>,
#[serde(rename = "name")]
pub(crate) collection_name: Option<String>,
Expand Down Expand Up @@ -368,10 +372,10 @@ pub struct NftTransferCommon {
pub(crate) log_index: Option<u64>,
pub(crate) value: Option<BigDecimal>,
pub(crate) transaction_type: Option<String>,
pub(crate) token_address: String,
pub(crate) token_address: Address,
pub(crate) token_id: BigDecimal,
pub(crate) from_address: String,
pub(crate) to_address: String,
pub(crate) from_address: Address,
pub(crate) to_address: Address,
pub(crate) amount: BigDecimal,
pub(crate) verified: Option<u64>,
pub(crate) operator: Option<String>,
Expand Down Expand Up @@ -446,7 +450,7 @@ pub struct TxMeta {
impl From<Nft> for TxMeta {
fn from(nft_db: Nft) -> Self {
TxMeta {
token_address: nft_db.common.token_address,
token_address: eth_add_to_hex(&nft_db.common.token_address),
token_id: nft_db.common.token_id,
token_uri: nft_db.common.token_uri,
collection_name: nft_db.common.collection_name,
Expand All @@ -455,3 +459,17 @@ impl From<Nft> for TxMeta {
}
}
}

pub(crate) struct NftCtx {
pub(crate) guard: Arc<AsyncMutex<()>>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of a guard you should have the nft_cache_db in the Nft context https://github.com/KomodoPlatform/komodo-defi-framework/blob/806ac632a2f65ce09e2215af9dc25298458d45e2/mm2src/coins/lp_coins.rs#L2577
then it can be initialized only once when from_ctx first called and then retrieved on subsequent calls and this is done using from_ctx https://github.com/KomodoPlatform/komodo-defi-framework/blob/eaed80ef9e839e6a51812723fbc2cb66aa15d8d9/mm2src/mm2_core/src/mm_ctx.rs#L608
like how it's used in CoinsContext https://github.com/KomodoPlatform/komodo-defi-framework/blob/806ac632a2f65ce09e2215af9dc25298458d45e2/mm2src/coins/lp_coins.rs#L2562
This is not a blocker since guard works right now, but it's better code structure since now we have CoinsContext and NftCtx and nft_cache_db is in CoinsContext. I think you can remove the need to build storage in every nft function also following from the above.

@laruh laruh Jul 16, 2023

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of a guard you should have the nft_cache_db in the Nft context

without mutex guard, we will have the same mm2 problem due to race condition.

{"mmrpc":"2.0","error":"DB error ErrorSaving(\"Error uploading an item: \\\"DomException { obj: Object { obj: JsValue(ConstraintError: Unable to add key to index 'chain_tx_hash_index': at least one key does not satisfy the uniqueness requirements.\\\\nundefined) } }\\\"\")","error_path":"nft.wasm_storage.object_store","error_trace":"nft:115] wasm_storage:362] object_store:42]","error_type":"DbError","error_data":"ErrorSaving(\"Error uploading an item: \\\"DomException { obj: Object { obj: JsValue(ConstraintError: Unable to add key to index 'chain_tx_hash_index': at least one key does not satisfy the uniqueness requirements.\\\\nundefined) } }\\\"\")","id":null}

PS: I also caught this problem in native target. Sql propagates the error that you are trying to add item with identical primary key.

I mean if we just move nft_cache_db to NftCtx, we will have the same situation. Instead of CoinsContext::from_ctx(ctx) we will just have NftCtx::from_ctx(&ctx) here
image
Also dont forget, that nft_cache_db is for wasm target, for native we use sqlite_connection from mmctx.

We need to lock the whole function before using storage and sending the moralis request. So I believe it doesn't mater, where we have nft_cache_db or build storage, we need to prevent the access to storage and sending identical moralis request before previous RPC is done.

But yeah, it is a good idea, bcz nft_cache_db should be a part of context of related feature. I didnt notice it.

I think you can remove the need to build storage in every nft function also following from the above.

yep, I have it in my plans. Actually I was trying to move the process of NftBuilder creation and NftStorage building to NftCtx as in this example, but I faced with problem during creating of Box with type impl NftListStorageOps + NftTxHistoryStorageOps. Moreover traits have type Error: NftStorageError. So I postponed this idea until the next refactoring.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved nft_cache_db field to struct NftCtx

}

impl NftCtx {
pub(crate) fn from_ctx(ctx: &MmArc) -> Result<Arc<NftCtx>, String> {
Ok(try_s!(from_ctx(&ctx.nft_ctx, move || {
Ok(NftCtx {
guard: Arc::new(AsyncMutex::new(())),
})
})))
}
}
Loading