diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet_storage/wasm_storage.rs index a9e11143e2..76ad67494f 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet_storage/wasm_storage.rs @@ -10,7 +10,6 @@ use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, DbTable, DbTransact TableSignature, WeakDb}; use mm2_err_handle::prelude::*; -const DB_NAME: &str = "hd_wallet"; const DB_VERSION: u32 = 1; /// An index of the `HDAccountTable` table that consists of the following properties: /// * coin - coin ticker @@ -142,7 +141,7 @@ pub struct HDWalletDb { #[async_trait] impl DbInstance for HDWalletDb { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "hd_wallet"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index c84d0cbe24..0f0cb942e0 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -9,27 +9,35 @@ pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; use crate::{coin_conf, get_my_address, MyAddressReq, WithdrawError}; -use nft_errors::{GetInfoFromUriError, GetNftInfoError, UpdateNftError}; +use nft_errors::{GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721}; -use crate::nft::nft_errors::ProtectFromSpamError; -use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetadataReq, TransferMeta, TransferStatus, - UriMeta}; +use crate::nft::nft_errors::{MetaFromUrlError, ProtectFromSpamError, 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::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps}; -use common::{parse_rfc3339_to_timestamp, APPLICATION_JSON}; +use common::parse_rfc3339_to_timestamp; use crypto::StandardHDCoinAddress; use ethereum_types::Address; -use http::header::ACCEPT; use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_net::transport::send_post_request_to_uri; use mm2_number::BigDecimal; use regex::Regex; use serde_json::Value as Json; use std::cmp::Ordering; +use std::collections::HashSet; use std::str::FromStr; +#[cfg(not(target_arch = "wasm32"))] +use mm2_net::native_http::send_request_to_uri; + +#[cfg(target_arch = "wasm32")] +use mm2_net::wasm_http::send_request_to_uri; + 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"; @@ -37,9 +45,33 @@ const MORALIS_FORMAT_QUERY_VALUE: &str = "decimal"; /// The minimum block number from which to get the transfers const MORALIS_FROM_BLOCK_QUERY_NAME: &str = "from_block"; +const BLOCKLIST_ENDPOINT: &str = "api/blocklist"; +const BLOCKLIST_CONTRACT: &str = "contract"; +const BLOCKLIST_DOMAIN: &str = "domain"; +const BLOCKLIST_SCAN: &str = "scan"; + +/// `WithdrawNftResult` type represents the result of an NFT withdrawal operation. On success, it provides the details +/// of the generated transaction meant for transferring the NFT. On failure, it details the encountered error. pub type WithdrawNftResult = Result>; -/// `get_nft_list` function returns list of NFTs on requested chains owned by user. +/// Fetches a list of user-owned NFTs across specified chains. +/// +/// The function aggregates NFTs based on provided chains, supports pagination, and +/// allows for result limits and filters. If the `protect_from_spam` flag is true, +/// NFTs are checked and redacted for potential spam. +/// +/// # Parameters +/// +/// * `ctx`: Shared context with configurations/resources. +/// * `req`: Request specifying chains, pagination, and filters. +/// +/// # Returns +/// +/// On success, returns a detailed `NftList` containing NFTs, total count, and skipped count. +/// # Errors +/// +/// Returns `GetNftInfoError` variants for issues like invalid requests, transport failures, +/// database errors, and spam protection errors. pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; let _lock = nft_ctx.guard.lock().await; @@ -51,18 +83,35 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult MmResult { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; let _lock = nft_ctx.guard.lock().await; @@ -79,13 +128,32 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult MmResult { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; let _lock = nft_ctx.guard.lock().await; @@ -101,14 +169,27 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> 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)?; let _lock = nft_ctx.guard.lock().await; @@ -125,29 +206,27 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft None }; let nft_transfers = get_moralis_nft_transfers(&ctx, chain, from_block, &req.url).await?; - storage.add_transfers_to_history(chain, nft_transfers).await?; + storage.add_transfers_to_history(*chain, nft_transfers).await?; let nft_block = match NftListStorageOps::get_last_block_number(&storage, chain).await { Ok(Some(block)) => block, Ok(None) => { - // if there are no rows in NFT LIST table we can try to get all info from moralis. - let nfts = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url).await?; - update_meta_in_transfers(&storage, chain, nfts).await?; - update_transfers_with_empty_meta(&storage, chain, &req.url).await?; + // if there are no rows in NFT LIST table we can try to get nft list from moralis. + let nft_list = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url, &req.url_antispam).await?; + update_meta_in_transfers(&storage, chain, nft_list).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?; continue; }, Err(_) => { - // if there is an error, then NFT LIST table doesnt exist, so we need to cache from mroalis. + // if there is an error, then NFT LIST table doesnt exist, so we need to cache nft list from moralis. NftListStorageOps::init(&storage, chain).await?; - let nft_list = get_moralis_nft_list(&ctx, chain, &req.url).await?; - let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, chain) - .await? - .unwrap_or(0); - storage - .add_nfts_to_list(chain, nft_list.clone(), last_scanned_block) - .await?; + let nft_list = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url, &req.url_antispam).await?; update_meta_in_transfers(&storage, chain, nft_list).await?; - update_transfers_with_empty_meta(&storage, chain, &req.url).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?; continue; }, }; @@ -166,53 +245,291 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft last_nft_block: nft_block.to_string(), }); } - update_nft_list(ctx.clone(), &storage, chain, scanned_block + 1, &req.url).await?; - update_transfers_with_empty_meta(&storage, chain, &req.url).await?; + update_nft_list( + ctx.clone(), + &storage, + chain, + scanned_block + 1, + &req.url, + &req.url_antispam, + ) + .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?; + } + Ok(()) +} + +/// `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 + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let token_addresses = storage.get_token_addresses(chain).await?; + if !token_addresses.is_empty() { + let addresses = token_addresses + .iter() + .map(eth_addr_to_hex) + .collect::>() + .join(","); + let spam_res = send_spam_request(&chain, url_antispam, addresses).await?; + for (address, is_spam) in spam_res.result.into_iter() { + if is_spam { + let address_hex = eth_addr_to_hex(&address); + storage + .update_nft_spam_by_token_address(&chain, address_hex.clone(), is_spam) + .await?; + storage + .update_transfer_spam_by_token_address(&chain, address_hex, is_spam) + .await?; + } + } + } + Ok(()) +} + +async fn update_phishing(storage: &T, chain: &Chain, url_antispam: &Url) -> MmResult<(), UpdateSpamPhishingError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let transfer_domains = storage.get_domains(chain).await?; + let nft_domains = storage.get_animation_external_domains(chain).await?; + let domains: HashSet = transfer_domains.union(&nft_domains).cloned().collect(); + if !domains.is_empty() { + let domains = domains.into_iter().collect::>().join(","); + let domain_res = send_phishing_request(url_antispam, domains).await?; + for (domain, is_phishing) in domain_res.result.into_iter() { + if is_phishing { + storage + .update_nft_phishing_by_domain(chain, domain.clone(), is_phishing) + .await?; + storage + .update_transfer_phishing_by_domain(chain, domain, is_phishing) + .await?; + } + } } Ok(()) } +/// `send_spam_request` function sends request to antispam api to scan contract addresses for spam. +async fn send_spam_request( + chain: &Chain, + url_antispam: &Url, + addresses: String, +) -> MmResult { + let scan_contract_uri = prepare_uri_for_blocklist_endpoint(url_antispam, BLOCKLIST_CONTRACT, BLOCKLIST_SCAN)?; + let req_spam = SpamContractReq { + network: *chain, + addresses, + }; + let req_spam_json = serde_json::to_string(&req_spam)?; + let scan_contract_res = send_post_request_to_uri(scan_contract_uri.as_str(), req_spam_json).await?; + let spam_res: SpamContractRes = serde_json::from_slice(&scan_contract_res)?; + Ok(spam_res) +} + +/// `send_spam_request` function sends request to antispam api to scan domains for phishing. +async fn send_phishing_request( + url_antispam: &Url, + domains: String, +) -> MmResult { + let scan_contract_uri = prepare_uri_for_blocklist_endpoint(url_antispam, BLOCKLIST_DOMAIN, BLOCKLIST_SCAN)?; + let req_phishing = PhishingDomainReq { domains }; + let req_phishing_json = serde_json::to_string(&req_phishing)?; + let scan_domains_res = send_post_request_to_uri(scan_contract_uri.as_str(), req_phishing_json).await?; + let phishing_res: PhishingDomainRes = serde_json::from_slice(&scan_domains_res)?; + Ok(phishing_res) +} + +/// `prepare_uri_for_blocklist_endpoint` function constructs the URI required for the antispam API request. +/// It appends the required path segments to the given base URL and returns the completed URI. +fn prepare_uri_for_blocklist_endpoint( + url_antispam: &Url, + blocklist_type: &str, + blocklist_action_or_network: &str, +) -> MmResult { + let mut uri = url_antispam.clone(); + uri.set_path(BLOCKLIST_ENDPOINT); + uri.path_segments_mut() + .map_to_mm(|_| UpdateSpamPhishingError::Internal("Invalid URI".to_string()))? + .push(blocklist_type) + .push(blocklist_action_or_network); + Ok(uri) +} + +/// Refreshes and updates metadata associated with a specific NFT. +/// +/// The function obtains updated metadata for an NFT using its token address and token id. +/// It fetches the metadata from the provided `url` and validates it against possible spam and +/// 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)?; let _lock = nft_ctx.guard.lock().await; let storage = NftStorageBuilder::new(&ctx).build()?; - let moralis_meta = get_moralis_metadata( - format!("{:#02x}", req.token_address), + let token_address_str = eth_addr_to_hex(&req.token_address); + let moralis_meta = match get_moralis_metadata( + token_address_str.clone(), req.token_id.clone(), &req.chain, &req.url, + &req.url_antispam, ) - .await?; - let req = NftMetadataReq { - token_address: req.token_address, - token_id: req.token_id, - chain: req.chain, - protect_from_spam: false, + .await + { + Ok(moralis_meta) => moralis_meta, + Err(_) => { + storage + .update_nft_spam_by_token_address(&req.chain, token_address_str.clone(), true) + .await?; + storage + .update_transfer_spam_by_token_address(&req.chain, token_address_str.clone(), true) + .await?; + return Ok(()); + }, }; - let mut nft_db = get_nft_metadata(ctx.clone(), req).await?; + let mut nft_db = storage + .get_nft(&req.chain, token_address_str.clone(), req.token_id.clone()) + .await? + .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { + token_address: token_address_str, + token_id: req.token_id.to_string(), + })?; 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 token_domain = get_domain_from_url(token_uri.as_deref()); + let uri_meta = get_uri_meta( + token_uri.as_deref(), + moralis_meta.common.metadata.as_deref(), + &req.url_antispam, + ) + .await; + // Gather domains for phishing checks + let domains = gather_domains(&token_domain, &uri_meta); nft_db.common.collection_name = moralis_meta.common.collection_name; nft_db.common.symbol = moralis_meta.common.symbol; nft_db.common.token_uri = token_uri; + nft_db.common.token_domain = token_domain; nft_db.common.metadata = moralis_meta.common.metadata; nft_db.common.last_token_uri_sync = moralis_meta.common.last_token_uri_sync; nft_db.common.last_metadata_sync = moralis_meta.common.last_metadata_sync; nft_db.common.possible_spam = moralis_meta.common.possible_spam; nft_db.uri_meta = uri_meta; - drop_mutability!(nft_db); + if !nft_db.common.possible_spam { + refresh_possible_spam(&storage, &req.chain, &mut nft_db, &req.url_antispam).await?; + }; + if !nft_db.possible_phishing { + refresh_possible_phishing(&storage, &req.chain, domains, &mut nft_db, &req.url_antispam).await?; + }; storage .refresh_nft_metadata(&moralis_meta.chain, nft_db.clone()) .await?; - let transfer_meta = TransferMeta::from(nft_db.clone()); + update_transfer_meta_using_nft(&storage, &req.chain, &mut nft_db).await?; + Ok(()) +} + +/// The `update_transfer_meta_using_nft` function updates the transfer metadata associated with the given NFT. +/// If metadata info contains potential spam links, function sets `possible_spam` true. +async fn update_transfer_meta_using_nft(storage: &T, chain: &Chain, nft: &mut Nft) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let transfer_meta = TransferMeta::from(nft.clone()); storage - .update_transfers_meta_by_token_addr_id(&nft_db.chain, transfer_meta) + .update_transfers_meta_by_token_addr_id(chain, transfer_meta, nft.common.possible_spam) .await?; Ok(()) } -async fn get_moralis_nft_list(ctx: &MmArc, chain: &Chain, url: &Url) -> MmResult, GetNftInfoError> { +/// Extracts domains from uri_meta and token_domain. +fn gather_domains(token_domain: &Option, uri_meta: &UriMeta) -> HashSet { + let mut domains = HashSet::new(); + if let Some(domain) = token_domain { + domains.insert(domain.clone()); + } + if let Some(domain) = &uri_meta.image_domain { + domains.insert(domain.clone()); + } + if let Some(domain) = &uri_meta.animation_domain { + domains.insert(domain.clone()); + } + if let Some(domain) = &uri_meta.external_domain { + domains.insert(domain.clone()); + } + domains +} + +/// Refreshes the `possible_spam` flag based on spam results. +async fn refresh_possible_spam( + storage: &T, + chain: &Chain, + nft_db: &mut Nft, + url_antispam: &Url, +) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let address_hex = eth_addr_to_hex(&nft_db.common.token_address); + let spam_res = send_spam_request(chain, url_antispam, address_hex.clone()).await?; + if let Some(true) = spam_res.result.get(&nft_db.common.token_address) { + nft_db.common.possible_spam = true; + storage + .update_nft_spam_by_token_address(chain, address_hex.clone(), true) + .await?; + storage + .update_transfer_spam_by_token_address(chain, address_hex, true) + .await?; + } + Ok(()) +} + +/// Refreshes the `possible_phishing` flag based on phishing results. +async fn refresh_possible_phishing( + storage: &T, + chain: &Chain, + domains: HashSet, + nft_db: &mut Nft, + url_antispam: &Url, +) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + if !domains.is_empty() { + let domain_list = domains.into_iter().collect::>().join(","); + let domain_res = send_phishing_request(url_antispam, domain_list).await?; + for (domain, is_phishing) in domain_res.result.into_iter() { + if is_phishing { + nft_db.possible_phishing = true; + storage + .update_transfer_phishing_by_domain(chain, domain.clone(), is_phishing) + .await?; + storage + .update_nft_phishing_by_domain(chain, domain, is_phishing) + .await?; + } + } + } + Ok(()) +} + +async fn get_moralis_nft_list( + ctx: &MmArc, + chain: &Chain, + url: &Url, + url_antispam: &Url, +) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); let ticker = chain.to_ticker(); let conf = coin_conf(ctx, &ticker); @@ -243,7 +560,8 @@ async fn get_moralis_nft_list(ctx: &MmArc, chain: &Chain, url: &Url) -> MmResult Some(contract_type) => contract_type, None => continue, }; - let nft = build_nft_from_moralis(chain, nft_moralis, contract_type).await; + let mut nft = build_nft_from_moralis(*chain, nft_moralis, contract_type, url_antispam).await; + protect_from_nft_spam_links(&mut nft, false)?; // collect NFTs from the page res_list.push(nft); } @@ -329,10 +647,13 @@ async fn get_moralis_nft_transfers( 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, }; // collect NFTs transfers from the page res_list.push(transfer_history); @@ -351,7 +672,13 @@ async fn get_moralis_nft_transfers( Ok(res_list) } -/// **Caution:** ERC-1155 token can have a total supply more than 1, which means there could be several owners +/// Implements request to the Moralis "Get NFT metadata" endpoint. +/// +/// [Moralis Documentation Link](https://docs.moralis.io/web3-data-api/evm/reference/get-nft-metadata) +/// +/// **Caution:** +/// +/// ERC-1155 token can have a total supply more than 1, which means there could be several owners /// of the same token. `get_nft_metadata` returns NFTs info with the most recent owner. /// **Dont** use this function to get specific info about owner address, amount etc, you will get info not related to my_address. async fn get_moralis_metadata( @@ -359,6 +686,7 @@ async fn get_moralis_metadata( token_id: BigDecimal, chain: &Chain, url: &Url, + url_antispam: &Url, ) -> MmResult { let mut uri = url.clone(); uri.set_path(MORALIS_API_ENDPOINT); @@ -378,7 +706,8 @@ async fn get_moralis_metadata( Some(contract_type) => contract_type, None => return MmError::err(GetNftInfoError::ContractTypeIsNull), }; - let nft_metadata = build_nft_from_moralis(chain, nft_moralis, contract_type).await; + let mut nft_metadata = build_nft_from_moralis(*chain, nft_moralis, contract_type, url_antispam).await; + protect_from_nft_spam_links(&mut nft_metadata, false)?; Ok(nft_metadata) } @@ -392,57 +721,6 @@ pub async fn withdraw_nft(ctx: MmArc, req: WithdrawNftReq) -> WithdrawNftResult } } -#[cfg(not(target_arch = "wasm32"))] -async fn send_request_to_uri(uri: &str) -> MmResult { - use http::header::HeaderValue; - use mm2_net::transport::slurp_req_body; - - let request = http::Request::builder() - .method("GET") - .uri(uri) - .header(ACCEPT, HeaderValue::from_static(APPLICATION_JSON)) - .body(hyper::Body::from(""))?; - - let (status, _header, body) = slurp_req_body(request).await?; - if !status.is_success() { - return Err(MmError::new(GetInfoFromUriError::Transport(format!( - "Response !200 from {}: {}, {}", - uri, status, body - )))); - } - Ok(body) -} - -#[cfg(target_arch = "wasm32")] -async fn send_request_to_uri(uri: &str) -> MmResult { - use mm2_net::wasm_http::FetchRequest; - - macro_rules! try_or { - ($exp:expr, $errtype:ident) => { - match $exp { - Ok(x) => x, - Err(e) => return Err(MmError::new(GetInfoFromUriError::$errtype(ERRL!("{:?}", e)))), - } - }; - } - - let result = FetchRequest::get(uri) - .header(ACCEPT.as_str(), APPLICATION_JSON) - .request_str() - .await; - let (status_code, response_str) = try_or!(result, Transport); - if !status_code.is_success() { - return Err(MmError::new(GetInfoFromUriError::Transport(ERRL!( - "!200: {}, {}", - status_code, - response_str - )))); - } - - let response: Json = try_or!(serde_json::from_str(&response_str), InvalidResponse); - Ok(response) -} - /// `check_moralis_ipfs_bafy` inspects a given token URI and modifies it if certain conditions are met. /// /// It checks if the URI points to the Moralis IPFS domain `"ipfs.moralis.io"` and starts with a specific path prefix `"/ipfs/bafy"`. @@ -465,26 +743,46 @@ fn check_moralis_ipfs_bafy(token_uri: Option<&str>) -> Option { }) } -async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>) -> UriMeta { +async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>, url_antispam: &Url) -> UriMeta { let mut uri_meta = UriMeta::default(); + // Fetching data from the URL if token_uri is provided if let Some(token_uri) = token_uri { - if let Ok(response_meta) = send_request_to_uri(token_uri).await { - if let Ok(token_uri_meta) = serde_json::from_value(response_meta) { - uri_meta = token_uri_meta; - } + if let Some(url) = construct_camo_url_with_token(token_uri, url_antispam) { + uri_meta = fetch_meta_from_url(url).await.unwrap_or_default(); } } + // Filling fields from metadata if provided if let Some(metadata) = metadata { if let Ok(meta_from_meta) = serde_json::from_str::(metadata) { - uri_meta.try_to_fill_missing_fields_from(meta_from_meta) + uri_meta.try_to_fill_missing_fields_from(meta_from_meta); } } - uri_meta.image_url = check_moralis_ipfs_bafy(uri_meta.image_url.as_deref()); - uri_meta.animation_url = check_moralis_ipfs_bafy(uri_meta.animation_url.as_deref()); + update_uri_moralis_ipfs_fields(&mut uri_meta); drop_mutability!(uri_meta); uri_meta } +fn construct_camo_url_with_token(token_uri: &str, url_antispam: &Url) -> Option { + let mut url = url_antispam.clone(); + url.set_path("url/decode"); + url.path_segments_mut().ok()?.push(hex::encode(token_uri).as_str()); + Some(url) +} + +async fn fetch_meta_from_url(url: Url) -> MmResult { + let response_meta = send_request_to_uri(url.as_str()).await?; + serde_json::from_value(response_meta).map_err(|e| e.into()) +} + +fn update_uri_moralis_ipfs_fields(uri_meta: &mut UriMeta) { + uri_meta.image_url = check_moralis_ipfs_bafy(uri_meta.image_url.as_deref()); + uri_meta.image_domain = get_domain_from_url(uri_meta.image_url.as_deref()); + uri_meta.animation_url = check_moralis_ipfs_bafy(uri_meta.animation_url.as_deref()); + uri_meta.animation_domain = get_domain_from_url(uri_meta.animation_url.as_deref()); + uri_meta.external_url = check_moralis_ipfs_bafy(uri_meta.external_url.as_deref()); + uri_meta.external_domain = get_domain_from_url(uri_meta.external_url.as_deref()); +} + fn get_transfer_status(my_wallet: &str, to_address: &str) -> TransferStatus { // if my_wallet == from_address && my_wallet == to_address it is incoming transfer, so we can check just to_address. if my_wallet.to_lowercase() == to_address.to_lowercase() { @@ -502,15 +800,16 @@ async fn update_nft_list( chain: &Chain, scan_from_block: u64, url: &Url, + url_antispam: &Url, ) -> MmResult<(), UpdateNftError> { - let transfers = storage.get_transfers_from_block(chain, scan_from_block).await?; + let transfers = storage.get_transfers_from_block(*chain, scan_from_block).await?; let req = MyAddressReq { coin: chain.to_ticker(), path_to_address: StandardHDCoinAddress::default(), }; let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); for transfer in transfers.into_iter() { - handle_nft_transfer(storage, chain, url, transfer, &my_address).await?; + handle_nft_transfer(storage, chain, url, url_antispam, transfer, &my_address).await?; } Ok(()) } @@ -519,17 +818,18 @@ async fn handle_nft_transfer MmResult<(), UpdateNftError> { match (transfer.status, transfer.contract_type) { (TransferStatus::Send, ContractType::Erc721) => handle_send_erc721(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc721) => { - handle_receive_erc721(storage, chain, transfer, url, my_address).await + handle_receive_erc721(storage, chain, transfer, url, url_antispam, my_address).await }, (TransferStatus::Send, ContractType::Erc1155) => handle_send_erc1155(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc1155) => { - handle_receive_erc1155(storage, chain, transfer, url, my_address).await + handle_receive_erc1155(storage, chain, transfer, url, url_antispam, my_address).await }, } } @@ -539,7 +839,7 @@ async fn handle_send_erc721 chain: &Chain, transfer: NftTransferHistory, ) -> MmResult<(), UpdateNftError> { - let nft_db = storage + storage .get_nft( chain, eth_addr_to_hex(&transfer.common.token_address), @@ -550,10 +850,6 @@ async fn handle_send_erc721 token_address: eth_addr_to_hex(&transfer.common.token_address), token_id: transfer.common.token_id.to_string(), })?; - let transfer_meta = TransferMeta::from(nft_db); - storage - .update_transfers_meta_by_token_addr_id(chain, transfer_meta) - .await?; storage .remove_nft_from_list( chain, @@ -570,14 +866,12 @@ async fn handle_receive_erc721 MmResult<(), UpdateNftError> { - let nft = match storage - .get_nft( - chain, - eth_addr_to_hex(&transfer.common.token_address), - transfer.common.token_id.clone(), - ) + let token_address_str = eth_addr_to_hex(&transfer.common.token_address); + match storage + .get_nft(chain, token_address_str.clone(), transfer.common.token_id.clone()) .await? { Some(mut nft_db) => { @@ -589,37 +883,39 @@ async fn handle_receive_erc721 { - let mut nft = get_moralis_metadata( - eth_addr_to_hex(&transfer.common.token_address), - transfer.common.token_id, + let mut nft = match get_moralis_metadata( + token_address_str.clone(), + transfer.common.token_id.clone(), chain, url, + url_antispam, ) - .await?; - // sometimes moralis updates Get All NFTs (which also affects Get Metadata) later - // than History by Wallet update - nft.common.owner_of = - Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?; - nft.block_number = transfer.block_number; - drop_mutability!(nft); + .await + { + Ok(mut moralis_meta) => { + // sometimes moralis updates Get All NFTs (which also affects Get Metadata) later + // than History by Wallet update + moralis_meta.common.owner_of = + Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?; + moralis_meta.block_number = transfer.block_number; + moralis_meta + }, + Err(_) => { + mark_as_spam_and_build_empty_meta(storage, chain, token_address_str, &transfer, my_address).await? + }, + }; storage - .add_nfts_to_list(chain, vec![nft.clone()], transfer.block_number) + .add_nfts_to_list(*chain, vec![nft.clone()], transfer.block_number) .await?; - nft + update_transfer_meta_using_nft(storage, chain, &mut nft).await?; }, - }; - let transfer_meta = TransferMeta::from(nft); - storage - .update_transfers_meta_by_token_addr_id(chain, transfer_meta) - .await?; + } Ok(()) } @@ -628,15 +924,12 @@ async fn handle_send_erc1155 MmResult<(), UpdateNftError> { + let token_address_str = eth_addr_to_hex(&transfer.common.token_address); let mut nft_db = storage - .get_nft( - chain, - eth_addr_to_hex(&transfer.common.token_address), - transfer.common.token_id.clone(), - ) + .get_nft(chain, token_address_str.clone(), transfer.common.token_id.clone()) .await? .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { - token_address: eth_addr_to_hex(&transfer.common.token_address), + token_address: token_address_str.clone(), token_id: transfer.common.token_id.to_string(), })?; match nft_db.common.amount.cmp(&transfer.common.amount) { @@ -644,7 +937,7 @@ async fn handle_send_erc1155 MmResult<(), UpdateNftError> { - let nft = match storage - .get_nft( - chain, - eth_addr_to_hex(&transfer.common.token_address), - transfer.common.token_id.clone(), - ) + let token_address_str = eth_addr_to_hex(&transfer.common.token_address); + let mut nft = match storage + .get_nft(chain, token_address_str.clone(), transfer.common.token_id.clone()) .await? { Some(mut nft_db) => { @@ -700,49 +987,98 @@ async fn handle_receive_erc1155 { - let moralis_meta = get_moralis_metadata( - eth_addr_to_hex(&transfer.common.token_address), + let nft = match get_moralis_metadata( + token_address_str.clone(), transfer.common.token_id.clone(), chain, url, + url_antispam, ) - .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: transfer.common.amount, - 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, - token_uri, - metadata: moralis_meta.common.metadata, - last_token_uri_sync: moralis_meta.common.last_token_uri_sync, - last_metadata_sync: moralis_meta.common.last_metadata_sync, - minter_address: moralis_meta.common.minter_address, - possible_spam: moralis_meta.common.possible_spam, + .await + { + Ok(moralis_meta) => { + create_nft_from_moralis_metadata(moralis_meta, &transfer, my_address, chain, url_antispam).await? + }, + Err(_) => { + mark_as_spam_and_build_empty_meta(storage, chain, token_address_str, &transfer, my_address).await? }, - chain: *chain, - block_number_minted: moralis_meta.block_number_minted, - block_number: transfer.block_number, - contract_type: moralis_meta.contract_type, - uri_meta, }; storage - .add_nfts_to_list(chain, [nft.clone()], transfer.block_number) + .add_nfts_to_list(*chain, [nft.clone()], transfer.block_number) .await?; nft }, }; - let transfer_meta = TransferMeta::from(nft); + update_transfer_meta_using_nft(storage, chain, &mut nft).await?; + Ok(()) +} + +async fn create_nft_from_moralis_metadata( + moralis_meta: Nft, + transfer: &NftTransferHistory, + my_address: &str, + chain: &Chain, + url_antispam: &Url, +) -> MmResult { + let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref()); + let token_domain = get_domain_from_url(token_uri.as_deref()); + let uri_meta = get_uri_meta( + token_uri.as_deref(), + moralis_meta.common.metadata.as_deref(), + url_antispam, + ) + .await; + let nft = Nft { + common: NftCommon { + token_address: moralis_meta.common.token_address, + token_id: moralis_meta.common.token_id, + amount: transfer.common.amount.clone(), + 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, + token_uri, + token_domain, + metadata: moralis_meta.common.metadata, + last_token_uri_sync: moralis_meta.common.last_token_uri_sync, + last_metadata_sync: moralis_meta.common.last_metadata_sync, + minter_address: moralis_meta.common.minter_address, + possible_spam: moralis_meta.common.possible_spam, + }, + chain: *chain, + block_number_minted: moralis_meta.block_number_minted, + block_number: transfer.block_number, + contract_type: moralis_meta.contract_type, + possible_phishing: false, + uri_meta, + }; + Ok(nft) +} + +async fn mark_as_spam_and_build_empty_meta( + storage: &T, + chain: &Chain, + token_address_str: String, + transfer: &NftTransferHistory, + my_address: &str, +) -> MmResult { storage - .update_transfers_meta_by_token_addr_id(chain, transfer_meta) + .update_nft_spam_by_token_address(chain, token_address_str.clone(), true) .await?; - Ok(()) + storage + .update_transfer_spam_by_token_address(chain, token_address_str, true) + .await?; + + Ok(build_nft_with_empty_meta(BuildNftFields { + token_address: transfer.common.token_address, + token_id: transfer.common.token_id.clone(), + amount: transfer.common.amount.clone(), + owner_of: Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?, + contract_type: transfer.contract_type, + possible_spam: true, + chain: transfer.chain, + block_number: transfer.block_number, + })) } /// `find_wallet_nft_amount` function returns NFT amount of cached NFT. @@ -775,13 +1111,14 @@ async fn cache_nfts_from_moralis MmResult, UpdateNftError> { - let nft_list = get_moralis_nft_list(ctx, chain, url).await?; + let nft_list = get_moralis_nft_list(ctx, chain, url, url_antispam).await?; let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(storage, chain) .await? .unwrap_or(0); storage - .add_nfts_to_list(chain, nft_list.clone(), last_scanned_block) + .add_nfts_to_list(*chain, nft_list.clone(), last_scanned_block) .await?; Ok(nft_list) } @@ -791,27 +1128,45 @@ async fn update_meta_in_transfers(storage: &T, chain: &Chain, nfts: Vec) where T: NftListStorageOps + NftTransferHistoryStorageOps, { - for nft in nfts.into_iter() { - let transfer_meta = TransferMeta::from(nft); - storage - .update_transfers_meta_by_token_addr_id(chain, transfer_meta) - .await?; + for mut nft in nfts.into_iter() { + update_transfer_meta_using_nft(storage, chain, &mut nft).await?; } Ok(()) } /// `update_transfers_with_empty_meta` function updates empty metadata in transfers. -async fn update_transfers_with_empty_meta(storage: &T, chain: &Chain, url: &Url) -> MmResult<(), UpdateNftError> +async fn update_transfers_with_empty_meta( + storage: &T, + chain: &Chain, + url: &Url, + url_antispam: &Url, +) -> MmResult<(), UpdateNftError> where T: NftListStorageOps + NftTransferHistoryStorageOps, { - let nft_token_addr_id = storage.get_transfers_with_empty_meta(chain).await?; + 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 nft_meta = get_moralis_metadata(addr_id_pair.token_address, addr_id_pair.token_id, chain, url).await?; - let transfer_meta = TransferMeta::from(nft_meta); - storage - .update_transfers_meta_by_token_addr_id(chain, transfer_meta) - .await?; + let mut nft_meta = match get_moralis_metadata( + addr_id_pair.token_address.clone(), + addr_id_pair.token_id, + chain, + url, + url_antispam, + ) + .await + { + Ok(nft_meta) => nft_meta, + Err(_) => { + storage + .update_nft_spam_by_token_address(chain, addr_id_pair.token_address.clone(), true) + .await?; + storage + .update_transfer_spam_by_token_address(chain, addr_id_pair.token_address, true) + .await?; + continue; + }, + }; + update_transfer_meta_using_nft(storage, chain, &mut nft_meta).await?; } Ok(()) } @@ -824,26 +1179,31 @@ fn contains_disallowed_url(text: &str) -> Result { Ok(url_regex.is_match(text)) } -/// `check_and_redact_if_spam` checks if the text contains any links. +/// `process_text_for_spam_link` checks if the text contains any links and optionally redacts it. /// It doesn't matter if the link is valid or not, as this is a spam check. -/// If text contains some link, then it is a spam. -fn check_and_redact_if_spam(text: &mut Option) -> Result { +/// If text contains some link, then function returns `true`. +fn process_text_for_spam_link(text: &mut Option, redact: bool) -> Result { match text { Some(s) if contains_disallowed_url(s)? => { - *text = Some("URL redacted for user protection".to_string()); + if redact { + *text = Some("URL redacted for user protection".to_string()); + } Ok(true) }, _ => Ok(false), } } -/// `protect_from_history_spam` function checks and redact spam in `NftTransferHistory`. +/// `protect_from_history_spam_links` function checks and redact spam in `NftTransferHistory`. /// /// `collection_name` and `token_name` in `NftTransferHistory` shouldn't contain any links, /// they must be just an arbitrary text, which represents NFT names. -fn protect_from_history_spam(transfer: &mut NftTransferHistory) -> MmResult<(), ProtectFromSpamError> { - let collection_name_spam = check_and_redact_if_spam(&mut transfer.collection_name)?; - let token_name_spam = check_and_redact_if_spam(&mut transfer.token_name)?; +fn protect_from_history_spam_links( + transfer: &mut NftTransferHistory, + redact: bool, +) -> MmResult<(), ProtectFromSpamError> { + let collection_name_spam = process_text_for_spam_link(&mut transfer.collection_name, redact)?; + let token_name_spam = process_text_for_spam_link(&mut transfer.token_name, redact)?; if collection_name_spam || token_name_spam { transfer.common.possible_spam = true; @@ -851,65 +1211,82 @@ fn protect_from_history_spam(transfer: &mut NftTransferHistory) -> MmResult<(), Ok(()) } -/// `protect_from_nft_spam` function checks and redact spam in `Nft`. +/// `protect_from_nft_spam_links` function checks and optionally redacts spam links in `Nft`. /// /// `collection_name` and `token_name` in `Nft` shouldn't contain any links, /// they must be just an arbitrary text, which represents NFT names. /// `symbol` also must be a text or sign that represents a symbol. -fn protect_from_nft_spam(nft: &mut Nft) -> MmResult<(), ProtectFromSpamError> { - let collection_name_spam = check_and_redact_if_spam(&mut nft.common.collection_name)?; - let symbol_spam = check_and_redact_if_spam(&mut nft.common.symbol)?; - let token_name_spam = check_and_redact_if_spam(&mut nft.uri_meta.token_name)?; - let meta_spam = check_nft_metadata_for_spam(nft)?; +/// This function also checks `metadata` field for spam. +fn protect_from_nft_spam_links(nft: &mut Nft, redact: bool) -> MmResult<(), ProtectFromSpamError> { + let collection_name_spam = process_text_for_spam_link(&mut nft.common.collection_name, redact)?; + let symbol_spam = process_text_for_spam_link(&mut nft.common.symbol, redact)?; + let token_name_spam = process_text_for_spam_link(&mut nft.uri_meta.token_name, redact)?; + let meta_spam = process_metadata_for_spam_link(nft, redact)?; if collection_name_spam || symbol_spam || token_name_spam || meta_spam { nft.common.possible_spam = true; } Ok(()) } -/// `check_nft_metadata_for_spam` function checks and redact spam in `metadata` field from `Nft`. + +/// The `process_metadata_for_spam_link` function checks and optionally redacts spam link in the `metadata` field of `Nft`. /// /// **note:** `token_name` is usually called `name` in `metadata`. -fn check_nft_metadata_for_spam(nft: &mut Nft) -> MmResult { +fn process_metadata_for_spam_link(nft: &mut Nft, redact: bool) -> MmResult { if let Some(Ok(mut metadata)) = nft .common .metadata .as_ref() .map(|t| serde_json::from_str::>(t)) { - if check_spam_and_redact_metadata_field(&mut metadata, "name")? { + let spam_detected = process_metadata_field(&mut metadata, "name", redact)?; + if redact && spam_detected { nft.common.metadata = Some(serde_json::to_string(&metadata)?); - return Ok(true); } + return Ok(spam_detected); } Ok(false) } -/// The `check_spam_and_redact_metadata_field` function scans a specified field in a JSON metadata object for potential spam. +/// The `process_metadata_field` function scans a specified field in a JSON metadata object for potential spam. /// /// This function checks the provided `metadata` map for a field matching the `field` parameter. /// If this field is found and its value contains some link, it's considered to contain spam. -/// To protect users, function redacts field containing spam link. -/// The function returns `true` if it detected spam link, or `false` otherwise. -fn check_spam_and_redact_metadata_field( +/// Depending on the `redact` flag, it will either redact the spam link or leave it as it is. +/// The function returns `true` if it detected a spam link, or `false` otherwise. +fn process_metadata_field( metadata: &mut serde_json::Map, field: &str, + redact: bool, ) -> MmResult { match metadata.get(field).and_then(|v| v.as_str()) { Some(text) if contains_disallowed_url(text)? => { - metadata.insert( - field.to_string(), - serde_json::Value::String("URL redacted for user protection".to_string()), - ); + if redact { + metadata.insert( + field.to_string(), + serde_json::Value::String("URL redacted for user protection".to_string()), + ); + } Ok(true) }, _ => Ok(false), } } -async fn build_nft_from_moralis(chain: &Chain, nft_moralis: NftFromMoralis, contract_type: ContractType) -> Nft { +async fn build_nft_from_moralis( + chain: Chain, + nft_moralis: NftFromMoralis, + contract_type: ContractType, + url_antispam: &Url, +) -> Nft { let token_uri = check_moralis_ipfs_bafy(nft_moralis.common.token_uri.as_deref()); - let uri_meta = get_uri_meta(token_uri.as_deref(), nft_moralis.common.metadata.as_deref()).await; + let uri_meta = get_uri_meta( + token_uri.as_deref(), + nft_moralis.common.metadata.as_deref(), + url_antispam, + ) + .await; + let token_domain = get_domain_from_url(token_uri.as_deref()); Nft { common: NftCommon { token_address: nft_moralis.common.token_address, @@ -920,16 +1297,24 @@ async fn build_nft_from_moralis(chain: &Chain, nft_moralis: NftFromMoralis, cont collection_name: nft_moralis.common.collection_name, symbol: nft_moralis.common.symbol, token_uri, + token_domain, metadata: nft_moralis.common.metadata, last_token_uri_sync: nft_moralis.common.last_token_uri_sync, last_metadata_sync: nft_moralis.common.last_metadata_sync, minter_address: nft_moralis.common.minter_address, possible_spam: nft_moralis.common.possible_spam, }, - chain: *chain, + chain, block_number_minted: nft_moralis.block_number_minted.map(|v| v.0), block_number: *nft_moralis.block_number, contract_type, + possible_phishing: false, uri_meta, } } + +#[inline(always)] +pub(crate) fn get_domain_from_url(url: Option<&str>) -> Option { + url.and_then(|uri| Url::parse(uri).ok()) + .and_then(|url| url.domain().map(String::from)) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index 719c481dfc..941348db67 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -5,10 +5,11 @@ use common::{HttpStatusCode, ParseRfc3339Err}; use derive_more::Display; use enum_from::EnumFromStringify; use http::StatusCode; -use mm2_net::transport::SlurpError; +use mm2_net::transport::{GetInfoFromUriError, SlurpError}; use serde::{Deserialize, Serialize}; use web3::Error; +/// Enumerates potential errors that can arise when fetching NFT information. #[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum GetNftInfoError { @@ -119,6 +120,16 @@ impl HttpStatusCode for GetNftInfoError { } } +/// Enumerates possible errors that can occur while updating NFT details in the database. +/// +/// The errors capture various issues that can arise during: +/// - Metadata refresh +/// - NFT transfer history updating +/// - NFT list updating +/// +/// The issues addressed include database errors, invalid hex strings, +/// inconsistencies in block numbers, and problems related to fetching or interpreting +/// fetched metadata. #[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum UpdateNftError { @@ -168,6 +179,11 @@ pub enum UpdateNftError { }, #[display(fmt = "Invalid hex string: {}", _0)] InvalidHexString(String), + UpdateSpamPhishingError(UpdateSpamPhishingError), + GetInfoFromUriError(GetInfoFromUriError), + #[from_stringify("serde_json::Error")] + SerdeError(String), + ProtectFromSpamError(ProtectFromSpamError), } impl From for UpdateNftError { @@ -190,6 +206,18 @@ impl From for UpdateNftError { fn from(err: T) -> Self { UpdateNftError::DbError(format!("{:?}", err)) } } +impl From for UpdateNftError { + fn from(e: UpdateSpamPhishingError) -> Self { UpdateNftError::UpdateSpamPhishingError(e) } +} + +impl From for UpdateNftError { + fn from(e: GetInfoFromUriError) -> Self { UpdateNftError::GetInfoFromUriError(e) } +} + +impl From for UpdateNftError { + fn from(e: ProtectFromSpamError) -> Self { UpdateNftError::ProtectFromSpamError(e) } +} + impl HttpStatusCode for UpdateNftError { fn status_code(&self) -> StatusCode { match self { @@ -202,15 +230,35 @@ impl HttpStatusCode for UpdateNftError { | UpdateNftError::InvalidBlockOrder { .. } | UpdateNftError::LastScannedBlockNotFound { .. } | UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { .. } - | UpdateNftError::InvalidHexString(_) => StatusCode::INTERNAL_SERVER_ERROR, + | UpdateNftError::InvalidHexString(_) + | UpdateNftError::UpdateSpamPhishingError(_) + | UpdateNftError::GetInfoFromUriError(_) + | UpdateNftError::SerdeError(_) + | UpdateNftError::ProtectFromSpamError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } +/// Enumerates the errors that can occur during spam protection operations. +/// +/// This includes issues such as regex failures during text validation and +/// serialization/deserialization problems. #[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] -pub(crate) enum GetInfoFromUriError { - /// `http::Error` can appear on an HTTP request [`http::Builder::build`] building. - #[from_stringify("http::Error")] +pub enum ProtectFromSpamError { + #[from_stringify("regex::Error")] + RegexError(String), + #[from_stringify("serde_json::Error")] + SerdeError(String), +} + +/// An enumeration representing the potential errors encountered +/// during the process of updating spam or phishing-related information. +/// +/// This error set captures various failures, from request malformation +/// to database interaction errors, providing a comprehensive view of +/// possible issues during the spam/phishing update operations. +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] +pub enum UpdateSpamPhishingError { #[display(fmt = "Invalid request: {}", _0)] InvalidRequest(String), #[display(fmt = "Transport: {}", _0)] @@ -220,24 +268,45 @@ pub(crate) enum GetInfoFromUriError { InvalidResponse(String), #[display(fmt = "Internal: {}", _0)] Internal(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), + GetMyAddressError(GetMyAddressError), } -impl From for GetInfoFromUriError { - fn from(e: SlurpError) -> Self { - let error_str = e.to_string(); +impl From for UpdateSpamPhishingError { + fn from(e: GetMyAddressError) -> Self { UpdateSpamPhishingError::GetMyAddressError(e) } +} + +impl From for UpdateSpamPhishingError { + fn from(e: GetInfoFromUriError) -> Self { match e { - SlurpError::ErrorDeserializing { .. } => GetInfoFromUriError::InvalidResponse(error_str), - SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetInfoFromUriError::Transport(error_str), - SlurpError::InvalidRequest(_) => GetInfoFromUriError::InvalidRequest(error_str), - SlurpError::Internal(_) => GetInfoFromUriError::Internal(error_str), + GetInfoFromUriError::InvalidRequest(e) => UpdateSpamPhishingError::InvalidRequest(e), + GetInfoFromUriError::Transport(e) => UpdateSpamPhishingError::Transport(e), + GetInfoFromUriError::InvalidResponse(e) => UpdateSpamPhishingError::InvalidResponse(e), + GetInfoFromUriError::Internal(e) => UpdateSpamPhishingError::Internal(e), } } } -#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] -pub enum ProtectFromSpamError { - #[from_stringify("regex::Error")] - RegexError(String), +impl From for UpdateSpamPhishingError { + fn from(err: T) -> Self { UpdateSpamPhishingError::DbError(format!("{:?}", err)) } +} + +/// 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. + UnsupportedChainType, +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum MetaFromUrlError { #[from_stringify("serde_json::Error")] - SerdeError(String), + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + GetInfoFromUriError(GetInfoFromUriError), +} + +impl From for MetaFromUrlError { + fn from(e: GetInfoFromUriError) -> Self { MetaFromUrlError::GetInfoFromUriError(e) } } diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 9d7a07b89a..165e7cbd93 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -6,32 +6,65 @@ 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::de::{self, Deserializer}; use serde::Deserialize; use serde_json::Value as Json; +use std::collections::HashMap; use std::fmt; use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::Arc; use url::Url; +use crate::nft::nft_errors::ParseChainTypeError; #[cfg(target_arch = "wasm32")] use mm2_db::indexed_db::{ConstructibleDb, SharedDb}; #[cfg(target_arch = "wasm32")] use crate::nft::storage::wasm::nft_idb::NftCacheIDB; +/// Represents a request to list NFTs owned by the user across specified chains. +/// +/// The request provides options such as pagination, limiting the number of results, +/// and applying specific filters to the list. #[derive(Debug, Deserialize)] pub struct NftListReq { + /// List of chains to fetch the NFTs from. pub(crate) chains: Vec, + /// Parameter indicating if the maximum number of NFTs should be fetched. + /// If true, then `limit` will be ignored. #[serde(default)] pub(crate) max: bool, + /// Limit to the number of NFTs returned in a single request. #[serde(default = "ten")] pub(crate) limit: usize, + /// Page number for pagination. pub(crate) page_number: Option, + /// Flag indicating if the returned list should be protected from potential spam. #[serde(default)] pub(crate) protect_from_spam: bool, + /// Optional filters to apply when listing the NFTs. + pub(crate) filters: Option, +} + +/// Filters that can be applied when listing NFTs to exclude potential threats or nuisances. +#[derive(Copy, Clone, Debug, Deserialize)] +pub struct NftListFilters { + /// Exclude NFTs that are flagged as possible spam. + #[serde(default)] + pub(crate) exclude_spam: bool, + /// Exclude NFTs that are flagged as phishing attempts. + #[serde(default)] + pub(crate) exclude_phishing: bool, } +/// Contains parameters required to fetch metadata for a specified NFT. +/// # Fields +/// * `token_address`: The address of the NFT token. +/// * `token_id`: The ID of the NFT token. +/// * `chain`: The blockchain where the NFT exists. +/// * `protect_from_spam`: Indicates whether to check and redact potential spam. If set to true, +/// the internal function `protect_from_nft_spam` is utilized. #[derive(Debug, Deserialize)] pub struct NftMetadataReq { pub(crate) token_address: Address, @@ -41,20 +74,26 @@ pub struct NftMetadataReq { pub(crate) protect_from_spam: bool, } +/// Contains parameters required to refresh metadata for a specified NFT. +/// # Fields +/// * `token_address`: The address of the NFT token whose metadata needs to be refreshed. +/// * `token_id`: The ID of the NFT token. +/// * `chain`: The blockchain where the NFT exists. +/// * `url`: URL to fetch the metadata. +/// * `url_antispam`: URL used to validate if the fetched contract addresses are associated +/// with spam contracts or if domain fields in the fetched metadata match known phishing domains. #[derive(Debug, Deserialize)] pub struct RefreshMetadataReq { pub(crate) token_address: Address, pub(crate) token_id: BigDecimal, pub(crate) chain: Chain, pub(crate) url: Url, + pub(crate) url_antispam: Url, } -#[derive(Debug, Display)] -pub enum ParseChainTypeError { - UnsupportedChainType, -} - -#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +/// Represents blockchains which are supported by NFT feature. +/// Currently there are only EVM based chains. +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum Chain { Avalanche, @@ -99,15 +138,31 @@ impl FromStr for Chain { 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), _ => Err(ParseChainTypeError::UnsupportedChainType), } } } +/// This implementation will use `FromStr` to deserialize `Chain`. +impl<'de> Deserialize<'de> for Chain { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + #[derive(Debug, Display)] pub(crate) enum ParseContractTypeError { UnsupportedContractType, @@ -156,12 +211,15 @@ pub(crate) struct UriMeta { #[serde(rename = "image")] pub(crate) raw_image_url: Option, pub(crate) image_url: Option, + pub(crate) image_domain: Option, #[serde(rename = "name")] pub(crate) token_name: Option, pub(crate) description: Option, pub(crate) attributes: Option, pub(crate) animation_url: Option, + pub(crate) animation_domain: Option, pub(crate) external_url: Option, + pub(crate) external_domain: Option, pub(crate) image_details: Option, } @@ -196,6 +254,7 @@ impl UriMeta { } /// [`NftCommon`] structure contains common fields from [`Nft`] and [`NftFromMoralis`] +/// The `possible_spam` field indicates if any potential spam has been detected. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NftCommon { pub(crate) token_address: Address, @@ -207,6 +266,7 @@ pub struct NftCommon { pub(crate) collection_name: Option, pub(crate) symbol: Option, pub(crate) token_uri: Option, + pub(crate) token_domain: Option, pub(crate) metadata: Option, pub(crate) last_token_uri_sync: Option, pub(crate) last_metadata_sync: Option, @@ -215,6 +275,9 @@ pub struct NftCommon { pub(crate) possible_spam: bool, } +/// Represents an NFT with specific chain details, contract type, and other relevant attributes. +/// This structure captures detailed information about an NFT. The `possible_phishing` +/// field indicates if any domains associated with the NFT have been marked as phishing. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Nft { #[serde(flatten)] @@ -223,10 +286,52 @@ pub struct Nft { pub(crate) block_number_minted: Option, pub(crate) block_number: u64, pub(crate) contract_type: ContractType, + #[serde(default)] + pub(crate) possible_phishing: bool, pub(crate) uri_meta: UriMeta, } -/// This structure is for deserializing moralis NFT json to struct. +pub(crate) struct BuildNftFields { + pub(crate) token_address: Address, + pub(crate) token_id: BigDecimal, + pub(crate) amount: BigDecimal, + pub(crate) owner_of: Address, + pub(crate) contract_type: ContractType, + pub(crate) possible_spam: bool, + pub(crate) chain: Chain, + pub(crate) block_number: u64, +} + +pub(crate) fn build_nft_with_empty_meta(nft_fields: BuildNftFields) -> Nft { + Nft { + common: NftCommon { + token_address: nft_fields.token_address, + token_id: nft_fields.token_id, + amount: nft_fields.amount, + owner_of: nft_fields.owner_of, + token_hash: None, + collection_name: None, + symbol: None, + token_uri: None, + token_domain: None, + metadata: None, + last_token_uri_sync: None, + last_metadata_sync: None, + minter_address: None, + possible_spam: nft_fields.possible_spam, + }, + chain: nft_fields.chain, + block_number_minted: None, + block_number: nft_fields.block_number, + contract_type: nft_fields.contract_type, + possible_phishing: false, + uri_meta: Default::default(), + } +} + +/// Represents an NFT structure specifically for deserialization from Moralis's JSON response. +/// +/// This structure is adapted to the specific format provided by Moralis's API. #[derive(Debug, Deserialize)] pub(crate) struct NftFromMoralis { #[serde(flatten)] @@ -259,6 +364,8 @@ impl std::ops::Deref for SerdeStringWrap { fn deref(&self) -> &T { &self.0 } } +/// Represents a detailed list of NFTs, including the total number of NFTs and the number of skipped NFTs. +/// It is used as response of `get_nft_list` if it is successful. #[derive(Debug, Serialize)] pub struct NftList { pub(crate) nfts: Vec, @@ -322,15 +429,26 @@ pub struct TransactionNftDetails { pub(crate) transaction_type: TransactionType, } +/// Represents a request to fetch the transfer history of NFTs owned by the user across specified chains. +/// +/// The request provides options such as pagination, limiting the number of results, +/// and applying specific filters to the history. #[derive(Debug, Deserialize)] pub struct NftTransfersReq { + /// List of chains to fetch the NFT transfer history from. pub(crate) chains: Vec, + /// Optional filters to apply when fetching the NFT transfer history. pub(crate) filters: Option, + /// Parameter indicating if the maximum number of transfer records should be fetched. + /// If true, then `limit` will be ignored. #[serde(default)] pub(crate) max: bool, + /// Limit to the number of transfer records returned in a single request. #[serde(default = "ten")] pub(crate) limit: usize, + /// Page number for pagination. pub(crate) page_number: Option, + /// Flag indicating if the returned transfer history should be protected from potential spam. #[serde(default)] pub(crate) protect_from_spam: bool, } @@ -340,7 +458,7 @@ pub(crate) enum ParseTransferStatusError { UnsupportedTransferStatus, } -#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] pub(crate) enum TransferStatus { Receive, Send, @@ -369,12 +487,13 @@ impl fmt::Display for TransferStatus { } /// [`NftTransferCommon`] structure contains common fields from [`NftTransferHistory`] and [`NftTransferHistoryFromMoralis`] +/// The `possible_spam` field indicates if any potential spam has been detected. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NftTransferCommon { pub(crate) block_hash: Option, /// Transaction hash in hexadecimal format pub(crate) transaction_hash: String, - pub(crate) transaction_index: Option, + pub(crate) transaction_index: Option, pub(crate) log_index: u32, pub(crate) value: Option, pub(crate) transaction_type: Option, @@ -383,12 +502,18 @@ pub struct NftTransferCommon { pub(crate) from_address: Address, pub(crate) to_address: Address, pub(crate) amount: BigDecimal, - pub(crate) verified: Option, + pub(crate) verified: Option, pub(crate) operator: Option, #[serde(default)] pub(crate) possible_spam: bool, } +/// Represents the historical transfer details of an NFT. +/// +/// Contains relevant information about the NFT transfer such as the chain, block details, +/// and contract type. Additionally, fields like `collection_name`, `token_name`, and +/// urls to metadata provide insight into the NFT's identity. The `possible_phishing` +/// field indicates if any domains associated with the NFT have been marked as phishing. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NftTransferHistory { #[serde(flatten)] @@ -398,13 +523,19 @@ pub struct NftTransferHistory { pub(crate) block_timestamp: u64, pub(crate) contract_type: ContractType, pub(crate) token_uri: Option, + pub(crate) token_domain: Option, pub(crate) collection_name: Option, pub(crate) image_url: Option, + pub(crate) image_domain: Option, pub(crate) token_name: Option, pub(crate) status: TransferStatus, + #[serde(default)] + pub(crate) possible_phishing: bool, } -/// This structure is for deserializing moralis NFT transfer json to struct. +/// Represents an NFT transfer structure specifically for deserialization from Moralis's JSON response. +/// +/// This structure is adapted to the specific format provided by Moralis's API. #[derive(Debug, Deserialize)] pub(crate) struct NftTransferHistoryFromMoralis { #[serde(flatten)] @@ -414,6 +545,9 @@ pub(crate) struct NftTransferHistoryFromMoralis { pub(crate) contract_type: Option, } +/// Represents the detailed transfer history of NFTs, including the total number of transfers +/// and the number of skipped transfers. +/// It is used as a response of `get_nft_transfers` if it is successful. #[derive(Debug, Serialize)] pub struct NftsTransferHistoryList { pub(crate) transfer_history: Vec, @@ -421,20 +555,35 @@ pub struct NftsTransferHistoryList { pub(crate) total: usize, } +/// Filters that can be applied to the NFT transfer history. +/// +/// Allows filtering based on transaction type (send/receive), date range, +/// and whether to exclude spam or phishing-related transfers. #[derive(Copy, Clone, Debug, Deserialize)] pub struct NftTransferHistoryFilters { #[serde(default)] - pub receive: bool, + pub(crate) receive: bool, #[serde(default)] pub(crate) send: bool, pub(crate) from_date: Option, pub(crate) to_date: Option, + #[serde(default)] + pub(crate) exclude_spam: bool, + #[serde(default)] + pub(crate) exclude_phishing: bool, } +/// Contains parameters required to update NFT transfer history and NFT list. +/// # Fields +/// * `chains`: A list of blockchains for which the NFTs need to be updated. +/// * `url`: URL to fetch the NFT data. +/// * `url_antispam`: URL used to validate if the fetched contract addresses are associated +/// with spam contracts or if domain fields in the fetched metadata match known phishing domains. #[derive(Debug, Deserialize)] pub struct UpdateNftReq { pub(crate) chains: Vec, pub(crate) url: Url, + pub(crate) url_antispam: Url, } #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] @@ -448,8 +597,10 @@ pub struct TransferMeta { pub(crate) token_address: String, pub(crate) token_id: BigDecimal, pub(crate) token_uri: Option, + pub(crate) token_domain: Option, pub(crate) collection_name: Option, pub(crate) image_url: Option, + pub(crate) image_domain: Option, pub(crate) token_name: Option, } @@ -459,20 +610,32 @@ impl From for TransferMeta { token_address: eth_addr_to_hex(&nft_db.common.token_address), token_id: nft_db.common.token_id, token_uri: nft_db.common.token_uri, + token_domain: nft_db.common.token_domain, collection_name: nft_db.common.collection_name, image_url: nft_db.uri_meta.image_url, + image_domain: nft_db.uri_meta.image_domain, token_name: nft_db.uri_meta.token_name, } } } +/// The primary context for NFT operations within the MM environment. +/// +/// This struct provides an interface for interacting with the underlying data structures +/// required for NFT operations, including guarding against concurrent accesses and +/// dealing with platform-specific storage mechanisms. pub(crate) struct NftCtx { + /// An asynchronous mutex to guard against concurrent NFT operations, ensuring data consistency. pub(crate) guard: Arc>, #[cfg(target_arch = "wasm32")] + /// Platform-specific database for caching NFT data. pub(crate) nft_cache_db: SharedDb, } impl NftCtx { + /// Create a new `NftCtx` from the given MM context. + /// + /// If an `NftCtx` instance doesn't already exist in the MM context, it gets created and cached for subsequent use. pub(crate) fn from_ctx(ctx: &MmArc) -> Result, String> { Ok(try_s!(from_ctx(&ctx.nft_ctx, move || { Ok(NftCtx { @@ -483,3 +646,24 @@ impl NftCtx { }))) } } + +#[derive(Debug, Serialize)] +pub(crate) struct SpamContractReq { + pub(crate) network: Chain, + pub(crate) addresses: String, +} + +#[derive(Debug, Serialize)] +pub(crate) struct PhishingDomainReq { + pub(crate) domains: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct SpamContractRes { + pub(crate) result: HashMap, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct PhishingDomainRes { + pub(crate) result: HashMap, +} diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 02710e0ac4..ae10513987 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -1,194 +1,656 @@ -const NFT_LIST_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/0x394d86994f954ed931b86791b62fe64f4c5dac37/nft?chain=POLYGON&format=decimal"; -const NFT_HISTORY_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/0x394d86994f954ed931b86791b62fe64f4c5dac37/nft/transfers?chain=POLYGON&format=decimal"; -const NFT_METADATA_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/nft/0xed55e4477b795eaa9bb4bca24df42214e1a05c18/1111777?chain=POLYGON&format=decimal"; +use crate::eth::eth_addr_to_hex; +use crate::nft::nft_structs::{Chain, NftFromMoralis, NftListFilters, NftTransferHistoryFilters, + NftTransferHistoryFromMoralis, PhishingDomainReq, PhishingDomainRes, SpamContractReq, + SpamContractRes, TransferMeta, UriMeta}; +use crate::nft::storage::db_test_helpers::{init_nft_history_storage, init_nft_list_storage, nft, nft_list, + nft_transfer_history}; +use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps, RemoveNftResult}; +use crate::nft::{check_moralis_ipfs_bafy, get_domain_from_url, process_metadata_for_spam_link, + process_text_for_spam_link}; +use common::cross_test; +use ethereum_types::Address; +use mm2_net::transport::send_post_request_to_uri; +use mm2_number::BigDecimal; +use std::num::NonZeroUsize; +use std::str::FromStr; + +const MORALIS_API_ENDPOINT_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2"; const TEST_WALLET_ADDR_EVM: &str = "0x394d86994f954ed931b86791b62fe64f4c5dac37"; +const BLOCKLIST_API_ENDPOINT: &str = "https://nft.antispam.dragonhound.info"; +const TOKEN_ADD: &str = "0xfd913a305d70a60aac4faac70c739563738e1f81"; +const TOKEN_ID: &str = "214300044414"; +const TX_HASH: &str = "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe"; +const LOG_INDEX: u32 = 495; -#[cfg(all(test, not(target_arch = "wasm32")))] -mod native_tests { - use crate::eth::eth_addr_to_hex; - use crate::nft::nft_structs::{NftFromMoralis, NftTransferHistoryFromMoralis, UriMeta}; - use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; - use crate::nft::storage::db_test_helpers::*; - use crate::nft::{check_and_redact_if_spam, check_moralis_ipfs_bafy, check_nft_metadata_for_spam, - send_request_to_uri}; - use common::block_on; - - #[test] - fn test_moralis_ipfs_bafy() { - let uri = - "https://ipfs.moralis.io:2053/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; - let res_uri = check_moralis_ipfs_bafy(Some(uri)); - let expected = "https://ipfs.io/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; - assert_eq!(expected, res_uri.unwrap()); - } +#[cfg(not(target_arch = "wasm32"))] +use mm2_net::native_http::send_request_to_uri; - #[test] - fn test_invalid_moralis_ipfs_link() { - let uri = "example.com/bafy?1=ipfs.moralis.io&e=https://"; - let res_uri = check_moralis_ipfs_bafy(Some(uri)); - assert_eq!(uri, res_uri.unwrap()); - } +common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + use mm2_net::wasm_http::send_request_to_uri; +} - #[test] - fn test_check_for_spam() { - let mut spam_text = Some("https://arweave.net".to_string()); - assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); - let url_redacted = "URL redacted for user protection"; - assert_eq!(url_redacted, spam_text.unwrap()); - - let mut spam_text = Some("ftp://123path ".to_string()); - assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); - let url_redacted = "URL redacted for user protection"; - assert_eq!(url_redacted, spam_text.unwrap()); - - let mut spam_text = Some("/192.168.1.1/some.example.org?type=A".to_string()); - assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); - let url_redacted = "URL redacted for user protection"; - assert_eq!(url_redacted, spam_text.unwrap()); - - let mut spam_text = Some(r"C:\Users\path\".to_string()); - assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); - let url_redacted = "URL redacted for user protection"; - assert_eq!(url_redacted, spam_text.unwrap()); - - let mut valid_text = Some("Hello my name is NFT (The best ever!)".to_string()); - assert!(!check_and_redact_if_spam(&mut valid_text).unwrap()); - assert_eq!("Hello my name is NFT (The best ever!)", valid_text.unwrap()); - - let mut nft = nft(); - assert!(check_nft_metadata_for_spam(&mut nft).unwrap()); - let meta_redacted = "{\"name\":\"URL redacted for user protection\",\"image\":\"https://tikimetadata.s3.amazonaws.com/tiki_box.png\"}"; - assert_eq!(meta_redacted, nft.common.metadata.unwrap()) +cross_test!(test_moralis_ipfs_bafy, { + let uri = "https://ipfs.moralis.io:2053/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; + let res_uri = check_moralis_ipfs_bafy(Some(uri)); + let expected = "https://ipfs.io/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; + assert_eq!(expected, res_uri.unwrap()); +}); + +cross_test!(test_get_domain_from_url, { + let image_url = "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png"; + let res_domain = get_domain_from_url(Some(image_url)); + let expected = "public.nftstatic.com"; + assert_eq!(expected, res_domain.unwrap()); +}); + +cross_test!(test_invalid_moralis_ipfs_link, { + let uri = "example.com/bafy?1=ipfs.moralis.io&e=https://"; + let res_uri = check_moralis_ipfs_bafy(Some(uri)); + assert_eq!(uri, res_uri.unwrap()); +}); + +cross_test!(test_check_for_spam_links, { + let mut spam_text = Some("https://arweave.net".to_string()); + assert!(process_text_for_spam_link(&mut spam_text, true).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some("ftp://123path ".to_string()); + assert!(process_text_for_spam_link(&mut spam_text, true).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some("/192.168.1.1/some.example.org?type=A".to_string()); + assert!(process_text_for_spam_link(&mut spam_text, true).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some(r"C:\Users\path\".to_string()); + assert!(process_text_for_spam_link(&mut spam_text, true).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut valid_text = Some("Hello my name is NFT (The best ever!)".to_string()); + assert!(!process_text_for_spam_link(&mut valid_text, true).unwrap()); + assert_eq!("Hello my name is NFT (The best ever!)", valid_text.unwrap()); + + let mut nft = nft(); + assert!(process_metadata_for_spam_link(&mut nft, true).unwrap()); + let meta_redacted = "{\"name\":\"URL redacted for user protection\",\"image\":\"https://tikimetadata.s3.amazonaws.com/tiki_box.png\"}"; + assert_eq!(meta_redacted, nft.common.metadata.unwrap()) +}); + +cross_test!(test_moralis_requests, { + let uri_nft_list = format!( + "{}/{}/nft?chain=POLYGON&format=decimal", + MORALIS_API_ENDPOINT_TEST, TEST_WALLET_ADDR_EVM + ); + let response_nft_list = send_request_to_uri(uri_nft_list.as_str()).await.unwrap(); + let nfts_list = response_nft_list["result"].as_array().unwrap(); + for nft_json in nfts_list { + let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); + assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); } - #[test] - fn test_moralis_requests() { - let response_nft_list = block_on(send_request_to_uri(NFT_LIST_URL_TEST)).unwrap(); - let nfts_list = response_nft_list["result"].as_array().unwrap(); - for nft_json in nfts_list { - let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); - } - - let response_transfer_history = block_on(send_request_to_uri(NFT_HISTORY_URL_TEST)).unwrap(); - let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); - assert!(!transfer_list.is_empty()); - let first_transfer = transfer_list.remove(transfer_list.len() - 1); - let transfer_moralis: NftTransferHistoryFromMoralis = - serde_json::from_str(&first_transfer.to_string()).unwrap(); - assert_eq!( - TEST_WALLET_ADDR_EVM, - eth_addr_to_hex(&transfer_moralis.common.to_address) - ); - - let response_meta = block_on(send_request_to_uri(NFT_METADATA_URL_TEST)).unwrap(); - let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); - assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); - let token_uri = nft_moralis.common.token_uri.unwrap(); - let uri_response = block_on(send_request_to_uri(token_uri.as_str())).unwrap(); - serde_json::from_str::(&uri_response.to_string()).unwrap(); + let uri_history = format!( + "{}/{}/nft/transfers?chain=POLYGON&format=decimal", + MORALIS_API_ENDPOINT_TEST, TEST_WALLET_ADDR_EVM + ); + let response_transfer_history = send_request_to_uri(uri_history.as_str()).await.unwrap(); + let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); + assert!(!transfer_list.is_empty()); + let first_transfer = transfer_list.remove(transfer_list.len() - 1); + let transfer_moralis: NftTransferHistoryFromMoralis = serde_json::from_str(&first_transfer.to_string()).unwrap(); + assert_eq!( + TEST_WALLET_ADDR_EVM, + eth_addr_to_hex(&transfer_moralis.common.to_address) + ); + + let uri_meta = format!( + "{}/nft/0xed55e4477b795eaa9bb4bca24df42214e1a05c18/1111777?chain=POLYGON&format=decimal", + MORALIS_API_ENDPOINT_TEST + ); + let response_meta = send_request_to_uri(uri_meta.as_str()).await.unwrap(); + let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); + assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); +}); + +cross_test!(test_antispam_scan_endpoints, { + let req_spam = SpamContractReq { + network: Chain::Eth, + addresses: "0x0ded8542fc8b2b4e781b96e99fee6406550c9b7c,0x8d1355b65da254f2cc4611453adfa8b7a13f60ee".to_string(), + }; + let uri_contract = format!("{}/api/blocklist/contract/scan", BLOCKLIST_API_ENDPOINT); + let req_json = serde_json::to_string(&req_spam).unwrap(); + let contract_scan_res = send_post_request_to_uri(uri_contract.as_str(), req_json).await.unwrap(); + let spam_res: SpamContractRes = serde_json::from_slice(&contract_scan_res).unwrap(); + assert!(spam_res + .result + .get(&Address::from_str("0x0ded8542fc8b2b4e781b96e99fee6406550c9b7c").unwrap()) + .unwrap()); + assert!(spam_res + .result + .get(&Address::from_str("0x8d1355b65da254f2cc4611453adfa8b7a13f60ee").unwrap()) + .unwrap()); + + let req_phishing = PhishingDomainReq { + domains: "disposal-account-case-1f677.web.app,defi8090.vip".to_string(), + }; + let req_json = serde_json::to_string(&req_phishing).unwrap(); + let uri_domain = format!("{}/api/blocklist/domain/scan", BLOCKLIST_API_ENDPOINT); + let domain_scan_res = send_post_request_to_uri(uri_domain.as_str(), req_json).await.unwrap(); + let phishing_res: PhishingDomainRes = serde_json::from_slice(&domain_scan_res).unwrap(); + assert!(phishing_res.result.get("disposal-account-case-1f677.web.app").unwrap()); +}); + +cross_test!(test_camo, { + let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); + let uri_decode = format!("{}/url/decode/{}", BLOCKLIST_API_ENDPOINT, hex_token_uri); + let decode_res = send_request_to_uri(&uri_decode).await.unwrap(); + let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); + assert_eq!( + uri_meta.raw_image_url.unwrap(), + "https://tikimetadata.s3.amazonaws.com/tiki_box.png" + ); +}); + +cross_test!(test_add_get_nfts, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let nft = storage + .get_nft(&chain, TOKEN_ADD.to_string(), token_id) + .await + .unwrap() + .unwrap(); + assert_eq!(nft.block_number, 28056721); +}); + +cross_test!(test_last_nft_block, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let last_block = NftListStorageOps::get_last_block_number(&storage, &chain) + .await + .unwrap() + .unwrap(); + assert_eq!(last_block, 28056726); +}); + +cross_test!(test_nft_list, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let nft_list = storage + .get_nft_list(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap()), None) + .await + .unwrap(); + assert_eq!(nft_list.nfts.len(), 1); + let nft = nft_list.nfts.get(0).unwrap(); + assert_eq!(nft.block_number, 28056721); + assert_eq!(nft_list.skipped, 2); + assert_eq!(nft_list.total, 4); +}); + +cross_test!(test_remove_nft, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let remove_rslt = storage + .remove_nft_from_list(&chain, TOKEN_ADD.to_string(), token_id, 28056800) + .await + .unwrap(); + assert_eq!(remove_rslt, RemoveNftResult::NftRemoved); + let list_len = storage + .get_nft_list(vec![chain], true, 1, None, None) + .await + .unwrap() + .nfts + .len(); + assert_eq!(list_len, 3); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 28056800); +}); + +cross_test!(test_nft_amount, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let mut nft = nft(); + storage + .add_nfts_to_list(chain, vec![nft.clone()], 25919780) + .await + .unwrap(); + + nft.common.amount -= BigDecimal::from(1); + storage.update_nft_amount(&chain, nft.clone(), 25919800).await.unwrap(); + let amount = storage + .get_nft_amount( + &chain, + eth_addr_to_hex(&nft.common.token_address), + nft.common.token_id.clone(), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(amount, "1"); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 25919800); + + nft.common.amount += BigDecimal::from(1); + nft.block_number = 25919900; + storage + .update_nft_amount_and_block_number(&chain, nft.clone()) + .await + .unwrap(); + let amount = storage + .get_nft_amount(&chain, eth_addr_to_hex(&nft.common.token_address), nft.common.token_id) + .await + .unwrap() + .unwrap(); + assert_eq!(amount, "2"); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 25919900); +}); + +cross_test!(test_refresh_metadata, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let new_symbol = "NEW_SYMBOL"; + let mut nft = nft(); + storage + .add_nfts_to_list(chain, vec![nft.clone()], 25919780) + .await + .unwrap(); + nft.common.symbol = Some(new_symbol.to_string()); + drop_mutability!(nft); + let token_add = eth_addr_to_hex(&nft.common.token_address); + let token_id = nft.common.token_id.clone(); + storage.refresh_nft_metadata(&chain, nft).await.unwrap(); + let nft_upd = storage.get_nft(&chain, token_add, token_id).await.unwrap().unwrap(); + assert_eq!(new_symbol.to_string(), nft_upd.common.symbol.unwrap()); +}); + +cross_test!(test_update_nft_spam_by_token_address, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + storage + .update_nft_spam_by_token_address(&chain, TOKEN_ADD.to_string(), true) + .await + .unwrap(); + let nfts = storage + .get_nfts_by_token_address(chain, TOKEN_ADD.to_string()) + .await + .unwrap(); + for nft in nfts { + assert!(nft.common.possible_spam); } - - #[test] - fn test_add_get_nfts() { block_on(test_add_get_nfts_impl()) } - - #[test] - fn test_last_nft_blocks() { block_on(test_last_nft_blocks_impl()) } - - #[test] - fn test_nft_list() { block_on(test_nft_list_impl()) } - - #[test] - fn test_remove_nft() { block_on(test_remove_nft_impl()) } - - #[test] - fn test_refresh_metadata() { block_on(test_refresh_metadata_impl()) } - - #[test] - fn test_nft_amount() { block_on(test_nft_amount_impl()) } - - #[test] - fn test_add_get_transfers() { block_on(test_add_get_transfers_impl()) } - - #[test] - fn test_last_transfer_block() { block_on(test_last_transfer_block_impl()) } - - #[test] - fn test_transfer_history() { block_on(test_transfer_history_impl()) } - - #[test] - fn test_transfer_history_filters() { block_on(test_transfer_history_filters_impl()) } - - #[test] - fn test_get_update_transfer_meta() { block_on(test_get_update_transfer_meta_impl()) } -} - -#[cfg(target_arch = "wasm32")] -mod wasm_tests { - use crate::eth::eth_addr_to_hex; - use crate::nft::nft_structs::{NftFromMoralis, NftTransferHistoryFromMoralis}; - use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; - use crate::nft::send_request_to_uri; - use crate::nft::storage::db_test_helpers::*; - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - async fn test_moralis_requests() { - let response_nft_list = send_request_to_uri(NFT_LIST_URL_TEST).await.unwrap(); - let nfts_list = response_nft_list["result"].as_array().unwrap(); - for nft_json in nfts_list { - let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); - } - - let response_transfer_history = send_request_to_uri(NFT_HISTORY_URL_TEST).await.unwrap(); - let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); - assert!(!transfer_list.is_empty()); - let first_transfer = transfer_list.remove(transfer_list.len() - 1); - let transfer_moralis: NftTransferHistoryFromMoralis = - serde_json::from_str(&first_transfer.to_string()).unwrap(); - assert_eq!( - TEST_WALLET_ADDR_EVM, - eth_addr_to_hex(&transfer_moralis.common.to_address) - ); - - let response_meta = send_request_to_uri(NFT_METADATA_URL_TEST).await.unwrap(); - let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); - assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); +}); + +cross_test!(test_exclude_nft_spam, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let filters = NftListFilters { + exclude_spam: true, + exclude_phishing: false, + }; + let nft_list = storage + .get_nft_list(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap(); + assert_eq!(nft_list.nfts.len(), 3); +}); + +cross_test!(test_get_animation_external_domains, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let domains = storage.get_animation_external_domains(&chain).await.unwrap(); + assert_eq!(2, domains.len()); + assert!(domains.contains("tikimetadata.s3.amazonaws.com")); + assert!(domains.contains("public.nftstatic.com")); +}); + +cross_test!(test_update_nft_phishing_by_domain, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + let domains = vec![ + "tikimetadata.s3.amazonaws.com".to_string(), + "public.nftstatic.com".to_string(), + ]; + for domain in domains.into_iter() { + storage + .update_nft_phishing_by_domain(&chain, domain, true) + .await + .unwrap(); } - - #[wasm_bindgen_test] - async fn test_add_get_nfts() { test_add_get_nfts_impl().await } - - #[wasm_bindgen_test] - async fn test_last_nft_blocks() { test_last_nft_blocks_impl().await } - - #[wasm_bindgen_test] - async fn test_nft_list() { test_nft_list_impl().await } - - #[wasm_bindgen_test] - async fn test_remove_nft() { test_remove_nft_impl().await } - - #[wasm_bindgen_test] - async fn test_nft_amount() { test_nft_amount_impl().await } - - #[wasm_bindgen_test] - async fn test_refresh_metadata() { test_refresh_metadata_impl().await } - - #[wasm_bindgen_test] - async fn test_add_get_transfers() { test_add_get_transfers_impl().await } - - #[wasm_bindgen_test] - async fn test_last_transfer_block() { test_last_transfer_block_impl().await } - - #[wasm_bindgen_test] - async fn test_transfer_history() { test_transfer_history_impl().await } - - #[wasm_bindgen_test] - async fn test_transfer_history_filters() { test_transfer_history_filters_impl().await } - - #[wasm_bindgen_test] - async fn test_get_update_transfer_meta() { test_get_update_transfer_meta_impl().await } -} + let nfts = storage + .get_nft_list(vec![chain], true, 1, None, None) + .await + .unwrap() + .nfts; + for nft in nfts.into_iter() { + assert!(nft.possible_phishing); + } +}); + +cross_test!(test_exclude_nft_phishing_spam, { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(chain, nft_list, 28056726).await.unwrap(); + + storage + .update_nft_phishing_by_domain(&chain, "tikimetadata.s3.amazonaws.com".to_string(), true) + .await + .unwrap(); + let filters = NftListFilters { + exclude_spam: true, + exclude_phishing: true, + }; + let nfts = storage + .get_nft_list(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap() + .nfts; + assert_eq!(nfts.len(), 2); +}); + +cross_test!(test_add_get_transfers, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let transfer1 = storage + .get_transfers_by_token_addr_id(chain, TOKEN_ADD.to_string(), token_id) + .await + .unwrap() + .get(0) + .unwrap() + .clone(); + assert_eq!(transfer1.block_number, 28056721); + let transfer2 = storage + .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) + .await + .unwrap() + .unwrap(); + assert_eq!(transfer2.block_number, 28056726); + let transfer_from = storage.get_transfers_from_block(chain, 28056721).await.unwrap(); + assert_eq!(transfer_from.len(), 3); +}); + +cross_test!(test_last_transfer_block, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let last_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, &chain) + .await + .unwrap() + .unwrap(); + assert_eq!(last_block, 28056726); +}); + +cross_test!(test_transfer_history, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let transfer_history = storage + .get_transfer_history(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap()), None) + .await + .unwrap(); + assert_eq!(transfer_history.transfer_history.len(), 1); + let transfer = transfer_history.transfer_history.get(0).unwrap(); + assert_eq!(transfer.block_number, 28056721); + assert_eq!(transfer_history.skipped, 2); + assert_eq!(transfer_history.total, 4); +}); + +cross_test!(test_transfer_history_filters, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let filters = NftTransferHistoryFilters { + receive: true, + send: false, + from_date: None, + to_date: None, + exclude_spam: false, + exclude_phishing: false, + }; + + let filters1 = NftTransferHistoryFilters { + receive: false, + send: false, + from_date: None, + to_date: Some(1677166110), + exclude_spam: false, + exclude_phishing: false, + }; + + let filters2 = NftTransferHistoryFilters { + receive: false, + send: false, + from_date: Some(1677166110), + to_date: Some(1683627417), + exclude_spam: false, + exclude_phishing: false, + }; + + let transfer_history = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap(); + assert_eq!(transfer_history.transfer_history.len(), 4); + let transfer = transfer_history.transfer_history.get(0).unwrap(); + assert_eq!(transfer.block_number, 28056726); + + let transfer_history1 = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters1)) + .await + .unwrap(); + assert_eq!(transfer_history1.transfer_history.len(), 1); + let transfer1 = transfer_history1.transfer_history.get(0).unwrap(); + assert_eq!(transfer1.block_number, 25919780); + + let transfer_history2 = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters2)) + .await + .unwrap(); + assert_eq!(transfer_history2.transfer_history.len(), 2); + let transfer_0 = transfer_history2.transfer_history.get(0).unwrap(); + assert_eq!(transfer_0.block_number, 28056721); + let transfer_1 = transfer_history2.transfer_history.get(1).unwrap(); + assert_eq!(transfer_1.block_number, 25919780); +}); + +cross_test!(test_get_update_transfer_meta, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let vec_token_add_id = storage.get_transfers_with_empty_meta(chain).await.unwrap(); + assert_eq!(vec_token_add_id.len(), 3); + + let token_add = "0x5c7d6712dfaf0cb079d48981781c8705e8417ca0".to_string(); + let transfer_meta = TransferMeta { + token_address: token_add.clone(), + token_id: Default::default(), + token_uri: None, + token_domain: None, + collection_name: None, + image_url: None, + image_domain: None, + token_name: Some("Tiki box".to_string()), + }; + storage + .update_transfers_meta_by_token_addr_id(&chain, transfer_meta, true) + .await + .unwrap(); + let transfer_upd = storage + .get_transfers_by_token_addr_id(chain, token_add, Default::default()) + .await + .unwrap(); + let transfer_upd = transfer_upd.get(0).unwrap(); + assert_eq!(transfer_upd.token_name, Some("Tiki box".to_string())); + assert!(transfer_upd.common.possible_spam); +}); + +cross_test!(test_update_transfer_spam_by_token_address, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + storage + .update_transfer_spam_by_token_address(&chain, TOKEN_ADD.to_string(), true) + .await + .unwrap(); + let transfers = storage + .get_transfers_by_token_address(chain, TOKEN_ADD.to_string()) + .await + .unwrap(); + for transfers in transfers { + assert!(transfers.common.possible_spam); + } +}); + +cross_test!(test_get_token_addresses, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let token_addresses = storage.get_token_addresses(chain).await.unwrap(); + assert_eq!(token_addresses.len(), 2); +}); + +cross_test!(test_exclude_transfer_spam, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let filters = NftTransferHistoryFilters { + receive: true, + send: true, + from_date: None, + to_date: None, + exclude_spam: true, + exclude_phishing: false, + }; + let transfer_history = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap(); + assert_eq!(transfer_history.transfer_history.len(), 3); +}); + +cross_test!(test_get_domains, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let domains = storage.get_domains(&chain).await.unwrap(); + assert_eq!(2, domains.len()); + assert!(domains.contains("tikimetadata.s3.amazonaws.com")); + assert!(domains.contains("public.nftstatic.com")); +}); + +cross_test!(test_update_transfer_phishing_by_domain, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + let domains = vec![ + "tikimetadata.s3.amazonaws.com".to_string(), + "public.nftstatic.com".to_string(), + ]; + for domain in domains.into_iter() { + storage + .update_transfer_phishing_by_domain(&chain, domain, true) + .await + .unwrap(); + } + let transfers = storage + .get_transfer_history(vec![chain], true, 1, None, None) + .await + .unwrap() + .transfer_history; + for transfer in transfers.into_iter() { + assert!(transfer.possible_phishing); + } +}); + +cross_test!(test_exclude_transfer_phishing_spam, { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + storage + .update_transfer_phishing_by_domain(&chain, "tikimetadata.s3.amazonaws.com".to_string(), true) + .await + .unwrap(); + let filters = NftTransferHistoryFilters { + receive: true, + send: true, + from_date: None, + to_date: None, + exclude_spam: false, + exclude_phishing: true, + }; + let transfers = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap() + .transfer_history; + assert_eq!(transfers.len(), 2); + + let filters1 = NftTransferHistoryFilters { + receive: true, + send: true, + from_date: None, + to_date: None, + exclude_spam: true, + exclude_phishing: true, + }; + let transfers = storage + .get_transfer_history(vec![chain], true, 1, None, Some(filters1)) + .await + .unwrap() + .transfer_history; + assert_eq!(transfers.len(), 1); +}); diff --git a/mm2src/coins/nft/storage/db_test_helpers.rs b/mm2src/coins/nft/storage/db_test_helpers.rs index 7961d4c1ea..9d4e8bfe14 100644 --- a/mm2src/coins/nft/storage/db_test_helpers.rs +++ b/mm2src/coins/nft/storage/db_test_helpers.rs @@ -1,24 +1,11 @@ -use crate::eth::eth_addr_to_hex; use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCommon, NftTransferCommon, NftTransferHistory, - NftTransferHistoryFilters, TransferMeta, TransferStatus, UriMeta}; -use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps, RemoveNftResult}; + TransferStatus, UriMeta}; +use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTransferHistoryStorageOps}; use ethereum_types::Address; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; -use std::num::NonZeroUsize; use std::str::FromStr; -cfg_wasm32! { - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); -} - -const TOKEN_ADD: &str = "0xfd913a305d70a60aac4faac70c739563738e1f81"; -const TOKEN_ID: &str = "214300044414"; -const TX_HASH: &str = "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe"; -const LOG_INDEX: u32 = 495; - pub(crate) fn nft() -> Nft { Nft { common: NftCommon { @@ -30,6 +17,7 @@ pub(crate) fn nft() -> Nft { collection_name: None, symbol: None, token_uri: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.json".to_string()), + token_domain: None, metadata: Some( "{\"name\":\"https://arweave.net\",\"image\":\"https://tikimetadata.s3.amazonaws.com/tiki_box.png\"}" .to_string(), @@ -37,13 +25,13 @@ pub(crate) fn nft() -> Nft { last_token_uri_sync: Some("2023-02-07T17:10:08.402Z".to_string()), last_metadata_sync: Some("2023-02-07T17:10:16.858Z".to_string()), minter_address: Some("ERC1155 tokens don't have a single minter".to_string()), - possible_spam: false, + possible_spam: true, }, chain: Chain::Bsc, block_number_minted: Some(25465916), block_number: 25919780, contract_type: ContractType::Erc1155, - + possible_phishing: false, uri_meta: UriMeta { image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), raw_image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), @@ -51,43 +39,16 @@ pub(crate) fn nft() -> Nft { description: Some("Born to usher in Bull markets.".to_string()), attributes: None, animation_url: None, + animation_domain: None, external_url: None, + external_domain: None, image_details: None, + image_domain: Some("tikimetadata.s3.amazonaws.com".to_string()), }, } } -fn transfer() -> NftTransferHistory { - NftTransferHistory { - common: NftTransferCommon { - block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), - transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), - transaction_index: Some(198), - log_index: 495, - value: Default::default(), - transaction_type: Some("Single".to_string()), - token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), - token_id: BigDecimal::from_str("214300047252").unwrap(), - from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), - to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), - amount: BigDecimal::from_str("1").unwrap(), - verified: Some(1), - operator: None, - possible_spam: false, - }, - chain: Chain::Bsc, - block_number: 28056726, - block_timestamp: 1683627432, - contract_type: ContractType::Erc721, - token_uri: None, - collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), - image_url: Some("https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string()), - token_name: Some("Nebula Nodes".to_string()), - status: TransferStatus::Receive, - } -} - -fn nft_list() -> Vec { +pub(crate) fn nft_list() -> Vec { let nft = Nft { common: NftCommon { token_address: Address::from_str("0x5c7d6712dfaf0cb079d48981781c8705e8417ca0").unwrap(), @@ -98,6 +59,7 @@ fn nft_list() -> Vec { collection_name: None, symbol: None, token_uri: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.json".to_string()), + token_domain: None, metadata: Some("{\"name\":\"Tiki box\"}".to_string()), last_token_uri_sync: Some("2023-02-07T17:10:08.402Z".to_string()), last_metadata_sync: Some("2023-02-07T17:10:16.858Z".to_string()), @@ -108,6 +70,7 @@ fn nft_list() -> Vec { block_number_minted: Some(25465916), block_number: 25919780, contract_type: ContractType::Erc1155, + possible_phishing: false, uri_meta: UriMeta { image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), raw_image_url: None, @@ -115,8 +78,11 @@ fn nft_list() -> Vec { description: Some("Born to usher in Bull markets.".to_string()), attributes: None, animation_url: None, + animation_domain: Some("tikimetadata.s3.amazonaws.com".to_string()), external_url: None, + external_domain: None, image_details: None, + image_domain: None, }, }; @@ -130,6 +96,7 @@ fn nft_list() -> Vec { collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), symbol: Some("BMBBBF".to_string()), token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300047252".to_string()), + token_domain: Some("public.nftstatic.com".to_string()), metadata: Some( "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" .to_string(), @@ -137,13 +104,13 @@ fn nft_list() -> Vec { last_token_uri_sync: Some("2023-02-16T16:35:52.392Z".to_string()), last_metadata_sync: Some("2023-02-16T16:36:04.283Z".to_string()), minter_address: Some("0xdbdeb0895f3681b87fb3654b5cf3e05546ba24a9".to_string()), - possible_spam: false, + possible_spam: true, }, chain: Chain::Bsc, - block_number_minted: Some(25721963), block_number: 28056726, contract_type: ContractType::Erc721, + possible_phishing: false, uri_meta: UriMeta { image_url: Some( "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), @@ -153,8 +120,11 @@ fn nft_list() -> Vec { description: Some("Interchain nodes".to_string()), attributes: None, animation_url: None, + animation_domain: None, external_url: None, + external_domain: None, image_details: None, + image_domain: None, }, }; @@ -168,6 +138,7 @@ fn nft_list() -> Vec { collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), symbol: Some("BMBBBF".to_string()), token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300047252".to_string()), + token_domain: None, metadata: Some( "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" .to_string(), @@ -178,10 +149,10 @@ fn nft_list() -> Vec { possible_spam: false, }, chain: Chain::Bsc, - block_number_minted: Some(25721963), block_number: 28056726, contract_type: ContractType::Erc721, + possible_phishing: false, uri_meta: UriMeta { image_url: Some( "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), @@ -191,8 +162,11 @@ fn nft_list() -> Vec { description: Some("Interchain nodes".to_string()), attributes: None, animation_url: None, + animation_domain: None, external_url: None, + external_domain: None, image_details: None, + image_domain: Some("public.nftstatic.com".to_string()), }, }; @@ -206,6 +180,7 @@ fn nft_list() -> Vec { collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), symbol: Some("BMBBBF".to_string()), token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300044414".to_string()), + token_domain: None, metadata: Some( "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" .to_string(), @@ -216,10 +191,10 @@ fn nft_list() -> Vec { possible_spam: false, }, chain: Chain::Bsc, - block_number_minted: Some(25810308), block_number: 28056721, contract_type: ContractType::Erc721, + possible_phishing: false, uri_meta: UriMeta { image_url: Some( "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), @@ -229,14 +204,17 @@ fn nft_list() -> Vec { description: Some("Interchain nodes".to_string()), attributes: None, animation_url: None, + animation_domain: None, external_url: None, + external_domain: Some("public.nftstatic.com".to_string()), image_details: None, + image_domain: None, }, }; vec![nft, nft1, nft2, nft3] } -fn nft_transfer_history() -> Vec { +pub(crate) fn nft_transfer_history() -> Vec { let transfer = NftTransferHistory { common: NftTransferCommon { block_hash: Some("0xcb41654fc5cf2bf5d7fd3f061693405c74d419def80993caded0551ecfaeaae5".to_string()), @@ -259,10 +237,13 @@ fn nft_transfer_history() -> Vec { block_timestamp: 1677166110, contract_type: ContractType::Erc1155, token_uri: None, + token_domain: Some("tikimetadata.s3.amazonaws.com".to_string()), collection_name: None, image_url: None, + image_domain: None, token_name: None, status: TransferStatus::Receive, + possible_phishing: false, }; let transfer1 = NftTransferHistory { @@ -280,19 +261,20 @@ fn nft_transfer_history() -> Vec { amount: BigDecimal::from_str("1").unwrap(), verified: Some(1), operator: None, - possible_spam: false, + possible_spam: true, }, chain: Chain::Bsc, block_number: 28056726, block_timestamp: 1683627432, contract_type: ContractType::Erc721, - token_uri: None, + token_domain: Some("public.nftstatic.com".to_string()), collection_name: None, image_url: None, + image_domain: None, token_name: None, - status: TransferStatus::Receive, + possible_phishing: false, }; // Same as transfer1 but with different log_index, meaning that transfer1 and transfer2 are part of one batch/multi token transaction @@ -317,13 +299,14 @@ fn nft_transfer_history() -> Vec { block_number: 28056726, block_timestamp: 1683627432, contract_type: ContractType::Erc721, - token_uri: None, + token_domain: None, collection_name: None, image_url: None, + image_domain: Some("public.nftstatic.com".to_string()), token_name: None, - status: TransferStatus::Receive, + possible_phishing: false, }; let transfer3 = NftTransferHistory { @@ -346,20 +329,20 @@ fn nft_transfer_history() -> Vec { chain: Chain::Bsc, block_number: 28056721, block_timestamp: 1683627417, - contract_type: ContractType::Erc721, - token_uri: None, + token_domain: None, collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), image_url: Some("https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string()), + image_domain: Some("tikimetadata.s3.amazonaws.com".to_string()), token_name: Some("Nebula Nodes".to_string()), - status: TransferStatus::Receive, + possible_phishing: false, }; vec![transfer, transfer1, transfer2, transfer3] } -async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { +pub(crate) async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { let ctx = mm_ctx_with_custom_db(); let storage = NftStorageBuilder::new(&ctx).build().unwrap(); NftListStorageOps::init(&storage, chain).await.unwrap(); @@ -368,7 +351,7 @@ async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTra storage } -async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { +pub(crate) async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + NftTransferHistoryStorageOps { let ctx = mm_ctx_with_custom_db(); let storage = NftStorageBuilder::new(&ctx).build().unwrap(); NftTransferHistoryStorageOps::init(&storage, chain).await.unwrap(); @@ -378,282 +361,3 @@ async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + Nft assert!(is_initialized); storage } - -pub(crate) async fn test_add_get_nfts_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let nft_list = nft_list(); - storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); - - let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); - let nft = storage - .get_nft(&chain, TOKEN_ADD.to_string(), token_id) - .await - .unwrap() - .unwrap(); - assert_eq!(nft.block_number, 28056721); -} - -pub(crate) async fn test_last_nft_blocks_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let nft_list = nft_list(); - storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); - - let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); - let nft = storage - .get_nft(&chain, TOKEN_ADD.to_string(), token_id) - .await - .unwrap() - .unwrap(); - assert_eq!(nft.block_number, 28056721); -} - -pub(crate) async fn test_nft_list_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let nft_list = nft_list(); - storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); - - let nft_list = storage - .get_nft_list(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap())) - .await - .unwrap(); - assert_eq!(nft_list.nfts.len(), 1); - let nft = nft_list.nfts.get(0).unwrap(); - assert_eq!(nft.block_number, 28056721); - assert_eq!(nft_list.skipped, 2); - assert_eq!(nft_list.total, 4); -} - -pub(crate) async fn test_remove_nft_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let nft_list = nft_list(); - storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); - - let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); - let remove_rslt = storage - .remove_nft_from_list(&chain, TOKEN_ADD.to_string(), token_id, 28056800) - .await - .unwrap(); - assert_eq!(remove_rslt, RemoveNftResult::NftRemoved); - let list_len = storage - .get_nft_list(vec![chain], true, 1, None) - .await - .unwrap() - .nfts - .len(); - assert_eq!(list_len, 3); - let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); - assert_eq!(last_scanned_block, 28056800); -} - -pub(crate) async fn test_nft_amount_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let mut nft = nft(); - storage - .add_nfts_to_list(&chain, vec![nft.clone()], 25919780) - .await - .unwrap(); - - nft.common.amount -= BigDecimal::from(1); - storage.update_nft_amount(&chain, nft.clone(), 25919800).await.unwrap(); - let amount = storage - .get_nft_amount( - &chain, - eth_addr_to_hex(&nft.common.token_address), - nft.common.token_id.clone(), - ) - .await - .unwrap() - .unwrap(); - assert_eq!(amount, "1"); - let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); - assert_eq!(last_scanned_block, 25919800); - - nft.common.amount += BigDecimal::from(1); - nft.block_number = 25919900; - storage - .update_nft_amount_and_block_number(&chain, nft.clone()) - .await - .unwrap(); - let amount = storage - .get_nft_amount(&chain, eth_addr_to_hex(&nft.common.token_address), nft.common.token_id) - .await - .unwrap() - .unwrap(); - assert_eq!(amount, "2"); - let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); - assert_eq!(last_scanned_block, 25919900); -} - -pub(crate) async fn test_refresh_metadata_impl() { - let chain = Chain::Bsc; - let storage = init_nft_list_storage(&chain).await; - let new_symbol = "NEW_SYMBOL"; - let mut nft = nft(); - storage - .add_nfts_to_list(&chain, vec![nft.clone()], 25919780) - .await - .unwrap(); - nft.common.symbol = Some(new_symbol.to_string()); - drop_mutability!(nft); - let token_add = eth_addr_to_hex(&nft.common.token_address); - let token_id = nft.common.token_id.clone(); - storage.refresh_nft_metadata(&chain, nft).await.unwrap(); - let nft_upd = storage.get_nft(&chain, token_add, token_id).await.unwrap().unwrap(); - assert_eq!(new_symbol.to_string(), nft_upd.common.symbol.unwrap()); -} - -pub(crate) async fn test_add_get_transfers_impl() { - let chain = Chain::Bsc; - let storage = init_nft_history_storage(&chain).await; - let transfers = nft_transfer_history(); - storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - - let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); - let transfer1 = storage - .get_transfers_by_token_addr_id(&chain, TOKEN_ADD.to_string(), token_id) - .await - .unwrap() - .get(0) - .unwrap() - .clone(); - assert_eq!(transfer1.block_number, 28056721); - let transfer2 = storage - .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) - .await - .unwrap() - .unwrap(); - assert_eq!(transfer2.block_number, 28056726); - let transfer_from = storage.get_transfers_from_block(&chain, 28056721).await.unwrap(); - assert_eq!(transfer_from.len(), 3); -} - -pub(crate) async fn test_last_transfer_block_impl() { - let chain = Chain::Bsc; - let storage = init_nft_history_storage(&chain).await; - let transfers = nft_transfer_history(); - storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - - let last_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, &chain) - .await - .unwrap() - .unwrap(); - assert_eq!(last_block, 28056726); -} - -pub(crate) async fn test_transfer_history_impl() { - let chain = Chain::Bsc; - let storage = init_nft_history_storage(&chain).await; - let transfers = nft_transfer_history(); - storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - - let transfer_history = storage - .get_transfer_history(vec![chain], false, 1, Some(NonZeroUsize::new(3).unwrap()), None) - .await - .unwrap(); - assert_eq!(transfer_history.transfer_history.len(), 1); - let transfer = transfer_history.transfer_history.get(0).unwrap(); - assert_eq!(transfer.block_number, 28056721); - assert_eq!(transfer_history.skipped, 2); - assert_eq!(transfer_history.total, 4); -} - -pub(crate) async fn test_transfer_history_filters_impl() { - let chain = Chain::Bsc; - let storage = init_nft_history_storage(&chain).await; - let transfers = nft_transfer_history(); - storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - - let filters = NftTransferHistoryFilters { - receive: true, - send: false, - from_date: None, - to_date: None, - }; - - let filters1 = NftTransferHistoryFilters { - receive: false, - send: false, - from_date: None, - to_date: Some(1677166110), - }; - - let filters2 = NftTransferHistoryFilters { - receive: false, - send: false, - from_date: Some(1677166110), - to_date: Some(1683627417), - }; - - let transfer_history = storage - .get_transfer_history(vec![chain], true, 1, None, Some(filters)) - .await - .unwrap(); - assert_eq!(transfer_history.transfer_history.len(), 4); - let transfer = transfer_history.transfer_history.get(0).unwrap(); - assert_eq!(transfer.block_number, 28056726); - - let transfer_history1 = storage - .get_transfer_history(vec![chain], true, 1, None, Some(filters1)) - .await - .unwrap(); - assert_eq!(transfer_history1.transfer_history.len(), 1); - let transfer1 = transfer_history1.transfer_history.get(0).unwrap(); - assert_eq!(transfer1.block_number, 25919780); - - let transfer_history2 = storage - .get_transfer_history(vec![chain], true, 1, None, Some(filters2)) - .await - .unwrap(); - assert_eq!(transfer_history2.transfer_history.len(), 2); - let transfer_0 = transfer_history2.transfer_history.get(0).unwrap(); - assert_eq!(transfer_0.block_number, 28056721); - let transfer_1 = transfer_history2.transfer_history.get(1).unwrap(); - assert_eq!(transfer_1.block_number, 25919780); -} - -pub(crate) async fn test_get_update_transfer_meta_impl() { - let chain = Chain::Bsc; - let storage = init_nft_history_storage(&chain).await; - let transfers = nft_transfer_history(); - storage.add_transfers_to_history(&chain, transfers).await.unwrap(); - - let vec_token_add_id = storage.get_transfers_with_empty_meta(&chain).await.unwrap(); - assert_eq!(vec_token_add_id.len(), 3); - - let token_add = "0x5c7d6712dfaf0cb079d48981781c8705e8417ca0".to_string(); - let transfer_meta = TransferMeta { - token_address: token_add.clone(), - token_id: Default::default(), - token_uri: None, - collection_name: None, - image_url: None, - token_name: Some("Tiki box".to_string()), - }; - storage - .update_transfers_meta_by_token_addr_id(&chain, transfer_meta) - .await - .unwrap(); - let transfer_upd = storage - .get_transfers_by_token_addr_id(&chain, token_add, Default::default()) - .await - .unwrap(); - let transfer_upd = transfer_upd.get(0).unwrap(); - assert_eq!(transfer_upd.token_name, Some("Tiki box".to_string())); - - let transfer_meta = transfer(); - storage - .update_transfer_meta_by_hash_and_log_index(&chain, transfer_meta) - .await - .unwrap(); - let transfer_by_hash = storage - .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) - .await - .unwrap() - .unwrap(); - assert_eq!(transfer_by_hash.token_name, Some("Nebula Nodes".to_string())) -} diff --git a/mm2src/coins/nft/storage/mod.rs b/mm2src/coins/nft/storage/mod.rs index 0a2e906ccc..14cc9243f0 100644 --- a/mm2src/coins/nft/storage/mod.rs +++ b/mm2src/coins/nft/storage/mod.rs @@ -1,13 +1,15 @@ -use crate::nft::nft_structs::{Chain, Nft, NftList, NftTokenAddrId, NftTransferHistory, NftTransferHistoryFilters, - NftsTransferHistoryList, TransferMeta}; +use crate::nft::nft_structs::{Chain, Nft, NftList, NftListFilters, NftTokenAddrId, NftTransferHistory, + NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta}; use crate::WithdrawError; use async_trait::async_trait; use derive_more::Display; +use ethereum_types::Address; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::mm_error::MmResult; use mm2_err_handle::mm_error::{NotEqual, NotMmError}; use mm2_number::BigDecimal; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::num::NonZeroUsize; #[cfg(any(test, target_arch = "wasm32"))] @@ -15,23 +17,28 @@ pub(crate) mod db_test_helpers; #[cfg(not(target_arch = "wasm32"))] pub(crate) mod sql_storage; #[cfg(target_arch = "wasm32")] pub(crate) mod wasm; +/// Represents the outcome of an attempt to remove an NFT. #[derive(Debug, PartialEq)] pub enum RemoveNftResult { + /// Indicates that the NFT was successfully removed. NftRemoved, + /// Indicates that the NFT did not exist in the storage. NftDidNotExist, } +/// Defines the standard errors that can occur in NFT storage operations pub trait NftStorageError: std::fmt::Debug + NotMmError + NotEqual + Send {} impl From for WithdrawError { fn from(err: T) -> Self { WithdrawError::DbError(format!("{:?}", err)) } } +/// Provides asynchronous operations for handling and querying NFT listings. #[async_trait] pub trait NftListStorageOps { type Error: NftStorageError; - /// Initializes tables in storage for the specified chain type. + /// Prepares the storage by initializing required tables for a specified chain type. async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error>; /// Whether tables are initialized for the specified chain. @@ -43,9 +50,10 @@ pub trait NftListStorageOps { max: bool, limit: usize, page_number: Option, + filters: Option, ) -> MmResult; - async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + async fn add_nfts_to_list(&self, chain: Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send; @@ -85,13 +93,34 @@ pub trait NftListStorageOps { async fn update_nft_amount(&self, chain: &Chain, nft: Nft, scanned_block: u64) -> MmResult<(), Self::Error>; async fn update_nft_amount_and_block_number(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error>; + + /// `get_nfts_by_token_address` function returns list of NFTs which have specified token address. + async fn get_nfts_by_token_address(&self, chain: Chain, token_address: String) -> MmResult, Self::Error>; + + /// `update_nft_spam_by_token_address` function updates `possible_spam` field in NFTs which have specified token address. + async fn update_nft_spam_by_token_address( + &self, + chain: &Chain, + token_address: String, + possible_spam: bool, + ) -> MmResult<(), Self::Error>; + + async fn get_animation_external_domains(&self, chain: &Chain) -> MmResult, Self::Error>; + + async fn update_nft_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error>; } +/// Provides asynchronous operations related to the history of NFT transfers. #[async_trait] pub trait NftTransferHistoryStorageOps { type Error: NftStorageError; - /// Initializes tables in storage for the specified chain type. + /// Prepares the storage by initializing required tables for a specified chain type. async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error>; /// Whether tables are initialized for the specified chain. @@ -106,7 +135,7 @@ pub trait NftTransferHistoryStorageOps { filters: Option, ) -> MmResult; - async fn add_transfers_to_history(&self, chain: &Chain, transfers: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, chain: Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send; @@ -117,13 +146,13 @@ pub trait NftTransferHistoryStorageOps { /// block_number in ascending order. It is needed to update the NFT LIST table correctly. async fn get_transfers_from_block( &self, - chain: &Chain, + chain: Chain, from_block: u64, ) -> MmResult, Self::Error>; async fn get_transfers_by_token_addr_id( &self, - chain: &Chain, + chain: Chain, token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error>; @@ -135,21 +164,47 @@ pub trait NftTransferHistoryStorageOps { log_index: u32, ) -> MmResult, Self::Error>; - async fn update_transfer_meta_by_hash_and_log_index( + /// Updates the metadata for NFT transfers identified by their token address and ID. + /// Flags the transfers as `possible_spam` if `set_spam` is true. + async fn update_transfers_meta_by_token_addr_id( &self, chain: &Chain, - transfer: NftTransferHistory, + transfer_meta: TransferMeta, + set_spam: bool, ) -> MmResult<(), Self::Error>; - async fn update_transfers_meta_by_token_addr_id( + async fn get_transfers_with_empty_meta(&self, chain: Chain) -> MmResult, Self::Error>; + + /// `get_transfers_by_token_address` function returns list of NFT transfers which have specified token address. + async fn get_transfers_by_token_address( + &self, + chain: Chain, + token_address: String, + ) -> MmResult, Self::Error>; + + /// `update_transfer_spam_by_token_address` function updates `possible_spam` field in NFT transfers which have specified token address. + async fn update_transfer_spam_by_token_address( &self, chain: &Chain, - transfer_meta: TransferMeta, + token_address: String, + possible_spam: bool, ) -> MmResult<(), Self::Error>; - async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error>; + /// `get_token_addresses` return all unique token addresses. + async fn get_token_addresses(&self, chain: Chain) -> MmResult, Self::Error>; + + /// `get_domains` return all unique token domain fields. + async fn get_domains(&self, chain: &Chain) -> MmResult, Self::Error>; + + async fn update_transfer_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error>; } +/// Represents potential errors that can occur when creating an NFT storage. #[derive(Debug, Deserialize, Display, Serialize)] pub enum CreateNftStorageError { Internal(String), @@ -164,12 +219,13 @@ impl From for WithdrawError { } /// `NftStorageBuilder` is used to create an instance that implements the [`NftListStorageOps`] -/// and [`NftTransferHistoryStorageOps`] traits.Also has guard to lock write operations. +/// and [`NftTransferHistoryStorageOps`] traits. pub struct NftStorageBuilder<'a> { ctx: &'a MmArc, } impl<'a> NftStorageBuilder<'a> { + /// Creates a new `NftStorageBuilder` instance with the provided context. #[inline] pub fn new(ctx: &MmArc) -> NftStorageBuilder<'_> { NftStorageBuilder { ctx } } @@ -193,3 +249,27 @@ fn get_offset_limit(max: bool, limit: usize, page_number: Option, None => (0, limit), } } + +/// `NftDetailsJson` structure contains immutable parameters that are not needed for queries. +/// This is what `details_json` string contains in db table. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct NftDetailsJson { + pub(crate) owner_of: Address, + pub(crate) token_hash: Option, + pub(crate) minter_address: Option, + pub(crate) block_number_minted: Option, +} + +/// `TransferDetailsJson` structure contains immutable parameters that are not needed for queries. +/// This is what `details_json` string contains in db table. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct TransferDetailsJson { + pub(crate) block_hash: Option, + pub(crate) transaction_index: Option, + pub(crate) value: Option, + pub(crate) transaction_type: Option, + pub(crate) verified: Option, + pub(crate) operator: Option, + pub(crate) from_address: Address, + pub(crate) to_address: Address, +} diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs index 9179704467..4e76b93249 100644 --- a/mm2src/coins/nft/storage/sql_storage.rs +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -1,34 +1,51 @@ use crate::nft::eth_addr_to_hex; -use crate::nft::nft_structs::{Chain, ConvertChain, Nft, NftList, NftTokenAddrId, NftTransferHistory, - NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta}; -use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftStorageError, - NftTransferHistoryStorageOps, RemoveNftResult}; +use crate::nft::nft_structs::{Chain, ContractType, ConvertChain, Nft, NftCommon, NftList, NftListFilters, + NftTokenAddrId, NftTransferCommon, NftTransferHistory, NftTransferHistoryFilters, + NftsTransferHistoryList, TransferMeta, UriMeta}; +use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftDetailsJson, NftListStorageOps, NftStorageError, + NftTransferHistoryStorageOps, RemoveNftResult, TransferDetailsJson}; use async_trait::async_trait; use common::async_blocking; use db_common::sql_build::{SqlCondition, SqlQuery}; use db_common::sqlite::rusqlite::types::{FromSqlError, Type}; -use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, Statement}; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, Statement}; use db_common::sqlite::sql_builder::SqlBuilder; use db_common::sqlite::{query_single_row, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL}; +use ethereum_types::Address; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::map_to_mm::MapToMmResult; use mm2_err_handle::mm_error::{MmError, MmResult}; use mm2_number::BigDecimal; +use serde_json::Value as Json; use serde_json::{self as json}; +use std::collections::HashSet; use std::convert::TryInto; use std::num::NonZeroUsize; use std::str::FromStr; use std::sync::{Arc, Mutex}; -fn nft_list_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_list" } +impl Chain { + fn nft_list_table_name(&self) -> SqlResult { + let name = self.to_ticker() + "_nft_list"; + validate_table_name(&name)?; + Ok(name) + } -fn nft_transfer_history_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_transfer_history" } + fn transfer_history_table_name(&self) -> SqlResult { + let name = self.to_ticker() + "_nft_transfer_history"; + validate_table_name(&name)?; + Ok(name) + } +} -fn scanned_nft_blocks_table_name() -> String { "scanned_nft_blocks".to_string() } +fn scanned_nft_blocks_table_name() -> SqlResult { + let name = "scanned_nft_blocks".to_string(); + validate_table_name(&name)?; + Ok(name) +} fn create_nft_list_table_sql(chain: &Chain) -> MmResult { - let table_name = nft_list_table_name(chain); - validate_table_name(&table_name)?; + let table_name = chain.nft_list_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( token_address VARCHAR(256) NOT NULL, @@ -37,6 +54,26 @@ fn create_nft_list_table_sql(chain: &Chain) -> MmResult { amount VARCHAR(256) NOT NULL, block_number INTEGER NOT NULL, contract_type TEXT NOT NULL, + possible_spam INTEGER DEFAULT 0 NOT NULL, + possible_phishing INTEGER DEFAULT 0 NOT NULL, + collection_name TEXT, + symbol TEXT, + token_uri TEXT, + token_domain TEXT, + metadata TEXT, + last_token_uri_sync TEXT, + last_metadata_sync TEXT, + raw_image_url TEXT, + image_url TEXT, + image_domain TEXT, + token_name TEXT, + description TEXT, + attributes TEXT, + animation_url TEXT, + animation_domain TEXT, + external_url TEXT, + external_domain TEXT, + image_details TEXT, details_json TEXT, PRIMARY KEY (token_address, token_id) );", @@ -46,8 +83,7 @@ fn create_nft_list_table_sql(chain: &Chain) -> MmResult { } fn create_transfer_history_table_sql(chain: &Chain) -> MmResult { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(&table_name)?; + let table_name = chain.transfer_history_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( transaction_hash VARCHAR(256) NOT NULL, @@ -60,9 +96,13 @@ fn create_transfer_history_table_sql(chain: &Chain) -> MmResult MmResult MmResult { - let table_name = scanned_nft_blocks_table_name(); - validate_table_name(&table_name)?; + let table_name = scanned_nft_blocks_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( chain TEXT PRIMARY KEY, @@ -101,13 +140,15 @@ impl SqliteNftStorage { } } -fn get_nft_list_builder_preimage(chains: Vec) -> MmResult { +fn get_nft_list_builder_preimage( + chains: Vec, + filters: Option, +) -> MmResult { let union_sql_strings = chains .iter() .map(|chain| { - let table_name = nft_list_table_name(chain); - validate_table_name(&table_name)?; - let sql_builder = SqlBuilder::select_from(table_name.as_str()); + let table_name = chain.nft_list_table_name()?; + let sql_builder = nft_list_builder_preimage(table_name.as_str(), filters)?; let sql_string = sql_builder .sql() .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))? @@ -123,6 +164,20 @@ fn get_nft_list_builder_preimage(chains: Vec) -> MmResult) -> Result { + let mut sql_builder = SqlBuilder::select_from(table_name); + if let Some(filters) = filters { + if filters.exclude_spam { + sql_builder.and_where("possible_spam == 0"); + } + if filters.exclude_phishing { + sql_builder.and_where("possible_phishing == 0"); + } + } + drop_mutability!(sql_builder); + Ok(sql_builder) +} + fn get_nft_transfer_builder_preimage( chains: Vec, filters: Option, @@ -130,8 +185,7 @@ fn get_nft_transfer_builder_preimage( let union_sql_strings = chains .into_iter() .map(|chain| { - let table_name = nft_transfer_history_table_name(&chain); - validate_table_name(&table_name)?; + let table_name = chain.transfer_history_table_name()?; let sql_builder = nft_history_table_builder_preimage(table_name.as_str(), filters)?; let sql_string = sql_builder .sql() @@ -165,18 +219,20 @@ fn nft_history_table_builder_preimage( if let Some(date) = filters.to_date { sql_builder.and_where(format!("block_timestamp <= {}", date)); } + if filters.exclude_spam { + sql_builder.and_where("possible_spam == 0"); + } + if filters.exclude_phishing { + sql_builder.and_where("possible_phishing == 0"); + } } drop_mutability!(sql_builder); Ok(sql_builder) } -fn finalize_nft_list_sql_builder( - mut sql_builder: SqlBuilder, - offset: usize, - limit: usize, -) -> MmResult { +fn finalize_sql_builder(mut sql_builder: SqlBuilder, offset: usize, limit: usize) -> MmResult { let sql = sql_builder - .field("nft_list.details_json") + .field("*") .offset(offset) .limit(limit) .sql() @@ -184,28 +240,158 @@ fn finalize_nft_list_sql_builder( Ok(sql) } -fn finalize_nft_history_sql_builder( - mut sql_builder: SqlBuilder, - offset: usize, - limit: usize, -) -> MmResult { - let sql = sql_builder - .field("nft_history.details_json") - .offset(offset) - .limit(limit) - .sql() - .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; - Ok(sql) +fn get_and_parse(row: &Row<'_>, column: &str) -> Result { + let value_str: String = row.get(column)?; + value_str.parse().map_err(|_| SqlError::from(FromSqlError::InvalidType)) } fn nft_from_row(row: &Row<'_>) -> Result { - let json_string: String = row.get(0)?; - json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) + let token_address = get_and_parse(row, "token_address")?; + let token_id = get_and_parse(row, "token_id")?; + let chain = get_and_parse(row, "chain")?; + let amount = get_and_parse(row, "amount")?; + let block_number: u64 = row.get("block_number")?; + let contract_type = get_and_parse(row, "contract_type")?; + let possible_spam: i32 = row.get("possible_spam")?; + let possible_phishing: i32 = row.get("possible_phishing")?; + let collection_name: Option = row.get("collection_name")?; + let symbol: Option = row.get("symbol")?; + let token_uri: Option = row.get("token_uri")?; + let token_domain: Option = row.get("token_domain")?; + let metadata: Option = row.get("metadata")?; + let last_token_uri_sync: Option = row.get("last_token_uri_sync")?; + let last_metadata_sync: Option = row.get("last_metadata_sync")?; + let raw_image_url: Option = row.get("raw_image_url")?; + let image_url: Option = row.get("image_url")?; + let image_domain: Option = row.get("image_domain")?; + let token_name: Option = row.get("token_name")?; + let description: Option = row.get("description")?; + let attributes_str: Option = row.get("attributes")?; + let attributes: Option = attributes_str + .as_deref() + .map(json::from_str) + .transpose() + .map_err(|e| SqlError::FromSqlConversionFailure(21, Type::Text, Box::new(e)))?; + let animation_url: Option = row.get("animation_url")?; + let animation_domain: Option = row.get("animation_domain")?; + let external_url: Option = row.get("external_url")?; + let external_domain: Option = row.get("external_domain")?; + let image_details_str: Option = row.get("image_details")?; + let image_details: Option = image_details_str + .as_deref() + .map(json::from_str) + .transpose() + .map_err(|e| SqlError::FromSqlConversionFailure(26, Type::Text, Box::new(e)))?; + let details_json: String = row.get("details_json")?; + let nft_details: NftDetailsJson = + json::from_str(&details_json).map_err(|e| SqlError::FromSqlConversionFailure(27, Type::Text, Box::new(e)))?; + + let uri_meta = UriMeta { + raw_image_url, + image_url, + image_domain, + token_name, + description, + attributes, + animation_url, + animation_domain, + external_url, + external_domain, + image_details, + }; + + let common = NftCommon { + token_address, + token_id, + amount, + owner_of: nft_details.owner_of, + token_hash: nft_details.token_hash, + collection_name, + symbol, + token_uri, + token_domain, + metadata, + last_token_uri_sync, + last_metadata_sync, + minter_address: nft_details.minter_address, + possible_spam: possible_spam != 0, + }; + let nft = Nft { + common, + chain, + block_number_minted: nft_details.block_number_minted, + block_number, + contract_type, + possible_phishing: possible_phishing != 0, + uri_meta, + }; + Ok(nft) } fn transfer_history_from_row(row: &Row<'_>) -> Result { - let json_string: String = row.get(0)?; - json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) + let transaction_hash: String = row.get("transaction_hash")?; + let log_index: u32 = row.get("log_index")?; + let chain: Chain = get_and_parse(row, "chain")?; + let block_number: u64 = row.get("block_number")?; + let block_timestamp: u64 = row.get("block_timestamp")?; + let contract_type: ContractType = get_and_parse(row, "contract_type")?; + let token_address: Address = get_and_parse(row, "token_address")?; + let token_id: BigDecimal = get_and_parse(row, "token_id")?; + let status = get_and_parse(row, "status")?; + let amount: BigDecimal = get_and_parse(row, "amount")?; + let token_uri: Option = row.get("token_uri")?; + let token_domain: Option = row.get("token_domain")?; + let collection_name: Option = row.get("collection_name")?; + let image_url: Option = row.get("image_url")?; + let image_domain: Option = row.get("image_domain")?; + let token_name: Option = row.get("token_name")?; + let possible_spam: i32 = row.get("possible_spam")?; + let possible_phishing: i32 = row.get("possible_phishing")?; + let details_json: String = row.get("details_json")?; + let details: TransferDetailsJson = + json::from_str(&details_json).map_err(|e| SqlError::FromSqlConversionFailure(19, Type::Text, Box::new(e)))?; + + let common = NftTransferCommon { + block_hash: details.block_hash, + transaction_hash, + transaction_index: details.transaction_index, + log_index, + value: details.value, + transaction_type: details.transaction_type, + token_address, + token_id, + from_address: details.from_address, + to_address: details.to_address, + amount, + verified: details.verified, + operator: details.operator, + possible_spam: possible_spam != 0, + }; + + let transfer_history = NftTransferHistory { + common, + chain, + block_number, + block_timestamp, + contract_type, + token_uri, + token_domain, + collection_name, + image_url, + image_domain, + token_name, + status, + possible_phishing: possible_phishing != 0, + }; + + Ok(transfer_history) +} + +fn address_from_row(row: &Row<'_>) -> Result { + let address: String = row.get(0)?; + address + .parse() + .map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) } fn token_address_id_from_row(row: &Row<'_>) -> Result { @@ -219,14 +405,17 @@ fn token_address_id_from_row(row: &Row<'_>) -> Result } fn insert_nft_in_list_sql(chain: &Chain) -> MmResult { - let table_name = nft_list_table_name(chain); - validate_table_name(&table_name)?; - + let table_name = chain.nft_list_table_name()?; let sql = format!( "INSERT INTO {} ( - token_address, token_id, chain, amount, block_number, contract_type, details_json + token_address, token_id, chain, amount, block_number, contract_type, possible_spam, + possible_phishing, collection_name, symbol, token_uri, token_domain, metadata, + last_token_uri_sync, last_metadata_sync, raw_image_url, image_url, image_domain, + token_name, description, attributes, animation_url, animation_domain, external_url, + external_domain, image_details, details_json ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7 + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, + ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27 );", table_name ); @@ -234,15 +423,14 @@ fn insert_nft_in_list_sql(chain: &Chain) -> MmResult { } fn insert_transfer_in_history_sql(chain: &Chain) -> MmResult { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(&table_name)?; - + let table_name = chain.transfer_history_table_name()?; let sql = format!( "INSERT INTO {} ( transaction_hash, log_index, chain, block_number, block_timestamp, contract_type, - token_address, token_id, status, amount, collection_name, image_url, token_name, details_json + token_address, token_id, status, amount, token_uri, token_domain, collection_name, image_url, image_domain, + token_name, possible_spam, possible_phishing, details_json ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14 + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19 );", table_name ); @@ -250,8 +438,7 @@ fn insert_transfer_in_history_sql(chain: &Chain) -> MmResult { } fn upsert_last_scanned_block_sql() -> MmResult { - let table_name = scanned_nft_blocks_table_name(); - validate_table_name(&table_name)?; + let table_name = scanned_nft_blocks_table_name()?; let sql = format!( "INSERT OR REPLACE INTO {} (chain, last_scanned_block) VALUES (?1, ?2);", table_name @@ -259,75 +446,37 @@ fn upsert_last_scanned_block_sql() -> MmResult { Ok(sql) } -fn update_details_json_by_token_add_id_sql(chain: &Chain, table_name_creator: F) -> MmResult -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - - validate_table_name(&table_name)?; +fn refresh_nft_metadata_sql(chain: &Chain) -> MmResult { + let table_name = chain.nft_list_table_name()?; let sql = format!( - "UPDATE {} SET details_json = ?1 WHERE token_address = ?2 AND token_id = ?3;", + "UPDATE {} SET possible_spam = ?1, possible_phishing = ?2, collection_name = ?3, symbol = ?4, token_uri = ?5, token_domain = ?6, metadata = ?7, \ + last_token_uri_sync = ?8, last_metadata_sync = ?9, raw_image_url = ?10, image_url = ?11, image_domain = ?12, token_name = ?13, description = ?14, \ + attributes = ?15, animation_url = ?16, animation_domain = ?17, external_url = ?18, external_domain = ?19, image_details = ?20 WHERE token_address = ?21 AND token_id = ?22;", table_name ); Ok(sql) } -fn update_meta_by_tx_hash_and_log_index_sql(chain: &Chain) -> MmResult { - let table_name = nft_transfer_history_table_name(chain); - - validate_table_name(&table_name)?; +fn update_transfers_meta_by_token_addr_id_sql(chain: &Chain) -> MmResult { + let table_name = chain.transfer_history_table_name()?; let sql = format!( - "UPDATE {} SET token_uri = ?1, collection_name = ?2, image_url = ?3, token_name = ?4, details_json = ?5 WHERE transaction_hash = ?6 AND log_index = ?7;", + "UPDATE {} SET token_uri = ?1, token_domain = ?2, collection_name = ?3, image_url = ?4, image_domain = ?5, \ + token_name = ?6 WHERE token_address = ?7 AND token_id = ?8;", table_name ); Ok(sql) } -fn update_nft_amount_sql(chain: &Chain, table_name_creator: F) -> MmResult -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - - validate_table_name(&table_name)?; - let sql = format!( - "UPDATE {} SET amount = ?1, details_json = ?2 WHERE token_address = ?3 AND token_id = ?4;", - table_name - ); - Ok(sql) -} - -fn update_nft_amount_and_block_number_sql(chain: &Chain, table_name_creator: F) -> MmResult -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - - validate_table_name(&table_name)?; +fn update_transfer_spam_by_token_addr_id(chain: &Chain) -> MmResult { + let table_name = chain.transfer_history_table_name()?; let sql = format!( - "UPDATE {} SET amount = ?1, block_number = ?2, details_json = ?3 WHERE token_address = ?4 AND token_id = ?5;", + "UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2 AND token_id = ?3;", table_name ); Ok(sql) } -fn get_nft_metadata_sql(chain: &Chain) -> MmResult { - let table_name = nft_list_table_name(chain); - validate_table_name(&table_name)?; - let sql = format!( - "SELECT details_json FROM {} WHERE token_address=?1 AND token_id=?2", - table_name - ); - Ok(sql) -} - -fn select_last_block_number_sql(chain: &Chain, table_name_creator: F) -> MmResult -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - validate_table_name(&table_name)?; +fn select_last_block_number_sql(table_name: String) -> MmResult { let sql = format!( "SELECT block_number FROM {} ORDER BY block_number DESC LIMIT 1", table_name @@ -336,31 +485,12 @@ where } fn select_last_scanned_block_sql() -> MmResult { - let table_name = scanned_nft_blocks_table_name(); - validate_table_name(&table_name)?; + let table_name = scanned_nft_blocks_table_name()?; let sql = format!("SELECT last_scanned_block FROM {} WHERE chain=?1", table_name,); Ok(sql) } -fn get_nft_amount_sql(chain: &Chain, table_name_creator: F) -> MmResult -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - validate_table_name(&table_name)?; - let sql = format!( - "SELECT amount FROM {} WHERE token_address=?1 AND token_id=?2", - table_name - ); - Ok(sql) -} - -fn delete_nft_sql(chain: &Chain, table_name_creator: F) -> Result> -where - F: FnOnce(&Chain) -> String, -{ - let table_name = table_name_creator(chain); - validate_table_name(&table_name)?; +fn delete_nft_sql(table_name: String) -> Result> { let sql = format!("DELETE FROM {} WHERE token_address=?1 AND token_id=?2", table_name); Ok(sql) } @@ -369,43 +499,40 @@ fn block_number_from_row(row: &Row<'_>) -> Result { row.get::<_, fn nft_amount_from_row(row: &Row<'_>) -> Result { row.get(0) } -fn get_transfers_from_block_builder<'a>( - conn: &'a Connection, - chain: &'a Chain, - from_block: u64, -) -> MmResult, SqlError> { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(table_name.as_str())?; - let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; - sql_builder - .sql_builder() - .and_where(format!("block_number >= '{}'", from_block)) - .order_asc("block_number") - .field("details_json"); - drop_mutability!(sql_builder); - Ok(sql_builder) +fn get_nfts_by_token_address_statement(conn: &Connection, table_name: String) -> MmResult { + let sql_query = format!("SELECT * FROM {} WHERE token_address = ?", table_name); + let stmt = conn.prepare(&sql_query)?; + Ok(stmt) } -fn get_transfers_by_token_addr_id_statement<'a>( - conn: &'a Connection, - chain: &'a Chain, -) -> MmResult, SqlError> { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(table_name.as_str())?; +fn get_token_addresses_statement(conn: &Connection, table_name: String) -> MmResult { + let sql_query = format!("SELECT DISTINCT token_address FROM {}", table_name); + let stmt = conn.prepare(&sql_query)?; + Ok(stmt) +} + +fn get_transfers_from_block_statement<'a>(conn: &'a Connection, chain: &'a Chain) -> MmResult, SqlError> { + let table_name = chain.transfer_history_table_name()?; let sql_query = format!( - "SELECT details_json FROM {} WHERE token_address = ? AND token_id = ?", + "SELECT * FROM {} WHERE block_number >= ? ORDER BY block_number ASC", table_name ); let stmt = conn.prepare(&sql_query)?; Ok(stmt) } +fn get_transfers_by_token_addr_id_statement(conn: &Connection, chain: Chain) -> MmResult { + let table_name = chain.transfer_history_table_name()?; + let sql_query = format!("SELECT * FROM {} WHERE token_address = ? AND token_id = ?", table_name); + let stmt = conn.prepare(&sql_query)?; + Ok(stmt) +} + fn get_transfers_with_empty_meta_builder<'a>( conn: &'a Connection, chain: &'a Chain, ) -> MmResult, SqlError> { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(table_name.as_str())?; + let table_name = chain.transfer_history_table_name()?; let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; sql_builder .sql_builder() @@ -420,16 +547,6 @@ fn get_transfers_with_empty_meta_builder<'a>( Ok(sql_builder) } -fn get_transfer_by_tx_hash_and_log_index_sql(chain: &Chain) -> MmResult { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(&table_name)?; - let sql = format!( - "SELECT details_json FROM {} WHERE transaction_hash=?1 AND log_index = ?2", - table_name - ); - Ok(sql) -} - #[async_trait] impl NftListStorageOps for SqliteNftStorage { type Error = SqlError; @@ -447,8 +564,7 @@ impl NftListStorageOps for SqliteNftStorage { } async fn is_initialized(&self, chain: &Chain) -> MmResult { - let table_name = nft_list_table_name(chain); - validate_table_name(&table_name)?; + let table_name = chain.nft_list_table_name()?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -456,7 +572,7 @@ impl NftListStorageOps for SqliteNftStorage { let scanned_nft_blocks_initialized = query_single_row( &conn, CHECK_TABLE_EXISTS_SQL, - [scanned_nft_blocks_table_name()], + [scanned_nft_blocks_table_name()?], string_from_row, )?; Ok(nft_list_initialized.is_some() && scanned_nft_blocks_initialized.is_some()) @@ -470,11 +586,12 @@ impl NftListStorageOps for SqliteNftStorage { max: bool, limit: usize, page_number: Option, + filters: Option, ) -> MmResult { let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_nft_list_builder_preimage(chains)?; + let sql_builder = get_nft_list_builder_preimage(chains, filters)?; let total_count_builder_sql = sql_builder .clone() .count("*") @@ -486,7 +603,7 @@ impl NftListStorageOps for SqliteNftStorage { let count_total = total.try_into().expect("count should not be failed"); let (offset, limit) = get_offset_limit(max, limit, page_number, count_total); - let sql = finalize_nft_list_sql_builder(sql_builder, offset, limit)?; + let sql = finalize_sql_builder(sql_builder, offset, limit)?; let nfts = conn .prepare(&sql)? .query_map([], nft_from_row)? @@ -501,19 +618,24 @@ impl NftListStorageOps for SqliteNftStorage { .await } - async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + async fn add_nfts_to_list(&self, chain: Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, { let selfi = self.clone(); - let chain = *chain; async_blocking(move || { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; for nft in nfts { - let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let details_json = NftDetailsJson { + owner_of: nft.common.owner_of, + token_hash: nft.common.token_hash, + minter_address: nft.common.minter_address, + block_number_minted: nft.block_number_minted, + }; + let details_json = json::to_string(&details_json).expect("serialization should not fail"); let params = [ Some(eth_addr_to_hex(&nft.common.token_address)), Some(nft.common.token_id.to_string()), @@ -521,7 +643,27 @@ impl NftListStorageOps for SqliteNftStorage { Some(nft.common.amount.to_string()), Some(nft.block_number.to_string()), Some(nft.contract_type.to_string()), - Some(nft_json), + Some(i32::from(nft.common.possible_spam).to_string()), + Some(i32::from(nft.possible_phishing).to_string()), + nft.common.collection_name, + nft.common.symbol, + nft.common.token_uri, + nft.common.token_domain, + nft.common.metadata, + nft.common.last_token_uri_sync, + nft.common.last_metadata_sync, + nft.uri_meta.raw_image_url, + nft.uri_meta.image_url, + nft.uri_meta.image_domain, + nft.uri_meta.token_name, + nft.uri_meta.description, + nft.uri_meta.attributes.map(|v| v.to_string()), + nft.uri_meta.animation_url, + nft.uri_meta.animation_domain, + nft.uri_meta.external_url, + nft.uri_meta.external_domain, + nft.uri_meta.image_details.map(|v| v.to_string()), + Some(details_json), ]; sql_transaction.execute(&insert_nft_in_list_sql(&chain)?, params)?; } @@ -539,7 +681,8 @@ impl NftListStorageOps for SqliteNftStorage { token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error> { - let sql = get_nft_metadata_sql(chain)?; + let table_name = chain.nft_list_table_name()?; + let sql = format!("SELECT * FROM {} WHERE token_address=?1 AND token_id=?2", table_name); let params = [token_address, token_id.to_string()]; let selfi = self.clone(); async_blocking(move || { @@ -556,7 +699,8 @@ impl NftListStorageOps for SqliteNftStorage { token_id: BigDecimal, scanned_block: u64, ) -> MmResult { - let sql = delete_nft_sql(chain, nft_list_table_name)?; + let table_name = chain.nft_list_table_name()?; + let sql = delete_nft_sql(table_name)?; let params = [token_address, token_id.to_string()]; let scanned_block_params = [chain.to_ticker(), scanned_block.to_string()]; let selfi = self.clone(); @@ -583,7 +727,11 @@ impl NftListStorageOps for SqliteNftStorage { token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error> { - let sql = get_nft_amount_sql(chain, nft_list_table_name)?; + let table_name = chain.nft_list_table_name()?; + let sql = format!( + "SELECT amount FROM {} WHERE token_address=?1 AND token_id=?2", + table_name + ); let params = [token_address, token_id.to_string()]; let selfi = self.clone(); async_blocking(move || { @@ -594,16 +742,34 @@ impl NftListStorageOps for SqliteNftStorage { } async fn refresh_nft_metadata(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { - let sql = update_details_json_by_token_add_id_sql(chain, nft_list_table_name)?; - let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let sql = refresh_nft_metadata_sql(chain)?; let selfi = self.clone(); async_blocking(move || { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; let params = [ - nft_json, - eth_addr_to_hex(&nft.common.token_address), - nft.common.token_id.to_string(), + Some(i32::from(nft.common.possible_spam).to_string()), + Some(i32::from(nft.possible_phishing).to_string()), + nft.common.collection_name, + nft.common.symbol, + nft.common.token_uri, + nft.common.token_domain, + nft.common.metadata, + nft.common.last_token_uri_sync, + nft.common.last_metadata_sync, + nft.uri_meta.raw_image_url, + nft.uri_meta.image_url, + nft.uri_meta.image_domain, + nft.uri_meta.token_name, + nft.uri_meta.description, + nft.uri_meta.attributes.map(|v| v.to_string()), + nft.uri_meta.animation_url, + nft.uri_meta.animation_domain, + nft.uri_meta.external_url, + nft.uri_meta.external_domain, + nft.uri_meta.image_details.map(|v| v.to_string()), + Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_id.to_string()), ]; sql_transaction.execute(&sql, params)?; sql_transaction.commit()?; @@ -613,7 +779,8 @@ impl NftListStorageOps for SqliteNftStorage { } async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { - let sql = select_last_block_number_sql(chain, nft_list_table_name)?; + let table_name = chain.nft_list_table_name()?; + let sql = select_last_block_number_sql(table_name)?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -640,8 +807,11 @@ impl NftListStorageOps for SqliteNftStorage { } async fn update_nft_amount(&self, chain: &Chain, nft: Nft, scanned_block: u64) -> MmResult<(), Self::Error> { - let sql = update_nft_amount_sql(chain, nft_list_table_name)?; - let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let table_name = chain.nft_list_table_name()?; + let sql = format!( + "UPDATE {} SET amount = ?1 WHERE token_address = ?2 AND token_id = ?3;", + table_name + ); let scanned_block_params = [chain.to_ticker(), scanned_block.to_string()]; let selfi = self.clone(); async_blocking(move || { @@ -649,7 +819,6 @@ impl NftListStorageOps for SqliteNftStorage { let sql_transaction = conn.transaction()?; let params = [ Some(nft.common.amount.to_string()), - Some(nft_json), Some(eth_addr_to_hex(&nft.common.token_address)), Some(nft.common.token_id.to_string()), ]; @@ -662,8 +831,11 @@ impl NftListStorageOps for SqliteNftStorage { } async fn update_nft_amount_and_block_number(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { - let sql = update_nft_amount_and_block_number_sql(chain, nft_list_table_name)?; - let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let table_name = chain.nft_list_table_name()?; + let sql = format!( + "UPDATE {} SET amount = ?1, block_number = ?2 WHERE token_address = ?3 AND token_id = ?4;", + table_name + ); let scanned_block_params = [chain.to_ticker(), nft.block_number.to_string()]; let selfi = self.clone(); async_blocking(move || { @@ -672,7 +844,6 @@ impl NftListStorageOps for SqliteNftStorage { let params = [ Some(nft.common.amount.to_string()), Some(nft.block_number.to_string()), - Some(nft_json), Some(eth_addr_to_hex(&nft.common.token_address)), Some(nft.common.token_id.to_string()), ]; @@ -683,6 +854,87 @@ impl NftListStorageOps for SqliteNftStorage { }) .await } + + async fn get_nfts_by_token_address(&self, chain: Chain, token_address: String) -> MmResult, Self::Error> { + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let table_name = chain.nft_list_table_name()?; + let mut stmt = get_nfts_by_token_address_statement(&conn, table_name)?; + let nfts = stmt + .query_map([token_address], nft_from_row)? + .collect::, _>>()?; + Ok(nfts) + }) + .await + } + + async fn update_nft_spam_by_token_address( + &self, + chain: &Chain, + token_address: String, + possible_spam: bool, + ) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + let table_name = chain.nft_list_table_name()?; + let sql = format!("UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", table_name); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [Some(i32::from(possible_spam).to_string()), Some(token_address.clone())]; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_animation_external_domains(&self, chain: &Chain) -> MmResult, Self::Error> { + let selfi = self.clone(); + let table_name = chain.nft_list_table_name()?; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_query = format!( + "SELECT DISTINCT animation_domain FROM {} + UNION + SELECT DISTINCT external_domain FROM {}", + table_name, table_name + ); + let mut stmt = conn.prepare(&sql_query)?; + let domains = stmt + .query_map([], |row| row.get::<_, Option>(0))? + .collect::, _>>()?; + let domains = domains.into_iter().flatten().collect(); + Ok(domains) + }) + .await + } + + async fn update_nft_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + + let table_name = chain.nft_list_table_name()?; + let sql = format!( + "UPDATE {} SET possible_phishing = ?1 WHERE token_domain = ?2 + OR image_domain = ?2 OR animation_domain = ?2 OR external_domain = ?2;", + table_name + ); + + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [Some(i32::from(possible_phishing).to_string()), Some(domain)]; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } } #[async_trait] @@ -701,8 +953,7 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { } async fn is_initialized(&self, chain: &Chain) -> MmResult { - let table_name = nft_transfer_history_table_name(chain); - validate_table_name(&table_name)?; + let table_name = chain.transfer_history_table_name()?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -735,7 +986,7 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { let count_total = total.try_into().expect("count should not be failed"); let (offset, limit) = get_offset_limit(max, limit, page_number, count_total); - let sql = finalize_nft_history_sql_builder(sql_builder, offset, limit)?; + let sql = finalize_sql_builder(sql_builder, offset, limit)?; let transfers = conn .prepare(&sql)? .query_map([], transfer_history_from_row)? @@ -750,19 +1001,28 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { .await } - async fn add_transfers_to_history(&self, chain: &Chain, transfers: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, chain: Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, { let selfi = self.clone(); - let chain = *chain; async_blocking(move || { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; for transfer in transfers { - let transfer_json = json::to_string(&transfer).expect("serialization should not fail"); + let details_json = TransferDetailsJson { + block_hash: transfer.common.block_hash, + transaction_index: transfer.common.transaction_index, + value: transfer.common.value, + transaction_type: transfer.common.transaction_type, + verified: transfer.common.verified, + operator: transfer.common.operator, + from_address: transfer.common.from_address, + to_address: transfer.common.from_address, + }; + let transfer_json = json::to_string(&details_json).expect("serialization should not fail"); let params = [ Some(transfer.common.transaction_hash), Some(transfer.common.log_index.to_string()), @@ -774,9 +1034,14 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { Some(transfer.common.token_id.to_string()), Some(transfer.status.to_string()), Some(transfer.common.amount.to_string()), + transfer.token_uri, + transfer.token_domain, transfer.collection_name, transfer.image_url, + transfer.image_domain, transfer.token_name, + Some(i32::from(transfer.common.possible_spam).to_string()), + Some(i32::from(transfer.possible_phishing).to_string()), Some(transfer_json), ]; sql_transaction.execute(&insert_transfer_in_history_sql(&chain)?, params)?; @@ -788,7 +1053,8 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { } async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { - let sql = select_last_block_number_sql(chain, nft_transfer_history_table_name)?; + let table_name = chain.transfer_history_table_name()?; + let sql = select_last_block_number_sql(table_name)?; let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -802,15 +1068,16 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { async fn get_transfers_from_block( &self, - chain: &Chain, + chain: Chain, from_block: u64, ) -> MmResult, Self::Error> { let selfi = self.clone(); - let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_transfers_from_block_builder(&conn, &chain, from_block)?; - let transfers = sql_builder.query(transfer_history_from_row)?; + let mut stmt = get_transfers_from_block_statement(&conn, &chain)?; + let transfers = stmt + .query_map([from_block], transfer_history_from_row)? + .collect::, _>>()?; Ok(transfers) }) .await @@ -818,15 +1085,14 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { async fn get_transfers_by_token_addr_id( &self, - chain: &Chain, + chain: Chain, token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error> { let selfi = self.clone(); - let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let mut stmt = get_transfers_by_token_addr_id_statement(&conn, &chain)?; + let mut stmt = get_transfers_by_token_addr_id_statement(&conn, chain)?; let transfers = stmt .query_map([token_address, token_id.to_string()], transfer_history_from_row)? .collect::, _>>()?; @@ -841,7 +1107,11 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { transaction_hash: String, log_index: u32, ) -> MmResult, Self::Error> { - let sql = get_transfer_by_tx_hash_and_log_index_sql(chain)?; + let table_name = chain.transfer_history_table_name()?; + let sql = format!( + "SELECT * FROM {} WHERE transaction_hash=?1 AND log_index = ?2", + table_name + ); let selfi = self.clone(); async_blocking(move || { let conn = selfi.0.lock().unwrap(); @@ -856,63 +1126,150 @@ impl NftTransferHistoryStorageOps for SqliteNftStorage { .await } - async fn update_transfer_meta_by_hash_and_log_index( + async fn update_transfers_meta_by_token_addr_id( &self, chain: &Chain, - transfer: NftTransferHistory, + transfer_meta: TransferMeta, + set_spam: bool, ) -> MmResult<(), Self::Error> { - let sql = update_meta_by_tx_hash_and_log_index_sql(chain)?; - let transfer_json = json::to_string(&transfer).expect("serialization should not fail"); + let sql = update_transfers_meta_by_token_addr_id_sql(chain)?; let params = [ - transfer.token_uri, - transfer.collection_name, - transfer.image_url, - transfer.token_name, - Some(transfer_json), - Some(transfer.common.transaction_hash), - Some(transfer.common.log_index.to_string()), + transfer_meta.token_uri, + transfer_meta.token_domain, + transfer_meta.collection_name, + transfer_meta.image_url, + transfer_meta.image_domain, + transfer_meta.token_name, + Some(transfer_meta.token_address.clone()), + Some(transfer_meta.token_id.to_string()), + ]; + let sql_spam = update_transfer_spam_by_token_addr_id(chain)?; + let params_spam = [ + Some(i32::from(true).to_string()), + Some(transfer_meta.token_address), + Some(transfer_meta.token_id.to_string()), ]; let selfi = self.clone(); async_blocking(move || { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; sql_transaction.execute(&sql, params)?; + if set_spam { + sql_transaction.execute(&sql_spam, params_spam)?; + } sql_transaction.commit()?; Ok(()) }) .await } - async fn update_transfers_meta_by_token_addr_id( + async fn get_transfers_with_empty_meta(&self, chain: Chain) -> MmResult, Self::Error> { + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_builder = get_transfers_with_empty_meta_builder(&conn, &chain)?; + let token_addr_id_pair = sql_builder.query(token_address_id_from_row)?; + Ok(token_addr_id_pair) + }) + .await + } + + async fn get_transfers_by_token_address( + &self, + chain: Chain, + token_address: String, + ) -> MmResult, Self::Error> { + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let table_name = chain.transfer_history_table_name()?; + let mut stmt = get_nfts_by_token_address_statement(&conn, table_name)?; + let nfts = stmt + .query_map([token_address], transfer_history_from_row)? + .collect::, _>>()?; + Ok(nfts) + }) + .await + } + + async fn update_transfer_spam_by_token_address( &self, chain: &Chain, - transfer_meta: TransferMeta, + token_address: String, + possible_spam: bool, ) -> MmResult<(), Self::Error> { let selfi = self.clone(); - let transfers = selfi - .get_transfers_by_token_addr_id(chain, transfer_meta.token_address, transfer_meta.token_id) - .await?; - for mut transfer in transfers.into_iter() { - transfer.token_uri = transfer_meta.token_uri.clone(); - transfer.collection_name = transfer_meta.collection_name.clone(); - transfer.image_url = transfer_meta.image_url.clone(); - transfer.token_name = transfer_meta.token_name.clone(); - drop_mutability!(transfer); - selfi - .update_transfer_meta_by_hash_and_log_index(chain, transfer) - .await?; - } - Ok(()) + + let table_name = chain.transfer_history_table_name()?; + let sql = format!("UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", table_name); + + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [Some(i32::from(possible_spam).to_string()), Some(token_address.clone())]; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await } - async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + async fn get_token_addresses(&self, chain: Chain) -> MmResult, Self::Error> { let selfi = self.clone(); - let chain = *chain; async_blocking(move || { let conn = selfi.0.lock().unwrap(); - let sql_builder = get_transfers_with_empty_meta_builder(&conn, &chain)?; - let token_addr_id_pair = sql_builder.query(token_address_id_from_row)?; - Ok(token_addr_id_pair) + let table_name = chain.transfer_history_table_name()?; + let mut stmt = get_token_addresses_statement(&conn, table_name)?; + let addresses = stmt + .query_map([], address_from_row)? + .collect::, _>>()?; + Ok(addresses) + }) + .await + } + + async fn get_domains(&self, chain: &Chain) -> MmResult, Self::Error> { + let selfi = self.clone(); + let table_name = chain.transfer_history_table_name()?; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_query = format!( + "SELECT DISTINCT token_domain FROM {} + UNION + SELECT DISTINCT image_domain FROM {}", + table_name, table_name + ); + let mut stmt = conn.prepare(&sql_query)?; + let domains = stmt + .query_map([], |row| row.get::<_, Option>(0))? + .collect::, _>>()?; + let domains = domains.into_iter().flatten().collect(); + Ok(domains) + }) + .await + } + + async fn update_transfer_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + + let table_name = chain.transfer_history_table_name()?; + let sql = format!( + "UPDATE {} SET possible_phishing = ?1 WHERE token_domain = ?2 OR image_domain = ?2;", + table_name + ); + + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [Some(i32::from(possible_phishing).to_string()), Some(domain)]; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) }) .await } diff --git a/mm2src/coins/nft/storage/wasm/nft_idb.rs b/mm2src/coins/nft/storage/wasm/nft_idb.rs index 0d7758d61a..054f1c058e 100644 --- a/mm2src/coins/nft/storage/wasm/nft_idb.rs +++ b/mm2src/coins/nft/storage/wasm/nft_idb.rs @@ -3,17 +3,27 @@ use async_trait::async_trait; use mm2_db::indexed_db::InitDbResult; use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder}; -const DB_NAME: &str = "nft_cache"; const DB_VERSION: u32 = 1; + +/// Represents a locked instance of the `NftCacheIDB` database. +/// +/// This type ensures that while the database is being accessed or modified, +/// no other operations can interfere, maintaining data integrity. pub type NftCacheIDBLocked<'a> = DbLocked<'a, NftCacheIDB>; +/// Represents the IndexedDB instance specifically designed for caching NFT data. +/// +/// This struct provides an abstraction over the raw IndexedDB, offering methods +/// to interact with the database and ensuring that the database is initialized with the +/// required tables and configurations. pub struct NftCacheIDB { + /// The underlying raw IndexedDb instance. inner: IndexedDb, } #[async_trait] impl DbInstance for NftCacheIDB { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "nft_cache"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) @@ -28,5 +38,8 @@ impl DbInstance for NftCacheIDB { } impl NftCacheIDB { + /// Get a reference to the underlying `IndexedDb` instance. + /// + /// This method allows for direct interaction with the raw database, bypassing any abstractions. pub(crate) fn get_inner(&self) -> &IndexedDb { &self.inner } } diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index 058b6cdffd..789ec069da 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -1,12 +1,13 @@ use crate::eth::eth_addr_to_hex; -use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCtx, NftList, NftTransferHistory, NftsTransferHistoryList, - TransferMeta, TransferStatus}; +use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCtx, NftList, NftListFilters, NftTransferHistory, + NftsTransferHistoryList, TransferMeta, TransferStatus}; use crate::nft::storage::wasm::nft_idb::{NftCacheIDB, NftCacheIDBLocked}; use crate::nft::storage::wasm::{WasmNftCacheError, WasmNftCacheResult}; use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftTokenAddrId, NftTransferHistoryFilters, NftTransferHistoryStorageOps, RemoveNftResult}; use async_trait::async_trait; use common::is_initial_upgrade; +use ethereum_types::Address; use mm2_core::mm_ctx::MmArc; use mm2_db::indexed_db::{BeBigUint, DbTable, DbUpgrader, MultiIndex, OnUpgradeResult, SharedDb, TableSignature}; use mm2_err_handle::map_mm_error::MapMmError; @@ -19,12 +20,27 @@ use std::collections::HashSet; use std::num::NonZeroUsize; use std::str::FromStr; +const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; +const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; +const CHAIN_TOKEN_ADD_INDEX: &str = "chain_token_add_index"; +const CHAIN_TOKEN_DOMAIN_INDEX: &str = "chain_token_domain_index"; +const CHAIN_IMAGE_DOMAIN_INDEX: &str = "chain_image_domain_index"; + +/// Provides methods for interacting with the IndexedDB storage specifically designed for NFT data. +/// +/// This struct abstracts the intricacies of fetching and storing NFT data in the IndexedDB, +/// ensuring optimal performance and data integrity. #[derive(Clone)] pub struct IndexedDbNftStorage { + /// The underlying shared database instance for caching NFT data. db: SharedDb, } impl IndexedDbNftStorage { + /// Construct a new `IndexedDbNftStorage` using the given MM context. + /// + /// This method ensures that a proper NFT context (`NftCtx`) exists within the MM context + /// and initializes the underlying storage as required. pub fn new(ctx: &MmArc) -> MmResult { let nft_ctx = NftCtx::from_ctx(ctx).map_to_mm(CreateNftStorageError::Internal)?; Ok(IndexedDbNftStorage { @@ -32,6 +48,7 @@ impl IndexedDbNftStorage { }) } + /// Lock the underlying database to ensure exclusive access, maintaining data consistency during operations. async fn lock_db(&self) -> WasmNftCacheResult> { self.db.get_or_initialize().await.mm_err(WasmNftCacheError::from) } @@ -52,6 +69,24 @@ impl IndexedDbNftStorage { }) } + fn filter_nfts(nfts: I, filters: Option) -> WasmNftCacheResult> + where + I: Iterator, + { + let mut filtered_nfts = Vec::new(); + for nft_table in nfts { + let nft = nft_details_from_item(nft_table)?; + if let Some(filters) = &filters { + if filters.passes_spam_filter(&nft) && filters.passes_phishing_filter(&nft) { + filtered_nfts.push(nft); + } + } else { + filtered_nfts.push(nft); + } + } + Ok(filtered_nfts) + } + fn take_transfers_according_to_paging_opts( mut transfers: Vec, max: bool, @@ -68,7 +103,7 @@ impl IndexedDbNftStorage { }) } - fn take_transfers_according_to_filters( + fn filter_transfers( transfers: I, filters: Option, ) -> WasmNftCacheResult> @@ -79,7 +114,11 @@ impl IndexedDbNftStorage { for transfers_table in transfers { let transfer = transfer_details_from_item(transfers_table)?; if let Some(filters) = &filters { - if filters.is_status_match(&transfer) && filters.is_date_match(&transfer) { + if filters.is_status_match(&transfer) + && filters.is_date_match(&transfer) + && filters.passes_spam_filter(&transfer) + && filters.passes_phishing_filter(&transfer) + { filtered_transfers.push(transfer); } } else { @@ -90,6 +129,12 @@ impl IndexedDbNftStorage { } } +impl NftListFilters { + fn passes_spam_filter(&self, nft: &Nft) -> bool { !self.exclude_spam || !nft.common.possible_spam } + + fn passes_phishing_filter(&self, nft: &Nft) -> bool { !self.exclude_phishing || !nft.possible_phishing } +} + impl NftTransferHistoryFilters { fn is_status_match(&self, transfer: &NftTransferHistory) -> bool { (!self.receive && !self.send) @@ -101,6 +146,14 @@ impl NftTransferHistoryFilters { self.from_date.map_or(true, |from| transfer.block_timestamp >= from) && self.to_date.map_or(true, |to| transfer.block_timestamp <= to) } + + fn passes_spam_filter(&self, transfer: &NftTransferHistory) -> bool { + !self.exclude_spam || !transfer.common.possible_spam + } + + fn passes_phishing_filter(&self, transfer: &NftTransferHistory) -> bool { + !self.exclude_phishing || !transfer.possible_phishing + } } #[async_trait] @@ -117,22 +170,25 @@ impl NftListStorageOps for IndexedDbNftStorage { max: bool, limit: usize, page_number: Option, + filters: Option, ) -> MmResult { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; let mut nfts = Vec::new(); for chain in chains { - let items = table.get_items("chain", chain.to_string()).await?; - for (_item_id, item) in items.into_iter() { - let nft_detail = nft_details_from_item(item)?; - nfts.push(nft_detail); - } + let nft_tables = table + .get_items("chain", chain.to_string()) + .await? + .into_iter() + .map(|(_item_id, nft)| nft); + let filtered = Self::filter_nfts(nft_tables, filters)?; + nfts.extend(filtered); } Self::take_nft_according_to_paging_opts(nfts, max, limit, page_number) } - async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + async fn add_nfts_to_list(&self, chain: Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, @@ -164,7 +220,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&token_address)? .with_value(token_id.to_string())?; @@ -188,7 +244,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let nft_table = db_transaction.table::().await?; let last_scanned_block_table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&token_address)? .with_value(token_id.to_string())?; @@ -218,7 +274,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&token_address)? .with_value(token_id.to_string())?; @@ -234,7 +290,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(eth_addr_to_hex(&nft.common.token_address))? .with_value(nft.common.token_id.to_string())?; @@ -248,7 +304,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - get_last_block_from_table(chain, table, NftListTable::CHAIN_BLOCK_NUMBER_INDEX).await + get_last_block_from_table(chain, table, CHAIN_BLOCK_NUMBER_INDEX).await } async fn get_last_scanned_block(&self, chain: &Chain) -> MmResult, Self::Error> { @@ -272,7 +328,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let nft_table = db_transaction.table::().await?; let last_scanned_block_table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(eth_addr_to_hex(&nft.common.token_address))? .with_value(nft.common.token_id.to_string())?; @@ -297,7 +353,7 @@ impl NftListStorageOps for IndexedDbNftStorage { let nft_table = db_transaction.table::().await?; let last_scanned_block_table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(eth_addr_to_hex(&nft.common.token_address))? .with_value(nft.common.token_id.to_string())?; @@ -315,6 +371,99 @@ impl NftListStorageOps for IndexedDbNftStorage { .await?; Ok(()) } + + async fn get_nfts_by_token_address(&self, chain: Chain, token_address: String) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)?; + + table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| nft_details_from_item(item)) + .collect() + } + + async fn update_nft_spam_by_token_address( + &self, + chain: &Chain, + token_address: String, + possible_spam: bool, + ) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let chain_str = chain.to_string(); + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_INDEX) + .with_value(&chain_str)? + .with_value(&token_address)?; + + let nfts: Result, _> = table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| nft_details_from_item(item)) + .collect(); + let nfts = nfts?; + + for mut nft in nfts { + nft.common.possible_spam = possible_spam; + drop_mutability!(nft); + + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(&chain_str)? + .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_id.to_string())?; + + let item = NftListTable::from_nft(&nft)?; + table.replace_item_by_unique_multi_index(index_keys, &item).await?; + } + Ok(()) + } + + async fn get_animation_external_domains(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let mut domains = HashSet::new(); + let nft_tables = table.get_items("chain", chain.to_string()).await?; + for (_item_id, nft) in nft_tables.into_iter() { + if let Some(domain) = nft.animation_domain { + domains.insert(domain); + } + if let Some(domain) = nft.external_domain { + domains.insert(domain); + } + } + Ok(domains) + } + + async fn update_nft_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let chain_str = chain.to_string(); + update_nft_phishing_for_index(&table, &chain_str, CHAIN_TOKEN_DOMAIN_INDEX, &domain, possible_phishing).await?; + update_nft_phishing_for_index(&table, &chain_str, CHAIN_IMAGE_DOMAIN_INDEX, &domain, possible_phishing).await?; + let animation_index = NftListTable::CHAIN_ANIMATION_DOMAIN_INDEX; + update_nft_phishing_for_index(&table, &chain_str, animation_index, &domain, possible_phishing).await?; + let external_index = NftListTable::CHAIN_EXTERNAL_DOMAIN_INDEX; + update_nft_phishing_for_index(&table, &chain_str, external_index, &domain, possible_phishing).await?; + Ok(()) + } } #[async_trait] @@ -343,13 +492,13 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { .await? .into_iter() .map(|(_item_id, transfer)| transfer); - let filtered = Self::take_transfers_according_to_filters(transfer_tables, filters)?; + let filtered = Self::filter_transfers(transfer_tables, filters)?; transfers.extend(filtered); } Self::take_transfers_according_to_paging_opts(transfers, max, limit, page_number) } - async fn add_transfers_to_history(&self, _chain: &Chain, transfers: I) -> MmResult<(), Self::Error> + async fn add_transfers_to_history(&self, _chain: Chain, transfers: I) -> MmResult<(), Self::Error> where I: IntoIterator + Send + 'static, I::IntoIter: Send, @@ -368,12 +517,12 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - get_last_block_from_table(chain, table, NftTransferHistoryTable::CHAIN_BLOCK_NUMBER_INDEX).await + get_last_block_from_table(chain, table, CHAIN_BLOCK_NUMBER_INDEX).await } async fn get_transfers_from_block( &self, - chain: &Chain, + chain: Chain, from_block: u64, ) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; @@ -384,7 +533,7 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { .only("chain", chain.to_string()) .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? .bound("block_number", BeBigUint::from(from_block), BeBigUint::from(u64::MAX)) - .open_cursor(NftTransferHistoryTable::CHAIN_BLOCK_NUMBER_INDEX) + .open_cursor(CHAIN_BLOCK_NUMBER_INDEX) .await .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? .collect() @@ -401,7 +550,7 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { async fn get_transfers_by_token_addr_id( &self, - chain: &Chain, + chain: Chain, token_address: String, token_id: BigDecimal, ) -> MmResult, Self::Error> { @@ -409,7 +558,7 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&token_address)? .with_value(token_id.to_string())?; @@ -443,45 +592,44 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { } } - async fn update_transfer_meta_by_hash_and_log_index( + async fn update_transfers_meta_by_token_addr_id( &self, chain: &Chain, - transfer: NftTransferHistory, + transfer_meta: TransferMeta, + set_spam: bool, ) -> MmResult<(), Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) - .with_value(chain.to_string())? - .with_value(&transfer.common.transaction_hash)? - .with_value(transfer.common.log_index)?; + let chain_str = chain.to_string(); + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(&chain_str)? + .with_value(&transfer_meta.token_address)? + .with_value(transfer_meta.token_id.to_string())?; - let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; - table.replace_item_by_unique_multi_index(index_keys, &item).await?; - Ok(()) - } + let transfers: Result, _> = table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| transfer_details_from_item(item)) + .collect(); + let transfers = transfers?; - async fn update_transfers_meta_by_token_addr_id( - &self, - chain: &Chain, - transfer_meta: TransferMeta, - ) -> MmResult<(), Self::Error> { - let transfers: Vec = self - .get_transfers_by_token_addr_id(chain, transfer_meta.token_address, transfer_meta.token_id) - .await?; - let locked_db = self.lock_db().await?; - let db_transaction = locked_db.get_inner().transaction().await?; - let table = db_transaction.table::().await?; for mut transfer in transfers { transfer.token_uri = transfer_meta.token_uri.clone(); + transfer.token_domain = transfer_meta.token_domain.clone(); transfer.collection_name = transfer_meta.collection_name.clone(); transfer.image_url = transfer_meta.image_url.clone(); + transfer.image_domain = transfer_meta.image_domain.clone(); transfer.token_name = transfer_meta.token_name.clone(); + if set_spam { + transfer.common.possible_spam = true; + } drop_mutability!(transfer); let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) - .with_value(chain.to_string())? + .with_value(&chain_str)? .with_value(&transfer.common.transaction_hash)? .with_value(transfer.common.log_index)?; @@ -491,7 +639,7 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { Ok(()) } - async fn get_transfers_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + async fn get_transfers_with_empty_meta(&self, chain: Chain) -> MmResult, Self::Error> { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let table = db_transaction.table::().await?; @@ -521,6 +669,163 @@ impl NftTransferHistoryStorageOps for IndexedDbNftStorage { } Ok(res.into_iter().collect()) } + + async fn get_transfers_by_token_address( + &self, + chain: Chain, + token_address: String, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)?; + + table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| transfer_details_from_item(item)) + .collect() + } + + async fn update_transfer_spam_by_token_address( + &self, + chain: &Chain, + token_address: String, + possible_spam: bool, + ) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let chain_str = chain.to_string(); + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_INDEX) + .with_value(&chain_str)? + .with_value(&token_address)?; + + let transfers: Result, _> = table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| transfer_details_from_item(item)) + .collect(); + let transfers = transfers?; + + for mut transfer in transfers { + transfer.common.possible_spam = possible_spam; + drop_mutability!(transfer); + + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + .with_value(&chain_str)? + .with_value(&transfer.common.transaction_hash)? + .with_value(transfer.common.log_index)?; + + let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; + table.replace_item_by_unique_multi_index(index_keys, &item).await?; + } + Ok(()) + } + + async fn get_token_addresses(&self, chain: Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let items = table.get_items("chain", chain.to_string()).await?; + let mut token_addresses = HashSet::new(); + for (_item_id, item) in items.into_iter() { + let transfer = transfer_details_from_item(item)?; + token_addresses.insert(transfer.common.token_address); + } + Ok(token_addresses) + } + + async fn get_domains(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let mut domains = HashSet::new(); + let transfer_tables = table.get_items("chain", chain.to_string()).await?; + for (_item_id, transfer) in transfer_tables.into_iter() { + if let Some(domain) = transfer.token_domain { + domains.insert(domain); + } + if let Some(domain) = transfer.image_domain { + domains.insert(domain); + } + } + Ok(domains) + } + + async fn update_transfer_phishing_by_domain( + &self, + chain: &Chain, + domain: String, + possible_phishing: bool, + ) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let chain_str = chain.to_string(); + update_transfer_phishing_for_index(&table, &chain_str, CHAIN_TOKEN_DOMAIN_INDEX, &domain, possible_phishing) + .await?; + update_transfer_phishing_for_index(&table, &chain_str, CHAIN_IMAGE_DOMAIN_INDEX, &domain, possible_phishing) + .await?; + Ok(()) + } +} + +async fn update_transfer_phishing_for_index( + table: &DbTable<'_, NftTransferHistoryTable>, + chain: &str, + index: &str, + domain: &str, + possible_phishing: bool, +) -> MmResult<(), WasmNftCacheError> { + let index_keys = MultiIndex::new(index).with_value(chain)?.with_value(domain)?; + let transfers_table = table.get_items_by_multi_index(index_keys).await?; + for (_item_id, item) in transfers_table.into_iter() { + let mut transfer = transfer_details_from_item(item)?; + transfer.possible_phishing = possible_phishing; + drop_mutability!(transfer); + let transfer_item = NftTransferHistoryTable::from_transfer_history(&transfer)?; + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + .with_value(chain)? + .with_value(&transfer.common.transaction_hash)? + .with_value(transfer.common.log_index)?; + table + .replace_item_by_unique_multi_index(index_keys, &transfer_item) + .await?; + } + Ok(()) +} + +async fn update_nft_phishing_for_index( + table: &DbTable<'_, NftListTable>, + chain: &str, + index: &str, + domain: &str, + possible_phishing: bool, +) -> MmResult<(), WasmNftCacheError> { + let index_keys = MultiIndex::new(index).with_value(chain)?.with_value(domain)?; + let nfts_table = table.get_items_by_multi_index(index_keys).await?; + for (_item_id, item) in nfts_table.into_iter() { + let mut nft = nft_details_from_item(item)?; + nft.possible_phishing = possible_phishing; + drop_mutability!(nft); + let nft_item = NftListTable::from_nft(&nft)?; + let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain)? + .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_id.to_string())?; + table.replace_item_by_unique_multi_index(index_keys, &nft_item).await?; + } + Ok(()) } /// `get_last_block_from_table` function returns the highest block in the table related to certain blockchain type. @@ -577,13 +882,18 @@ pub(crate) struct NftListTable { amount: String, block_number: BeBigUint, contract_type: ContractType, + possible_spam: bool, + possible_phishing: bool, + token_domain: Option, + image_domain: Option, + animation_domain: Option, + external_domain: Option, details_json: Json, } impl NftListTable { - const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; - - const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; + const CHAIN_ANIMATION_DOMAIN_INDEX: &str = "chain_animation_domain_index"; + const CHAIN_EXTERNAL_DOMAIN_INDEX: &str = "chain_external_domain_index"; fn from_nft(nft: &Nft) -> WasmNftCacheResult { let details_json = json::to_value(nft).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; @@ -594,6 +904,12 @@ impl NftListTable { amount: nft.common.amount.to_string(), block_number: BeBigUint::from(nft.block_number), contract_type: nft.contract_type, + possible_spam: nft.common.possible_spam, + possible_phishing: nft.possible_phishing, + token_domain: nft.common.token_domain.clone(), + image_domain: nft.uri_meta.image_domain.clone(), + animation_domain: nft.uri_meta.animation_domain.clone(), + external_domain: nft.uri_meta.external_domain.clone(), details_json, }) } @@ -606,11 +922,20 @@ impl TableSignature for NftListTable { if is_initial_upgrade(old_version, new_version) { let table = upgrader.create_table(Self::table_name())?; table.create_multi_index( - Self::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], true, )?; - table.create_multi_index(Self::CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; + table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; + table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; + table.create_multi_index( + Self::CHAIN_ANIMATION_DOMAIN_INDEX, + &["chain", "animation_domain"], + false, + )?; + table.create_multi_index(Self::CHAIN_EXTERNAL_DOMAIN_INDEX, &["chain", "external_domain"], false)?; table.create_index("chain", false)?; table.create_index("block_number", false)?; } @@ -631,19 +956,19 @@ pub(crate) struct NftTransferHistoryTable { status: TransferStatus, amount: String, token_uri: Option, + token_domain: Option, collection_name: Option, image_url: Option, + image_domain: Option, token_name: Option, + possible_spam: bool, + possible_phishing: bool, details_json: Json, } impl NftTransferHistoryTable { - const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; - const CHAIN_TX_HASH_LOG_INDEX_INDEX: &str = "chain_tx_hash_log_index_index"; - const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; - fn from_transfer_history(transfer: &NftTransferHistory) -> WasmNftCacheResult { let details_json = json::to_value(transfer).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; @@ -659,9 +984,13 @@ impl NftTransferHistoryTable { status: transfer.status, amount: transfer.common.amount.to_string(), token_uri: transfer.token_uri.clone(), + token_domain: transfer.token_domain.clone(), collection_name: transfer.collection_name.clone(), image_url: transfer.image_url.clone(), + image_domain: transfer.image_domain.clone(), token_name: transfer.token_name.clone(), + possible_spam: transfer.common.possible_spam, + possible_phishing: transfer.possible_phishing, details_json, }) } @@ -674,7 +1003,7 @@ impl TableSignature for NftTransferHistoryTable { if is_initial_upgrade(old_version, new_version) { let table = upgrader.create_table(Self::table_name())?; table.create_multi_index( - Self::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], false, )?; @@ -683,7 +1012,10 @@ impl TableSignature for NftTransferHistoryTable { &["chain", "transaction_hash", "log_index"], true, )?; - table.create_multi_index(Self::CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; + table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; + table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; table.create_index("block_number", false)?; table.create_index("chain", false)?; } diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs index c88fd3defc..b646e7cefc 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_db.rs @@ -3,7 +3,6 @@ use crate::tx_history_storage::wasm::tx_history_storage_v2::{TxCacheTableV2, TxH use async_trait::async_trait; use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, InitDbResult}; -const DB_NAME: &str = "tx_history"; const DB_VERSION: u32 = 1; pub type TxHistoryDbLocked<'a> = DbLocked<'a, TxHistoryDb>; @@ -14,7 +13,7 @@ pub struct TxHistoryDb { #[async_trait] impl DbInstance for TxHistoryDb { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "tx_history"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs index 13abdd4ed5..e958300e47 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs @@ -12,7 +12,6 @@ use serialization::Reader; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; use std::collections::HashMap; -const DB_NAME: &str = "block_headers_cache"; const DB_VERSION: u32 = 1; pub type IDBBlockHeadersStorageRes = MmResult; @@ -24,7 +23,7 @@ pub struct IDBBlockHeadersInner { #[async_trait] impl DbInstance for IDBBlockHeadersInner { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "block_headers_cache"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/coins/z_coin/storage/blockdb/block_idb.rs b/mm2src/coins/z_coin/storage/blockdb/block_idb.rs index e70cfce122..a1d4eda6d9 100644 --- a/mm2src/coins/z_coin/storage/blockdb/block_idb.rs +++ b/mm2src/coins/z_coin/storage/blockdb/block_idb.rs @@ -2,7 +2,6 @@ use async_trait::async_trait; use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, OnUpgradeResult, TableSignature}; -const DB_NAME: &str = "z_compactblocks_cache"; const DB_VERSION: u32 = 1; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -39,7 +38,7 @@ impl BlockDbInner { #[async_trait] impl DbInstance for BlockDbInner { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "z_compactblocks_cache"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs index 129097dbe6..e3abf591ce 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs @@ -2,7 +2,6 @@ use async_trait::async_trait; use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, OnUpgradeResult, TableSignature}; -const DB_NAME: &str = "wallet_db_cache"; const DB_VERSION: u32 = 1; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -224,7 +223,7 @@ impl WalletDbInner { #[async_trait] impl DbInstance for WalletDbInner { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "wallet_db_cache"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 058661f42c..6fecf68462 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -103,6 +103,19 @@ macro_rules! some_or_return_ok_none { }; } +#[macro_export] +macro_rules! cross_test { + ($test_name:ident, $test_code:block) => { + #[cfg(not(target_arch = "wasm32"))] + #[tokio::test(flavor = "multi_thread")] + async fn $test_name() { $test_code } + + #[cfg(target_arch = "wasm32")] + #[wasm_bindgen_test] + async fn $test_name() { $test_code } + }; +} + #[macro_use] pub mod jsonrpc_client; #[macro_use] diff --git a/mm2src/mm2_db/src/indexed_db/indexed_db.rs b/mm2src/mm2_db/src/indexed_db/indexed_db.rs index 3bad052869..c1d56cd20d 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_db.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_db.rs @@ -65,10 +65,15 @@ pub trait TableSignature: DeserializeOwned + Serialize + 'static { fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()>; } +/// Essential operations for initializing an IndexedDb instance. #[async_trait] pub trait DbInstance: Sized { - fn db_name() -> &'static str; + /// Returns the static name of the database. + const DB_NAME: &'static str; + /// Initialize the database with the provided identifier. + /// This method ensures that the database is properly set up with the correct version + /// and has the required tables. async fn init(db_id: DbIdentifier) -> InitDbResult; } @@ -89,7 +94,7 @@ impl DbIdentifier { DbIdentifier { namespace_id, wallet_rmd160, - db_name: Db::db_name(), + db_name: Db::DB_NAME, } } diff --git a/mm2src/mm2_event_stream/src/controller.rs b/mm2src/mm2_event_stream/src/controller.rs index 098c6e4bb7..72870308b4 100644 --- a/mm2src/mm2_event_stream/src/controller.rs +++ b/mm2src/mm2_event_stream/src/controller.rs @@ -103,24 +103,13 @@ impl GuardedReceiver { #[cfg(any(test, target_arch = "wasm32"))] mod tests { use super::*; + use common::cross_test; common::cfg_wasm32! { use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); } - macro_rules! cross_test { - ($test_name:ident, $test_code:block) => { - #[cfg(not(target_arch = "wasm32"))] - #[tokio::test(flavor = "multi_thread")] - async fn $test_name() { $test_code } - - #[cfg(target_arch = "wasm32")] - #[wasm_bindgen_test] - async fn $test_name() { $test_code } - }; - } - cross_test!(test_create_channel_and_broadcast, { let mut controller = Controller::new(); let mut guard_receiver = controller.create_channel(1); diff --git a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs index 8392294c78..cbf9967d42 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs @@ -11,7 +11,6 @@ use mm2_number::BigDecimal; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; -const DB_NAME: &str = "gui_account_storage"; const DB_VERSION: u32 = 1; type AccountDbLocked<'a> = DbLocked<'a, AccountDb>; @@ -342,7 +341,7 @@ struct AccountDb { #[async_trait] impl DbInstance for AccountDb { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "gui_account_storage"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs index 812a802999..c96c2ef024 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs @@ -9,7 +9,6 @@ pub use mm2_db::indexed_db::{cursor_prelude, DbTransactionError, DbTransactionRe pub use tables::{MyActiveMakerOrdersTable, MyActiveTakerOrdersTable, MyFilteringHistoryOrdersTable, MyHistoryOrdersTable}; -const DB_NAME: &str = "ordermatch"; const DB_VERSION: u32 = 1; pub struct OrdermatchDb { @@ -18,7 +17,7 @@ pub struct OrdermatchDb { #[async_trait] impl DbInstance for OrdermatchDb { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "ordermatch"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs index d680ba4eb8..4b1b16aaf1 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs @@ -8,7 +8,6 @@ pub use mm2_db::indexed_db::{cursor_prelude, DbTransactionError, DbTransactionRe ItemId}; pub use tables::{MySwapsFiltersTable, SavedSwapTable, SwapLockTable}; -const DB_NAME: &str = "swap"; const DB_VERSION: u32 = 1; pub struct SwapDb { @@ -17,7 +16,7 @@ pub struct SwapDb { #[async_trait] impl DbInstance for SwapDb { - fn db_name() -> &'static str { DB_NAME } + const DB_NAME: &'static str = "swap"; async fn init(db_id: DbIdentifier) -> InitDbResult { let inner = IndexedDbBuilder::new(db_id) diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index 9a79611835..924b4e8448 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -13,6 +13,7 @@ use async_trait::async_trait; use futures::channel::oneshot::Canceled; +use http::header::ACCEPT; use http::{header, HeaderValue, Request}; use hyper::client::connect::Connect; use hyper::client::ResponseFuture; @@ -23,7 +24,7 @@ use common::wio::{drive03, HYPER}; use common::APPLICATION_JSON; use mm2_err_handle::prelude::*; -use super::transport::{SlurpError, SlurpResult, SlurpResultJson}; +use super::transport::{GetInfoFromUriError, SlurpError, SlurpResult, SlurpResultJson}; /// Provides requesting http through it /// @@ -231,6 +232,28 @@ impl From for SlurpError { fn from(e: http::Error) -> Self { SlurpError::InvalidRequest(e.to_string()) } } +/// Sends a GET request to the given URI and expects a 2xx status code in response. +/// +/// # Errors +/// +/// Returns an error if the HTTP status code of the response is not in the 2xx range. +pub async fn send_request_to_uri(uri: &str) -> MmResult { + let request = http::Request::builder() + .method("GET") + .uri(uri) + .header(ACCEPT, HeaderValue::from_static(APPLICATION_JSON)) + .body(hyper::Body::from(""))?; + + let (status, _header, body) = slurp_req_body(request).await?; + if !status.is_success() { + return Err(MmError::new(GetInfoFromUriError::Transport(format!( + "Status code not in 2xx range from {}: {}, {}", + uri, status, body + )))); + } + Ok(body) +} + #[cfg(test)] mod tests { use crate::native_http::slurp_url; diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index 2ba04b65e1..27c039d556 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -84,3 +84,53 @@ pub struct GuiAuthValidation { pub timestamp_message: i64, pub signature: String, } + +/// Errors encountered when making HTTP requests to fetch information from a URI. +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum GetInfoFromUriError { + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +/// `http::Error` can appear on an HTTP request [`http::Builder::build`] building. +impl From for GetInfoFromUriError { + fn from(e: http::Error) -> Self { GetInfoFromUriError::InvalidRequest(e.to_string()) } +} + +impl From for GetInfoFromUriError { + fn from(e: serde_json::Error) -> Self { GetInfoFromUriError::InvalidRequest(e.to_string()) } +} + +impl From for GetInfoFromUriError { + fn from(e: SlurpError) -> Self { + let error_str = e.to_string(); + match e { + SlurpError::ErrorDeserializing { .. } => GetInfoFromUriError::InvalidResponse(error_str), + SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetInfoFromUriError::Transport(error_str), + SlurpError::InvalidRequest(_) => GetInfoFromUriError::InvalidRequest(error_str), + SlurpError::Internal(_) => GetInfoFromUriError::Internal(error_str), + } + } +} + +/// Sends a POST request to the given URI and expects a 2xx status code in response. +/// +/// # Errors +/// +/// Returns an error if the HTTP status code of the response is not in the 2xx range. +pub async fn send_post_request_to_uri(uri: &str, body: String) -> MmResult, GetInfoFromUriError> { + let (status, _header, body) = slurp_post_json(uri, body).await?; + if !status.is_success() { + return Err(MmError::new(GetInfoFromUriError::Transport(format!( + "Status code not in 2xx range from {}: {}", + uri, status, + )))); + } + Ok(body) +} diff --git a/mm2src/mm2_net/src/wasm_http.rs b/mm2src/mm2_net/src/wasm_http.rs index 3767c2f40c..66362d60e3 100644 --- a/mm2src/mm2_net/src/wasm_http.rs +++ b/mm2src/mm2_net/src/wasm_http.rs @@ -1,11 +1,13 @@ -use crate::transport::{SlurpError, SlurpResult}; +use crate::transport::{GetInfoFromUriError, SlurpError, SlurpResult}; use common::executor::spawn_local; use common::{stringify_js_error, APPLICATION_JSON}; use futures::channel::oneshot; -use http::header::CONTENT_TYPE; +use gstuff::ERRL; +use http::header::{ACCEPT, CONTENT_TYPE}; use http::{HeaderMap, StatusCode}; use js_sys::Uint8Array; use mm2_err_handle::prelude::*; +use serde_json::Value as Json; use std::collections::HashMap; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -282,6 +284,38 @@ impl RequestBody { } } +/// Sends a GET request to the given URI and expects a 2xx status code in response. +/// +/// # Errors +/// +/// Returns an error if the HTTP status code of the response is not in the 2xx range. +pub async fn send_request_to_uri(uri: &str) -> MmResult { + macro_rules! try_or { + ($exp:expr, $errtype:ident) => { + match $exp { + Ok(x) => x, + Err(e) => return Err(MmError::new(GetInfoFromUriError::$errtype(ERRL!("{:?}", e)))), + } + }; + } + + let result = FetchRequest::get(uri) + .header(ACCEPT.as_str(), APPLICATION_JSON) + .request_str() + .await; + let (status_code, response_str) = try_or!(result, Transport); + if !status_code.is_success() { + return Err(MmError::new(GetInfoFromUriError::Transport(ERRL!( + "Status code not in 2xx range from: {}, {}", + status_code, + response_str + )))); + } + + let response: Json = try_or!(serde_json::from_str(&response_str), InvalidResponse); + Ok(response) +} + mod tests { use super::*; use wasm_bindgen_test::*;