diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e6c1e600..833fbb69ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## v1.0.4-beta - 2023-05-12 **Features:** +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - Proxy support was added [#1775](https://github.com/KomodoPlatform/atomicDEX-API/pull/1775) **Enhancements/Fixes:** - Some enhancements were done for `enable_bch_with_tokens`,`enable_eth_with_tokens`,`enable_tendermint_with_assets` RPCs in [#1762](https://github.com/KomodoPlatform/atomicDEX-API/pull/1762) diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index cb05e52d33..5ef4d12a77 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -15,7 +15,6 @@ enable-solana = [ "dep:spl-token", "dep:spl-associated-token-account" ] -enable-nft-integration = [] default = [] [lib] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 2944315728..aaa2e4a3ca 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,7 +21,6 @@ // Copyright © 2022 AtomicDEX. All rights reserved. // use super::eth::Action::{Call, Create}; -#[cfg(feature = "enable-nft-integration")] use crate::nft::nft_structs::{ContractType, ConvertChain, NftListReq, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use async_trait::async_trait; @@ -100,9 +99,7 @@ pub use rlp; mod web3_transport; #[path = "eth/v2_activation.rs"] pub mod v2_activation; -#[cfg(feature = "enable-nft-integration")] use crate::nft::{find_wallet_amount, WithdrawNftResult}; -#[cfg(feature = "enable-nft-integration")] use crate::{lp_coinfind_or_err, MmCoinEnum, TransactionType}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; @@ -867,31 +864,38 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }) } -#[cfg(feature = "enable-nft-integration")] /// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, /// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. -pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftResult { - let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; - let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; +pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155, url: String) -> WithdrawNftResult { + let coin = lp_coinfind_or_err(&ctx, &withdraw_type.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = + get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; let my_address = eth_coin.my_address()?; // todo check amount in nft cache, instead of sending new moralis req // dont use `get_nft_metadata` for erc1155, it can return info related to other owner. let nft_req = NftListReq { - chains: vec![req.chain], + chains: vec![withdraw_type.chain], + url, }; - let wallet_amount = find_wallet_amount(ctx, nft_req, req.token_address.clone(), req.token_id.clone()).await?; - - let amount_dec = if req.max { + let wallet_amount = find_wallet_amount( + ctx, + nft_req, + withdraw_type.token_address.clone(), + withdraw_type.token_id.clone(), + ) + .await?; + + let amount_dec = if withdraw_type.max { wallet_amount.clone() } else { - req.amount.unwrap_or_else(|| 1.into()) + withdraw_type.amount.unwrap_or_else(|| 1.into()) }; if amount_dec > wallet_amount { return MmError::err(WithdrawError::NotEnoughNftsAmount { - token_address: req.token_address, - token_id: req.token_id.to_string(), + token_address: withdraw_type.token_address, + token_id: withdraw_type.token_id.to_string(), available: wallet_amount, required: amount_dec, }); @@ -900,7 +904,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftRe let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC1155_CONTRACT.function("safeTransferFrom")?; - let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + let token_id_u256 = U256::from_dec_str(&withdraw_type.token_id.to_string()) .map_err(|e| format!("{:?}", e)) .map_to_mm(NumConversError::new)?; let amount_u256 = U256::from_dec_str(&amount_dec.to_string()) @@ -921,8 +925,15 @@ pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftRe )) }, }; - let (gas, gas_price) = - get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; + let (gas, gas_price) = get_eth_gas_details( + ð_coin, + withdraw_type.fee, + eth_value, + data.clone().into(), + call_addr, + false, + ) + .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) .compat() @@ -948,10 +959,10 @@ pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftRe tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash()), from: vec![my_address], - to: vec![req.to], + to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, - token_address: req.token_address, - token_id: req.token_id, + token_address: withdraw_type.token_address, + token_id: withdraw_type.token_id, amount: amount_dec, fee_details: Some(fee_details.into()), coin: eth_coin.ticker.clone(), @@ -962,18 +973,18 @@ pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftRe }) } -#[cfg(feature = "enable-nft-integration")] /// `withdraw_erc721` function returns details of `ERC-721` transaction including tx hex, /// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. -pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResult { - let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; - let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; +pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> WithdrawNftResult { + let coin = lp_coinfind_or_err(&ctx, &withdraw_type.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = + get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; let my_address = eth_coin.my_address()?; let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC721_CONTRACT.function("safeTransferFrom")?; - let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + let token_id_u256 = U256::from_dec_str(&withdraw_type.token_id.to_string()) .map_err(|e| format!("{:?}", e)) .map_to_mm(NumConversError::new)?; let data = function.encode_input(&[ @@ -989,8 +1000,15 @@ pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResu )) }, }; - let (gas, gas_price) = - get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; + let (gas, gas_price) = get_eth_gas_details( + ð_coin, + withdraw_type.fee, + eth_value, + data.clone().into(), + call_addr, + false, + ) + .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) .compat() @@ -1016,10 +1034,10 @@ pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResu tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash()), from: vec![my_address], - to: vec![req.to], + to: vec![withdraw_type.to], contract_type: ContractType::Erc721, - token_address: req.token_address, - token_id: req.token_id, + token_address: withdraw_type.token_address, + token_id: withdraw_type.token_id, amount: 1.into(), fee_details: Some(fee_details.into()), coin: eth_coin.ticker.clone(), @@ -5136,7 +5154,6 @@ pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult for WithdrawError { fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } } @@ -1889,17 +1897,16 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } | WithdrawError::UnexpectedUserAction { .. } - | WithdrawError::ActionNotAllowed(_) => StatusCode::BAD_REQUEST, - WithdrawError::HwError(_) => StatusCode::GONE, - #[cfg(target_arch = "wasm32")] - WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, - WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - #[cfg(feature = "enable-nft-integration")] - WithdrawError::GetNftInfoError(_) + | WithdrawError::ActionNotAllowed(_) + | WithdrawError::GetNftInfoError(_) | WithdrawError::AddressMismatchError { .. } | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) | WithdrawError::CoinDoesntSupportNftWithdraw { .. } | WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST, + WithdrawError::HwError(_) => StatusCode::GONE, + #[cfg(target_arch = "wasm32")] + WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, + WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -1934,7 +1941,6 @@ impl From for WithdrawError { fn from(e: TimeoutError) -> Self { WithdrawError::Timeout(e.duration) } } -#[cfg(feature = "enable-nft-integration")] impl From for WithdrawError { fn from(e: GetValidEthWithdrawAddError) -> Self { match e { diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 2b768738bd..906275af40 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -11,13 +11,13 @@ use nft_structs::{Chain, ConvertChain, Nft, NftList, NftListReq, NftMetadataReq, TransactionNftDetails, WithdrawNftReq}; use crate::eth::{get_eth_address, withdraw_erc1155, withdraw_erc721}; -use common::{APPLICATION_JSON, X_API_KEY}; +use crate::nft::nft_structs::WithdrawNftType; +use common::APPLICATION_JSON; use http::header::ACCEPT; use mm2_number::BigDecimal; use serde_json::Value as Json; -/// url for moralis requests -const URL_MORALIS: &str = "https://deep-index.moralis.io/api/v2/"; +const MORALIS_API_ENDPOINT: &str = "/api/v2/"; /// query parameter for moralis request: The format of the token ID const FORMAT_DECIMAL_MORALIS: &str = "format=decimal"; /// query parameter for moralis request: The transfer direction @@ -27,25 +27,20 @@ pub type WithdrawNftResult = Result MmResult { - let api_key = ctx.conf["api_key"] - .as_str() - .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; - let mut res_list = Vec::new(); - for chain in req.chains { let (coin_str, chain_str) = chain.to_ticker_chain(); let my_address = get_eth_address(&ctx, &coin_str).await?; - let uri_without_cursor = format!( - "{}{}/nft?chain={}&{}", - URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS - ); + let req_url = &req.url; + let wallet_address = my_address.wallet_address; + let uri_without_cursor = + format!("{req_url}{MORALIS_API_ENDPOINT}{wallet_address}/nft?chain={chain_str}&{FORMAT_DECIMAL_MORALIS}"); // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); loop { let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_moralis_request(uri.as_str(), api_key).await?; + let response = send_moralis_request(uri.as_str()).await?; if let Some(nfts_list) = response["result"].as_array() { for nft_json in nfts_list { let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string())?; @@ -66,6 +61,7 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult MmResult MmResult { - let api_key = ctx.conf["api_key"] - .as_str() - .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; +pub async fn get_nft_metadata(_ctx: MmArc, req: NftMetadataReq) -> MmResult { let chain_str = match req.chain { Chain::Avalanche => "avalanche", Chain::Bsc => "bsc", @@ -105,11 +98,13 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult "fantom", Chain::Polygon => "polygon", }; + let req_url = &req.url; + let token_address = &req.token_address; + let token_id = &req.token_id; let uri = format!( - "{}nft/{}/{}?chain={}&{}", - URL_MORALIS, req.token_address, req.token_id, chain_str, FORMAT_DECIMAL_MORALIS + "{req_url}{MORALIS_API_ENDPOINT}nft/{token_address}/{token_id}?chain={chain_str}&{FORMAT_DECIMAL_MORALIS}" ); - let response = send_moralis_request(uri.as_str(), api_key).await?; + let response = send_moralis_request(uri.as_str()).await?; let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string())?; let nft_metadata = Nft { chain: req.chain, @@ -128,6 +123,7 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult MmResult MmResult { - let api_key = ctx.conf["api_key"] - .as_str() - .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; - let mut res_list = Vec::new(); for chain in req.chains { @@ -150,16 +142,18 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult ("MATIC", "polygon"), }; let my_address = get_eth_address(&ctx, coin_str).await?; + let req_url = &req.url; + let wallet_address = my_address.wallet_address; let uri_without_cursor = format!( - "{}{}/nft/transfers?chain={}&{}&{}", - URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS, DIRECTION_BOTH_MORALIS + "{req_url}{MORALIS_API_ENDPOINT}{wallet_address}/nft/transfers?chain={chain_str}&{FORMAT_DECIMAL_MORALIS}&{DIRECTION_BOTH_MORALIS}", + ); // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); loop { let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_moralis_request(uri.as_str(), api_key).await?; + let response = send_moralis_request(uri.as_str()).await?; if let Some(transfer_list) = response["result"].as_array() { for transfer in transfer_list { let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&transfer.to_string())?; @@ -181,6 +175,7 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult MmResult WithdrawNftResult { - match req_type { - WithdrawNftReq::WithdrawErc1155(erc1155_req) => withdraw_erc1155(ctx, erc1155_req).await, - WithdrawNftReq::WithdrawErc721(erc721_req) => withdraw_erc721(ctx, erc721_req).await, +pub async fn withdraw_nft(ctx: MmArc, req: WithdrawNftReq) -> WithdrawNftResult { + match req.withdraw_type { + WithdrawNftType::WithdrawErc1155(erc1155_withdraw) => withdraw_erc1155(ctx, erc1155_withdraw, req.url).await, + WithdrawNftType::WithdrawErc721(erc721_withdraw) => withdraw_erc721(ctx, erc721_withdraw).await, } } #[cfg(not(target_arch = "wasm32"))] -async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult { +async fn send_moralis_request(uri: &str) -> MmResult { use http::header::HeaderValue; use mm2_net::transport::slurp_req_body; let request = http::Request::builder() .method("GET") .uri(uri) - .header(X_API_KEY, api_key) .header(ACCEPT, HeaderValue::from_static(APPLICATION_JSON)) .body(hyper::Body::from(""))?; @@ -236,7 +230,7 @@ async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult MmResult { +async fn send_moralis_request(uri: &str) -> MmResult { use mm2_net::wasm_http::FetchRequest; macro_rules! try_or { @@ -251,7 +245,6 @@ async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult StatusCode::BAD_REQUEST, GetNftInfoError::InvalidResponse(_) => StatusCode::FAILED_DEPENDENCY, - GetNftInfoError::ApiKeyError => StatusCode::FORBIDDEN, GetNftInfoError::Transport(_) | GetNftInfoError::Internal(_) | GetNftInfoError::GetEthAddressError(_) diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 09e247967c..435cc22b7e 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -7,6 +7,7 @@ use std::str::FromStr; #[derive(Debug, Deserialize)] pub struct NftListReq { pub(crate) chains: Vec, + pub(crate) url: String, } #[derive(Debug, Deserialize)] @@ -14,6 +15,7 @@ pub struct NftMetadataReq { pub(crate) token_address: String, pub(crate) token_id: BigDecimal, pub(crate) chain: Chain, + pub(crate) url: String, } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] @@ -97,6 +99,7 @@ pub struct Nft { pub(crate) last_token_uri_sync: Option, pub(crate) last_metadata_sync: Option, pub(crate) minter_address: Option, + pub(crate) possible_spam: Option, } /// This structure is for deserializing NFT json to struct. @@ -118,6 +121,7 @@ pub(crate) struct NftWrapper { pub(crate) last_token_uri_sync: Option, pub(crate) last_metadata_sync: Option, pub(crate) minter_address: Option, + pub(crate) possible_spam: Option, } #[derive(Debug)] @@ -169,9 +173,16 @@ pub struct WithdrawErc721 { pub(crate) fee: Option, } +#[derive(Clone, Deserialize)] +pub struct WithdrawNftReq { + pub(crate) url: String, + pub(crate) withdraw_type: WithdrawNftType, +} + #[derive(Clone, Deserialize)] #[serde(tag = "type", content = "withdraw_data")] -pub enum WithdrawNftReq { +#[serde(rename_all = "snake_case")] +pub enum WithdrawNftType { WithdrawErc1155(WithdrawErc1155), WithdrawErc721(WithdrawErc721), } @@ -206,6 +217,7 @@ pub struct TransactionNftDetails { #[derive(Debug, Deserialize)] pub struct NftTransfersReq { pub(crate) chains: Vec, + pub(crate) url: String, } #[derive(Debug, Serialize)] @@ -228,6 +240,7 @@ pub(crate) struct NftTransferHistory { pub(crate) amount: BigDecimal, pub(crate) verified: u64, pub(crate) operator: Option, + pub(crate) possible_spam: Option, } #[derive(Debug, Deserialize)] @@ -249,6 +262,7 @@ pub(crate) struct NftTransferHistoryWrapper { pub(crate) amount: SerdeStringWrap, pub(crate) verified: u64, pub(crate) operator: Option, + pub(crate) possible_spam: Option, } #[derive(Debug, Serialize)] diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 4d806ab933..9825269f9b 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -16,7 +16,6 @@ custom-swap-locktime = [] # only for testing purposes, should never be activated native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] -enable-nft-integration = ["coins/enable-nft-integration"] run-docker-tests = [] # TODO enable-solana = [] diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 43ab6b8c9e..48eec45170 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -11,7 +11,7 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s mm2::rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -#[cfg(feature = "enable-nft-integration")] use coins::nft; +use coins::nft; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels, ibc_withdraw}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, @@ -49,7 +49,6 @@ use http::Response; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; -#[cfg(feature = "enable-nft-integration")] use nft::{get_nft_list, get_nft_metadata, get_nft_transfers, withdraw_nft}; use serde::de::DeserializeOwned; use serde_json::{self as json, Value as Json}; @@ -170,11 +169,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_locked_amount_rpc).await, "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, "get_new_address" => handle_mmrpc(ctx, request, get_new_address).await, - #[cfg(feature = "enable-nft-integration")] "get_nft_list" => handle_mmrpc(ctx, request, get_nft_list).await, - #[cfg(feature = "enable-nft-integration")] "get_nft_metadata" => handle_mmrpc(ctx, request, get_nft_metadata).await, - #[cfg(feature = "enable-nft-integration")] "get_nft_transfers" => handle_mmrpc(ctx, request, get_nft_transfers).await, "get_public_key" => handle_mmrpc(ctx, request, get_public_key).await, "get_public_key_hash" => handle_mmrpc(ctx, request, get_public_key_hash).await, @@ -200,7 +196,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, ibc_withdraw).await, "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, - #[cfg(feature = "enable-nft-integration")] "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods {