From f02fd9bc099d6e89434c6300acb54ef57396261e Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 23 Jan 2024 19:33:52 +0300 Subject: [PATCH 01/53] implement bare types for websocket transport Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/mod.rs | 7 ++++ .../eth/web3_transport/websocket_transport.rs | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 mm2src/coins/eth/web3_transport/websocket_transport.rs diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index f8b7d62fbd..4d4450ed18 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -13,12 +13,15 @@ use web3::{Error, RequestId, Transport}; pub(crate) mod http_transport; #[cfg(target_arch = "wasm32")] pub(crate) mod metamask_transport; +pub(crate) mod websocket_transport; type Web3SendOut = BoxFuture<'static, Result>; #[derive(Clone, Debug)] pub(crate) enum Web3Transport { Http(http_transport::HttpTransport), + #[allow(dead_code)] // TODO: remove this + Websocket(websocket_transport::WebsocketTransport), #[cfg(target_arch = "wasm32")] Metamask(metamask_transport::MetamaskTransport), } @@ -52,6 +55,8 @@ impl Web3Transport { pub fn gui_auth_validation_generator_as_mut(&mut self) -> Option<&mut GuiAuthValidationGenerator> { match self { Web3Transport::Http(http) => http.gui_auth_validation_generator.as_mut(), + // TODO + Web3Transport::Websocket(_) => None, #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(_) => None, } @@ -64,6 +69,7 @@ impl Transport for Web3Transport { fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { match self { Web3Transport::Http(http) => http.prepare(method, params), + Web3Transport::Websocket(websocket) => websocket.prepare(method, params), #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(metamask) => metamask.prepare(method, params), } @@ -72,6 +78,7 @@ impl Transport for Web3Transport { fn send(&self, id: RequestId, request: Call) -> Self::Out { match self { Web3Transport::Http(http) => http.send(id, request), + Web3Transport::Websocket(websocket) => websocket.send(id, request), #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(metamask) => metamask.send(id, request), } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs new file mode 100644 index 0000000000..a5771d1311 --- /dev/null +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -0,0 +1,34 @@ +#![allow(unused)] // TODO: remove this + +use crate::eth::web3_transport::Web3SendOut; +use futures::lock::Mutex as AsyncMutex; +use jsonrpc_core::Call; +use std::sync::Arc; +use web3::{RequestId, Transport}; + +#[derive(Clone, Debug)] +pub struct WebsocketTransportNode { + pub(crate) uri: http::Uri, + pub(crate) gui_auth: bool, +} + +#[derive(Debug)] +struct WebsocketTransportRpcClient(AsyncMutex); + +#[derive(Debug)] +struct WebsocketTransportRpcClientImpl { + nodes: Vec, +} + +#[derive(Clone, Debug)] +pub struct WebsocketTransport { + client: Arc, +} + +impl Transport for WebsocketTransport { + type Out = Web3SendOut; + + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { todo!() } + + fn send(&self, id: RequestId, request: Call) -> Self::Out { todo!() } +} From 7977a7257569130ec0c73345fe70a688f9fb3748 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 24 Jan 2024 15:46:26 +0300 Subject: [PATCH 02/53] ws transport support and removal of eth::web3 field Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 136 ++++++++++-------- mm2src/coins/eth/eth_tests.rs | 65 ++------- mm2src/coins/eth/v2_activation.rs | 89 ++++++------ mm2src/coins/eth/web3_transport/mod.rs | 12 +- .../eth/web3_transport/websocket_transport.rs | 16 +++ mm2src/coins/nft.rs | 4 +- 6 files changed, 157 insertions(+), 165 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 641765e78b..852be9bbc6 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,6 +21,7 @@ // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // use super::eth::Action::{Call, Create}; +use crate::eth::web3_transport::websocket_transport::WebsocketTransportNode; use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use crate::{DexFee, ValidateWatcherSpendInput, WatcherSpendType}; @@ -47,7 +48,7 @@ use ethkey::{sign, verify_address}; use futures::compat::Future01CompatExt; use futures::future::{join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; -use http::StatusCode; +use http::{StatusCode, Uri}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; @@ -424,7 +425,6 @@ pub struct EthCoinImpl { swap_contract_address: Address, fallback_swap_contract: Option
, contract_supports_watchers: bool, - pub(crate) web3: Web3, /// The separate web3 instances kept to get nonce, will replace the web3 completely soon web3_instances: Vec, decimals: u8, @@ -485,6 +485,7 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { } impl EthCoinImpl { + pub(crate) fn web3(&self) -> Web3 { todo!() } /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` fn erc20_transfer_events( &self, @@ -510,7 +511,7 @@ impl EthCoinImpl { } Box::new( - self.web3 + self.web3() .eth() .logs(filter.build()) .compat() @@ -538,7 +539,7 @@ impl EthCoinImpl { } Box::new( - self.web3 + self.web3() .trace() .filter(filter.build()) .compat() @@ -657,7 +658,7 @@ impl EthCoinImpl { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } /// Try to parse address from string. @@ -696,7 +697,7 @@ async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransactionResult { let web3_tx = coin - .web3 + .web3() .eth() .transaction(TransactionId::Hash(tx_hash)) .await? @@ -1185,7 +1186,7 @@ impl SwapOps for EthCoin { Some(event) => { let transaction = try_s!( selfi - .web3 + .web3() .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await @@ -1742,7 +1743,7 @@ impl WatcherOps for EthCoin { let decimals = self.decimals; let fut = async move { - let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.web3().eth().transaction(TransactionId::Hash(tx.hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx)) @@ -2152,7 +2153,7 @@ impl MarketCoinOps for EthCoin { } let bytes = try_fus!(hex::decode(tx)); Box::new( - self.web3 + self.web3() .eth() .send_raw_transaction(bytes.into()) .compat() @@ -2163,7 +2164,7 @@ impl MarketCoinOps for EthCoin { fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { Box::new( - self.web3 + self.web3() .eth() .send_raw_transaction(tx.into()) .compat() @@ -2339,7 +2340,7 @@ impl MarketCoinOps for EthCoin { if let Some(event) = found { if let Some(tx_hash) = event.transaction_hash { - let transaction = match selfi.web3.eth().transaction(TransactionId::Hash(tx_hash)).await { + let transaction = match selfi.web3().eth().transaction(TransactionId::Hash(tx_hash)).await { Ok(Some(t)) => t, Ok(None) => { info!("Tx {} not found yet", tx_hash); @@ -2371,7 +2372,7 @@ impl MarketCoinOps for EthCoin { fn current_block(&self) -> Box + Send> { Box::new( - self.web3 + self.web3() .eth() .block_number() .compat() @@ -2598,7 +2599,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().await { + let current_block = match self.web3().eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -2781,7 +2782,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); let web3_tx = match self - .web3 + .web3() .eth() .transaction(TransactionId::Hash(trace.transaction_hash.unwrap())) .await @@ -2815,7 +2816,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); let receipt = match self - .web3 + .web3() .eth() .transaction_receipt(trace.transaction_hash.unwrap()) .await @@ -2873,7 +2874,7 @@ impl EthCoin { let raw = signed_tx_from_web3_tx(web3_tx).unwrap(); let block = match self - .web3 + .web3() .eth() .block(BlockId::Number(BlockNumber::Number(trace.block_number.into()))) .await @@ -2957,7 +2958,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().await { + let current_block = match self.web3().eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -3160,7 +3161,7 @@ impl EthCoin { "coin" => self.ticker.clone(), "client" => "ethereum", "method" => "tx_detail_by_hash"); let web3_tx = match self - .web3 + .web3() .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await @@ -3196,7 +3197,7 @@ impl EthCoin { }; let receipt = match self - .web3 + .web3() .eth() .transaction_receipt(event.transaction_hash.unwrap()) .await @@ -3232,7 +3233,7 @@ impl EthCoin { }; let block_number = event.block_number.unwrap(); let block = match self - .web3 + .web3() .eth() .block(BlockId::Number(BlockNumber::Number(block_number))) .await @@ -3959,7 +3960,7 @@ impl EthCoin { let coin = self.clone(); let fut = async move { match coin.coin_type { - EthCoinType::Eth => Ok(coin.web3.eth().balance(address, Some(BlockNumber::Latest)).await?), + EthCoinType::Eth => Ok(coin.web3().eth().balance(address, Some(BlockNumber::Latest)).await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; @@ -4015,7 +4016,7 @@ impl EthCoin { fn estimate_gas(&self, req: CallRequest) -> Box + Send> { // always using None block number as old Geth version accept only single argument in this RPC - Box::new(self.web3.eth().estimate_gas(req, None).compat()) + Box::new(self.web3().eth().estimate_gas(req, None).compat()) } /// Estimates how much gas is necessary to allow the contract call to complete. @@ -4049,7 +4050,7 @@ impl EthCoin { fn eth_balance(&self) -> BalanceFut { Box::new( - self.web3 + self.web3() .eth() .balance(self.my_address, Some(BlockNumber::Latest)) .compat() @@ -4068,7 +4069,7 @@ impl EthCoin { ..CallRequest::default() }; - self.web3 + self.web3() .eth() .call(request, Some(BlockId::Number(BlockNumber::Latest))) .await @@ -4173,7 +4174,7 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` @@ -4191,7 +4192,7 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } fn validate_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -4232,7 +4233,7 @@ impl EthCoin { ))); } - let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.web3().eth().transaction(TransactionId::Hash(tx.hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.hash)) })?; @@ -4522,13 +4523,16 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await) - { - Some(t) => t, - None => { - return ERR!("Found ReceiverSpent event, but transaction {:02x} is missing", tx_hash) - }, - }; + let transaction = + match try_s!(self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await) { + Some(t) => t, + None => { + return ERR!( + "Found ReceiverSpent event, but transaction {:02x} is missing", + tx_hash + ) + }, + }; return Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -4548,13 +4552,16 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await) - { - Some(t) => t, - None => { - return ERR!("Found SenderRefunded event, but transaction {:02x} is missing", tx_hash) - }, - }; + let transaction = + match try_s!(self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await) { + Some(t) => t, + None => { + return ERR!( + "Found SenderRefunded event, but transaction {:02x} is missing", + tx_hash + ) + }, + }; return Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -4607,7 +4614,7 @@ impl EthCoin { None => None, }; - let eth_gas_price = match coin.web3.eth().gas_price().await { + let eth_gas_price = match coin.web3().eth().gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); @@ -4615,7 +4622,7 @@ impl EthCoin { }, }; - let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3.api(); + let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3().api(); let eth_fee_history_price = match fee_history_namespace .eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]) .await @@ -4706,7 +4713,7 @@ impl EthCoin { ))); } - let web3_receipt = match selfi.web3.eth().transaction_receipt(payment_hash).await { + let web3_receipt = match selfi.web3().eth().transaction_receipt(payment_hash).await { Ok(r) => r, Err(e) => { error!( @@ -4754,7 +4761,7 @@ impl EthCoin { ))); } - match selfi.web3.eth().block_number().await { + match selfi.web3().eth().block_number().await { Ok(current_block) => { if current_block >= block_number { break Ok(()); @@ -5155,7 +5162,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let fut = async move { let expected_value = wei_from_big_decimal(&amount, coin.decimals)?; - let tx_from_rpc = coin.web3.eth().transaction(TransactionId::Hash(fee_tx_hash)).await?; + let tx_from_rpc = coin.web3().eth().transaction(TransactionId::Hash(fee_tx_hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", fee_tx_hash)) @@ -5473,15 +5480,6 @@ pub async fn eth_coin_from_conf_and_request( let mut rng = small_rng(); urls.as_mut_slice().shuffle(&mut rng); - let mut nodes = vec![]; - for url in urls.iter() { - nodes.push(HttpTransportNode { - uri: try_s!(url.parse()), - gui_auth: false, - }); - } - drop_mutability!(nodes); - let swap_contract_address: Address = try_s!(json::from_value(req["swap_contract_address"].clone())); if swap_contract_address == Address::default() { return ERR!("swap_contract_address can't be zero address"); @@ -5503,16 +5501,31 @@ pub async fn eth_coin_from_conf_and_request( let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); - for node in nodes.iter() { - let transport = Web3Transport::new_http(vec![node.clone()], event_handlers.clone()); + for url in urls.iter() { + let uri: Uri = try_s!(url.parse()); + + let transport = match uri.scheme_str() { + Some("ws") | Some("wss") => { + let node = WebsocketTransportNode { uri, gui_auth: false }; + + Web3Transport::new_websocket(vec![node], event_handlers.clone()) + }, + _ => { + let node = HttpTransportNode { uri, gui_auth: false }; + + Web3Transport::new_http(vec![node], event_handlers.clone()) + }, + }; + let web3 = Web3::new(transport); let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", node.uri, e); + error!("Couldn't get client version for url {}: {}", url, e); continue; }, }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), @@ -5523,9 +5536,6 @@ pub async fn eth_coin_from_conf_and_request( return ERR!("Failed to get client version for all urls"); } - let transport = Web3Transport::new_http(nodes, event_handlers); - let web3 = Web3::new(transport); - let (coin_type, decimals) = match protocol { CoinProtocol::ETH => (EthCoinType::Eth, ETH_DECIMALS), CoinProtocol::ERC20 { @@ -5534,7 +5544,8 @@ pub async fn eth_coin_from_conf_and_request( } => { let token_addr = try_s!(valid_addr_from_str(&contract_address)); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => try_s!(get_token_decimals(&web3, token_addr).await), + // TODO + None | Some(0) => try_s!(get_token_decimals(&web3_instances.first().unwrap().web3, token_addr).await), Some(d) => d as u8, }; (EthCoinType::Erc20 { platform, token_addr }, decimals) @@ -5595,7 +5606,6 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - web3, web3_instances, history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 152f68436f..f63829b2f2 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -142,11 +142,7 @@ fn eth_coin_from_keypair( fallback_swap_contract, contract_supports_watchers: false, ticker, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -324,11 +320,7 @@ fn send_and_refund_erc20_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -410,11 +402,7 @@ fn send_and_refund_eth_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -510,7 +498,7 @@ fn test_nonce_several_urls() { contract_supports_watchers: false, web3_instances: vec![ Web3Instance { - web3: web3_devnet.clone(), + web3: web3_devnet, is_parity: false, }, Web3Instance { @@ -522,7 +510,6 @@ fn test_nonce_several_urls() { is_parity: false, }, ], - web3: web3_devnet, decimals: 18, gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -575,11 +562,7 @@ fn test_wait_for_payment_spend_timeout() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -645,11 +628,7 @@ fn test_search_for_swap_tx_spend_was_spent() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -756,11 +735,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "BAT".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -1166,7 +1141,7 @@ fn validate_dex_fee_invalid_sender_eth() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1200,7 +1175,7 @@ fn validate_dex_fee_invalid_sender_erc() { ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1236,7 +1211,7 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1272,7 +1247,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1434,11 +1409,7 @@ fn test_message_hash() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1479,11 +1450,7 @@ fn test_sign_verify_message() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: false }], decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1536,11 +1503,7 @@ fn test_eth_extract_secret() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: true, - }], - web3, + web3_instances: vec![Web3Instance { web3, is_parity: true }], ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index eec453ae54..efc18a78df 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -74,13 +74,13 @@ impl Default for EthPrivKeyActivationPolicy { #[derive(Clone, Debug, Deserialize, Serialize)] pub enum EthRpcMode { - Http, + Default, #[cfg(target_arch = "wasm32")] Metamask, } impl Default for EthRpcMode { - fn default() -> Self { EthRpcMode::Http } + fn default() -> Self { EthRpcMode::Default } } #[derive(Clone, Deserialize)] @@ -155,7 +155,7 @@ impl EthCoin { let conf = coin_conf(&ctx, &ticker); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => get_token_decimals(&self.web3, protocol.token_addr) + None | Some(0) => get_token_decimals(&self.web3(), protocol.token_addr) .await .map_err(Erc20TokenActivationError::InternalError)?, Some(d) => d as u8, @@ -177,12 +177,6 @@ impl EthCoin { }) .collect(); - let mut transport = self.web3.transport().clone(); - if let Some(auth) = transport.gui_auth_validation_generator_as_mut() { - auth.coin_ticker = ticker.clone(); - } - let web3 = Web3::new(transport); - let required_confirmations = activation_params .required_confirmations .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) @@ -208,7 +202,6 @@ impl EthCoin { gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, gas_station_policy: self.gas_station_policy.clone(), - web3, web3_instances, history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), ctx: self.ctx.clone(), @@ -255,16 +248,16 @@ pub async fn eth_coin_from_conf_and_request_v2( let chain_id = conf["chain_id"].as_u64(); - let (web3, web3_instances) = match (req.rpc_mode, &priv_key_policy) { + let web3_instances = match (req.rpc_mode, &priv_key_policy) { ( - EthRpcMode::Http, + EthRpcMode::Default, EthPrivKeyPolicy::Iguana(key_pair) | EthPrivKeyPolicy::HDWallet { activated_key: key_pair, .. }, - ) => build_http_transport(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await?, - (EthRpcMode::Http, EthPrivKeyPolicy::Trezor) => { + ) => build_web3_instances(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await?, + (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, )); @@ -277,7 +270,7 @@ pub async fn eth_coin_from_conf_and_request_v2( build_metamask_transport(ctx, ticker.clone(), chain_id).await? }, #[cfg(target_arch = "wasm32")] - (EthRpcMode::Http, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { + (EthRpcMode::Default, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { let error = r#"priv_key_policy="Metamask" and rpc_mode="Metamask" should be used both"#.to_string(); return MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }); }, @@ -317,7 +310,6 @@ pub async fn eth_coin_from_conf_and_request_v2( gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, - web3, web3_instances, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), ctx: ctx.weak(), @@ -383,58 +375,62 @@ pub(crate) async fn build_address_and_priv_key_policy( } } -async fn build_http_transport( +async fn build_web3_instances( ctx: &MmArc, coin_ticker: String, address: String, key_pair: &KeyPair, eth_nodes: &[EthNode], -) -> MmResult<(Web3, Vec), EthActivationV2Error> { +) -> MmResult, EthActivationV2Error> { if eth_nodes.is_empty() { return MmError::err(EthActivationV2Error::AtLeastOneNodeRequired); } - let mut http_nodes = vec![]; - for node in eth_nodes { - let uri = node - .url - .parse() - .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", node.url)))?; + let mut urls: Vec = eth_nodes.iter().map(|n| n.url.clone()).collect(); + let mut rng = small_rng(); + urls.as_mut_slice().shuffle(&mut rng); + drop_mutability!(urls); - http_nodes.push(HttpTransportNode { - uri, - gui_auth: node.gui_auth, - }); - } + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); - let mut rng = small_rng(); - http_nodes.as_mut_slice().shuffle(&mut rng); + let mut web3_instances = Vec::with_capacity(urls.len()); + for url in urls { + let uri: Uri = url + .parse() + .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", url)))?; - drop_mutability!(http_nodes); + let transport = match uri.scheme_str() { + Some("ws") | Some("wss") => { + let node = WebsocketTransportNode { uri, gui_auth: false }; - let mut web3_instances = Vec::with_capacity(http_nodes.len()); - let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); - for node in http_nodes.iter() { - let transport = build_single_http_transport( - coin_ticker.clone(), - address.clone(), - key_pair, - vec![node.clone()], - event_handlers.clone(), - ); + Web3Transport::new_websocket(vec![node], event_handlers.clone()) + }, + _ => { + let node = HttpTransportNode { uri, gui_auth: false }; + + build_single_http_transport( + coin_ticker.clone(), + address.clone(), + key_pair, + vec![node], + event_handlers.clone(), + ) + }, + }; let web3 = Web3::new(transport); let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", node.uri, e); + error!("Couldn't get client version for url {}: {}", url, e); continue; }, }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), - }) + }); } if web3_instances.is_empty() { @@ -443,10 +439,7 @@ async fn build_http_transport( ); } - let transport = build_single_http_transport(coin_ticker, address, key_pair, http_nodes, event_handlers); - let web3 = Web3::new(transport); - - Ok((web3, web3_instances)) + Ok(web3_instances) } fn build_single_http_transport( diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 4d4450ed18..8b8bbe0880 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -20,7 +20,6 @@ type Web3SendOut = BoxFuture<'static, Result>; #[derive(Clone, Debug)] pub(crate) enum Web3Transport { Http(http_transport::HttpTransport), - #[allow(dead_code)] // TODO: remove this Websocket(websocket_transport::WebsocketTransport), #[cfg(target_arch = "wasm32")] Metamask(metamask_transport::MetamaskTransport), @@ -34,6 +33,13 @@ impl Web3Transport { http_transport::HttpTransport::with_event_handlers(nodes, event_handlers).into() } + pub fn new_websocket( + nodes: Vec, + event_handlers: Vec, + ) -> Web3Transport { + websocket_transport::WebsocketTransport::with_event_handlers(nodes, event_handlers).into() + } + #[cfg(target_arch = "wasm32")] pub(crate) fn new_metamask( eth_config: metamask_transport::MetamaskEthConfig, @@ -89,6 +95,10 @@ impl From for Web3Transport { fn from(http: http_transport::HttpTransport) -> Self { Web3Transport::Http(http) } } +impl From for Web3Transport { + fn from(websocket: websocket_transport::WebsocketTransport) -> Self { Web3Transport::Websocket(websocket) } +} + #[cfg(target_arch = "wasm32")] impl From for Web3Transport { fn from(metamask: metamask_transport::MetamaskTransport) -> Self { Web3Transport::Metamask(metamask) } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index a5771d1311..d3e8950ef0 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,6 +1,7 @@ #![allow(unused)] // TODO: remove this use crate::eth::web3_transport::Web3SendOut; +use crate::eth::RpcTransportEventHandlerShared; use futures::lock::Mutex as AsyncMutex; use jsonrpc_core::Call; use std::sync::Arc; @@ -23,6 +24,21 @@ struct WebsocketTransportRpcClientImpl { #[derive(Clone, Debug)] pub struct WebsocketTransport { client: Arc, + event_handlers: Vec, +} + +impl WebsocketTransport { + pub fn with_event_handlers( + nodes: Vec, + event_handlers: Vec, + ) -> Self { + let client_impl = WebsocketTransportRpcClientImpl { nodes }; + + WebsocketTransport { + client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), + event_handlers, + } + } } impl Transport for WebsocketTransport { diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 40736a6414..3feedea3ad 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -729,7 +729,7 @@ async fn get_moralis_nft_transfers( async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let hash = H256::from_str(transaction_hash).ok()?; - let receipt = eth_coin.web3.eth().transaction_receipt(hash).await.ok()?; + let receipt = eth_coin.web3().eth().transaction_receipt(hash).await.ok()?; let fee_coin = match eth_coin.coin_type { EthCoinType::Eth => eth_coin.ticker(), EthCoinType::Erc20 { .. } => return None, @@ -742,7 +742,7 @@ async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option EthTxFeeDetails::new(gas_used, gas_price, fee_coin).ok(), None => { let web3_tx = eth_coin - .web3 + .web3() .eth() .transaction(TransactionId::Hash(hash)) .await From 2734cd1b1157537ab874fb504dfea5e12464dbef Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 25 Jan 2024 16:13:53 +0300 Subject: [PATCH 03/53] implement `fn prepare` for websocket_transport Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index d3e8950ef0..aca7b6f837 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -4,8 +4,9 @@ use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; use futures::lock::Mutex as AsyncMutex; use jsonrpc_core::Call; -use std::sync::Arc; -use web3::{RequestId, Transport}; +use std::sync::{atomic::{AtomicUsize, Ordering}, + Arc}; +use web3::{helpers::build_request, RequestId, Transport}; #[derive(Clone, Debug)] pub struct WebsocketTransportNode { @@ -23,6 +24,7 @@ struct WebsocketTransportRpcClientImpl { #[derive(Clone, Debug)] pub struct WebsocketTransport { + request_id: Arc, client: Arc, event_handlers: Vec, } @@ -37,6 +39,7 @@ impl WebsocketTransport { WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, + request_id: Arc::new(AtomicUsize::new(0)), } } } @@ -44,7 +47,16 @@ impl WebsocketTransport { impl Transport for WebsocketTransport { type Out = Web3SendOut; - fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { todo!() } + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + let request_id = self.request_id.fetch_add(1, Ordering::SeqCst); + let request = build_request(request_id, method, params); - fn send(&self, id: RequestId, request: Call) -> Self::Out { todo!() } + (request_id, request) + } + + fn send(&self, id: RequestId, request: Call) -> Self::Out { + // send this request to another thread that continiously handles ws connection as a + // background task; filter the responses with the given ID and return its reponse data. + todo!() + } } From 6973845bb9bb70a3746691b522e8f52ed6fe52ec Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 25 Jan 2024 20:11:50 +0300 Subject: [PATCH 04/53] save dev state Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index aca7b6f837..a5bd4ef0d3 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,11 +1,14 @@ #![allow(unused)] // TODO: remove this -use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; +use crate::{eth::web3_transport::Web3SendOut, rpc_command::lightning::nodes}; +use common::log; use futures::lock::Mutex as AsyncMutex; use jsonrpc_core::Call; +use mm2_net::transport::GuiAuthValidationGenerator; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; +use web3::error::Error; use web3::{helpers::build_request, RequestId, Transport}; #[derive(Clone, Debug)] @@ -42,6 +45,24 @@ impl WebsocketTransport { request_id: Arc::new(AtomicUsize::new(0)), } } + + async fn start_connection_loop(&self) { + for node in (*self.client.0.lock().await).nodes.clone() { + let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { + Ok(ws) => ws, + Err(e) => { + log::error!("{e}"); + continue; + }, + }; + + // TODO + } + } + + async fn stop_connection_loop(&self) { todo!() } + + async fn rpc_send_and_receive(&self) { todo!() } } impl Transport for WebsocketTransport { @@ -60,3 +81,12 @@ impl Transport for WebsocketTransport { todo!() } } + +async fn send_request( + request: Call, + client: Arc, + event_handlers: Vec, + gui_auth_validation_generator: Option, +) -> Result { + todo!() +} From 37324bcc046504c4998abda7ea4487a69c03a51d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 29 Jan 2024 14:58:33 +0300 Subject: [PATCH 05/53] partial design of handling request and socket events Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index a5bd4ef0d3..faee7ba648 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,16 +1,24 @@ #![allow(unused)] // TODO: remove this +use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; -use crate::{eth::web3_transport::Web3SendOut, rpc_command::lightning::nodes}; use common::log; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::lock::Mutex as AsyncMutex; +use futures_util::{FutureExt, StreamExt}; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; +use std::collections::HashSet; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use web3::error::Error; use web3::{helpers::build_request, RequestId, Transport}; +enum RequestMessage { + Request(Call), + Response(Result), +} + #[derive(Clone, Debug)] pub struct WebsocketTransportNode { pub(crate) uri: http::Uri, @@ -30,6 +38,8 @@ pub struct WebsocketTransport { request_id: Arc, client: Arc, event_handlers: Vec, + message_sender: UnboundedSender, + // message_handler: UnboundedReceiver, } impl WebsocketTransport { @@ -43,11 +53,16 @@ impl WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, request_id: Arc::new(AtomicUsize::new(0)), + // message_handler: todo!(), + message_sender: todo!(), } } async fn start_connection_loop(&self) { for node in (*self.client.0.lock().await).nodes.clone() { + // id list of awaiting requests + let mut awaiting_requests: HashSet = HashSet::default(); + let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { Ok(ws) => ws, Err(e) => { @@ -57,12 +72,26 @@ impl WebsocketTransport { }; // TODO + loop { + futures_util::select! { + message = wsocket.next().fuse() => { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(_))) => todo!(), + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => todo!(), + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => todo!(), + Some(Err(e)) => {}, + None => {}, + } + } + } + } + } } - async fn stop_connection_loop(&self) { todo!() } + async fn stop_connection(self) { todo!() } - async fn rpc_send_and_receive(&self) { todo!() } + async fn rpc_send_and_receive(&self) -> Result { todo!() } } impl Transport for WebsocketTransport { From 1de6a3f12abb73ead897f4afb713b2bf4e98cf8d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 29 Jan 2024 14:58:53 +0300 Subject: [PATCH 06/53] fix WASM errors Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 6 +++--- mm2src/coins/eth/eth_wasm_tests.rs | 3 +-- mm2src/coins/eth/v2_activation.rs | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 852be9bbc6..1f0bac6564 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -828,7 +828,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = coin.web3.eth().send_transaction(tx_to_send).await?; + let tx_hash = coin.web3().eth().send_transaction(tx_to_send).await?; let signed_tx = coin .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -2522,7 +2522,7 @@ async fn sign_and_send_transaction_with_metamask( // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = try_tx_s!(coin.web3.eth().send_transaction(tx_to_send).await); + let tx_hash = try_tx_s!(coin.web3().eth().send_transaction(tx_to_send).await); let maybe_signed_tx = try_tx_s!( coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -4684,7 +4684,7 @@ impl EthCoin { ) -> Web3RpcResult> { let wait_until = wait_until_ms(wait_rpc_timeout_ms); while now_ms() < wait_until { - let maybe_tx = self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await?; + let maybe_tx = self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; return Ok(Some(signed_tx)); diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 1b4e7de7ed..105631f53b 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -34,10 +34,9 @@ async fn test_send() { fallback_swap_contract: None, contract_supports_watchers: false, web3_instances: vec![Web3Instance { - web3: web3.clone(), + web3, is_parity: false, }], - web3, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index efc18a78df..f737573b55 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -465,7 +465,7 @@ async fn build_metamask_transport( ctx: &MmArc, coin_ticker: String, chain_id: u64, -) -> MmResult<(Web3, Vec), EthActivationV2Error> { +) -> MmResult, EthActivationV2Error> { let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); let eth_config = web3_transport::metamask_transport::MetamaskEthConfig { chain_id }; @@ -478,11 +478,11 @@ async fn build_metamask_transport( // MetaMask doesn't use Parity nodes. So `MetamaskTransport` doesn't support `parity_nextNonce` RPC. // An example of the `web3_clientVersion` RPC - `MetaMask/v10.22.1`. let web3_instances = vec![Web3Instance { - web3: web3.clone(), + web3: web3, is_parity: false, }]; - Ok((web3, web3_instances)) + Ok(web3_instances) } /// This method is based on the fact that `MetamaskTransport` tries to switch the `ChainId` From b969898258c9b98ef1c00ee741d054ebf9e1d0ce Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 29 Jan 2024 17:22:05 +0300 Subject: [PATCH 07/53] implement keepalive for ws connections Signed-off-by: onur-ozkan --- Cargo.lock | 1 + mm2src/coins/Cargo.toml | 3 +- mm2src/coins/eth/eth_wasm_tests.rs | 5 +-- mm2src/coins/eth/v2_activation.rs | 5 +-- .../eth/web3_transport/websocket_transport.rs | 37 +++++++++++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 858a2e5be1..fecdc4d2ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,7 @@ dependencies = [ "ethkey", "futures 0.1.29", "futures 0.3.28", + "futures-ticker", "futures-util", "group 0.8.0", "gstuff", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 3b361e269b..d4e6039f6a 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -49,9 +49,9 @@ ethereum-types = { version = "0.13", default-features = false, features = ["std" ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } # Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. #enum_dispatch = "0.1" -tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} futures01 = { version = "0.1", package = "futures" } futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +futures-ticker = "0.0.3" # using select macro requires the crate to be named futures, compilation failed with futures03 name futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } group = "0.8.0" @@ -100,6 +100,7 @@ sha3 = "0.9" utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs tendermint-rpc = { version = "=0.23.7", default-features = false } +tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} tiny-bip39 = "0.8.0" url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 105631f53b..bb262bbb59 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -33,10 +33,7 @@ async fn test_send() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3, - is_parity: false, - }], + web3_instances: vec![Web3Instance { web3, is_parity: false }], decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index f737573b55..2dd96bc60e 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -477,10 +477,7 @@ async fn build_metamask_transport( // MetaMask doesn't use Parity nodes. So `MetamaskTransport` doesn't support `parity_nextNonce` RPC. // An example of the `web3_clientVersion` RPC - `MetaMask/v10.22.1`. - let web3_instances = vec![Web3Instance { - web3: web3, - is_parity: false, - }]; + let web3_instances = vec![Web3Instance { web3, is_parity: false }]; Ok(web3_instances) } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index faee7ba648..8a822a033c 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -5,7 +5,9 @@ use crate::eth::RpcTransportEventHandlerShared; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::lock::Mutex as AsyncMutex; -use futures_util::{FutureExt, StreamExt}; +use futures_ticker::Ticker; +use futures_util::{FutureExt, SinkExt, StreamExt}; +use instant::Duration; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; use std::collections::HashSet; @@ -16,7 +18,7 @@ use web3::{helpers::build_request, RequestId, Transport}; enum RequestMessage { Request(Call), - Response(Result), + Response(serde_json::Value), } #[derive(Clone, Debug)] @@ -70,22 +72,41 @@ impl WebsocketTransport { continue; }, }; + let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); - // TODO loop { futures_util::select! { + _ = keepalive_interval.next().fuse() => { + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id":67}"#; + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { + log::error!("{e}"); + continue; + } + } + + // TODO: handle `RequestMessage` for sending requests + message = wsocket.next().fuse() => { match message { - Some(Ok(tokio_tungstenite_wasm::Message::Text(_))) => todo!(), + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if let Some(id) = inc_event.get("id") { + awaiting_requests.remove(&(id.as_u64().unwrap_or_default() as usize)); + // TODO: return response with `RequestMessage` + } + } + }, Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => todo!(), - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => todo!(), - Some(Err(e)) => {}, - None => {}, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, + Some(Err(e)) => { + log::error!("{e}"); + break; + }, + None => continue, } } } } - } } From b6f7c6d768e17367928ed1641eb6a7be3fe155e2 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 29 Jan 2024 18:25:44 +0300 Subject: [PATCH 08/53] save dev state Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 76 +++++++++++++++---- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 8a822a033c..6b11f05dec 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -14,13 +14,9 @@ use std::collections::HashSet; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use web3::error::Error; +use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; -enum RequestMessage { - Request(Call), - Response(serde_json::Value), -} - #[derive(Clone, Debug)] pub struct WebsocketTransportNode { pub(crate) uri: http::Uri, @@ -40,8 +36,20 @@ pub struct WebsocketTransport { request_id: Arc, client: Arc, event_handlers: Vec, - message_sender: UnboundedSender, - // message_handler: UnboundedReceiver, + request_handler: RequestHandler, + response_handler: ResponseHandler, +} + +#[derive(Clone, Debug)] +struct RequestHandler { + tx: UnboundedSender, + rx: Arc>>, +} + +#[derive(Clone, Debug)] +struct ResponseHandler { + tx: UnboundedSender, + rx: Arc>>, } impl WebsocketTransport { @@ -51,20 +59,26 @@ impl WebsocketTransport { ) -> Self { let client_impl = WebsocketTransportRpcClientImpl { nodes }; + let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); + let (res_tx, res_rx) = futures::channel::mpsc::unbounded(); + WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, request_id: Arc::new(AtomicUsize::new(0)), - // message_handler: todo!(), - message_sender: todo!(), + request_handler: RequestHandler { + tx: req_tx, + rx: Arc::new(AsyncMutex::new(req_rx)), + }, + response_handler: ResponseHandler { + tx: res_tx, + rx: Arc::new(AsyncMutex::new(res_rx)), + }, } } async fn start_connection_loop(&self) { for node in (*self.client.0.lock().await).nodes.clone() { - // id list of awaiting requests - let mut awaiting_requests: HashSet = HashSet::default(); - let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { Ok(ws) => ws, Err(e) => { @@ -72,27 +86,43 @@ impl WebsocketTransport { continue; }, }; + + // id list of awaiting requests + let mut awaiting_requests: HashSet = HashSet::default(); let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); + let mut req_rx = self.request_handler.rx.lock().await; + let mut res_tx = self.response_handler.tx.clone(); loop { futures_util::select! { _ = keepalive_interval.next().fuse() => { - const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id":67}"#; + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { log::error!("{e}"); + // TODO: try couple times at least before continue continue; } } - // TODO: handle `RequestMessage` for sending requests + request = req_rx.next().fuse() => { + if let Some(request) = request { + let serialized_request = to_string(&request); + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { + log::error!("{e}"); + // TODO: try couple times at least before continue + continue; + } + } + } message = wsocket.next().fuse() => { match message { Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { if let Ok(inc_event) = serde_json::from_str::(&inc_event) { if let Some(id) = inc_event.get("id") { + res_tx.send(inc_event.clone()).await.expect("TODO"); awaiting_requests.remove(&(id.as_u64().unwrap_or_default() as usize)); - // TODO: return response with `RequestMessage` } } }, @@ -112,7 +142,21 @@ impl WebsocketTransport { async fn stop_connection(self) { todo!() } - async fn rpc_send_and_receive(&self) -> Result { todo!() } + async fn rpc_send_and_receive(&self, request: Call, request_id: usize) -> Result { + let mut tx = self.request_handler.tx.clone(); + let mut rx = self.response_handler.rx.lock().await; + tx.send(request).await.expect("TODO"); + + while let Some(response) = rx.next().await { + if let Some(id) = response.get("id") { + if id.as_u64().unwrap_or_default() as usize == request_id { + return Ok(response); + } + } + } + + Err(Error::Internal) + } } impl Transport for WebsocketTransport { From 4d02fb06e3716fc8d2a351b165286da0691afbc9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 29 Jan 2024 18:45:17 +0300 Subject: [PATCH 09/53] add TODO Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6b11f05dec..6dd2e8d969 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -142,6 +142,7 @@ impl WebsocketTransport { async fn stop_connection(self) { todo!() } + // TODO: implement timeouts async fn rpc_send_and_receive(&self, request: Call, request_id: usize) -> Result { let mut tx = self.request_handler.tx.clone(); let mut rx = self.response_handler.rx.lock().await; From d06c19507d82b354eefc244a2874d2b279c9bf0d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 30 Jan 2024 14:28:34 +0300 Subject: [PATCH 10/53] make the websocket transport working Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 11 ++- mm2src/coins/eth/v2_activation.rs | 2 +- mm2src/coins/eth/web3_transport/mod.rs | 14 +++- .../eth/web3_transport/websocket_transport.rs | 75 +++++++++---------- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 1f0bac6564..a636660037 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -30,6 +30,7 @@ use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError, Timer}; +use common::executor::{AbortSettings, SpawnAbortable}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -485,7 +486,11 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { } impl EthCoinImpl { - pub(crate) fn web3(&self) -> Web3 { todo!() } + pub(crate) fn web3(&self) -> Web3 { + // TODO + self.web3_instances.first().unwrap().web3.clone() + } + /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` fn erc20_transfer_events( &self, @@ -5508,7 +5513,7 @@ pub async fn eth_coin_from_conf_and_request( Some("ws") | Some("wss") => { let node = WebsocketTransportNode { uri, gui_auth: false }; - Web3Transport::new_websocket(vec![node], event_handlers.clone()) + Web3Transport::new_websocket(ctx, vec![node], event_handlers.clone()) }, _ => { let node = HttpTransportNode { uri, gui_auth: false }; @@ -5606,7 +5611,7 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - web3_instances, + web3_instances: web3_instances.clone(), history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 2dd96bc60e..1af1ff1a9c 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -403,7 +403,7 @@ async fn build_web3_instances( Some("ws") | Some("wss") => { let node = WebsocketTransportNode { uri, gui_auth: false }; - Web3Transport::new_websocket(vec![node], event_handlers.clone()) + Web3Transport::new_websocket(ctx, vec![node], event_handlers.clone()) }, _ => { let node = HttpTransportNode { uri, gui_auth: false }; diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 8b8bbe0880..2892e988ce 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -1,7 +1,9 @@ -use crate::RpcTransportEventHandlerShared; +use crate::{MmCoin, RpcTransportEventHandlerShared}; +use common::executor::{AbortSettings, SpawnAbortable}; use ethereum_types::U256; use futures::future::BoxFuture; use jsonrpc_core::Call; +use mm2_core::mm_ctx::MmArc; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskResult; use mm2_net::transport::GuiAuthValidationGenerator; use serde_json::Value as Json; @@ -34,10 +36,18 @@ impl Web3Transport { } pub fn new_websocket( + ctx: &MmArc, nodes: Vec, event_handlers: Vec, ) -> Web3Transport { - websocket_transport::WebsocketTransport::with_event_handlers(nodes, event_handlers).into() + let transport = websocket_transport::WebsocketTransport::with_event_handlers(nodes, event_handlers); + + // TODO: Don't do this here + let fut = transport.clone().start_connection_loop(); + let settings = AbortSettings::info_on_abort("TODO".to_string()); + ctx.spawner().spawn_with_settings(fut, settings); + + transport.into() } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6dd2e8d969..5f0b098a74 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,7 +1,10 @@ #![allow(unused)] // TODO: remove this +// TODO: Reduce the lock overhead + use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; +use common::executor::Timer; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::lock::Mutex as AsyncMutex; @@ -10,7 +13,8 @@ use futures_util::{FutureExt, SinkExt, StreamExt}; use instant::Duration; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; -use std::collections::HashSet; +use parking_lot::RwLock; +use std::collections::{HashMap, HashSet}; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use web3::error::Error; @@ -36,14 +40,14 @@ pub struct WebsocketTransport { request_id: Arc, client: Arc, event_handlers: Vec, + responses: Arc>>, request_handler: RequestHandler, - response_handler: ResponseHandler, } #[derive(Clone, Debug)] struct RequestHandler { - tx: UnboundedSender, - rx: Arc>>, + tx: UnboundedSender<(RequestId, Call)>, + rx: Arc>>, } #[derive(Clone, Debug)] @@ -60,24 +64,20 @@ impl WebsocketTransport { let client_impl = WebsocketTransportRpcClientImpl { nodes }; let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); - let (res_tx, res_rx) = futures::channel::mpsc::unbounded(); WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, - request_id: Arc::new(AtomicUsize::new(0)), + responses: Arc::new(AsyncMutex::new(HashMap::default())), + request_id: Arc::new(AtomicUsize::new(1)), request_handler: RequestHandler { tx: req_tx, rx: Arc::new(AsyncMutex::new(req_rx)), }, - response_handler: ResponseHandler { - tx: res_tx, - rx: Arc::new(AsyncMutex::new(res_rx)), - }, } } - async fn start_connection_loop(&self) { + pub async fn start_connection_loop(self) { for node in (*self.client.0.lock().await).nodes.clone() { let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { Ok(ws) => ws, @@ -88,10 +88,8 @@ impl WebsocketTransport { }; // id list of awaiting requests - let mut awaiting_requests: HashSet = HashSet::default(); let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); let mut req_rx = self.request_handler.rx.lock().await; - let mut res_tx = self.response_handler.tx.clone(); loop { futures_util::select! { @@ -106,7 +104,7 @@ impl WebsocketTransport { } request = req_rx.next().fuse() => { - if let Some(request) = request { + if let Some((request_id, request)) = request { let serialized_request = to_string(&request); if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { log::error!("{e}"); @@ -120,9 +118,13 @@ impl WebsocketTransport { match message { Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + continue; + } + if let Some(id) = inc_event.get("id") { - res_tx.send(inc_event.clone()).await.expect("TODO"); - awaiting_requests.remove(&(id.as_u64().unwrap_or_default() as usize)); + let request_id = id.as_u64().unwrap_or_default() as usize; + let _ = self.responses.lock().await.insert(request_id, inc_event.get("result").expect("TODO").clone()); } } }, @@ -141,23 +143,25 @@ impl WebsocketTransport { } async fn stop_connection(self) { todo!() } +} - // TODO: implement timeouts - async fn rpc_send_and_receive(&self, request: Call, request_id: usize) -> Result { - let mut tx = self.request_handler.tx.clone(); - let mut rx = self.response_handler.rx.lock().await; - tx.send(request).await.expect("TODO"); +async fn rpc_send_and_receive( + transport: WebsocketTransport, + request: Call, + request_id: RequestId, +) -> Result { + let mut tx = transport.request_handler.tx.clone(); + tx.send((request_id, request)).await.expect("TODO"); - while let Some(response) = rx.next().await { - if let Some(id) = response.get("id") { - if id.as_u64().unwrap_or_default() as usize == request_id { - return Ok(response); - } - } + // TODO: Timeout + loop { + if let Some(response) = transport.responses.lock().await.remove(&request_id) { + return Ok(response); } - - Err(Error::Internal) + Timer::sleep(1.).await } + + Err(Error::Internal) } impl Transport for WebsocketTransport { @@ -171,17 +175,6 @@ impl Transport for WebsocketTransport { } fn send(&self, id: RequestId, request: Call) -> Self::Out { - // send this request to another thread that continiously handles ws connection as a - // background task; filter the responses with the given ID and return its reponse data. - todo!() + Box::pin(rpc_send_and_receive(self.clone(), request, id)) } } - -async fn send_request( - request: Call, - client: Arc, - event_handlers: Vec, - gui_auth_validation_generator: Option, -) -> Result { - todo!() -} From 059971e1d6747174fe37c080f51742b52d281ed4 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 31 Jan 2024 16:00:01 +0300 Subject: [PATCH 11/53] improve websocket_transport performance by a lot Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 5 +- mm2src/coins/eth/web3_transport/mod.rs | 2 +- .../eth/web3_transport/websocket_transport.rs | 65 +++++++++++++------ 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index a636660037..cea47ce923 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -30,7 +30,6 @@ use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError, Timer}; -use common::executor::{AbortSettings, SpawnAbortable}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -486,9 +485,9 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { } impl EthCoinImpl { - pub(crate) fn web3(&self) -> Web3 { + pub(crate) fn web3(&self) -> &Web3 { // TODO - self.web3_instances.first().unwrap().web3.clone() + &self.web3_instances[0].web3 } /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 2892e988ce..49039f4d3e 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -1,4 +1,4 @@ -use crate::{MmCoin, RpcTransportEventHandlerShared}; +use crate::RpcTransportEventHandlerShared; use common::executor::{AbortSettings, SpawnAbortable}; use ethereum_types::U256; use futures::future::BoxFuture; diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 5f0b098a74..7ac7b49619 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,12 +1,11 @@ #![allow(unused)] // TODO: remove this -// TODO: Reduce the lock overhead - use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; use common::executor::Timer; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; @@ -40,22 +39,30 @@ pub struct WebsocketTransport { request_id: Arc, client: Arc, event_handlers: Vec, - responses: Arc>>, + // TODO: explain why this is used and how safe it is + responses: SafeMapPtr, request_handler: RequestHandler, } #[derive(Clone, Debug)] struct RequestHandler { - tx: UnboundedSender<(RequestId, Call)>, - rx: Arc>>, + tx: UnboundedSender, + rx: Arc>>, } -#[derive(Clone, Debug)] -struct ResponseHandler { - tx: UnboundedSender, - rx: Arc>>, +#[derive(Debug)] +struct WsRequest { + request: Call, + request_id: RequestId, + response_notifier: oneshot::Sender<()>, } +#[derive(Clone, Debug)] +struct SafeMapPtr(*mut HashMap); + +unsafe impl Send for SafeMapPtr {} +unsafe impl Sync for SafeMapPtr {} + impl WebsocketTransport { pub fn with_event_handlers( nodes: Vec, @@ -65,10 +72,12 @@ impl WebsocketTransport { let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); + let mut hashmap = HashMap::default(); + WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, - responses: Arc::new(AsyncMutex::new(HashMap::default())), + responses: SafeMapPtr(Box::into_raw(hashmap.into())), request_id: Arc::new(AtomicUsize::new(1)), request_handler: RequestHandler { tx: req_tx, @@ -78,6 +87,9 @@ impl WebsocketTransport { } pub async fn start_connection_loop(self) { + // TODO: clear disconnected channels every 30s or so. + let mut response_map: HashMap> = HashMap::new(); + for node in (*self.client.0.lock().await).nodes.clone() { let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { Ok(ws) => ws, @@ -104,10 +116,12 @@ impl WebsocketTransport { } request = req_rx.next().fuse() => { - if let Some((request_id, request)) = request { + if let Some( WsRequest { request_id, request, response_notifier }) = request { let serialized_request = to_string(&request); + response_map.insert(request_id, response_notifier); if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { log::error!("{e}"); + let _ = response_map.remove(&request_id); // TODO: try couple times at least before continue continue; } @@ -124,7 +138,12 @@ impl WebsocketTransport { if let Some(id) = inc_event.get("id") { let request_id = id.as_u64().unwrap_or_default() as usize; - let _ = self.responses.lock().await.insert(request_id, inc_event.get("result").expect("TODO").clone()); + + if let Some(notifier) = response_map.remove(&request_id) { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); + notifier.send(()).expect("TODO"); + } } } }, @@ -151,15 +170,23 @@ async fn rpc_send_and_receive( request_id: RequestId, ) -> Result { let mut tx = transport.request_handler.tx.clone(); - tx.send((request_id, request)).await.expect("TODO"); - // TODO: Timeout - loop { - if let Some(response) = transport.responses.lock().await.remove(&request_id) { - return Ok(response); + let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); + + tx.send(WsRequest { + request_id, + request, + response_notifier: notification_sender, + }) + .await + .expect("TODO"); + + if let Ok(_ping) = notification_receiver.await { + let response_map = unsafe { &mut *transport.responses.0 }; + if let Some(response) = response_map.remove(&request_id) { + return Ok(response.clone()); } - Timer::sleep(1.).await - } + }; Err(Error::Internal) } From d770af993485c0d43c702460ccb3b38a8d601ce0 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 31 Jan 2024 16:24:51 +0300 Subject: [PATCH 12/53] add some doc-comments Signed-off-by: onur-ozkan --- mm2src/coins/eth/v2_activation.rs | 2 +- .../eth/web3_transport/websocket_transport.rs | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 1af1ff1a9c..4cc3ca94ea 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -155,7 +155,7 @@ impl EthCoin { let conf = coin_conf(&ctx, &ticker); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => get_token_decimals(&self.web3(), protocol.token_addr) + None | Some(0) => get_token_decimals(self.web3(), protocol.token_addr) .await .map_err(Erc20TokenActivationError::InternalError)?, Some(d) => d as u8, diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 7ac7b49619..f082c51774 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,8 +1,11 @@ -#![allow(unused)] // TODO: remove this +//! This module offers a transport layer for managing request-response style communication +//! with Ethereum nodes using websockets in a wait and lock-free manner. In comparison to +//! HTTP transport, this approach proves to be much quicker (low-latency) and consumes less +//! bandwidth. This efficiency is achieved by avoiding the handling of TCP +//! handshakes (connection reusability) for each request. use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; -use common::executor::Timer; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::oneshot; @@ -11,8 +14,6 @@ use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; use instant::Duration; use jsonrpc_core::Call; -use mm2_net::transport::GuiAuthValidationGenerator; -use parking_lot::RwLock; use std::collections::{HashMap, HashSet}; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; @@ -39,7 +40,6 @@ pub struct WebsocketTransport { request_id: Arc, client: Arc, event_handlers: Vec, - // TODO: explain why this is used and how safe it is responses: SafeMapPtr, request_handler: RequestHandler, } @@ -57,6 +57,14 @@ struct WsRequest { response_notifier: oneshot::Sender<()>, } +/// A wrapper type for raw pointers used as a mutable Send & Sync HashMap reference. +/// +/// Safety notes: +/// +/// The implemented algorithm for socket request-response is already thread-safe, +/// so we don't care about race conditions. +/// +/// TODO: handle deallocation #[derive(Clone, Debug)] struct SafeMapPtr(*mut HashMap); @@ -69,10 +77,8 @@ impl WebsocketTransport { event_handlers: Vec, ) -> Self { let client_impl = WebsocketTransportRpcClientImpl { nodes }; - let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); - - let mut hashmap = HashMap::default(); + let hashmap = HashMap::default(); WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), @@ -90,7 +96,7 @@ impl WebsocketTransport { // TODO: clear disconnected channels every 30s or so. let mut response_map: HashMap> = HashMap::new(); - for node in (*self.client.0.lock().await).nodes.clone() { + for node in self.client.0.lock().await.nodes.clone() { let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { Ok(ws) => ws, Err(e) => { @@ -164,7 +170,7 @@ impl WebsocketTransport { async fn stop_connection(self) { todo!() } } -async fn rpc_send_and_receive( +async fn send_request( transport: WebsocketTransport, request: Call, request_id: RequestId, @@ -184,7 +190,7 @@ async fn rpc_send_and_receive( if let Ok(_ping) = notification_receiver.await { let response_map = unsafe { &mut *transport.responses.0 }; if let Some(response) = response_map.remove(&request_id) { - return Ok(response.clone()); + return Ok(response); } }; @@ -201,7 +207,5 @@ impl Transport for WebsocketTransport { (request_id, request) } - fn send(&self, id: RequestId, request: Call) -> Self::Out { - Box::pin(rpc_send_and_receive(self.clone(), request, id)) - } + fn send(&self, id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(self.clone(), request, id)) } } From f89191ebe7a80d4fcd65f3b38090f29d8bf1d96b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 31 Jan 2024 16:52:58 +0300 Subject: [PATCH 13/53] make the connection loop continious Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index f082c51774..2f5acf50ea 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -64,7 +64,7 @@ struct WsRequest { /// The implemented algorithm for socket request-response is already thread-safe, /// so we don't care about race conditions. /// -/// TODO: handle deallocation +/// TODO: gracefully deallocations #[derive(Clone, Debug)] struct SafeMapPtr(*mut HashMap); @@ -96,70 +96,72 @@ impl WebsocketTransport { // TODO: clear disconnected channels every 30s or so. let mut response_map: HashMap> = HashMap::new(); - for node in self.client.0.lock().await.nodes.clone() { - let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { - Ok(ws) => ws, - Err(e) => { - log::error!("{e}"); - continue; - }, - }; - - // id list of awaiting requests - let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); - let mut req_rx = self.request_handler.rx.lock().await; - - loop { - futures_util::select! { - _ = keepalive_interval.next().fuse() => { - const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; - - if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { - log::error!("{e}"); - // TODO: try couple times at least before continue - continue; - } - } - - request = req_rx.next().fuse() => { - if let Some( WsRequest { request_id, request, response_notifier }) = request { - let serialized_request = to_string(&request); - response_map.insert(request_id, response_notifier); - if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { + loop { + for node in self.client.0.lock().await.nodes.clone() { + let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { + Ok(ws) => ws, + Err(e) => { + log::error!("{e}"); + continue; + }, + }; + + // id list of awaiting requests + let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); + let mut req_rx = self.request_handler.rx.lock().await; + + loop { + futures_util::select! { + _ = keepalive_interval.next().fuse() => { + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { log::error!("{e}"); - let _ = response_map.remove(&request_id); // TODO: try couple times at least before continue continue; } } - } - message = wsocket.next().fuse() => { - match message { - Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { - if let Ok(inc_event) = serde_json::from_str::(&inc_event) { - if !inc_event.is_object() { - continue; - } + request = req_rx.next().fuse() => { + if let Some( WsRequest { request_id, request, response_notifier }) = request { + let serialized_request = to_string(&request); + response_map.insert(request_id, response_notifier); + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { + log::error!("{e}"); + let _ = response_map.remove(&request_id); + // TODO: try couple times at least before continue + continue; + } + } + } + + message = wsocket.next().fuse() => { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + continue; + } - if let Some(id) = inc_event.get("id") { - let request_id = id.as_u64().unwrap_or_default() as usize; + if let Some(id) = inc_event.get("id") { + let request_id = id.as_u64().unwrap_or_default() as usize; - if let Some(notifier) = response_map.remove(&request_id) { - let response_map = unsafe { &mut *self.responses.0 }; - let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); - notifier.send(()).expect("TODO"); + if let Some(notifier) = response_map.remove(&request_id) { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); + notifier.send(()).expect("TODO"); + } } } - } - }, - Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => todo!(), - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, - Some(Err(e)) => { - log::error!("{e}"); - break; - }, - None => continue, + }, + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => todo!(), + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, + Some(Err(e)) => { + log::error!("{e}"); + break; + }, + None => continue, + } } } } From d336dbfa6d6f66d971b79c9fb643cc1f3f60563d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 1 Feb 2024 17:30:29 +0300 Subject: [PATCH 14/53] implement memory deallication on map pointer Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 2f5acf50ea..7619230982 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -14,7 +14,7 @@ use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; use instant::Duration; use jsonrpc_core::Call; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use web3::error::Error; @@ -41,13 +41,18 @@ pub struct WebsocketTransport { client: Arc, event_handlers: Vec, responses: SafeMapPtr, - request_handler: RequestHandler, + controller_channel: ControllerChannel, } #[derive(Clone, Debug)] -struct RequestHandler { - tx: UnboundedSender, - rx: Arc>>, +struct ControllerChannel { + tx: UnboundedSender, + rx: Arc>>, +} + +enum ControllerMessage { + Request(WsRequest), + Close, } #[derive(Debug)] @@ -64,9 +69,24 @@ struct WsRequest { /// The implemented algorithm for socket request-response is already thread-safe, /// so we don't care about race conditions. /// -/// TODO: gracefully deallocations +/// As for deallocations, we use a mutex as a pointer guard in the socket connection. +/// Whenever it is no longer held there and `drop` is called for `WebsocketTransport`, +/// this means that the pointer is no longer in use, so we can switch raw pointer into +/// a smart pointer (`Box`) for letting compiler to clean up the memory. #[derive(Clone, Debug)] -struct SafeMapPtr(*mut HashMap); +struct SafeMapPtr { + ptr: *mut HashMap, + guard: Arc>, +} + +impl Drop for WebsocketTransport { + fn drop(&mut self) { + if !self.responses.guard.try_lock().is_none() { + // let the compiler do the job + let _ = unsafe { Box::from_raw(self.responses.ptr) }; + } + } +} unsafe impl Send for SafeMapPtr {} unsafe impl Sync for SafeMapPtr {} @@ -83,9 +103,12 @@ impl WebsocketTransport { WebsocketTransport { client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), event_handlers, - responses: SafeMapPtr(Box::into_raw(hashmap.into())), + responses: SafeMapPtr { + ptr: Box::into_raw(hashmap.into()), + guard: Arc::new(AsyncMutex::new(())), + }, request_id: Arc::new(AtomicUsize::new(1)), - request_handler: RequestHandler { + controller_channel: ControllerChannel { tx: req_tx, rx: Arc::new(AsyncMutex::new(req_rx)), }, @@ -93,6 +116,7 @@ impl WebsocketTransport { } pub async fn start_connection_loop(self) { + let _ptr_guard = self.responses.guard.lock().await; // TODO: clear disconnected channels every 30s or so. let mut response_map: HashMap> = HashMap::new(); @@ -108,7 +132,7 @@ impl WebsocketTransport { // id list of awaiting requests let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); - let mut req_rx = self.request_handler.rx.lock().await; + let mut req_rx = self.controller_channel.rx.lock().await; loop { futures_util::select! { @@ -123,15 +147,19 @@ impl WebsocketTransport { } request = req_rx.next().fuse() => { - if let Some( WsRequest { request_id, request, response_notifier }) = request { - let serialized_request = to_string(&request); - response_map.insert(request_id, response_notifier); - if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { - log::error!("{e}"); - let _ = response_map.remove(&request_id); - // TODO: try couple times at least before continue - continue; - } + match request { + Some(ControllerMessage::Request(WsRequest { request_id, request, response_notifier })) => { + let serialized_request = to_string(&request); + response_map.insert(request_id, response_notifier); + if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { + log::error!("{e}"); + let _ = response_map.remove(&request_id); + // TODO: try couple times at least before continue + continue; + } + }, + Some(ControllerMessage::Close) => return, + _ => {}, } } @@ -147,7 +175,7 @@ impl WebsocketTransport { let request_id = id.as_u64().unwrap_or_default() as usize; if let Some(notifier) = response_map.remove(&request_id) { - let response_map = unsafe { &mut *self.responses.0 }; + let response_map = unsafe { &mut *self.responses.ptr }; let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); notifier.send(()).expect("TODO"); } @@ -169,7 +197,11 @@ impl WebsocketTransport { } } - async fn stop_connection(self) { todo!() } + async fn stop_connection(self) { + let mut tx = self.controller_channel.tx.clone(); + tx.send(ControllerMessage::Close).await.expect("TODO"); + let _ = unsafe { Box::from_raw(self.responses.ptr) }; + } } async fn send_request( @@ -177,20 +209,20 @@ async fn send_request( request: Call, request_id: RequestId, ) -> Result { - let mut tx = transport.request_handler.tx.clone(); + let mut tx = transport.controller_channel.tx.clone(); let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); - tx.send(WsRequest { + tx.send(ControllerMessage::Request(WsRequest { request_id, request, response_notifier: notification_sender, - }) + })) .await .expect("TODO"); if let Ok(_ping) = notification_receiver.await { - let response_map = unsafe { &mut *transport.responses.0 }; + let response_map = unsafe { &mut *transport.responses.ptr }; if let Some(response) = response_map.remove(&request_id) { return Ok(response); } From 73d71fb5abba55616a1f837d8fdc4c2750ea0093 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 1 Feb 2024 17:32:03 +0300 Subject: [PATCH 15/53] add todo note Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 7619230982..e0a023eb1b 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -221,6 +221,7 @@ async fn send_request( .await .expect("TODO"); + // TODO: we need timeout here if let Ok(_ping) = notification_receiver.await { let response_map = unsafe { &mut *transport.responses.ptr }; if let Some(response) = response_map.remove(&request_id) { From 1e0cba426bab96ac98b95c7c9dbf48eb2ac73e11 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 2 Feb 2024 12:31:59 +0300 Subject: [PATCH 16/53] implement `ExpirableEntry` common module Signed-off-by: onur-ozkan --- Cargo.lock | 1 + mm2src/common/Cargo.toml | 1 + mm2src/common/common.rs | 1 + mm2src/common/expirable_map.rs | 109 +++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 mm2src/common/expirable_map.rs diff --git a/Cargo.lock b/Cargo.lock index fecdc4d2ab..3d672a38c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ dependencies = [ "primitive-types", "rand 0.7.3", "regex", + "rustc-hash", "ser_error", "ser_error_derive", "serde", diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 47608b2b02..4637818b35 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -35,6 +35,7 @@ parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } +rustc-hash = "1.1.0" regex = "1" serde = "1" serde_derive = "1" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 79132a6d39..6a89bfeff7 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -128,6 +128,7 @@ pub mod crash_reports; pub mod custom_futures; pub mod custom_iter; #[path = "executor/mod.rs"] pub mod executor; +pub mod expirable_map; pub mod number_type_casting; pub mod password_policy; pub mod seri; diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs new file mode 100644 index 0000000000..7fa7ac8537 --- /dev/null +++ b/mm2src/common/expirable_map.rs @@ -0,0 +1,109 @@ +//! This module provides a cross-compatible map that associates values with keys and supports expiring entries. +//! +//! Designed for performance-oriented use-cases utilizing `FxHashMap` under the hood, +//! and is not suitable for cryptographic purposes. + +use instant::Duration; +use rustc_hash::FxHashMap; +use std::hash::Hash; + +use crate::get_local_duration_since_epoch; + +#[derive(Clone, Debug)] +struct ExpirableEntry { + exp: Duration, + created_at: Duration, + value: V, +} + +impl Default for ExpirableMap { + fn default() -> Self { Self::new() } +} + +/// A map that allows associating values with keys and expiring entries. +/// It is important to note that this implementation does not automatically +/// remove any entries; it is the caller's responsibility to invoke `clear_expired_entries` +/// at specified intervals. +/// +/// WARNING: This is designed for performance-oriented use-cases utilizing `FxHashMap` +/// under the hood and is not suitable for cryptographic purposes. +#[derive(Clone, Debug)] +pub struct ExpirableMap(FxHashMap>); + +impl ExpirableMap { + /// Creates a new empty `ExpirableMap` + #[inline] + pub fn new() -> Self { Self(FxHashMap::default()) } + + /// Inserts a key-value pair with an expiration duration. + /// + /// If a value already exists for the given key, it will be updated and then + /// the old one will be returned. + pub fn insert(&mut self, k: K, v: V, exp: Duration) -> Option { + let entry = ExpirableEntry { + exp, + created_at: get_local_duration_since_epoch().expect("Clock system is broken in the operating system."), + value: v, + }; + + self.0.insert(k, entry).map(|v| v.value) + } + + /// Removes expired entries from the map. + pub fn clear_expired_entries(&mut self) { + self.0.retain(|_k, v| { + let now = get_local_duration_since_epoch().expect("Clock system is broken in the operating system."); + (now - v.created_at) < v.exp + }); + } + + // Removes a key-value pair from the map and returns the associated value, if present. + #[inline] + pub fn remove(&mut self, k: &K) -> Option { self.0.remove(k).map(|v| v.value) } +} + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::cross_test; + + crate::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_clear_expired_entries, { + let mut expirable_map = ExpirableMap::new(); + let value = "test_value"; + let exp = Duration::from_secs(1); + + // Insert 2 entries with 1 sec expiration time + expirable_map.insert("key1".to_string(), value.to_string(), exp); + expirable_map.insert("key2".to_string(), value.to_string(), exp); + + // Wait for entries to expire + std::thread::sleep(Duration::from_secs(2)); + + // Clear expired entries + expirable_map.clear_expired_entries(); + + // We waited for 2 seconds, so we shouldn't have any entry accessible + assert_eq!(expirable_map.0.len(), 0); + + // Insert 5 entries + expirable_map.insert("key1".to_string(), value.to_string(), Duration::from_secs(5)); + expirable_map.insert("key2".to_string(), value.to_string(), Duration::from_secs(4)); + expirable_map.insert("key3".to_string(), value.to_string(), Duration::from_secs(7)); + expirable_map.insert("key4".to_string(), value.to_string(), Duration::from_secs(2)); + expirable_map.insert("key5".to_string(), value.to_string(), Duration::from_millis(3750)); + + // Wait 2 seconds to expire some entries + std::thread::sleep(Duration::from_secs(2)); + + // Clear expired entries + expirable_map.clear_expired_entries(); + + // We waited for 2 seconds, only one entry should expire + assert_eq!(expirable_map.0.len(), 4); + }); +} From 95152b59b449fd11a9dada6fe514159f53aba25a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 2 Feb 2024 12:32:33 +0300 Subject: [PATCH 17/53] fixed-TODO: clear disconnected/outdated channels Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index e0a023eb1b..4d4af36223 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -6,6 +6,7 @@ use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; +use common::expirable_map::ExpirableMap; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::oneshot; @@ -21,6 +22,8 @@ use web3::error::Error; use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; +const REQUEST_TIMEOUT_AS_SEC: u64 = 10; + #[derive(Clone, Debug)] pub struct WebsocketTransportNode { pub(crate) uri: http::Uri, @@ -81,7 +84,7 @@ struct SafeMapPtr { impl Drop for WebsocketTransport { fn drop(&mut self) { - if !self.responses.guard.try_lock().is_none() { + if self.responses.guard.try_lock().is_some() { // let the compiler do the job let _ = unsafe { Box::from_raw(self.responses.ptr) }; } @@ -117,8 +120,7 @@ impl WebsocketTransport { pub async fn start_connection_loop(self) { let _ptr_guard = self.responses.guard.lock().await; - // TODO: clear disconnected channels every 30s or so. - let mut response_map: HashMap> = HashMap::new(); + let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); loop { for node in self.client.0.lock().await.nodes.clone() { @@ -137,8 +139,10 @@ impl WebsocketTransport { loop { futures_util::select! { _ = keepalive_interval.next().fuse() => { - const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + // Drop expired response notifier channels + response_notifiers.clear_expired_entries(); + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { log::error!("{e}"); // TODO: try couple times at least before continue @@ -150,10 +154,10 @@ impl WebsocketTransport { match request { Some(ControllerMessage::Request(WsRequest { request_id, request, response_notifier })) => { let serialized_request = to_string(&request); - response_map.insert(request_id, response_notifier); + response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { log::error!("{e}"); - let _ = response_map.remove(&request_id); + let _ = response_notifiers.remove(&request_id); // TODO: try couple times at least before continue continue; } @@ -174,7 +178,7 @@ impl WebsocketTransport { if let Some(id) = inc_event.get("id") { let request_id = id.as_u64().unwrap_or_default() as usize; - if let Some(notifier) = response_map.remove(&request_id) { + if let Some(notifier) = response_notifiers.remove(&request_id) { let response_map = unsafe { &mut *self.responses.ptr }; let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); notifier.send(()).expect("TODO"); From ca790a478e855a3bf5e4a77b377aaf4327ce3d98 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 2 Feb 2024 14:18:06 +0300 Subject: [PATCH 18/53] fix comma Signed-off-by: onur-ozkan --- mm2src/common/expirable_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 7fa7ac8537..8897267e1a 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -57,7 +57,7 @@ impl ExpirableMap { }); } - // Removes a key-value pair from the map and returns the associated value, if present. + // Removes a key-value pair from the map and returns the associated value if present. #[inline] pub fn remove(&mut self, k: &K) -> Option { self.0.remove(k).map(|v| v.value) } } From 648832c984f3519f7e2aae8612b7c52eaade66f3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 5 Feb 2024 15:52:54 +0300 Subject: [PATCH 19/53] add client rotation on eth Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 520 ++++++++++++------ mm2src/coins/eth/eth_tests.rs | 76 ++- mm2src/coins/eth/eth_wasm_tests.rs | 4 +- mm2src/coins/eth/v2_activation.rs | 24 +- mm2src/coins/nft.rs | 4 +- .../src/erc20_token_activation.rs | 3 +- .../src/eth_with_token_activation.rs | 3 +- 7 files changed, 422 insertions(+), 212 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index cea47ce923..8948f3e4cc 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -24,7 +24,7 @@ use super::eth::Action::{Call, Create}; use crate::eth::web3_transport::websocket_transport::WebsocketTransportNode; use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; -use crate::{DexFee, ValidateWatcherSpendInput, WatcherSpendType}; +use crate::{DexFee, RpcCommonOps, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; @@ -70,6 +70,7 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; @@ -426,7 +427,7 @@ pub struct EthCoinImpl { fallback_swap_contract: Option
, contract_supports_watchers: bool, /// The separate web3 instances kept to get nonce, will replace the web3 completely soon - web3_instances: Vec, + client: EthClient, decimals: u8, gas_station_url: Option, gas_station_decimals: u8, @@ -446,6 +447,10 @@ pub struct EthCoinImpl { pub abortable_system: AbortableQueue, } +struct EthClient { + web3_instances: AsyncMutex>, +} + #[derive(Clone, Debug)] pub struct Web3Instance { web3: Web3, @@ -484,71 +489,49 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { Ok(result) } -impl EthCoinImpl { - pub(crate) fn web3(&self) -> &Web3 { - // TODO - &self.web3_instances[0].web3 - } - - /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` - fn erc20_transfer_events( - &self, - contract: Address, - from_addr: Option
, - to_addr: Option
, - from_block: BlockNumber, - to_block: BlockNumber, - limit: Option, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(ERC20_CONTRACT.event("Transfer")); - let topic0 = Some(vec![contract_event.signature()]); - let topic1 = from_addr.map(|addr| vec![addr.into()]); - let topic2 = to_addr.map(|addr| vec![addr.into()]); - let mut filter = FilterBuilder::default() - .topics(topic0, topic1, topic2, None) - .from_block(from_block) - .to_block(to_block) - .address(vec![contract]); - - if let Some(l) = limit { - filter = filter.limit(l); +#[async_trait] +impl RpcCommonOps for EthCoinImpl { + type RpcClient = Web3Instance; + type Error = Web3RpcError; + + async fn get_live_client(&self) -> Result { + let mut clients = self.client.web3_instances.lock().await; + + // try to find first live client + for (i, client) in clients.clone().into_iter().enumerate() { + match client + .web3 + .web3() + .client_version() + .timeout(Duration::from_secs(15)) + .await + { + Ok(Ok(_)) => { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(client); + }, + Ok(Err(rpc_error)) => { + debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); + }, + Err(timeout_error) => { + debug!( + "Client version timeout exceed on: {:?}. Error: {}", + &client, timeout_error + ); + }, + }; } - Box::new( - self.web3() - .eth() - .logs(filter.build()) - .compat() - .map_err(|e| ERRL!("{}", e)), - ) + return Err(Web3RpcError::Transport( + "All the current rpc nodes are unavailable.".to_string(), + )); } +} - /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` - fn eth_traces( - &self, - from_addr: Vec
, - to_addr: Vec
, - from_block: BlockNumber, - to_block: BlockNumber, - limit: Option, - ) -> Box, Error = String> + Send> { - let mut filter = TraceFilterBuilder::default() - .from_address(from_addr) - .to_address(to_addr) - .from_block(from_block) - .to_block(to_block); - - if let Some(l) = limit { - filter = filter.count(l); - } - - Box::new( - self.web3() - .trace() - .filter(filter.build()) - .compat() - .map_err(|e| ERRL!("{}", e)), - ) +impl EthCoinImpl { + pub(crate) async fn web3(&self) -> Result, Web3RpcError> { + self.get_live_client().await.map(|t| t.web3) } #[cfg(not(target_arch = "wasm32"))] @@ -647,24 +630,6 @@ impl EthCoinImpl { sha256(&input).to_vec() } - /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` - fn refund_events( - &self, - swap_contract_address: Address, - from_block: u64, - to_block: u64, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); - let filter = FilterBuilder::default() - .topics(Some(vec![contract_event.signature()]), None, None, None) - .from_block(BlockNumber::Number(from_block.into())) - .to_block(BlockNumber::Number(to_block.into())) - .address(vec![swap_contract_address]) - .build(); - - Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) - } - /// Try to parse address from string. pub fn address_from_str(&self, address: &str) -> Result { Ok(try_s!(valid_addr_from_str(address))) @@ -702,6 +667,7 @@ async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransactionResult { let web3_tx = coin .web3() + .await? .eth() .transaction(TransactionId::Hash(tx_hash)) .await? @@ -783,7 +749,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { // Todo: nonce_lock is still global for all addresses but this needs to be per address let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(my_address, coin.web3_instances.clone()) + let (nonce, _) = get_addr_nonce(my_address, coin.client.web3_instances.lock().await.to_vec()) .compat() .timeout_secs(30.) .await? @@ -832,7 +798,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = coin.web3().eth().send_transaction(tx_to_send).await?; + let tx_hash = coin.web3().await?.eth().send_transaction(tx_to_send).await?; let signed_tx = coin .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -943,11 +909,14 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; + let (nonce, _) = get_addr_nonce( + eth_coin.my_address, + eth_coin.client.web3_instances.lock().await.to_vec(), + ) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; let tx = UnSignedEthTx { nonce, @@ -1018,11 +987,14 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; + let (nonce, _) = get_addr_nonce( + eth_coin.my_address, + eth_coin.client.web3_instances.lock().await.to_vec(), + ) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; let tx = UnSignedEthTx { nonce, @@ -1189,8 +1161,7 @@ impl SwapOps for EthCoin { match found { Some(event) => { let transaction = try_s!( - selfi - .web3() + try_s!(selfi.web3().await) .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await @@ -1747,7 +1718,12 @@ impl WatcherOps for EthCoin { let decimals = self.decimals; let fut = async move { - let tx_from_rpc = selfi.web3().eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi + .web3() + .await? + .eth() + .transaction(TransactionId::Hash(tx.hash)) + .await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx)) @@ -2156,25 +2132,37 @@ impl MarketCoinOps for EthCoin { tx = &tx[2..]; } let bytes = try_fus!(hex::decode(tx)); - Box::new( - self.web3() + + let coin = self.clone(); + + let fut = async move { + let result = try_s!(coin.web3().await) .eth() .send_raw_transaction(bytes.into()) - .compat() + .await .map(|res| format!("{:02x}", res)) - .map_err(|e| ERRL!("{}", e)), - ) + .map_err(|e| ERRL!("{}", e)); + + result + }; + + Box::new(fut.boxed().compat()) } fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { - Box::new( - self.web3() + let coin = self.clone(); + + let tx = tx.to_owned(); + let fut = async move { + try_s!(coin.web3().await) .eth() .send_raw_transaction(tx.into()) - .compat() + .await .map(|res| format!("{:02x}", res)) - .map_err(|e| ERRL!("{}", e)), - ) + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { @@ -2344,7 +2332,11 @@ impl MarketCoinOps for EthCoin { if let Some(event) = found { if let Some(tx_hash) = event.transaction_hash { - let transaction = match selfi.web3().eth().transaction(TransactionId::Hash(tx_hash)).await { + let transaction = match try_tx_s!(selfi.web3().await) + .eth() + .transaction(TransactionId::Hash(tx_hash)) + .await + { Ok(Some(t)) => t, Ok(None) => { info!("Tx {} not found yet", tx_hash); @@ -2375,14 +2367,18 @@ impl MarketCoinOps for EthCoin { } fn current_block(&self) -> Box + Send> { - Box::new( - self.web3() + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) .eth() .block_number() - .compat() + .await .map(|res| res.as_u64()) - .map_err(|e| ERRL!("{}", e)), - ) + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } fn display_priv_key(&self) -> Result { @@ -2440,7 +2436,7 @@ async fn sign_transaction_with_keypair( let _nonce_lock = coin.nonce_lock.lock().await; status.status(tags!(), "get_addr_nonce…"); let (nonce, web3_instances_with_latest_nonce) = try_tx_s!( - get_addr_nonce(coin.my_address, coin.web3_instances.clone()) + get_addr_nonce(coin.my_address, coin.client.web3_instances.lock().await.to_vec()) .compat() .await ); @@ -2526,7 +2522,7 @@ async fn sign_and_send_transaction_with_metamask( // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = try_tx_s!(coin.web3().eth().send_transaction(tx_to_send).await); + let tx_hash = try_tx_s!(try_tx_s!(coin.web3().await).eth().send_transaction(tx_to_send).await); let maybe_signed_tx = try_tx_s!( coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -2578,6 +2574,103 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw } impl EthCoin { + /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` + fn refund_events( + &self, + swap_contract_address: Address, + from_block: u64, + to_block: u64, + ) -> Box, Error = String> + Send> { + let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); + let filter = FilterBuilder::default() + .topics(Some(vec![contract_event.signature()]), None, None, None) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) + .address(vec![swap_contract_address]) + .build(); + + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) + .eth() + .logs(filter) + .await + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) + } + + /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` + fn eth_traces( + &self, + from_addr: Vec
, + to_addr: Vec
, + from_block: BlockNumber, + to_block: BlockNumber, + limit: Option, + ) -> Box, Error = String> + Send> { + let mut filter = TraceFilterBuilder::default() + .from_address(from_addr) + .to_address(to_addr) + .from_block(from_block) + .to_block(to_block); + + if let Some(l) = limit { + filter = filter.count(l); + } + + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) + .trace() + .filter(filter.build()) + .await + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) + } + + /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` + fn erc20_transfer_events( + &self, + contract: Address, + from_addr: Option
, + to_addr: Option
, + from_block: BlockNumber, + to_block: BlockNumber, + limit: Option, + ) -> Box, Error = String> + Send> { + let contract_event = try_fus!(ERC20_CONTRACT.event("Transfer")); + let topic0 = Some(vec![contract_event.signature()]); + let topic1 = from_addr.map(|addr| vec![addr.into()]); + let topic2 = to_addr.map(|addr| vec![addr.into()]); + let mut filter = FilterBuilder::default() + .topics(topic0, topic1, topic2, None) + .from_block(from_block) + .to_block(to_block) + .address(vec![contract]); + + if let Some(l) = limit { + filter = filter.limit(l); + } + + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) + .eth() + .logs(filter.build()) + .await + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) + } + /// Downloads and saves ETH transaction history of my_address, relies on Parity trace_filter API /// https://wiki.parity.io/JSONRPC-trace-module#trace_filter, this requires tracing to be enabled /// in node config. Other ETH clients (Geth, etc.) are `not` supported (yet). @@ -2603,7 +2696,20 @@ impl EthCoin { }; } - let current_block = match self.web3().eth().block_number().await { + let web3 = match self.web3().await { + Ok(t) => t, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Couldn't connect to client. Error: {} - retrying..", e), + ); + Timer::sleep(3.).await; + continue; + }, + }; + + let current_block = match web3.eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -2785,8 +2891,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); - let web3_tx = match self - .web3() + let web3_tx = match web3 .eth() .transaction(TransactionId::Hash(trace.transaction_hash.unwrap())) .await @@ -2819,12 +2924,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); - let receipt = match self - .web3() - .eth() - .transaction_receipt(trace.transaction_hash.unwrap()) - .await - { + let receipt = match web3.eth().transaction_receipt(trace.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -2877,8 +2977,7 @@ impl EthCoin { } let raw = signed_tx_from_web3_tx(web3_tx).unwrap(); - let block = match self - .web3() + let block = match web3 .eth() .block(BlockId::Number(BlockNumber::Number(trace.block_number.into()))) .await @@ -2962,7 +3061,20 @@ impl EthCoin { }; } - let current_block = match self.web3().eth().block_number().await { + let web3 = match self.web3().await { + Ok(t) => t, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Couldn't connect to client. Error: {} - retrying..", e), + ); + Timer::sleep(3.).await; + continue; + }, + }; + + let current_block = match web3.eth().block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -3164,8 +3276,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "client" => "ethereum", "method" => "tx_detail_by_hash"); - let web3_tx = match self - .web3() + let web3_tx = match web3 .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await @@ -3200,12 +3311,7 @@ impl EthCoin { }, }; - let receipt = match self - .web3() - .eth() - .transaction_receipt(event.transaction_hash.unwrap()) - .await - { + let receipt = match web3.eth().transaction_receipt(event.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -3236,8 +3342,7 @@ impl EthCoin { None => None, }; let block_number = event.block_number.unwrap(); - let block = match self - .web3() + let block = match web3 .eth() .block(BlockId::Number(BlockNumber::Number(block_number))) .await @@ -3964,7 +4069,12 @@ impl EthCoin { let coin = self.clone(); let fut = async move { match coin.coin_type { - EthCoinType::Eth => Ok(coin.web3().eth().balance(address, Some(BlockNumber::Latest)).await?), + EthCoinType::Eth => Ok(coin + .web3() + .await? + .eth() + .balance(address, Some(BlockNumber::Latest)) + .await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; @@ -4019,8 +4129,19 @@ impl EthCoin { } fn estimate_gas(&self, req: CallRequest) -> Box + Send> { + let coin = self.clone(); + // always using None block number as old Geth version accept only single argument in this RPC - Box::new(self.web3().eth().estimate_gas(req, None).compat()) + let fut = async move { + coin.web3() + .await + .map_err(|_| web3::Error::Unreachable)? + .eth() + .estimate_gas(req, None) + .await + }; + + Box::new(fut.boxed().compat()) } /// Estimates how much gas is necessary to allow the contract call to complete. @@ -4053,13 +4174,18 @@ impl EthCoin { } fn eth_balance(&self) -> BalanceFut { - Box::new( - self.web3() + let coin = self.clone(); + + let fut = async move { + coin.web3() + .await + .map_err(|_| web3::Error::Unreachable)? .eth() - .balance(self.my_address, Some(BlockNumber::Latest)) - .compat() - .map_to_mm_fut(BalanceError::from), - ) + .balance(coin.my_address, Some(BlockNumber::Latest)) + .await + }; + + Box::new(fut.boxed().compat().map_to_mm_fut(BalanceError::from)) } async fn call_request(&self, to: Address, value: Option, data: Option) -> Result { @@ -4074,6 +4200,8 @@ impl EthCoin { }; self.web3() + .await + .map_err(|_| web3::Error::Unreachable)? .eth() .call(request, Some(BlockId::Number(BlockNumber::Latest))) .await @@ -4178,7 +4306,17 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) + .eth() + .logs(filter) + .await + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` @@ -4196,7 +4334,17 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3().eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + let coin = self.clone(); + + let fut = async move { + try_s!(coin.web3().await) + .eth() + .logs(filter) + .await + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } fn validate_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -4237,7 +4385,12 @@ impl EthCoin { ))); } - let tx_from_rpc = selfi.web3().eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi + .web3() + .await? + .eth() + .transaction(TransactionId::Hash(tx.hash)) + .await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.hash)) })?; @@ -4527,16 +4680,17 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = - match try_s!(self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await) { - Some(t) => t, - None => { - return ERR!( - "Found ReceiverSpent event, but transaction {:02x} is missing", - tx_hash - ) - }, - }; + let transaction = match try_s!( + try_s!(self.web3().await) + .eth() + .transaction(TransactionId::Hash(tx_hash)) + .await + ) { + Some(t) => t, + None => { + return ERR!("Found ReceiverSpent event, but transaction {:02x} is missing", tx_hash) + }, + }; return Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -4556,16 +4710,17 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = - match try_s!(self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await) { - Some(t) => t, - None => { - return ERR!( - "Found SenderRefunded event, but transaction {:02x} is missing", - tx_hash - ) - }, - }; + let transaction = match try_s!( + try_s!(self.web3().await) + .eth() + .transaction(TransactionId::Hash(tx_hash)) + .await + ) { + Some(t) => t, + None => { + return ERR!("Found SenderRefunded event, but transaction {:02x} is missing", tx_hash) + }, + }; return Ok(Some(FoundSwapTxSpend::Refunded(TransactionEnum::from(try_s!( signed_tx_from_web3_tx(transaction) @@ -4618,7 +4773,7 @@ impl EthCoin { None => None, }; - let eth_gas_price = match coin.web3().eth().gas_price().await { + let eth_gas_price = match coin.web3().await?.eth().gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); @@ -4626,7 +4781,7 @@ impl EthCoin { }, }; - let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3().api(); + let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3().await?.api(); let eth_fee_history_price = match fee_history_namespace .eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]) .await @@ -4663,7 +4818,10 @@ impl EthCoin { /// The function is endless, we just keep looping in case of a transport error hoping it will go away. async fn wait_for_addr_nonce_increase(&self, addr: Address, prev_nonce: U256) { repeatable!(async { - match get_addr_nonce(addr, self.web3_instances.clone()).compat().await { + match get_addr_nonce(addr, self.client.web3_instances.lock().await.to_vec()) + .compat() + .await + { Ok((new_nonce, _)) if new_nonce > prev_nonce => Ready(()), Ok((_nonce, _)) => Retry(()), Err(e) => { @@ -4688,7 +4846,12 @@ impl EthCoin { ) -> Web3RpcResult> { let wait_until = wait_until_ms(wait_rpc_timeout_ms); while now_ms() < wait_until { - let maybe_tx = self.web3().eth().transaction(TransactionId::Hash(tx_hash)).await?; + let maybe_tx = self + .web3() + .await? + .eth() + .transaction(TransactionId::Hash(tx_hash)) + .await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; return Ok(Some(signed_tx)); @@ -4717,7 +4880,7 @@ impl EthCoin { ))); } - let web3_receipt = match selfi.web3().eth().transaction_receipt(payment_hash).await { + let web3_receipt = match selfi.web3().await?.eth().transaction_receipt(payment_hash).await { Ok(r) => r, Err(e) => { error!( @@ -4765,7 +4928,7 @@ impl EthCoin { ))); } - match selfi.web3().eth().block_number().await { + match selfi.web3().await?.eth().block_number().await { Ok(current_block) => { if current_block >= block_number { break Ok(()); @@ -5166,7 +5329,12 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let fut = async move { let expected_value = wei_from_big_decimal(&amount, coin.decimals)?; - let tx_from_rpc = coin.web3().eth().transaction(TransactionId::Hash(fee_tx_hash)).await?; + let tx_from_rpc = coin + .web3() + .await? + .eth() + .transaction(TransactionId::Hash(fee_tx_hash)) + .await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", fee_tx_hash)) @@ -5610,7 +5778,9 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - web3_instances: web3_instances.clone(), + client: EthClient { + web3_instances: AsyncMutex::new(web3_instances), + }, history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index f63829b2f2..bd47b1970c 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -142,7 +142,9 @@ fn eth_coin_from_keypair( fallback_swap_contract, contract_supports_watchers: false, ticker, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -320,7 +322,9 @@ fn send_and_refund_erc20_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -402,7 +406,9 @@ fn send_and_refund_eth_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -496,20 +502,22 @@ fn test_nonce_several_urls() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![ - Web3Instance { - web3: web3_devnet, - is_parity: false, - }, - Web3Instance { - web3: web3_sepolia, - is_parity: false, - }, - Web3Instance { - web3: web3_failing, - is_parity: false, - }, - ], + client: EthClient { + web3_instances: AsyncMutex::new(vec![ + Web3Instance { + web3: web3_devnet, + is_parity: false, + }, + Web3Instance { + web3: web3_sepolia, + is_parity: false, + }, + Web3Instance { + web3: web3_failing, + is_parity: false, + }, + ]), + }, decimals: 18, gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -529,7 +537,7 @@ fn test_nonce_several_urls() { let payment = coin.send_to_address(coin.my_address, 200000000.into()).wait().unwrap(); log!("{:?}", payment); - let new_nonce = get_addr_nonce(coin.my_address, coin.web3_instances.clone()) + let new_nonce = get_addr_nonce(coin.my_address, block_on(coin.client.web3_instances.lock()).to_vec()) .wait() .unwrap(); log!("{:?}", new_nonce); @@ -562,7 +570,9 @@ fn test_wait_for_payment_spend_timeout() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -628,7 +638,9 @@ fn test_search_for_swap_tx_spend_was_spent() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -735,7 +747,9 @@ fn test_search_for_swap_tx_spend_was_refunded() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "BAT".into(), - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -1141,7 +1155,7 @@ fn validate_dex_fee_invalid_sender_eth() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1175,7 +1189,7 @@ fn validate_dex_fee_invalid_sender_erc() { ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1211,7 +1225,7 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1247,7 +1261,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3().eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1409,7 +1423,9 @@ fn test_message_hash() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1450,7 +1466,9 @@ fn test_sign_verify_message() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1503,7 +1521,9 @@ fn test_eth_extract_secret() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: vec![Web3Instance { web3, is_parity: true }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]), + }, ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index bb262bbb59..7cc5ff7b7f 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -33,7 +33,9 @@ async fn test_send() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: vec![Web3Instance { web3, is_parity: false }], + client: EthClient { + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + }, decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 4cc3ca94ea..ec15194c6e 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -116,6 +116,7 @@ pub struct EthNode { #[serde(tag = "error_type", content = "error_data")] pub enum Erc20TokenActivationError { InternalError(String), + ClientConnectionFailed(String), CouldNotFetchBalance(String), } @@ -155,14 +156,23 @@ impl EthCoin { let conf = coin_conf(&ctx, &ticker); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => get_token_decimals(self.web3(), protocol.token_addr) - .await - .map_err(Erc20TokenActivationError::InternalError)?, + None | Some(0) => get_token_decimals( + &self + .web3() + .await + .map_err(|e| Erc20TokenActivationError::ClientConnectionFailed(e.to_string()))?, + protocol.token_addr, + ) + .await + .map_err(Erc20TokenActivationError::InternalError)?, Some(d) => d as u8, }; let web3_instances: Vec = self + .client .web3_instances + .lock() + .await .iter() .map(|node| { let mut transport = node.web3.transport().clone(); @@ -202,7 +212,9 @@ impl EthCoin { gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, gas_station_policy: self.gas_station_policy.clone(), - web3_instances, + client: EthClient { + web3_instances: AsyncMutex::new(web3_instances), + }, history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), ctx: self.ctx.clone(), required_confirmations, @@ -310,7 +322,9 @@ pub async fn eth_coin_from_conf_and_request_v2( gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, - web3_instances, + client: EthClient { + web3_instances: AsyncMutex::new(web3_instances), + }, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 3feedea3ad..caa398be70 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -729,7 +729,7 @@ async fn get_moralis_nft_transfers( async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let hash = H256::from_str(transaction_hash).ok()?; - let receipt = eth_coin.web3().eth().transaction_receipt(hash).await.ok()?; + let receipt = eth_coin.web3().await.ok()?.eth().transaction_receipt(hash).await.ok()?; let fee_coin = match eth_coin.coin_type { EthCoinType::Eth => eth_coin.ticker(), EthCoinType::Erc20 { .. } => return None, @@ -743,6 +743,8 @@ async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let web3_tx = eth_coin .web3() + .await + .ok()? .eth() .transaction(TransactionId::Hash(hash)) .await diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 9a5dff22d9..851d188267 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -21,7 +21,8 @@ impl From for EnableTokenError { fn from(err: Erc20TokenActivationError) -> Self { match err { Erc20TokenActivationError::InternalError(e) => EnableTokenError::Internal(e), - Erc20TokenActivationError::CouldNotFetchBalance(e) => EnableTokenError::Transport(e), + Erc20TokenActivationError::CouldNotFetchBalance(e) + | Erc20TokenActivationError::ClientConnectionFailed(e) => EnableTokenError::Transport(e), } } } diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 7d1b695f55..abbc17caa0 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -80,7 +80,8 @@ impl From for InitTokensAsMmCoinsError { fn from(error: Erc20TokenActivationError) -> Self { match error { Erc20TokenActivationError::InternalError(e) => InitTokensAsMmCoinsError::Internal(e), - Erc20TokenActivationError::CouldNotFetchBalance(e) => InitTokensAsMmCoinsError::CouldNotFetchBalance(e), + Erc20TokenActivationError::CouldNotFetchBalance(e) + | Erc20TokenActivationError::ClientConnectionFailed(e) => InitTokensAsMmCoinsError::CouldNotFetchBalance(e), } } } From b045e8d0f900f0a369a90e9c43e6472585abe1c9 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 5 Feb 2024 15:54:09 +0300 Subject: [PATCH 20/53] add TODO task Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/http_transport.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 997ff034d7..2a931ccb12 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -47,6 +47,7 @@ struct HttpTransportRpcClient(AsyncMutex); #[derive(Debug)] struct HttpTransportRpcClientImpl { + // TODO: remove client rotation from this module as we already do that in protocol level nodes: Vec, } From 3c2313a5e2054110ed4de2281bb0d3fbbd19f524 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 5 Feb 2024 15:55:09 +0300 Subject: [PATCH 21/53] unify `with_polling` and `with_socket` Signed-off-by: onur-ozkan --- mm2src/coins/eth/eth_balance_events.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 7f31f9db87..b6c1441d73 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -79,9 +79,7 @@ impl EventBehaviour for EthCoin { async fn handle(self, interval: f64, tx: oneshot::Sender) { const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; - async fn with_socket(_coin: EthCoin, _ctx: MmArc) { todo!() } - - async fn with_polling(coin: EthCoin, ctx: MmArc, interval: f64) { + async fn start_polling(coin: EthCoin, ctx: MmArc, interval: f64) { let mut cache: HashMap = HashMap::new(); loop { @@ -147,7 +145,7 @@ impl EventBehaviour for EthCoin { tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); - with_polling(self, ctx, interval).await + start_polling(self, ctx, interval).await } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { From 277aa75feed0d335554b2d895d45796ad5aad5d3 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 5 Feb 2024 16:08:10 +0300 Subject: [PATCH 22/53] use event handlers in websocket transport Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 4d4af36223..b6311fdecb 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -6,6 +6,7 @@ use crate::eth::web3_transport::Web3SendOut; use crate::eth::RpcTransportEventHandlerShared; +use crate::RpcTransportEventHandler; use common::expirable_map::ExpirableMap; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -212,11 +213,15 @@ async fn send_request( transport: WebsocketTransport, request: Call, request_id: RequestId, + event_handlers: Vec, ) -> Result { let mut tx = transport.controller_channel.tx.clone(); let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); + let serialized_request = to_string(&request); + event_handlers.on_outgoing_request(serialized_request.as_bytes()); + tx.send(ControllerMessage::Request(WsRequest { request_id, request, @@ -229,6 +234,11 @@ async fn send_request( if let Ok(_ping) = notification_receiver.await { let response_map = unsafe { &mut *transport.responses.ptr }; if let Some(response) = response_map.remove(&request_id) { + let mut res_bytes: Vec = Vec::new(); + if let Ok(_) = serde_json::to_writer(&mut res_bytes, &response) { + event_handlers.on_incoming_response(&res_bytes); + } + return Ok(response); } }; @@ -246,5 +256,7 @@ impl Transport for WebsocketTransport { (request_id, request) } - fn send(&self, id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(self.clone(), request, id)) } + fn send(&self, id: RequestId, request: Call) -> Self::Out { + Box::pin(send_request(self.clone(), request, id, self.event_handlers.clone())) + } } From 19cfad774c81ce87b545149b57d12ad313e63e38 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 6 Feb 2024 15:31:29 +0300 Subject: [PATCH 23/53] implement proper node rotation on ETH Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 124 +++++---- mm2src/coins/eth/eth_tests.rs | 103 +++++-- mm2src/coins/eth/eth_wasm_tests.rs | 6 +- mm2src/coins/eth/v2_activation.rs | 30 ++- .../eth/web3_transport/http_transport.rs | 237 +++++++--------- mm2src/coins/eth/web3_transport/mod.rs | 32 +-- .../eth/web3_transport/websocket_transport.rs | 253 ++++++++++-------- 7 files changed, 429 insertions(+), 356 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 8948f3e4cc..07cd79dec6 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -1,3 +1,5 @@ +use self::web3_transport::websocket_transport::WebsocketTransport; + /****************************************************************************** * Copyright © 2023 Pampex LTD and TillyHK LTD * * * @@ -30,6 +32,7 @@ use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError, Timer}; +use common::executor::{AbortSettings, SpawnAbortable}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -489,51 +492,7 @@ async fn make_gas_station_request(url: &str) -> GasStationResult { Ok(result) } -#[async_trait] -impl RpcCommonOps for EthCoinImpl { - type RpcClient = Web3Instance; - type Error = Web3RpcError; - - async fn get_live_client(&self) -> Result { - let mut clients = self.client.web3_instances.lock().await; - - // try to find first live client - for (i, client) in clients.clone().into_iter().enumerate() { - match client - .web3 - .web3() - .client_version() - .timeout(Duration::from_secs(15)) - .await - { - Ok(Ok(_)) => { - // Bring the live client to the front of rpc_clients - clients.rotate_left(i); - return Ok(client); - }, - Ok(Err(rpc_error)) => { - debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); - }, - Err(timeout_error) => { - debug!( - "Client version timeout exceed on: {:?}. Error: {}", - &client, timeout_error - ); - }, - }; - } - - return Err(Web3RpcError::Transport( - "All the current rpc nodes are unavailable.".to_string(), - )); - } -} - impl EthCoinImpl { - pub(crate) async fn web3(&self) -> Result, Web3RpcError> { - self.get_live_client().await.map(|t| t.web3) - } - #[cfg(not(target_arch = "wasm32"))] fn eth_traces_path(&self, ctx: &MmArc) -> PathBuf { ctx.dbdir() @@ -2573,7 +2532,63 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw } } +#[async_trait] +impl RpcCommonOps for EthCoin { + type RpcClient = Web3Instance; + type Error = Web3RpcError; + + async fn get_live_client(&self) -> Result { + let mut clients = self.client.web3_instances.lock().await; + + // try to find first live client + for (i, client) in clients.clone().into_iter().enumerate() { + match client + .web3 + .web3() + .client_version() + .timeout(Duration::from_secs(15)) + .await + { + Ok(Ok(_)) => { + if let Web3Transport::Websocket(socket_transport) = &client.web3.transport() { + socket_transport.maybe_spawn_connection_loop(self.clone()); + }; + + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(client); + }, + Ok(Err(rpc_error)) => { + debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + Err(timeout_error) => { + debug!( + "Client version timeout exceed on: {:?}. Error: {}", + &client, timeout_error + ); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + }; + } + + return Err(Web3RpcError::Transport( + "All the current rpc nodes are unavailable.".to_string(), + )); + } +} + impl EthCoin { + pub(crate) async fn web3(&self) -> Result, Web3RpcError> { + self.get_live_client().await.map(|t| t.web3) + } + /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` fn refund_events( &self, @@ -5679,13 +5694,21 @@ pub async fn eth_coin_from_conf_and_request( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { let node = WebsocketTransportNode { uri, gui_auth: false }; + let websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); - Web3Transport::new_websocket(ctx, vec![node], event_handlers.clone()) + // Temporarily start the connection loop (we close the connection once we have the client version below). + // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or + // dropping websocket support on parity nodes. + let fut = websocket_transport.clone().start_connection_loop(); + let settings = AbortSettings::info_on_abort("TODO".to_string()); + ctx.spawner().spawn_with_settings(fut, settings); + + Web3Transport::Websocket(websocket_transport) }, _ => { let node = HttpTransportNode { uri, gui_auth: false }; - Web3Transport::new_http(vec![node], event_handlers.clone()) + Web3Transport::new_http(node, event_handlers.clone()) }, }; @@ -5694,10 +5717,19 @@ pub async fn eth_coin_from_conf_and_request( Ok(v) => v, Err(e) => { error!("Couldn't get client version for url {}: {}", url, e); + + if let Web3Transport::Websocket(socket_transport) = web3.transport() { + socket_transport.stop_connection_loop().await; + }; + continue; }, }; + if let Web3Transport::Websocket(socket_transport) = web3.transport() { + socket_transport.stop_connection_loop().await; + }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index bd47b1970c..444b081525 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -105,17 +105,21 @@ fn eth_coin_from_keypair( fallback_swap_contract: Option
, key_pair: KeyPair, ) -> (MmArc, EthCoin) { - let mut nodes = vec![]; + let mut web3_instances = vec![]; for url in urls.iter() { - nodes.push(HttpTransportNode { + let node = HttpTransportNode { uri: url.parse().unwrap(), gui_auth: false, - }); + }; + + let transport = Web3Transport::with_node(node); + let web3 = Web3::new(transport); + + web3_instances.push(Web3Instance { web3, is_parity: false }); } - drop_mutability!(nodes); - let transport = Web3Transport::with_nodes(nodes); - let web3 = Web3::new(transport); + drop_mutability!(web3_instances); + let conf = json!({ "coins":[ eth_testnet_conf(), @@ -143,7 +147,7 @@ fn eth_coin_from_keypair( contract_supports_watchers: false, ticker, client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(web3_instances), }, ctx: ctx.weak(), required_confirmations: 1.into(), @@ -307,7 +311,13 @@ fn send_and_refund_erc20_payment() { fill_eth(key_pair.address(), 0.001); fill_jst(key_pair.address(), 0.0001); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); + let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -394,7 +404,13 @@ fn send_and_refund_erc20_payment() { fn send_and_refund_eth_payment() { let key_pair = Random.generate().unwrap(); fill_eth(key_pair.address(), 0.001); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); + let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -483,10 +499,27 @@ fn test_nonce_several_urls() { ) .unwrap(); - let devnet_transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let sepolia_transport = Web3Transport::single_node("https://rpc2.sepolia.org", false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let devnet_transport = Web3Transport::with_node(node); + + let node = HttpTransportNode { + uri: "https://rpc2.sepolia.org".parse().unwrap(), + gui_auth: false, + }; + + let sepolia_transport = Web3Transport::with_node(node); + + let node = HttpTransportNode { + uri: "http://195.201.0.6:8989".parse().unwrap(), + gui_auth: false, + }; + // get nonce must succeed if some nodes are down at the moment for some reason - let failing_transport = Web3Transport::single_node("http://195.201.0.6:8989", false); + let failing_transport = Web3Transport::with_node(node); let web3_devnet = Web3::new(devnet_transport); let web3_sepolia = Web3::new(sepolia_transport); @@ -552,7 +585,12 @@ fn test_wait_for_payment_spend_timeout() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -619,7 +657,12 @@ fn test_search_for_swap_tx_spend_was_spent() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); + let node = HttpTransportNode { + uri: ETH_MAINNET_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -725,7 +768,12 @@ fn test_search_for_swap_tx_spend_was_refunded() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); + let node = HttpTransportNode { + uri: ETH_MAINNET_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1411,7 +1459,12 @@ fn test_message_hash() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -1453,7 +1506,13 @@ fn test_sign_verify_message() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1499,7 +1558,15 @@ fn test_eth_extract_secret() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node("https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", false); + let node = HttpTransportNode { + uri: "https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b" + .parse() + .unwrap(), + gui_auth: false, + }; + + let transport = Web3Transport::with_node(node); + let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 7cc5ff7b7f..4c5796eaa5 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -21,7 +21,11 @@ async fn test_send() { let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); let keypair = key_pair_from_seed(&seed).unwrap(); let key_pair = KeyPair::from_secret_slice(keypair.private_ref()).unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); + let node = HttpTransportNode { + uri: ETH_DEV_NODE.parse().unwrap(), + gui_auth: false, + }; + let transport = Web3Transport::with_node(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index ec15194c6e..2625ed71ef 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -6,6 +6,7 @@ use enum_from::EnumFromTrait; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; +use v2_activation::web3_transport::websocket_transport::WebsocketTransport; #[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -416,17 +417,25 @@ async fn build_web3_instances( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { let node = WebsocketTransportNode { uri, gui_auth: false }; + let websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); - Web3Transport::new_websocket(ctx, vec![node], event_handlers.clone()) + // Temporarily start the connection loop (we close the connection once we have the client version below). + // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or + // dropping websocket support on parity nodes. + let fut = websocket_transport.clone().start_connection_loop(); + let settings = AbortSettings::info_on_abort("TODO".to_string()); + ctx.spawner().spawn_with_settings(fut, settings); + + Web3Transport::Websocket(websocket_transport) }, _ => { let node = HttpTransportNode { uri, gui_auth: false }; - build_single_http_transport( + build_http_transport( coin_ticker.clone(), address.clone(), key_pair, - vec![node], + node, event_handlers.clone(), ) }, @@ -437,10 +446,19 @@ async fn build_web3_instances( Ok(v) => v, Err(e) => { error!("Couldn't get client version for url {}: {}", url, e); + + if let Web3Transport::Websocket(socket_transport) = web3.transport() { + socket_transport.stop_connection_loop().await; + }; + continue; }, }; + if let Web3Transport::Websocket(socket_transport) = web3.transport() { + socket_transport.stop_connection_loop().await; + }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), @@ -456,16 +474,16 @@ async fn build_web3_instances( Ok(web3_instances) } -fn build_single_http_transport( +fn build_http_transport( coin_ticker: String, address: String, key_pair: &KeyPair, - nodes: Vec, + node: HttpTransportNode, event_handlers: Vec, ) -> Web3Transport { use crate::eth::web3_transport::http_transport::HttpTransport; - let mut http_transport = HttpTransport::with_event_handlers(nodes, event_handlers); + let mut http_transport = HttpTransport::with_event_handlers(node, event_handlers); http_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { coin_ticker, secret: key_pair.secret().clone(), diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 2a931ccb12..594d4bb991 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -1,7 +1,6 @@ use crate::eth::{web3_transport::Web3SendOut, EthCoin, GuiAuthMessages, RpcTransportEventHandler, RpcTransportEventHandlerShared, Web3RpcError}; use common::APPLICATION_JSON; -use futures::lock::Mutex as AsyncMutex; use http::header::CONTENT_TYPE; use jsonrpc_core::{Call, Response}; use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; @@ -42,19 +41,10 @@ where } } -#[derive(Debug)] -struct HttpTransportRpcClient(AsyncMutex); - -#[derive(Debug)] -struct HttpTransportRpcClientImpl { - // TODO: remove client rotation from this module as we already do that in protocol level - nodes: Vec, -} - #[derive(Clone, Debug)] pub struct HttpTransport { id: Arc, - client: Arc, + node: HttpTransportNode, event_handlers: Vec, pub(crate) gui_auth_validation_generator: Option, } @@ -66,47 +56,26 @@ pub struct HttpTransportNode { } impl HttpTransport { - #[cfg(test)] #[inline] - pub fn new(nodes: Vec) -> Self { - let client_impl = HttpTransportRpcClientImpl { nodes }; + #[cfg(any(test, target_arch = "wasm32"))] + pub fn new(node: HttpTransportNode) -> Self { HttpTransport { id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + node, event_handlers: Default::default(), gui_auth_validation_generator: None, } } #[inline] - pub fn with_event_handlers( - nodes: Vec, - event_handlers: Vec, - ) -> Self { - let client_impl = HttpTransportRpcClientImpl { nodes }; + pub fn with_event_handlers(node: HttpTransportNode, event_handlers: Vec) -> Self { HttpTransport { id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + node, event_handlers, gui_auth_validation_generator: None, } } - - #[allow(dead_code)] - pub fn single_node(url: &'static str, gui_auth: bool) -> Self { - let nodes = vec![HttpTransportNode { - uri: url.parse().unwrap(), - gui_auth, - }]; - let client_impl = HttpTransportRpcClientImpl { nodes }; - - HttpTransport { - id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), - event_handlers: Default::default(), - gui_auth_validation_generator: None, - } - } } impl Transport for HttpTransport { @@ -123,7 +92,7 @@ impl Transport for HttpTransport { fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request( request, - self.client.clone(), + self.node.clone(), self.event_handlers.clone(), self.gui_auth_validation_generator.clone(), )) @@ -133,7 +102,7 @@ impl Transport for HttpTransport { fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request( request, - self.client.clone(), + self.node.clone(), self.event_handlers.clone(), self.gui_auth_validation_generator.clone(), )) @@ -182,7 +151,7 @@ fn handle_gui_auth_payload_if_activated( #[cfg(not(target_arch = "wasm32"))] async fn send_request( request: Call, - client: Arc, + node: HttpTransportNode, event_handlers: Vec, gui_auth_validation_generator: Option, ) -> Result { @@ -195,129 +164,110 @@ async fn send_request( const REQUEST_TIMEOUT_S: f64 = 20.; - let mut errors = Vec::new(); - let serialized_request = to_string(&request); - let mut client_impl = client.0.lock().await; - - for (i, node) in client_impl.nodes.clone().iter().enumerate() { - let serialized_request = - match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - errors.push(e); - continue; - }, + let serialized_request = match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, &node, &request) + { + Ok(Some(r)) => r, + Ok(None) => serialized_request.clone(), + Err(e) => { + return Err(request_failed_error(request, e)); + }, + }; + + event_handlers.on_outgoing_request(serialized_request.as_bytes()); + + let mut req = http::Request::new(serialized_request.into_bytes()); + *req.method_mut() = http::Method::POST; + *req.uri_mut() = node.uri.clone(); + req.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); + let timeout = Timer::sleep(REQUEST_TIMEOUT_S); + let req = Box::pin(slurp_req(req)); + let rc = select(req, timeout).await; + let res = match rc { + Either::Left((r, _t)) => r, + Either::Right((_t, _r)) => { + let (method, id) = match &request { + Call::MethodCall(m) => (m.method.clone(), m.id.clone()), + Call::Notification(n) => (n.method.clone(), jsonrpc_core::Id::Null), + Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), }; + let error = format!( + "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", + node.uri, REQUEST_TIMEOUT_S, method, id + ); + warn!("{}", error); + return Err(request_failed_error(request, Web3RpcError::Transport(error))); + }, + }; - event_handlers.on_outgoing_request(serialized_request.as_bytes()); - - let mut req = http::Request::new(serialized_request.into_bytes()); - *req.method_mut() = http::Method::POST; - *req.uri_mut() = node.uri.clone(); - req.headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); - let timeout = Timer::sleep(REQUEST_TIMEOUT_S); - let req = Box::pin(slurp_req(req)); - let rc = select(req, timeout).await; - let res = match rc { - Either::Left((r, _t)) => r, - Either::Right((_t, _r)) => { - let (method, id) = match &request { - Call::MethodCall(m) => (m.method.clone(), m.id.clone()), - Call::Notification(n) => (n.method.clone(), jsonrpc_core::Id::Null), - Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), - }; - let error = format!( - "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", - node.uri, REQUEST_TIMEOUT_S, method, id - ); - warn!("{}", error); - errors.push(Web3RpcError::Transport(error)); - continue; - }, - }; - - let (status, _headers, body) = match res { - Ok(r) => r, - Err(err) => { - errors.push(Web3RpcError::Transport(err.to_string())); - continue; - }, - }; - - event_handlers.on_incoming_response(&body); - - if !status.is_success() { - errors.push(Web3RpcError::Transport(format!( + let (status, _headers, body) = match res { + Ok(r) => r, + Err(err) => { + return Err(request_failed_error(request, Web3RpcError::Transport(err.to_string()))); + }, + }; + + event_handlers.on_incoming_response(&body); + + if !status.is_success() { + return Err(request_failed_error( + request, + Web3RpcError::Transport(format!( "Server: '{}', response !200: {}, {}", node.uri, status, binprint(&body, b'.') - ))); - continue; - } - - let res = match single_response(body, &node.uri.to_string()) { - Ok(r) => r, - Err(err) => { - errors.push(Web3RpcError::InvalidResponse(format!( - "Server: '{}', error: {}", - node.uri, err - ))); - continue; - }, - }; - - client_impl.nodes.rotate_left(i); - - return Ok(res); + )), + )); } - Err(request_failed_error(&request, &errors)) + let res = match single_response(body, &node.uri.to_string()) { + Ok(r) => r, + Err(err) => { + return Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", node.uri, err)), + )); + }, + }; + + Ok(res) } #[cfg(target_arch = "wasm32")] async fn send_request( request: Call, - client: Arc, + node: HttpTransportNode, event_handlers: Vec, gui_auth_validation_generator: Option, ) -> Result { let serialized_request = to_string(&request); - let mut errors = Vec::new(); - let mut client_impl = client.0.lock().await; - - for (i, node) in client_impl.nodes.clone().iter().enumerate() { - let serialized_request = - match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - errors.push(e); - continue; - }, - }; + let serialized_request = match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, &node, &request) + { + Ok(Some(r)) => r, + Ok(None) => serialized_request.clone(), + Err(e) => { + return Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), + )); + }, + }; - match send_request_once(serialized_request, &node.uri, &event_handlers).await { - Ok(response_json) => { - client_impl.nodes.rotate_left(i); - return Ok(response_json); - }, - Err(Error::Transport(e)) => { - errors.push(Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e))) - }, - Err(e) => errors.push(Web3RpcError::InvalidResponse(format!( - "Server: '{}', error: {}", - node.uri, e - ))), - } + match send_request_once(serialized_request, &node.uri, &event_handlers).await { + Ok(response_json) => Ok(response_json), + Err(Error::Transport(e)) => Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), + )), + Err(e) => Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", node.uri, e)), + )), } - - Err(request_failed_error(&request, &errors)) } #[cfg(target_arch = "wasm32")] @@ -361,8 +311,7 @@ async fn send_request_once( } } -fn request_failed_error(request: &Call, errors: &[Web3RpcError]) -> Error { - let errors: String = errors.iter().map(|e| format!("{:?}; ", e)).collect(); - let error = format!("request {:?} failed: {}", request, errors); +fn request_failed_error(request: Call, error: Web3RpcError) -> Error { + let error = format!("request {:?} failed: {}", request, error); Error::Transport(TransportError::Message(error)) } diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 49039f4d3e..89eacae3ac 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -1,9 +1,7 @@ use crate::RpcTransportEventHandlerShared; -use common::executor::{AbortSettings, SpawnAbortable}; use ethereum_types::U256; use futures::future::BoxFuture; use jsonrpc_core::Call; -use mm2_core::mm_ctx::MmArc; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskResult; use mm2_net::transport::GuiAuthValidationGenerator; use serde_json::Value as Json; @@ -29,25 +27,10 @@ pub(crate) enum Web3Transport { impl Web3Transport { pub fn new_http( - nodes: Vec, + node: http_transport::HttpTransportNode, event_handlers: Vec, ) -> Web3Transport { - http_transport::HttpTransport::with_event_handlers(nodes, event_handlers).into() - } - - pub fn new_websocket( - ctx: &MmArc, - nodes: Vec, - event_handlers: Vec, - ) -> Web3Transport { - let transport = websocket_transport::WebsocketTransport::with_event_handlers(nodes, event_handlers); - - // TODO: Don't do this here - let fut = transport.clone().start_connection_loop(); - let settings = AbortSettings::info_on_abort("TODO".to_string()); - ctx.spawner().spawn_with_settings(fut, settings); - - transport.into() + http_transport::HttpTransport::with_event_handlers(node, event_handlers).into() } #[cfg(target_arch = "wasm32")] @@ -58,14 +41,9 @@ impl Web3Transport { Ok(metamask_transport::MetamaskTransport::detect(eth_config, event_handlers)?.into()) } - #[cfg(test)] - pub fn with_nodes(nodes: Vec) -> Web3Transport { - http_transport::HttpTransport::new(nodes).into() - } - - #[allow(dead_code)] - pub fn single_node(url: &'static str, gui_auth: bool) -> Self { - http_transport::HttpTransport::single_node(url, gui_auth).into() + #[cfg(any(test, target_arch = "wasm32"))] + pub fn with_node(node: http_transport::HttpTransportNode) -> Web3Transport { + http_transport::HttpTransport::new(node).into() } pub fn gui_auth_validation_generator_as_mut(&mut self) -> Option<&mut GuiAuthValidationGenerator> { diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index b6311fdecb..ac8f43113b 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -5,8 +5,9 @@ //! handshakes (connection reusability) for each request. use crate::eth::web3_transport::Web3SendOut; -use crate::eth::RpcTransportEventHandlerShared; -use crate::RpcTransportEventHandler; +use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; +use crate::{MmCoin, RpcTransportEventHandler}; +use common::executor::{AbortSettings, SpawnAbortable}; use common::expirable_map::ExpirableMap; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -26,31 +27,26 @@ use web3::{helpers::build_request, RequestId, Transport}; const REQUEST_TIMEOUT_AS_SEC: u64 = 10; #[derive(Clone, Debug)] -pub struct WebsocketTransportNode { +pub(crate) struct WebsocketTransportNode { pub(crate) uri: http::Uri, + // TODO: We need to support this mechanism on the komodo-defi-proxy + #[allow(dead_code)] pub(crate) gui_auth: bool, } -#[derive(Debug)] -struct WebsocketTransportRpcClient(AsyncMutex); - -#[derive(Debug)] -struct WebsocketTransportRpcClientImpl { - nodes: Vec, -} - #[derive(Clone, Debug)] pub struct WebsocketTransport { request_id: Arc, - client: Arc, + node: WebsocketTransportNode, event_handlers: Vec, - responses: SafeMapPtr, - controller_channel: ControllerChannel, + responses: Arc, + controller_channel: Arc, + connection_guard: Arc>, } -#[derive(Clone, Debug)] +#[derive(Debug)] struct ControllerChannel { - tx: UnboundedSender, + tx: Arc>>, rx: Arc>>, } @@ -73,22 +69,14 @@ struct WsRequest { /// The implemented algorithm for socket request-response is already thread-safe, /// so we don't care about race conditions. /// -/// As for deallocations, we use a mutex as a pointer guard in the socket connection. -/// Whenever it is no longer held there and `drop` is called for `WebsocketTransport`, -/// this means that the pointer is no longer in use, so we can switch raw pointer into -/// a smart pointer (`Box`) for letting compiler to clean up the memory. -#[derive(Clone, Debug)] -struct SafeMapPtr { - ptr: *mut HashMap, - guard: Arc>, -} +/// As for deallocations, see the `Drop` implementation below. +#[derive(Debug)] +struct SafeMapPtr(*mut HashMap); -impl Drop for WebsocketTransport { +impl Drop for SafeMapPtr { fn drop(&mut self) { - if self.responses.guard.try_lock().is_some() { - // let the compiler do the job - let _ = unsafe { Box::from_raw(self.responses.ptr) }; - } + // Let the compiler do the job. + let _ = unsafe { Box::from_raw(self.0) }; } } @@ -96,105 +84,130 @@ unsafe impl Send for SafeMapPtr {} unsafe impl Sync for SafeMapPtr {} impl WebsocketTransport { - pub fn with_event_handlers( - nodes: Vec, + pub(crate) fn with_event_handlers( + node: WebsocketTransportNode, event_handlers: Vec, ) -> Self { - let client_impl = WebsocketTransportRpcClientImpl { nodes }; let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); - let hashmap = HashMap::default(); WebsocketTransport { - client: Arc::new(WebsocketTransportRpcClient(AsyncMutex::new(client_impl))), + node, event_handlers, - responses: SafeMapPtr { - ptr: Box::into_raw(hashmap.into()), - guard: Arc::new(AsyncMutex::new(())), - }, + responses: Arc::new(SafeMapPtr(Box::into_raw(Default::default()))), request_id: Arc::new(AtomicUsize::new(1)), controller_channel: ControllerChannel { - tx: req_tx, + tx: Arc::new(AsyncMutex::new(req_tx)), rx: Arc::new(AsyncMutex::new(req_rx)), - }, + } + .into(), + connection_guard: Arc::new(AsyncMutex::new(())), } } - pub async fn start_connection_loop(self) { - let _ptr_guard = self.responses.guard.lock().await; + pub(crate) async fn start_connection_loop(self) { + let _guard = self.connection_guard.lock().await; + let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); loop { - for node in self.client.0.lock().await.nodes.clone() { - let mut wsocket = match tokio_tungstenite_wasm::connect(node.uri.to_string()).await { - Ok(ws) => ws, - Err(e) => { - log::error!("{e}"); - continue; - }, - }; - - // id list of awaiting requests - let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); - let mut req_rx = self.controller_channel.rx.lock().await; - - loop { - futures_util::select! { - _ = keepalive_interval.next().fuse() => { - // Drop expired response notifier channels - response_notifiers.clear_expired_entries(); - - const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; - if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { - log::error!("{e}"); - // TODO: try couple times at least before continue - continue; - } + let mut wsocket = match tokio_tungstenite_wasm::connect(self.node.uri.to_string()).await { + Ok(ws) => ws, + Err(e) => { + log::error!("{e}"); + continue; + }, + }; + + // List of awaiting requests IDs + let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); + let mut req_rx = self.controller_channel.rx.lock().await; + + loop { + futures_util::select! { + _ = keepalive_interval.next().fuse() => { + // Drop expired response notifier channels + response_notifiers.clear_expired_entries(); + + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + + let mut should_continue = Default::default(); + for _ in 0..3 { + match wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + } + }; + } + + if should_continue { + continue; } + } - request = req_rx.next().fuse() => { - match request { - Some(ControllerMessage::Request(WsRequest { request_id, request, response_notifier })) => { - let serialized_request = to_string(&request); - response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); - if let Err(e) = wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request)).await { - log::error!("{e}"); - let _ = response_notifiers.remove(&request_id); - // TODO: try couple times at least before continue - continue; + request = req_rx.next().fuse() => { + match request { + Some(ControllerMessage::Request(WsRequest { request_id, request, response_notifier })) => { + let serialized_request = to_string(&request); + response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); + + let mut should_continue = Default::default(); + for _ in 0..3 { + match wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())).await { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + } } - }, - Some(ControllerMessage::Close) => return, - _ => {}, - } + } + + if should_continue { + let _ = response_notifiers.remove(&request_id); + continue; + } + }, + Some(ControllerMessage::Close) => { + break; + }, + _ => {}, } + } - message = wsocket.next().fuse() => { - match message { - Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { - if let Ok(inc_event) = serde_json::from_str::(&inc_event) { - if !inc_event.is_object() { - continue; - } + message = wsocket.next().fuse() => { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + continue; + } - if let Some(id) = inc_event.get("id") { - let request_id = id.as_u64().unwrap_or_default() as usize; + if let Some(id) = inc_event.get("id") { + let request_id = id.as_u64().unwrap_or_default() as usize; - if let Some(notifier) = response_notifiers.remove(&request_id) { - let response_map = unsafe { &mut *self.responses.ptr }; - let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); - notifier.send(()).expect("TODO"); - } + if let Some(notifier) = response_notifiers.remove(&request_id) { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); + + notifier.send(()).expect("receiver channel must be alive"); } } - }, - Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => todo!(), - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, - Some(Err(e)) => { - log::error!("{e}"); - break; - }, - None => continue, - } + } + }, + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => continue, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => return, + Some(Err(e)) => { + log::error!("{e}"); + return; + }, + None => continue, } } } @@ -202,10 +215,23 @@ impl WebsocketTransport { } } - async fn stop_connection(self) { - let mut tx = self.controller_channel.tx.clone(); - tx.send(ControllerMessage::Close).await.expect("TODO"); - let _ = unsafe { Box::from_raw(self.responses.ptr) }; + pub(crate) async fn stop_connection_loop(&self) { + let mut tx = self.controller_channel.tx.lock().await; + tx.send(ControllerMessage::Close) + .await + .expect("receiver channel must be alive"); + + let response_map = unsafe { &mut *self.responses.0 }; + response_map.clear(); + } + + pub(crate) fn maybe_spawn_connection_loop(&self, coin: EthCoin) { + // if we can acquire the lock here, it means connection loop is not alive + if self.connection_guard.try_lock().is_some() { + let fut = self.clone().start_connection_loop(); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); + coin.spawner().spawn_with_settings(fut, settings); + } } } @@ -215,7 +241,7 @@ async fn send_request( request_id: RequestId, event_handlers: Vec, ) -> Result { - let mut tx = transport.controller_channel.tx.clone(); + let mut tx = transport.controller_channel.tx.lock().await; let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); @@ -228,14 +254,13 @@ async fn send_request( response_notifier: notification_sender, })) .await - .expect("TODO"); + .expect("receiver channel must be alive"); - // TODO: we need timeout here if let Ok(_ping) = notification_receiver.await { - let response_map = unsafe { &mut *transport.responses.ptr }; + let response_map = unsafe { &mut *transport.responses.0 }; if let Some(response) = response_map.remove(&request_id) { let mut res_bytes: Vec = Vec::new(); - if let Ok(_) = serde_json::to_writer(&mut res_bytes, &response) { + if serde_json::to_writer(&mut res_bytes, &response).is_ok() { event_handlers.on_incoming_response(&res_bytes); } From 20179d15b104a366fd2ea27013b3e53164872743 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 6 Feb 2024 15:54:40 +0300 Subject: [PATCH 24/53] fix the leftover TODO Signed-off-by: onur-ozkan --- .../eth/web3_transport/http_transport.rs | 9 ++++----- .../eth/web3_transport/websocket_transport.rs | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 594d4bb991..32cb61cf23 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -5,7 +5,7 @@ use http::header::CONTENT_TYPE; use jsonrpc_core::{Call, Response}; use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; use serde_json::Value as Json; -#[cfg(not(target_arch = "wasm32"))] use std::ops::Deref; +use std::ops::Deref; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use web3::error::{Error, TransportError}; @@ -19,10 +19,9 @@ pub struct AuthPayload<'a> { pub signed_message: GuiAuthValidation, } -/// Parse bytes RPC response into `Result`. +/// Deserialize bytes RPC response into `Result`. /// Implementation copied from Web3 HTTP transport -#[cfg(not(target_arch = "wasm32"))] -fn single_response(response: T, rpc_url: &str) -> Result +pub(crate) fn de_rpc_response(response: T, rpc_url: &str) -> Result where T: Deref + std::fmt::Debug, { @@ -223,7 +222,7 @@ async fn send_request( )); } - let res = match single_response(body, &node.uri.to_string()) { + let res = match de_rpc_response(body, &node.uri.to_string()) { Ok(r) => r, Err(err) => { return Err(request_failed_error( diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index ac8f43113b..44b82b96cc 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -4,6 +4,7 @@ //! bandwidth. This efficiency is achieved by avoiding the handling of TCP //! handshakes (connection reusability) for each request. +use super::http_transport::de_rpc_response; use crate::eth::web3_transport::Web3SendOut; use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; use crate::{MmCoin, RpcTransportEventHandler}; @@ -71,7 +72,7 @@ struct WsRequest { /// /// As for deallocations, see the `Drop` implementation below. #[derive(Debug)] -struct SafeMapPtr(*mut HashMap); +struct SafeMapPtr(*mut HashMap>); impl Drop for SafeMapPtr { fn drop(&mut self) { @@ -193,10 +194,14 @@ impl WebsocketTransport { let request_id = id.as_u64().unwrap_or_default() as usize; if let Some(notifier) = response_notifiers.remove(&request_id) { - let response_map = unsafe { &mut *self.responses.0 }; - let _ = response_map.insert(request_id, inc_event.get("result").expect("TODO").clone()); + let mut res_bytes: Vec = Vec::new(); + if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, res_bytes); + + notifier.send(()).expect("receiver channel must be alive"); + } - notifier.send(()).expect("receiver channel must be alive"); } } } @@ -259,12 +264,9 @@ async fn send_request( if let Ok(_ping) = notification_receiver.await { let response_map = unsafe { &mut *transport.responses.0 }; if let Some(response) = response_map.remove(&request_id) { - let mut res_bytes: Vec = Vec::new(); - if serde_json::to_writer(&mut res_bytes, &response).is_ok() { - event_handlers.on_incoming_response(&res_bytes); - } + event_handlers.on_incoming_response(&response); - return Ok(response); + return de_rpc_response(response, &transport.node.uri.to_string()); } }; From acaada4b873472bd6da9c9498a24286c689c6720 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 6 Feb 2024 16:01:10 +0300 Subject: [PATCH 25/53] use `break` instead of `return` Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 44b82b96cc..7ba17c1a23 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -207,7 +207,7 @@ impl WebsocketTransport { } }, Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => continue, - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => return, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, Some(Err(e)) => { log::error!("{e}"); return; From 3ddf62ada46e980b7f09993d39b499ff01a7f498 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 7 Feb 2024 10:42:16 +0300 Subject: [PATCH 26/53] fix gui_auth related changes Signed-off-by: onur-ozkan --- mm2src/coins/eth/v2_activation.rs | 25 +++++++++++-------- .../eth/web3_transport/http_transport.rs | 2 +- mm2src/mm2_net/src/transport.rs | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 2625ed71ef..bcfcea699d 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -269,7 +269,7 @@ pub async fn eth_coin_from_conf_and_request_v2( activated_key: key_pair, .. }, - ) => build_web3_instances(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await?, + ) => build_web3_instances(ctx, ticker.clone(), my_address_str, key_pair, req.nodes.clone()).await?, (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, @@ -395,24 +395,24 @@ async fn build_web3_instances( coin_ticker: String, address: String, key_pair: &KeyPair, - eth_nodes: &[EthNode], + mut eth_nodes: Vec, ) -> MmResult, EthActivationV2Error> { if eth_nodes.is_empty() { return MmError::err(EthActivationV2Error::AtLeastOneNodeRequired); } - let mut urls: Vec = eth_nodes.iter().map(|n| n.url.clone()).collect(); let mut rng = small_rng(); - urls.as_mut_slice().shuffle(&mut rng); - drop_mutability!(urls); + eth_nodes.as_mut_slice().shuffle(&mut rng); + drop_mutability!(eth_nodes); let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); - let mut web3_instances = Vec::with_capacity(urls.len()); - for url in urls { - let uri: Uri = url + let mut web3_instances = Vec::with_capacity(eth_nodes.len()); + for eth_node in eth_nodes { + let uri: Uri = eth_node + .url .parse() - .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", url)))?; + .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", eth_node.url)))?; let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { @@ -429,7 +429,10 @@ async fn build_web3_instances( Web3Transport::Websocket(websocket_transport) }, _ => { - let node = HttpTransportNode { uri, gui_auth: false }; + let node = HttpTransportNode { + uri, + gui_auth: eth_node.gui_auth, + }; build_http_transport( coin_ticker.clone(), @@ -445,7 +448,7 @@ async fn build_web3_instances( let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", url, e); + error!("Couldn't get client version for url {}: {}", eth_node.url, e); if let Web3Transport::Websocket(socket_transport) = web3.transport() { socket_transport.stop_connection_loop().await; diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 32cb61cf23..bbdf22bb6e 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -12,7 +12,7 @@ use web3::error::{Error, TransportError}; use web3::helpers::{build_request, to_result_from_output, to_string}; use web3::{RequestId, Transport}; -#[derive(Serialize, Clone)] +#[derive(Clone, Serialize)] pub struct AuthPayload<'a> { #[serde(flatten)] pub request: &'a Call, diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index 27c039d556..1dc4d032c3 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -77,7 +77,7 @@ pub struct GuiAuthValidationGenerator { } /// gui-auth specific data-type that needed in order to perform gui-auth calls -#[derive(Serialize, Clone)] +#[derive(Clone, Serialize)] pub struct GuiAuthValidation { pub coin_ticker: String, pub address: String, From 03eb60a9a1316ea3d9828c29a48df59039de67b1 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 7 Feb 2024 14:56:57 +0300 Subject: [PATCH 27/53] support komodo-defi-proxy in new websocket transport Signed-off-by: onur-ozkan --- mm2src/coins/eth/v2_activation.rs | 30 +++++-- .../eth/web3_transport/http_transport.rs | 87 +++++-------------- mm2src/coins/eth/web3_transport/mod.rs | 39 ++++++++- .../eth/web3_transport/websocket_transport.rs | 50 ++++++++--- 4 files changed, 123 insertions(+), 83 deletions(-) diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index bcfcea699d..2a82c3adcc 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -416,8 +416,20 @@ async fn build_web3_instances( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { - let node = WebsocketTransportNode { uri, gui_auth: false }; - let websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); + let node = WebsocketTransportNode { + uri, + gui_auth: eth_node.gui_auth, + }; + + let mut websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); + + if eth_node.gui_auth { + websocket_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { + coin_ticker: coin_ticker.clone(), + secret: key_pair.secret().clone(), + address: address.clone(), + }); + } // Temporarily start the connection loop (we close the connection once we have the client version below). // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or @@ -486,12 +498,16 @@ fn build_http_transport( ) -> Web3Transport { use crate::eth::web3_transport::http_transport::HttpTransport; + let gui_auth = node.gui_auth; let mut http_transport = HttpTransport::with_event_handlers(node, event_handlers); - http_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { - coin_ticker, - secret: key_pair.secret().clone(), - address, - }); + + if gui_auth { + http_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { + coin_ticker, + secret: key_pair.secret().clone(), + address, + }); + } Web3Transport::from(http_transport) } diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index bbdf22bb6e..5c43c172e5 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -1,5 +1,5 @@ -use crate::eth::{web3_transport::Web3SendOut, EthCoin, GuiAuthMessages, RpcTransportEventHandler, - RpcTransportEventHandlerShared, Web3RpcError}; +use crate::eth::web3_transport::handle_gui_auth_payload; +use crate::eth::{web3_transport::Web3SendOut, RpcTransportEventHandler, RpcTransportEventHandlerShared, Web3RpcError}; use common::APPLICATION_JSON; use http::header::CONTENT_TYPE; use jsonrpc_core::{Call, Response}; @@ -108,45 +108,6 @@ impl Transport for HttpTransport { } } -/// Generates a signed message and inserts it into request -/// payload if gui_auth is activated. Returns false on errors. -fn handle_gui_auth_payload_if_activated( - gui_auth_validation_generator: &Option, - node: &HttpTransportNode, - request: &Call, -) -> Result, Web3RpcError> { - if !node.gui_auth { - return Ok(None); - } - - let generator = match gui_auth_validation_generator.clone() { - Some(gen) => gen, - None => { - return Err(Web3RpcError::Internal(format!( - "GuiAuthValidationGenerator is not provided for {:?} node", - node - ))); - }, - }; - - let signed_message = match EthCoin::generate_gui_auth_signed_validation(generator) { - Ok(t) => t, - Err(e) => { - return Err(Web3RpcError::Internal(format!( - "GuiAuth signed message generation failed for {:?} node, error: {:?}", - node, e - ))); - }, - }; - - let auth_request = AuthPayload { - request, - signed_message, - }; - - Ok(Some(to_string(&auth_request))) -} - #[cfg(not(target_arch = "wasm32"))] async fn send_request( request: Call, @@ -163,16 +124,16 @@ async fn send_request( const REQUEST_TIMEOUT_S: f64 = 20.; - let serialized_request = to_string(&request); + let mut serialized_request = to_string(&request); - let serialized_request = match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, &node, &request) - { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - return Err(request_failed_error(request, e)); - }, - }; + if node.gui_auth { + match handle_gui_auth_payload(&gui_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(request_failed_error(request, e)); + }, + }; + } event_handlers.on_outgoing_request(serialized_request.as_bytes()); @@ -242,19 +203,19 @@ async fn send_request( event_handlers: Vec, gui_auth_validation_generator: Option, ) -> Result { - let serialized_request = to_string(&request); - - let serialized_request = match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, &node, &request) - { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - return Err(request_failed_error( - request, - Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), - )); - }, - }; + let mut serialized_request = to_string(&request); + + if node.gui_auth { + match handle_gui_auth_payload(&gui_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), + )); + }, + }; + } match send_request_once(serialized_request, &node.uri, &event_handlers).await { Ok(response_json) => Ok(response_json), diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 89eacae3ac..4a96d0593f 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -1,4 +1,3 @@ -use crate::RpcTransportEventHandlerShared; use ethereum_types::U256; use futures::future::BoxFuture; use jsonrpc_core::Call; @@ -7,10 +6,14 @@ use mm2_net::transport::GuiAuthValidationGenerator; use serde_json::Value as Json; use serde_json::Value; use web3::api::Namespace; -use web3::helpers::{self, CallFuture}; +use web3::helpers::{self, to_string, CallFuture}; use web3::types::BlockNumber; use web3::{Error, RequestId, Transport}; +use self::http_transport::AuthPayload; +use super::{EthCoin, GuiAuthMessages, Web3RpcError}; +use crate::RpcTransportEventHandlerShared; + pub(crate) mod http_transport; #[cfg(target_arch = "wasm32")] pub(crate) mod metamask_transport; pub(crate) mod websocket_transport; @@ -132,3 +135,35 @@ impl EthFeeHistoryNamespace { CallFuture::new(self.transport.execute("eth_feeHistory", params)) } } + +/// Generates a signed message and inserts it into the request payload. +pub(super) fn handle_gui_auth_payload( + gui_auth_validation_generator: &Option, + request: &Call, +) -> Result { + let generator = match gui_auth_validation_generator.clone() { + Some(gen) => gen, + None => { + return Err(Web3RpcError::Internal( + "GuiAuthValidationGenerator is not provided for".to_string(), + )); + }, + }; + + let signed_message = match EthCoin::generate_gui_auth_signed_validation(generator) { + Ok(t) => t, + Err(e) => { + return Err(Web3RpcError::Internal(format!( + "GuiAuth signed message generation failed. Error: {:?}", + e + ))); + }, + }; + + let auth_request = AuthPayload { + request, + signed_message, + }; + + Ok(to_string(&auth_request)) +} diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 7ba17c1a23..b6fcb70508 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -4,11 +4,12 @@ //! bandwidth. This efficiency is achieved by avoiding the handling of TCP //! handshakes (connection reusability) for each request. +use super::handle_gui_auth_payload; use super::http_transport::de_rpc_response; use crate::eth::web3_transport::Web3SendOut; use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; use crate::{MmCoin, RpcTransportEventHandler}; -use common::executor::{AbortSettings, SpawnAbortable}; +use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::expirable_map::ExpirableMap; use common::log; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; @@ -18,10 +19,11 @@ use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; use instant::Duration; use jsonrpc_core::Call; +use mm2_net::transport::GuiAuthValidationGenerator; use std::collections::HashMap; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; -use web3::error::Error; +use web3::error::{Error, TransportError}; use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; @@ -30,8 +32,6 @@ const REQUEST_TIMEOUT_AS_SEC: u64 = 10; #[derive(Clone, Debug)] pub(crate) struct WebsocketTransportNode { pub(crate) uri: http::Uri, - // TODO: We need to support this mechanism on the komodo-defi-proxy - #[allow(dead_code)] pub(crate) gui_auth: bool, } @@ -40,6 +40,7 @@ pub struct WebsocketTransport { request_id: Arc, node: WebsocketTransportNode, event_handlers: Vec, + pub(crate) gui_auth_validation_generator: Option, responses: Arc, controller_channel: Arc, connection_guard: Arc>, @@ -58,7 +59,7 @@ enum ControllerMessage { #[derive(Debug)] struct WsRequest { - request: Call, + serialized_request: String, request_id: RequestId, response_notifier: oneshot::Sender<()>, } @@ -102,6 +103,7 @@ impl WebsocketTransport { } .into(), connection_guard: Arc::new(AsyncMutex::new(())), + gui_auth_validation_generator: None, } } @@ -111,10 +113,17 @@ impl WebsocketTransport { let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); loop { + let mut attempts = 0; let mut wsocket = match tokio_tungstenite_wasm::connect(self.node.uri.to_string()).await { Ok(ws) => ws, Err(e) => { - log::error!("{e}"); + attempts += 1; + if attempts > 3 { + log::error!("Connection could not established for {}. Error {e}", self.node.uri); + break; + } + + Timer::sleep(1.).await; continue; }, }; @@ -143,6 +152,8 @@ impl WebsocketTransport { should_continue = true; } }; + + Timer::sleep(1.).await; } if should_continue { @@ -152,8 +163,7 @@ impl WebsocketTransport { request = req_rx.next().fuse() => { match request { - Some(ControllerMessage::Request(WsRequest { request_id, request, response_notifier })) => { - let serialized_request = to_string(&request); + Some(ControllerMessage::Request(WsRequest { request_id, serialized_request, response_notifier })) => { response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); let mut should_continue = Default::default(); @@ -168,6 +178,8 @@ impl WebsocketTransport { should_continue = true; } } + + Timer::sleep(1.).await; } if should_continue { @@ -246,16 +258,29 @@ async fn send_request( request_id: RequestId, event_handlers: Vec, ) -> Result { + let mut serialized_request = to_string(&request); + + if transport.node.gui_auth { + match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(Error::Transport(TransportError::Message(format!( + "Couldn't generate signed message payload for {:?}. Error: {e}", + request + )))); + }, + }; + } + let mut tx = transport.controller_channel.tx.lock().await; let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); - let serialized_request = to_string(&request); event_handlers.on_outgoing_request(serialized_request.as_bytes()); tx.send(ControllerMessage::Request(WsRequest { request_id, - request, + serialized_request, response_notifier: notification_sender, })) .await @@ -270,7 +295,10 @@ async fn send_request( } }; - Err(Error::Internal) + Err(Error::Transport(TransportError::Message(format!( + "Sending {:?} failed.", + request + )))) } impl Transport for WebsocketTransport { From 3da4c34de6556fab225649d85c9c514e06604b7c Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 8 Feb 2024 13:15:39 +0300 Subject: [PATCH 28/53] implement heartbeat event Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/heartbeat_event.rs | 52 ++++++++++++++++++++++++++ mm2src/mm2_main/src/lp_native_dex.rs | 5 +++ mm2src/mm2_main/src/mm2.rs | 1 + 3 files changed, 58 insertions(+) create mode 100644 mm2src/mm2_main/src/heartbeat_event.rs diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs new file mode 100644 index 0000000000..9b4cb0809a --- /dev/null +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; +use common::{executor::{SpawnFuture, Timer}, + log::info}; +use futures::channel::oneshot::{self, Receiver, Sender}; +use mm2_core::mm_ctx::MmArc; +use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, + Event, EventStreamConfiguration}; + +pub struct HeartbeatEvent { + ctx: MmArc, +} + +impl HeartbeatEvent { + pub fn new(ctx: MmArc) -> Self { Self { ctx } } +} + +#[async_trait] +impl EventBehaviour for HeartbeatEvent { + const EVENT_NAME: &'static str = "HEARTBEAT"; + + async fn handle(self, interval: f64, tx: oneshot::Sender) { + tx.send(EventInitStatus::Success).unwrap(); + + loop { + self.ctx + .stream_channel_controller + .broadcast(Event::new(Self::EVENT_NAME.to_string(), json!({}).to_string())) + .await; + + Timer::sleep(interval).await; + } + } + + async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { + if let Some(event) = config.get_event(Self::EVENT_NAME) { + info!( + "{} event is activated with {} seconds interval.", + Self::EVENT_NAME, + event.stream_interval_seconds + ); + + let (tx, rx): (Sender, Receiver) = oneshot::channel(); + self.ctx.spawner().spawn(self.handle(event.stream_interval_seconds, tx)); + + rx.await.unwrap_or_else(|e| { + EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) + }) + } else { + EventInitStatus::Inactive + } + } +} diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 8f98bfc975..ccf6d78fc9 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -45,6 +45,7 @@ use std::time::Duration; #[cfg(not(target_arch = "wasm32"))] use crate::mm2::database::init_and_migrate_sql_db; +use crate::mm2::heartbeat_event::HeartbeatEvent; use crate::mm2::lp_message_service::{init_message_service, InitMessageServiceError}; use crate::mm2::lp_network::{lp_network_ports, p2p_event_process_loop, NetIdError}; use crate::mm2::lp_ordermatch::{broadcast_maker_orders_keep_alive_loop, clean_memory_loop, init_ordermatch_context, @@ -435,6 +436,10 @@ async fn init_event_streaming(ctx: &MmArc) -> MmInitResult<()> { if let EventInitStatus::Failed(err) = NetworkEvent::new(ctx.clone()).spawn_if_active(config).await { return MmError::err(MmInitError::NetworkEventInitFailed(err)); } + + if let EventInitStatus::Failed(err) = HeartbeatEvent::new(ctx.clone()).spawn_if_active(config).await { + return MmError::err(MmInitError::NetworkEventInitFailed(err)); + } } Ok(()) diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index a6310ba180..49c713ae43 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -58,6 +58,7 @@ use mm2_err_handle::prelude::*; #[path = "database.rs"] pub mod database; +#[path = "heartbeat_event.rs"] pub mod heartbeat_event; #[path = "lp_dispatcher.rs"] pub mod lp_dispatcher; #[path = "lp_message_service.rs"] pub mod lp_message_service; #[path = "lp_network.rs"] pub mod lp_network; From d9c0a5bc8eba38144d1636ea6cc002bc8422c39b Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 9 Feb 2024 12:58:16 +0300 Subject: [PATCH 29/53] fix eth `get_live_client` for ws transport Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 07cd79dec6..25266e500c 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2542,6 +2542,10 @@ impl RpcCommonOps for EthCoin { // try to find first live client for (i, client) in clients.clone().into_iter().enumerate() { + if let Web3Transport::Websocket(socket_transport) = &client.web3.transport() { + socket_transport.maybe_spawn_connection_loop(self.clone()); + }; + match client .web3 .web3() @@ -2550,10 +2554,6 @@ impl RpcCommonOps for EthCoin { .await { Ok(Ok(_)) => { - if let Web3Transport::Websocket(socket_transport) = &client.web3.transport() { - socket_transport.maybe_spawn_connection_loop(self.clone()); - }; - // Bring the live client to the front of rpc_clients clients.rotate_left(i); return Ok(client); From febad662aef10c956824e31f30a84f0d30ec3db5 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 12 Feb 2024 12:31:11 +0300 Subject: [PATCH 30/53] cleanup leftovers from dev Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 25 +++++++++++++------ mm2src/coins/eth/eth_tests.rs | 24 +++++++++--------- mm2src/coins/eth/eth_wasm_tests.rs | 2 +- mm2src/coins/eth/v2_activation.rs | 11 +++++--- mm2src/coins/eth/web3_transport/mod.rs | 9 +++---- .../eth/web3_transport/websocket_transport.rs | 2 +- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 25266e500c..25d1088aed 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -1,5 +1,3 @@ -use self::web3_transport::websocket_transport::WebsocketTransport; - /****************************************************************************** * Copyright © 2023 Pampex LTD and TillyHK LTD * * * @@ -23,7 +21,7 @@ use self::web3_transport::websocket_transport::WebsocketTransport; // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // use super::eth::Action::{Call, Create}; -use crate::eth::web3_transport::websocket_transport::WebsocketTransportNode; +use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use crate::{DexFee, RpcCommonOps, ValidateWatcherSpendInput, WatcherSpendType}; @@ -5693,14 +5691,17 @@ pub async fn eth_coin_from_conf_and_request( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { - let node = WebsocketTransportNode { uri, gui_auth: false }; + let node = WebsocketTransportNode { + uri: uri.clone(), + gui_auth: false, + }; let websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); // Temporarily start the connection loop (we close the connection once we have the client version below). // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or // dropping websocket support on parity nodes. let fut = websocket_transport.clone().start_connection_loop(); - let settings = AbortSettings::info_on_abort("TODO".to_string()); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); ctx.spawner().spawn_with_settings(fut, settings); Web3Transport::Websocket(websocket_transport) @@ -5708,7 +5709,7 @@ pub async fn eth_coin_from_conf_and_request( _ => { let node = HttpTransportNode { uri, gui_auth: false }; - Web3Transport::new_http(node, event_handlers.clone()) + Web3Transport::new_http_with_event_handlers(node, event_handlers.clone()) }, }; @@ -5748,8 +5749,16 @@ pub async fn eth_coin_from_conf_and_request( } => { let token_addr = try_s!(valid_addr_from_str(&contract_address)); let decimals = match conf["decimals"].as_u64() { - // TODO - None | Some(0) => try_s!(get_token_decimals(&web3_instances.first().unwrap().web3, token_addr).await), + None | Some(0) => try_s!( + get_token_decimals( + &web3_instances + .first() + .expect("web3_instances can't be empty in ETH activation") + .web3, + token_addr + ) + .await + ), Some(d) => d as u8, }; (EthCoinType::Erc20 { platform, token_addr }, decimals) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 444b081525..16b1c54bf3 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -112,7 +112,7 @@ fn eth_coin_from_keypair( gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); web3_instances.push(Web3Instance { web3, is_parity: false }); @@ -316,7 +316,7 @@ fn send_and_refund_erc20_payment() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -409,7 +409,7 @@ fn send_and_refund_eth_payment() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -504,14 +504,14 @@ fn test_nonce_several_urls() { gui_auth: false, }; - let devnet_transport = Web3Transport::with_node(node); + let devnet_transport = Web3Transport::new_http(node); let node = HttpTransportNode { uri: "https://rpc2.sepolia.org".parse().unwrap(), gui_auth: false, }; - let sepolia_transport = Web3Transport::with_node(node); + let sepolia_transport = Web3Transport::new_http(node); let node = HttpTransportNode { uri: "http://195.201.0.6:8989".parse().unwrap(), @@ -519,7 +519,7 @@ fn test_nonce_several_urls() { }; // get nonce must succeed if some nodes are down at the moment for some reason - let failing_transport = Web3Transport::with_node(node); + let failing_transport = Web3Transport::new_http(node); let web3_devnet = Web3::new(devnet_transport); let web3_sepolia = Web3::new(sepolia_transport); @@ -590,7 +590,7 @@ fn test_wait_for_payment_spend_timeout() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -662,7 +662,7 @@ fn test_search_for_swap_tx_spend_was_spent() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -773,7 +773,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1464,7 +1464,7 @@ fn test_message_hash() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { @@ -1512,7 +1512,7 @@ fn test_sign_verify_message() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1565,7 +1565,7 @@ fn test_eth_extract_secret() { gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 4c5796eaa5..16eeef7a14 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -25,7 +25,7 @@ async fn test_send() { uri: ETH_DEV_NODE.parse().unwrap(), gui_auth: false, }; - let transport = Web3Transport::with_node(node); + let transport = Web3Transport::new_http(node); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); let coin = EthCoin(Arc::new(EthCoinImpl { diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 2a82c3adcc..340c802e87 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -417,7 +417,7 @@ async fn build_web3_instances( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { let node = WebsocketTransportNode { - uri, + uri: uri.clone(), gui_auth: eth_node.gui_auth, }; @@ -435,7 +435,7 @@ async fn build_web3_instances( // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or // dropping websocket support on parity nodes. let fut = websocket_transport.clone().start_connection_loop(); - let settings = AbortSettings::info_on_abort("TODO".to_string()); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); ctx.spawner().spawn_with_settings(fut, settings); Web3Transport::Websocket(websocket_transport) @@ -520,7 +520,10 @@ async fn build_metamask_transport( let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); let eth_config = web3_transport::metamask_transport::MetamaskEthConfig { chain_id }; - let web3 = Web3::new(Web3Transport::new_metamask(eth_config, event_handlers)?); + let web3 = Web3::new(Web3Transport::new_metamask_with_event_handlers( + eth_config, + event_handlers, + )?); // Check if MetaMask supports the given `chain_id`. // Please note that this request may take a long time. @@ -549,7 +552,7 @@ async fn check_metamask_supports_chain_id( match web3.eth().chain_id().await { Ok(chain_id) if chain_id == U256::from(expected_chain_id) => Ok(()), - // The RPC client should have returned ChainId with which it has been created on [`Web3Transport::new_metamask`]. + // The RPC client should have returned ChainId with which it has been created on [`Web3Transport::new_metamask_with_event_handlers`]. Ok(unexpected_chain_id) => { let error = format!("Expected '{expected_chain_id}' ChainId, found '{unexpected_chain_id}'"); MmError::err(EthActivationV2Error::InternalError(error)) diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 4a96d0593f..622187aebc 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -29,7 +29,7 @@ pub(crate) enum Web3Transport { } impl Web3Transport { - pub fn new_http( + pub fn new_http_with_event_handlers( node: http_transport::HttpTransportNode, event_handlers: Vec, ) -> Web3Transport { @@ -37,7 +37,7 @@ impl Web3Transport { } #[cfg(target_arch = "wasm32")] - pub(crate) fn new_metamask( + pub(crate) fn new_metamask_with_event_handlers( eth_config: metamask_transport::MetamaskEthConfig, event_handlers: Vec, ) -> MetamaskResult { @@ -45,15 +45,14 @@ impl Web3Transport { } #[cfg(any(test, target_arch = "wasm32"))] - pub fn with_node(node: http_transport::HttpTransportNode) -> Web3Transport { + pub fn new_http(node: http_transport::HttpTransportNode) -> Web3Transport { http_transport::HttpTransport::new(node).into() } pub fn gui_auth_validation_generator_as_mut(&mut self) -> Option<&mut GuiAuthValidationGenerator> { match self { Web3Transport::Http(http) => http.gui_auth_validation_generator.as_mut(), - // TODO - Web3Transport::Websocket(_) => None, + Web3Transport::Websocket(websocket) => websocket.gui_auth_validation_generator.as_mut(), #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(_) => None, } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index b6fcb70508..fc1a747d70 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -110,6 +110,7 @@ impl WebsocketTransport { pub(crate) async fn start_connection_loop(self) { let _guard = self.connection_guard.lock().await; + // List of awaiting requests let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); loop { @@ -128,7 +129,6 @@ impl WebsocketTransport { }, }; - // List of awaiting requests IDs let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); let mut req_rx = self.controller_channel.rx.lock().await; From 3e47db630f416004cd77a716acf6ad3e09d1f324 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 14 Feb 2024 11:02:53 +0300 Subject: [PATCH 31/53] remove `EthClient` abstraction Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 44 +++++++------------ mm2src/coins/eth/eth_tests.rs | 68 +++++++++++------------------- mm2src/coins/eth/eth_wasm_tests.rs | 4 +- mm2src/coins/eth/v2_activation.rs | 9 +--- 4 files changed, 43 insertions(+), 82 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 25d1088aed..ac9775cd85 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -428,7 +428,7 @@ pub struct EthCoinImpl { fallback_swap_contract: Option
, contract_supports_watchers: bool, /// The separate web3 instances kept to get nonce, will replace the web3 completely soon - client: EthClient, + web3_instances: AsyncMutex>, decimals: u8, gas_station_url: Option, gas_station_decimals: u8, @@ -448,10 +448,6 @@ pub struct EthCoinImpl { pub abortable_system: AbortableQueue, } -struct EthClient { - web3_instances: AsyncMutex>, -} - #[derive(Clone, Debug)] pub struct Web3Instance { web3: Web3, @@ -706,7 +702,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { // Todo: nonce_lock is still global for all addresses but this needs to be per address let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(my_address, coin.client.web3_instances.lock().await.to_vec()) + let (nonce, _) = get_addr_nonce(my_address, coin.web3_instances.lock().await.to_vec()) .compat() .timeout_secs(30.) .await? @@ -866,14 +862,11 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce( - eth_coin.my_address, - eth_coin.client.web3_instances.lock().await.to_vec(), - ) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; + let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.lock().await.to_vec()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; let tx = UnSignedEthTx { nonce, @@ -944,14 +937,11 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce( - eth_coin.my_address, - eth_coin.client.web3_instances.lock().await.to_vec(), - ) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; + let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.lock().await.to_vec()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; let tx = UnSignedEthTx { nonce, @@ -2393,7 +2383,7 @@ async fn sign_transaction_with_keypair( let _nonce_lock = coin.nonce_lock.lock().await; status.status(tags!(), "get_addr_nonce…"); let (nonce, web3_instances_with_latest_nonce) = try_tx_s!( - get_addr_nonce(coin.my_address, coin.client.web3_instances.lock().await.to_vec()) + get_addr_nonce(coin.my_address, coin.web3_instances.lock().await.to_vec()) .compat() .await ); @@ -2536,7 +2526,7 @@ impl RpcCommonOps for EthCoin { type Error = Web3RpcError; async fn get_live_client(&self) -> Result { - let mut clients = self.client.web3_instances.lock().await; + let mut clients = self.web3_instances.lock().await; // try to find first live client for (i, client) in clients.clone().into_iter().enumerate() { @@ -4831,7 +4821,7 @@ impl EthCoin { /// The function is endless, we just keep looping in case of a transport error hoping it will go away. async fn wait_for_addr_nonce_increase(&self, addr: Address, prev_nonce: U256) { repeatable!(async { - match get_addr_nonce(addr, self.client.web3_instances.lock().await.to_vec()) + match get_addr_nonce(addr, self.web3_instances.lock().await.to_vec()) .compat() .await { @@ -5819,9 +5809,7 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - client: EthClient { - web3_instances: AsyncMutex::new(web3_instances), - }, + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 16b1c54bf3..3c6606c158 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -146,9 +146,7 @@ fn eth_coin_from_keypair( fallback_swap_contract, contract_supports_watchers: false, ticker, - client: EthClient { - web3_instances: AsyncMutex::new(web3_instances), - }, + web3_instances: AsyncMutex::new(web3_instances), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -332,9 +330,7 @@ fn send_and_refund_erc20_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -422,9 +418,7 @@ fn send_and_refund_eth_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -535,22 +529,20 @@ fn test_nonce_several_urls() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![ - Web3Instance { - web3: web3_devnet, - is_parity: false, - }, - Web3Instance { - web3: web3_sepolia, - is_parity: false, - }, - Web3Instance { - web3: web3_failing, - is_parity: false, - }, - ]), - }, + web3_instances: AsyncMutex::new(vec![ + Web3Instance { + web3: web3_devnet, + is_parity: false, + }, + Web3Instance { + web3: web3_sepolia, + is_parity: false, + }, + Web3Instance { + web3: web3_failing, + is_parity: false, + }, + ]), decimals: 18, gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -570,7 +562,7 @@ fn test_nonce_several_urls() { let payment = coin.send_to_address(coin.my_address, 200000000.into()).wait().unwrap(); log!("{:?}", payment); - let new_nonce = get_addr_nonce(coin.my_address, block_on(coin.client.web3_instances.lock()).to_vec()) + let new_nonce = get_addr_nonce(coin.my_address, block_on(coin.web3_instances.lock()).to_vec()) .wait() .unwrap(); log!("{:?}", new_nonce); @@ -608,9 +600,7 @@ fn test_wait_for_payment_spend_timeout() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -681,9 +671,7 @@ fn test_search_for_swap_tx_spend_was_spent() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -795,9 +783,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "BAT".into(), - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -1476,9 +1462,7 @@ fn test_message_hash() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1525,9 +1509,7 @@ fn test_sign_verify_message() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1588,9 +1570,7 @@ fn test_eth_extract_secret() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 16eeef7a14..f0c94aadfd 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -37,9 +37,7 @@ async fn test_send() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - client: EthClient { - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), - }, + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 340c802e87..7184eafd5d 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -170,7 +170,6 @@ impl EthCoin { }; let web3_instances: Vec = self - .client .web3_instances .lock() .await @@ -213,9 +212,7 @@ impl EthCoin { gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, gas_station_policy: self.gas_station_policy.clone(), - client: EthClient { - web3_instances: AsyncMutex::new(web3_instances), - }, + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), ctx: self.ctx.clone(), required_confirmations, @@ -323,9 +320,7 @@ pub async fn eth_coin_from_conf_and_request_v2( gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, - client: EthClient { - web3_instances: AsyncMutex::new(web3_instances), - }, + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(HistorySyncState::NotEnabled), ctx: ctx.weak(), required_confirmations, From df6682ce33bf230ecc0a43c985e411d9ad44857f Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 14 Feb 2024 11:03:57 +0300 Subject: [PATCH 32/53] update websocket_transport module doc Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index fc1a747d70..e116ed2845 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,8 +1,8 @@ //! This module offers a transport layer for managing request-response style communication -//! with Ethereum nodes using websockets in a wait and lock-free manner. In comparison to -//! HTTP transport, this approach proves to be much quicker (low-latency) and consumes less -//! bandwidth. This efficiency is achieved by avoiding the handling of TCP -//! handshakes (connection reusability) for each request. +//! with Ethereum nodes using websockets in a wait and lock-free manner (with unsafe raw-pointers). +//! In comparison to HTTP transport, this approach proves to be much quicker (low-latency) and consumes +//! less bandwidth. This efficiency is achieved by avoiding the handling of TCP handshakes (connection reusability) +//! for each request. use super::handle_gui_auth_payload; use super::http_transport::de_rpc_response; From 0fe4e9ae160ebac417889955d446fc5017031395 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 14 Feb 2024 14:53:27 +0300 Subject: [PATCH 33/53] add new error type `HeartbeatEventInitFailed` Signed-off-by: onur-ozkan --- mm2src/mm2_main/src/lp_native_dex.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index fa56c3d1d8..80c82553d8 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -206,6 +206,8 @@ pub enum MmInitError { InvalidPassphrase(String), #[display(fmt = "NETWORK event initialization failed: {}", _0)] NetworkEventInitFailed(String), + #[display(fmt = "HEARTBEAT event initialization failed: {}", _0)] + HeartbeatEventInitFailed(String), #[from_trait(WithHwRpcError::hw_rpc_error)] #[display(fmt = "{}", _0)] HwError(HwRpcError), @@ -438,7 +440,7 @@ async fn init_event_streaming(ctx: &MmArc) -> MmInitResult<()> { } if let EventInitStatus::Failed(err) = HeartbeatEvent::new(ctx.clone()).spawn_if_active(config).await { - return MmError::err(MmInitError::NetworkEventInitFailed(err)); + return MmError::err(MmInitError::HeartbeatEventInitFailed(err)); } } From 1e7a610f5a3e495c4eb70201a2bf8134813b542a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 14 Feb 2024 15:38:53 +0300 Subject: [PATCH 34/53] merge expirable map values Signed-off-by: onur-ozkan --- mm2src/common/expirable_map.rs | 27 +++++------ mm2src/common/time_cache.rs | 82 +++++++++++++++------------------- 2 files changed, 47 insertions(+), 62 deletions(-) diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 8897267e1a..8988da2155 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -3,17 +3,20 @@ //! Designed for performance-oriented use-cases utilizing `FxHashMap` under the hood, //! and is not suitable for cryptographic purposes. -use instant::Duration; +use instant::{Duration, Instant}; use rustc_hash::FxHashMap; use std::hash::Hash; -use crate::get_local_duration_since_epoch; - #[derive(Clone, Debug)] -struct ExpirableEntry { - exp: Duration, - created_at: Duration, - value: V, +pub struct ExpirableEntry { + pub(crate) value: V, + pub(crate) expires_at: Instant, +} + +impl ExpirableEntry { + pub fn get_element(&self) -> &V { &self.value } + + pub fn update_expiration(&mut self, expires_at: Instant) { self.expires_at = expires_at } } impl Default for ExpirableMap { @@ -41,8 +44,7 @@ impl ExpirableMap { /// the old one will be returned. pub fn insert(&mut self, k: K, v: V, exp: Duration) -> Option { let entry = ExpirableEntry { - exp, - created_at: get_local_duration_since_epoch().expect("Clock system is broken in the operating system."), + expires_at: Instant::now() + exp, value: v, }; @@ -50,12 +52,7 @@ impl ExpirableMap { } /// Removes expired entries from the map. - pub fn clear_expired_entries(&mut self) { - self.0.retain(|_k, v| { - let now = get_local_duration_since_epoch().expect("Clock system is broken in the operating system."); - (now - v.created_at) < v.exp - }); - } + pub fn clear_expired_entries(&mut self) { self.0.retain(|_k, v| Instant::now() < v.expires_at); } // Removes a key-value pair from the map and returns the associated value if present. #[inline] diff --git a/mm2src/common/time_cache.rs b/mm2src/common/time_cache.rs index aafa8a6aea..a1c3987ec2 100644 --- a/mm2src/common/time_cache.rs +++ b/mm2src/common/time_cache.rs @@ -28,81 +28,69 @@ use std::collections::hash_map::{self, use std::collections::VecDeque; use std::time::Duration; -#[derive(Debug)] -pub struct ExpiringElement { - /// The element that expires - element: Element, - /// The expire time. - expires: Instant, -} - -impl ExpiringElement { - pub fn get_element(&self) -> &Element { &self.element } - - pub fn update_expiration(&mut self, expires: Instant) { self.expires = expires } -} +use crate::expirable_map::ExpirableEntry; #[derive(Debug)] pub struct TimeCache { /// Mapping a key to its value together with its latest expire time (can be updated through /// reinserts). - map: FnvHashMap>, + map: FnvHashMap>, /// An ordered list of keys by expires time. - list: VecDeque>, + list: VecDeque>, /// The time elements remain in the cache. ttl: Duration, } pub struct OccupiedEntry<'a, K, V> { expiration: Instant, - entry: hash_map::OccupiedEntry<'a, K, ExpiringElement>, - list: &'a mut VecDeque>, + entry: hash_map::OccupiedEntry<'a, K, ExpirableEntry>, + list: &'a mut VecDeque>, } impl<'a, K, V> OccupiedEntry<'a, K, V> where K: Eq + std::hash::Hash + Clone, { - pub fn into_mut(self) -> &'a mut V { &mut self.entry.into_mut().element } + pub fn into_mut(self) -> &'a mut V { &mut self.entry.into_mut().value } #[allow(dead_code)] pub fn insert_without_updating_expiration(&mut self, value: V) -> V { //keep old expiration, only replace value of element - ::std::mem::replace(&mut self.entry.get_mut().element, value) + ::std::mem::replace(&mut self.entry.get_mut().value, value) } #[allow(dead_code)] pub fn insert_and_update_expiration(&mut self, value: V) -> V { //We push back an additional element, the first reference in the list will be ignored // since we also updated the expires in the map, see below. - self.list.push_back(ExpiringElement { - element: self.entry.key().clone(), - expires: self.expiration, + self.list.push_back(ExpirableEntry { + value: self.entry.key().clone(), + expires_at: self.expiration, }); self.entry - .insert(ExpiringElement { - element: value, - expires: self.expiration, + .insert(ExpirableEntry { + value, + expires_at: self.expiration, }) - .element + .value } pub fn into_mut_with_update_expiration(mut self) -> &'a mut V { //We push back an additional element, the first reference in the list will be ignored // since we also updated the expires in the map, see below. - self.list.push_back(ExpiringElement { - element: self.entry.key().clone(), - expires: self.expiration, + self.list.push_back(ExpirableEntry { + value: self.entry.key().clone(), + expires_at: self.expiration, }); self.entry.get_mut().update_expiration(self.expiration); - &mut self.entry.into_mut().element + &mut self.entry.into_mut().value } } pub struct VacantEntry<'a, K, V> { expiration: Instant, - entry: hash_map::VacantEntry<'a, K, ExpiringElement>, - list: &'a mut VecDeque>, + entry: hash_map::VacantEntry<'a, K, ExpirableEntry>, + list: &'a mut VecDeque>, } impl<'a, K, V> VacantEntry<'a, K, V> @@ -110,17 +98,17 @@ where K: Eq + std::hash::Hash + Clone, { pub fn insert(self, value: V) -> &'a mut V { - self.list.push_back(ExpiringElement { - element: self.entry.key().clone(), - expires: self.expiration, + self.list.push_back(ExpirableEntry { + value: self.entry.key().clone(), + expires_at: self.expiration, }); &mut self .entry - .insert(ExpiringElement { - element: value, - expires: self.expiration, + .insert(ExpirableEntry { + value, + expires_at: self.expiration, }) - .element + .value } } @@ -163,12 +151,12 @@ where fn remove_expired_keys(&mut self, now: Instant) { while let Some(element) = self.list.pop_front() { - if element.expires > now { + if element.expires_at > now { self.list.push_front(element); break; } - if let Occupied(entry) = self.map.entry(element.element.clone()) { - if entry.get().expires <= now { + if let Occupied(entry) = self.map.entry(element.value.clone()) { + if entry.get().expires_at <= now { entry.remove(); } } @@ -207,7 +195,7 @@ where // Removes a certain key even if it didn't expire plus removing other expired keys pub fn remove(&mut self, key: Key) -> Option { - let result = self.map.remove(&key).map(|el| el.element); + let result = self.map.remove(&key).map(|el| el.value); self.remove_expired_keys(Instant::now()); result } @@ -221,7 +209,7 @@ where pub fn contains_key(&self, key: &Key) -> bool { self.map.contains_key(key) } - pub fn get(&self, key: &Key) -> Option<&Value> { self.map.get(key).map(|e| &e.element) } + pub fn get(&self, key: &Key) -> Option<&Value> { self.map.get(key).map(|e| &e.value) } pub fn len(&self) -> usize { self.map.len() } @@ -229,9 +217,9 @@ where pub fn ttl(&self) -> Duration { self.ttl } - pub fn iter(&self) -> Iter> { self.map.iter() } + pub fn iter(&self) -> Iter> { self.map.iter() } - pub fn keys(&self) -> Keys> { self.map.keys() } + pub fn keys(&self) -> Keys> { self.map.keys() } } impl TimeCache @@ -242,7 +230,7 @@ where pub fn as_hash_map(&self) -> std::collections::HashMap { self.map .iter() - .map(|(key, expiring_el)| (key.clone(), expiring_el.element.clone())) + .map(|(key, expiring_el)| (key.clone(), expiring_el.value.clone())) .collect() } } From d389c51882af185a1e0e2b8aedd0fa0304609e37 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 14 Feb 2024 16:01:37 +0300 Subject: [PATCH 35/53] increase readibility in websocket_transport connection logic Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 207 ++++++++++-------- 1 file changed, 115 insertions(+), 92 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index e116ed2845..7002f6e95f 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -23,6 +23,7 @@ use mm2_net::transport::GuiAuthValidationGenerator; use std::collections::HashMap; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; +use tokio_tungstenite_wasm::WebSocketStream; use web3::error::{Error, TransportError}; use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; @@ -110,122 +111,144 @@ impl WebsocketTransport { pub(crate) async fn start_connection_loop(self) { let _guard = self.connection_guard.lock().await; + const MAX_ATTEMPTS: u32 = 3; + const SLEEP_DURATION: f64 = 1.; + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + const KEEPALIVE_DURATION: Duration = Duration::from_secs(10); + + async fn attempt_to_establish_socket_connection( + address: String, + max_attempts: u32, + sleep_duration_on_failure: f64, + ) -> tokio_tungstenite_wasm::Result { + loop { + let mut attempts = 0; + + match tokio_tungstenite_wasm::connect(address.clone()).await { + Ok(ws) => return Ok(ws), + Err(e) => { + attempts += 1; + if attempts > max_attempts { + return Err(e); + } + + Timer::sleep(sleep_duration_on_failure).await; + continue; + }, + }; + } + } + // List of awaiting requests let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); - loop { - let mut attempts = 0; - let mut wsocket = match tokio_tungstenite_wasm::connect(self.node.uri.to_string()).await { + let mut wsocket = + match attempt_to_establish_socket_connection(self.node.uri.to_string(), MAX_ATTEMPTS, SLEEP_DURATION).await + { Ok(ws) => ws, Err(e) => { - attempts += 1; - if attempts > 3 { - log::error!("Connection could not established for {}. Error {e}", self.node.uri); - break; - } - - Timer::sleep(1.).await; - continue; + log::error!("Connection could not established for {}. Error {e}", self.node.uri); + return; }, }; - let mut keepalive_interval = Ticker::new(Duration::from_secs(10)); - let mut req_rx = self.controller_channel.rx.lock().await; + let mut keepalive_interval = Ticker::new(KEEPALIVE_DURATION); + let mut req_rx = self.controller_channel.rx.lock().await; - loop { - futures_util::select! { - _ = keepalive_interval.next().fuse() => { - // Drop expired response notifier channels - response_notifiers.clear_expired_entries(); - - const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; - - let mut should_continue = Default::default(); - for _ in 0..3 { - match wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { - Ok(_) => { - should_continue = false; - break; - }, - Err(e) => { - log::error!("{e}"); - should_continue = true; - } - }; + loop { + futures_util::select! { + // KEEPALIVE HANDLING + _ = keepalive_interval.next().fuse() => { + // Drop expired response notifier channels + response_notifiers.clear_expired_entries(); + + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + } + }; - Timer::sleep(1.).await; - } + Timer::sleep(SLEEP_DURATION).await; + } - if should_continue { - continue; - } + if should_continue { + continue; } + } - request = req_rx.next().fuse() => { - match request { - Some(ControllerMessage::Request(WsRequest { request_id, serialized_request, response_notifier })) => { - response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); - - let mut should_continue = Default::default(); - for _ in 0..3 { - match wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())).await { - Ok(_) => { - should_continue = false; - break; - }, - Err(e) => { - log::error!("{e}"); - should_continue = true; - } + // SEND REQUESTS + request = req_rx.next().fuse() => { + match request { + Some(ControllerMessage::Request(WsRequest { request_id, serialized_request, response_notifier })) => { + response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); + + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())).await { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; } - - Timer::sleep(1.).await; } - if should_continue { - let _ = response_notifiers.remove(&request_id); - continue; - } - }, - Some(ControllerMessage::Close) => { - break; - }, - _ => {}, - } + Timer::sleep(SLEEP_DURATION).await; + } + + if should_continue { + let _ = response_notifiers.remove(&request_id); + continue; + } + }, + Some(ControllerMessage::Close) => { + break; + }, + _ => {}, } + } - message = wsocket.next().fuse() => { - match message { - Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { - if let Ok(inc_event) = serde_json::from_str::(&inc_event) { - if !inc_event.is_object() { - continue; - } - - if let Some(id) = inc_event.get("id") { - let request_id = id.as_u64().unwrap_or_default() as usize; + // HANDLE RESPONSES SENT FROM USER + message = wsocket.next().fuse() => { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + continue; + } - if let Some(notifier) = response_notifiers.remove(&request_id) { - let mut res_bytes: Vec = Vec::new(); - if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { - let response_map = unsafe { &mut *self.responses.0 }; - let _ = response_map.insert(request_id, res_bytes); + if let Some(id) = inc_event.get("id") { + let request_id = id.as_u64().unwrap_or_default() as usize; - notifier.send(()).expect("receiver channel must be alive"); - } + if let Some(notifier) = response_notifiers.remove(&request_id) { + let mut res_bytes: Vec = Vec::new(); + if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, res_bytes); + notifier.send(()).expect("receiver channel must be alive"); } + } } - }, - Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => continue, - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, - Some(Err(e)) => { - log::error!("{e}"); - return; - }, - None => continue, - } + } + }, + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => continue, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, + Some(Err(e)) => { + log::error!("{e}"); + return; + }, + None => continue, } } } From fb96d951917d532606479708a3aa66a7d8ca14e0 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 16 Feb 2024 11:20:28 +0300 Subject: [PATCH 36/53] improve node rotation and nit fixes Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 6 ++ .../eth/web3_transport/http_transport.rs | 97 +++++++++---------- .../eth/web3_transport/metamask_transport.rs | 17 +++- mm2src/coins/eth/web3_transport/mod.rs | 10 ++ .../eth/web3_transport/websocket_transport.rs | 7 ++ mm2src/common/expirable_map.rs | 5 +- 6 files changed, 88 insertions(+), 54 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index cd5e3b467d..36349a53c9 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2538,6 +2538,12 @@ impl RpcCommonOps for EthCoin { socket_transport.maybe_spawn_connection_loop(self.clone()); }; + if !client.web3.transport().is_last_request_failed() { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(client); + } + match client .web3 .web3() diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 5c43c172e5..4248d8173b 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -6,7 +6,7 @@ use jsonrpc_core::{Call, Response}; use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; use serde_json::Value as Json; use std::ops::Deref; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use web3::error::{Error, TransportError}; use web3::helpers::{build_request, to_result_from_output, to_string}; @@ -43,6 +43,7 @@ where #[derive(Clone, Debug)] pub struct HttpTransport { id: Arc, + pub(crate) last_request_failed: Arc, node: HttpTransportNode, event_handlers: Vec, pub(crate) gui_auth_validation_generator: Option, @@ -63,6 +64,7 @@ impl HttpTransport { node, event_handlers: Default::default(), gui_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), } } @@ -73,6 +75,7 @@ impl HttpTransport { node, event_handlers, gui_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), } } } @@ -88,33 +91,14 @@ impl Transport for HttpTransport { } #[cfg(not(target_arch = "wasm32"))] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - Box::pin(send_request( - request, - self.node.clone(), - self.event_handlers.clone(), - self.gui_auth_validation_generator.clone(), - )) - } + fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(request, self.clone())) } #[cfg(target_arch = "wasm32")] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - Box::pin(send_request( - request, - self.node.clone(), - self.event_handlers.clone(), - self.gui_auth_validation_generator.clone(), - )) - } + fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(request, self.clone())) } } #[cfg(not(target_arch = "wasm32"))] -async fn send_request( - request: Call, - node: HttpTransportNode, - event_handlers: Vec, - gui_auth_validation_generator: Option, -) -> Result { +async fn send_request(request: Call, transport: HttpTransport) -> Result { use common::executor::Timer; use common::log::warn; use futures::future::{select, Either}; @@ -124,22 +108,27 @@ async fn send_request( const REQUEST_TIMEOUT_S: f64 = 20.; + transport.last_request_failed.store(false, Ordering::SeqCst); + let mut serialized_request = to_string(&request); - if node.gui_auth { - match handle_gui_auth_payload(&gui_auth_validation_generator, &request) { + if transport.node.gui_auth { + match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { Ok(r) => serialized_request = r, Err(e) => { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error(request, e)); }, }; } - event_handlers.on_outgoing_request(serialized_request.as_bytes()); + transport + .event_handlers + .on_outgoing_request(serialized_request.as_bytes()); let mut req = http::Request::new(serialized_request.into_bytes()); *req.method_mut() = http::Method::POST; - *req.uri_mut() = node.uri.clone(); + *req.uri_mut() = transport.node.uri.clone(); req.headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); let timeout = Timer::sleep(REQUEST_TIMEOUT_S); @@ -153,9 +142,10 @@ async fn send_request( Call::Notification(n) => (n.method.clone(), jsonrpc_core::Id::Null), Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), }; + transport.last_request_failed.store(true, Ordering::SeqCst); let error = format!( "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", - node.uri, REQUEST_TIMEOUT_S, method, id + transport.node.uri, REQUEST_TIMEOUT_S, method, id ); warn!("{}", error); return Err(request_failed_error(request, Web3RpcError::Transport(error))); @@ -165,30 +155,33 @@ async fn send_request( let (status, _headers, body) = match res { Ok(r) => r, Err(err) => { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error(request, Web3RpcError::Transport(err.to_string()))); }, }; - event_handlers.on_incoming_response(&body); + transport.event_handlers.on_incoming_response(&body); if !status.is_success() { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error( request, Web3RpcError::Transport(format!( "Server: '{}', response !200: {}, {}", - node.uri, + transport.node.uri, status, binprint(&body, b'.') )), )); } - let res = match de_rpc_response(body, &node.uri.to_string()) { + let res = match de_rpc_response(body, &transport.node.uri.to_string()) { Ok(r) => r, Err(err) => { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error( request, - Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", node.uri, err)), + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, err)), )); }, }; @@ -197,36 +190,40 @@ async fn send_request( } #[cfg(target_arch = "wasm32")] -async fn send_request( - request: Call, - node: HttpTransportNode, - event_handlers: Vec, - gui_auth_validation_generator: Option, -) -> Result { +async fn send_request(request: Call, transport: HttpTransport) -> Result { + transport.last_request_failed.store(false, Ordering::SeqCst); + let mut serialized_request = to_string(&request); - if node.gui_auth { - match handle_gui_auth_payload(&gui_auth_validation_generator, &request) { + if transport.node.gui_auth { + match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { Ok(r) => serialized_request = r, Err(e) => { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error( request, - Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), + Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), )); }, }; } - match send_request_once(serialized_request, &node.uri, &event_handlers).await { + match send_request_once(serialized_request, &transport.node.uri, &transport.event_handlers).await { Ok(response_json) => Ok(response_json), - Err(Error::Transport(e)) => Err(request_failed_error( - request, - Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e)), - )), - Err(e) => Err(request_failed_error( - request, - Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", node.uri, e)), - )), + Err(Error::Transport(e)) => { + transport.last_request_failed.store(true, Ordering::SeqCst); + Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), + )) + }, + Err(e) => { + transport.last_request_failed.store(true, Ordering::SeqCst); + Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, e)), + )) + }, } } diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs index 5fe71d8dc2..7ec8e7fdda 100644 --- a/mm2src/coins/eth/web3_transport/metamask_transport.rs +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -4,6 +4,7 @@ use jsonrpc_core::Call; use mm2_metamask::{detect_metamask_provider, Eip1193Provider, MetamaskResult, MetamaskSession}; use serde_json::Value as Json; use std::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use web3::{RequestId, Transport}; @@ -15,6 +16,7 @@ pub(crate) struct MetamaskEthConfig { #[derive(Clone)] pub(crate) struct MetamaskTransport { inner: Arc, + pub(crate) last_request_failed: Arc, } struct MetamaskTransportInner { @@ -35,7 +37,10 @@ impl MetamaskTransport { eip1193, _event_handlers: event_handlers, }; - Ok(MetamaskTransport { inner: Arc::new(inner) }) + Ok(MetamaskTransport { + inner: Arc::new(inner), + last_request_failed: Arc::new(AtomicBool::new(false)), + }) } } @@ -59,9 +64,17 @@ impl fmt::Debug for MetamaskTransport { impl MetamaskTransport { async fn send_impl(&self, id: RequestId, request: Call) -> Result { + self.last_request_failed.store(false, Ordering::SeqCst); + // Hold the mutex guard until the request is finished. let _rpc_lock = self.request_preparation().await?; - self.inner.eip1193.send(id, request).await + match self.inner.eip1193.send(id, request).await { + Ok(t) => Ok(t), + Err(e) => { + self.last_request_failed.store(true, Ordering::SeqCst); + Err(e) + }, + } } /// Ensures that the MetaMask wallet is targeted to [`EthConfig::chain_id`]. diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 622187aebc..733e836d98 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -5,6 +5,7 @@ use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; use serde_json::Value as Json; use serde_json::Value; +use std::sync::atomic::Ordering; use web3::api::Namespace; use web3::helpers::{self, to_string, CallFuture}; use web3::types::BlockNumber; @@ -44,6 +45,15 @@ impl Web3Transport { Ok(metamask_transport::MetamaskTransport::detect(eth_config, event_handlers)?.into()) } + pub fn is_last_request_failed(&self) -> bool { + match self { + Web3Transport::Http(http) => http.last_request_failed.load(Ordering::SeqCst), + Web3Transport::Websocket(websocket) => websocket.last_request_failed.load(Ordering::SeqCst), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.last_request_failed.load(Ordering::SeqCst), + } + } + #[cfg(any(test, target_arch = "wasm32"))] pub fn new_http(node: http_transport::HttpTransportNode) -> Web3Transport { http_transport::HttpTransport::new(node).into() diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 7002f6e95f..7b071515aa 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -21,6 +21,7 @@ use instant::Duration; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; use std::collections::HashMap; +use std::sync::atomic::AtomicBool; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; use tokio_tungstenite_wasm::WebSocketStream; @@ -39,6 +40,7 @@ pub(crate) struct WebsocketTransportNode { #[derive(Clone, Debug)] pub struct WebsocketTransport { request_id: Arc, + pub(crate) last_request_failed: Arc, node: WebsocketTransportNode, event_handlers: Vec, pub(crate) gui_auth_validation_generator: Option, @@ -105,6 +107,7 @@ impl WebsocketTransport { .into(), connection_guard: Arc::new(AsyncMutex::new(())), gui_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), } } @@ -281,12 +284,14 @@ async fn send_request( request_id: RequestId, event_handlers: Vec, ) -> Result { + transport.last_request_failed.store(false, Ordering::SeqCst); let mut serialized_request = to_string(&request); if transport.node.gui_auth { match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { Ok(r) => serialized_request = r, Err(e) => { + transport.last_request_failed.store(true, Ordering::SeqCst); return Err(Error::Transport(TransportError::Message(format!( "Couldn't generate signed message payload for {:?}. Error: {e}", request @@ -318,6 +323,8 @@ async fn send_request( } }; + transport.last_request_failed.store(true, Ordering::SeqCst); + Err(Error::Transport(TransportError::Message(format!( "Sending {:?} failed.", request diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs index 8988da2155..73ebc1f580 100644 --- a/mm2src/common/expirable_map.rs +++ b/mm2src/common/expirable_map.rs @@ -63,6 +63,7 @@ impl ExpirableMap { mod tests { use super::*; use crate::cross_test; + use crate::executor::Timer; crate::cfg_wasm32! { use wasm_bindgen_test::*; @@ -79,7 +80,7 @@ mod tests { expirable_map.insert("key2".to_string(), value.to_string(), exp); // Wait for entries to expire - std::thread::sleep(Duration::from_secs(2)); + Timer::sleep(2.).await; // Clear expired entries expirable_map.clear_expired_entries(); @@ -95,7 +96,7 @@ mod tests { expirable_map.insert("key5".to_string(), value.to_string(), Duration::from_millis(3750)); // Wait 2 seconds to expire some entries - std::thread::sleep(Duration::from_secs(2)); + Timer::sleep(2.).await; // Clear expired entries expirable_map.clear_expired_entries(); From 09a7ba7775150685599f793cfa1a1a8919296ca6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Fri, 16 Feb 2024 11:28:19 +0300 Subject: [PATCH 37/53] shared ref on web3_instances Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 4 ++-- mm2src/coins/eth/eth_tests.rs | 21 +++++++++++---------- mm2src/coins/eth/eth_wasm_tests.rs | 2 +- mm2src/coins/eth/v2_activation.rs | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 36349a53c9..cea468a565 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -429,7 +429,7 @@ pub struct EthCoinImpl { fallback_swap_contract: Option
, contract_supports_watchers: bool, /// The separate web3 instances kept to get nonce, will replace the web3 completely soon - web3_instances: AsyncMutex>, + web3_instances: Arc>>, decimals: u8, gas_station_url: Option, gas_station_decimals: u8, @@ -5870,7 +5870,7 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - web3_instances: AsyncMutex::new(web3_instances), + web3_instances: AsyncMutex::new(web3_instances).into(), history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 3c6606c158..797de35c52 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -146,7 +146,7 @@ fn eth_coin_from_keypair( fallback_swap_contract, contract_supports_watchers: false, ticker, - web3_instances: AsyncMutex::new(web3_instances), + web3_instances: AsyncMutex::new(web3_instances).into(), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -330,7 +330,7 @@ fn send_and_refund_erc20_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -418,7 +418,7 @@ fn send_and_refund_eth_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -542,7 +542,8 @@ fn test_nonce_several_urls() { web3: web3_failing, is_parity: false, }, - ]), + ]) + .into(), decimals: 18, gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -600,7 +601,7 @@ fn test_wait_for_payment_spend_timeout() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -671,7 +672,7 @@ fn test_search_for_swap_tx_spend_was_spent() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -783,7 +784,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "BAT".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -1462,7 +1463,7 @@ fn test_message_hash() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1509,7 +1510,7 @@ fn test_sign_verify_message() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1570,7 +1571,7 @@ fn test_eth_extract_secret() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]).into(), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index f0c94aadfd..f420d8a438 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -37,7 +37,7 @@ async fn test_send() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index d968c0cab8..139e93465a 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -212,7 +212,7 @@ impl EthCoin { gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, gas_station_policy: self.gas_station_policy.clone(), - web3_instances: AsyncMutex::new(web3_instances), + web3_instances: AsyncMutex::new(web3_instances).into(), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), ctx: self.ctx.clone(), required_confirmations, @@ -320,7 +320,7 @@ pub async fn eth_coin_from_conf_and_request_v2( gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, - web3_instances: AsyncMutex::new(web3_instances), + web3_instances: AsyncMutex::new(web3_instances).into(), history_sync_state: Mutex::new(HistorySyncState::NotEnabled), ctx: ctx.weak(), required_confirmations, From 751ba29c380668bdb37f1b2b9194184ff1af55ab Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Sun, 18 Feb 2024 14:06:52 +0300 Subject: [PATCH 38/53] simplify `last_request_failed` handling Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 5 +++ .../eth/web3_transport/http_transport.rs | 32 +++++-------------- .../eth/web3_transport/metamask_transport.rs | 9 ++---- mm2src/coins/eth/web3_transport/mod.rs | 9 ++++++ .../eth/web3_transport/websocket_transport.rs | 4 --- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index cea468a565..7a854abe3d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2554,6 +2554,7 @@ impl RpcCommonOps for EthCoin { Ok(Ok(_)) => { // Bring the live client to the front of rpc_clients clients.rotate_left(i); + client.web3.transport().set_last_request_failed(false); return Ok(client); }, Ok(Err(rpc_error)) => { @@ -2562,6 +2563,8 @@ impl RpcCommonOps for EthCoin { if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; }; + + client.web3.transport().set_last_request_failed(true); }, Err(timeout_error) => { debug!( @@ -2572,6 +2575,8 @@ impl RpcCommonOps for EthCoin { if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; }; + + client.web3.transport().set_last_request_failed(true); }, }; } diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 4248d8173b..c200f66c8a 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -108,15 +108,12 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result serialized_request = r, Err(e) => { - transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error(request, e)); }, }; @@ -142,7 +139,6 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result (n.method.clone(), jsonrpc_core::Id::Null), Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), }; - transport.last_request_failed.store(true, Ordering::SeqCst); let error = format!( "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", transport.node.uri, REQUEST_TIMEOUT_S, method, id @@ -155,7 +151,6 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result r, Err(err) => { - transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error(request, Web3RpcError::Transport(err.to_string()))); }, }; @@ -163,7 +158,6 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result Result r, Err(err) => { - transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error( request, Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, err)), @@ -191,15 +184,12 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result Result { - transport.last_request_failed.store(false, Ordering::SeqCst); - let mut serialized_request = to_string(&request); if transport.node.gui_auth { match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { Ok(r) => serialized_request = r, Err(e) => { - transport.last_request_failed.store(true, Ordering::SeqCst); return Err(request_failed_error( request, Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), @@ -210,20 +200,14 @@ async fn send_request(request: Call, transport: HttpTransport) -> Result Ok(response_json), - Err(Error::Transport(e)) => { - transport.last_request_failed.store(true, Ordering::SeqCst); - Err(request_failed_error( - request, - Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), - )) - }, - Err(e) => { - transport.last_request_failed.store(true, Ordering::SeqCst); - Err(request_failed_error( - request, - Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, e)), - )) - }, + Err(Error::Transport(e)) => Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), + )), + Err(e) => Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, e)), + )), } } diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs index 7ec8e7fdda..4d5b87ad16 100644 --- a/mm2src/coins/eth/web3_transport/metamask_transport.rs +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -4,7 +4,7 @@ use jsonrpc_core::Call; use mm2_metamask::{detect_metamask_provider, Eip1193Provider, MetamaskResult, MetamaskSession}; use serde_json::Value as Json; use std::fmt; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use web3::{RequestId, Transport}; @@ -64,16 +64,11 @@ impl fmt::Debug for MetamaskTransport { impl MetamaskTransport { async fn send_impl(&self, id: RequestId, request: Call) -> Result { - self.last_request_failed.store(false, Ordering::SeqCst); - // Hold the mutex guard until the request is finished. let _rpc_lock = self.request_preparation().await?; match self.inner.eip1193.send(id, request).await { Ok(t) => Ok(t), - Err(e) => { - self.last_request_failed.store(true, Ordering::SeqCst); - Err(e) - }, + Err(e) => Err(e), } } diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 733e836d98..2968916fca 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -54,6 +54,15 @@ impl Web3Transport { } } + pub fn set_last_request_failed(&self, val: bool) { + match self { + Web3Transport::Http(http) => http.last_request_failed.store(val, Ordering::SeqCst), + Web3Transport::Websocket(websocket) => websocket.last_request_failed.store(val, Ordering::SeqCst), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.last_request_failed.store(val, Ordering::SeqCst), + } + } + #[cfg(any(test, target_arch = "wasm32"))] pub fn new_http(node: http_transport::HttpTransportNode) -> Web3Transport { http_transport::HttpTransport::new(node).into() diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 7b071515aa..6b2675b9b9 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -284,14 +284,12 @@ async fn send_request( request_id: RequestId, event_handlers: Vec, ) -> Result { - transport.last_request_failed.store(false, Ordering::SeqCst); let mut serialized_request = to_string(&request); if transport.node.gui_auth { match handle_gui_auth_payload(&transport.gui_auth_validation_generator, &request) { Ok(r) => serialized_request = r, Err(e) => { - transport.last_request_failed.store(true, Ordering::SeqCst); return Err(Error::Transport(TransportError::Message(format!( "Couldn't generate signed message payload for {:?}. Error: {e}", request @@ -323,8 +321,6 @@ async fn send_request( } }; - transport.last_request_failed.store(true, Ordering::SeqCst); - Err(Error::Transport(TransportError::Message(format!( "Sending {:?} failed.", request From aab065eb3ac91fd49d0b5b79813a561e8e6feeec Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 19 Feb 2024 15:17:25 +0300 Subject: [PATCH 39/53] implement temporary connections for websocket transport Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 171 +++++++++--------- mm2src/coins/eth/eth_tests.rs | 8 +- mm2src/coins/eth/v2_activation.rs | 16 +- .../eth/web3_transport/websocket_transport.rs | 25 ++- 4 files changed, 116 insertions(+), 104 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 7a854abe3d..11707f3c81 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -50,6 +50,7 @@ use futures::compat::Future01CompatExt; use futures::future::{join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; use http::{StatusCode, Uri}; +use instant::Instant; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; @@ -703,7 +704,9 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { // Todo: nonce_lock is still global for all addresses but this needs to be per address let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(my_address, coin.web3_instances.lock().await.to_vec()) + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) .compat() .timeout_secs(30.) .await? @@ -857,7 +860,9 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.lock().await.to_vec()) + let (nonce, _) = eth_coin + .clone() + .get_addr_nonce(eth_coin.my_address) .compat() .timeout_secs(30.) .await? @@ -941,7 +946,9 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd ) .await?; let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(my_address, eth_coin.web3_instances.lock().await.to_vec()) + let (nonce, _) = eth_coin + .clone() + .get_addr_nonce(my_address) .compat() .timeout_secs(30.) .await? @@ -2386,11 +2393,8 @@ async fn sign_transaction_with_keypair( } let _nonce_lock = coin.nonce_lock.lock().await; status.status(tags!(), "get_addr_nonce…"); - let (nonce, web3_instances_with_latest_nonce) = try_tx_s!( - get_addr_nonce(coin.my_address, coin.web3_instances.lock().await.to_vec()) - .compat() - .await - ); + let (nonce, web3_instances_with_latest_nonce) = + try_tx_s!(coin.clone().get_addr_nonce(coin.my_address).compat().await); status.status(tags!(), "get_gas_price…"); let gas_price = try_tx_s!(coin.get_gas_price().compat().await); @@ -4889,10 +4893,7 @@ impl EthCoin { /// The function is endless, we just keep looping in case of a transport error hoping it will go away. async fn wait_for_addr_nonce_increase(&self, addr: Address, prev_nonce: U256) { repeatable!(async { - match get_addr_nonce(addr, self.web3_instances.lock().await.to_vec()) - .compat() - .await - { + match self.clone().get_addr_nonce(addr).compat().await { Ok((new_nonce, _)) if new_nonce > prev_nonce => Ready(()), Ok((_nonce, _)) => Retry(()), Err(e) => { @@ -5029,6 +5030,75 @@ impl EthCoin { Ok(()) } + + /// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. + /// Transactions will be sent using the nodes that returned the highest nonce. + fn get_addr_nonce(self, addr: Address) -> Box), Error = String> + Send> { + const TMP_SOCKET_DURATION: Duration = Duration::from_secs(300); + + let fut = async move { + let mut errors: u32 = 0; + let web3_instances = self.web3_instances.lock().await.to_vec(); + loop { + let (futures, web3_instances): (Vec<_>, Vec<_>) = web3_instances + .iter() + .map(|instance| { + if let Web3Transport::Websocket(socket_transport) = instance.web3.transport() { + socket_transport.maybe_spawn_temporary_connection_loop( + self.clone(), + Instant::now() + TMP_SOCKET_DURATION, + ); + }; + + if instance.is_parity { + let parity: ParityNonce<_> = instance.web3.api(); + (Either::Left(parity.parity_next_nonce(addr)), instance.clone()) + } else { + ( + Either::Right(instance.web3.eth().transaction_count(addr, Some(BlockNumber::Pending))), + instance.clone(), + ) + } + }) + .unzip(); + + let nonces: Vec<_> = join_all(futures) + .await + .into_iter() + .zip(web3_instances.into_iter()) + .filter_map(|(nonce_res, instance)| match nonce_res { + Ok(n) => Some((n, instance)), + Err(e) => { + error!("Error getting nonce for addr {:?}: {}", addr, e); + None + }, + }) + .collect(); + if nonces.is_empty() { + // all requests errored + errors += 1; + if errors > 5 { + return ERR!("Couldn't get nonce after 5 errored attempts, aborting"); + } + } else { + let max = nonces + .iter() + .map(|(n, _)| *n) + .max() + .expect("nonces should not be empty!"); + break Ok(( + max, + nonces + .into_iter() + .filter_map(|(n, instance)| if n == max { Some(instance) } else { None }) + .collect(), + )); + } + Timer::sleep(1.).await + } + }; + Box::new(Box::pin(fut).compat()) + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -5747,6 +5817,8 @@ pub async fn eth_coin_from_conf_and_request( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { + const TMP_SOCKET_CONNECTION: Duration = Duration::from_secs(20); + let node = WebsocketTransportNode { uri: uri.clone(), gui_auth: false, @@ -5756,7 +5828,9 @@ pub async fn eth_coin_from_conf_and_request( // Temporarily start the connection loop (we close the connection once we have the client version below). // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or // dropping websocket support on parity nodes. - let fut = websocket_transport.clone().start_connection_loop(); + let fut = websocket_transport + .clone() + .start_connection_loop(Some(Instant::now() + TMP_SOCKET_CONNECTION)); let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); ctx.spawner().spawn_with_settings(fut, settings); @@ -5775,18 +5849,10 @@ pub async fn eth_coin_from_conf_and_request( Err(e) => { error!("Couldn't get client version for url {}: {}", url, e); - if let Web3Transport::Websocket(socket_transport) = web3.transport() { - socket_transport.stop_connection_loop().await; - }; - continue; }, }; - if let Web3Transport::Websocket(socket_transport) = web3.transport() { - socket_transport.stop_connection_loop().await; - }; - web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), @@ -5931,69 +5997,6 @@ pub(crate) fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", /// The input must be 0x prefixed hex string fn is_valid_checksum_addr(addr: &str) -> bool { addr == checksum_address(addr) } -/// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. -/// Transactions will be sent using the nodes that returned the highest nonce. -#[cfg_attr(test, mockable)] -fn get_addr_nonce( - addr: Address, - web3s: Vec, -) -> Box), Error = String> + Send> { - let fut = async move { - let mut errors: u32 = 0; - loop { - let (futures, web3s): (Vec<_>, Vec<_>) = web3s - .iter() - .map(|web3| { - if web3.is_parity { - let parity: ParityNonce<_> = web3.web3.api(); - (Either::Left(parity.parity_next_nonce(addr)), web3.clone()) - } else { - ( - Either::Right(web3.web3.eth().transaction_count(addr, Some(BlockNumber::Pending))), - web3.clone(), - ) - } - }) - .unzip(); - - let nonces: Vec<_> = join_all(futures) - .await - .into_iter() - .zip(web3s.into_iter()) - .filter_map(|(nonce_res, web3)| match nonce_res { - Ok(n) => Some((n, web3)), - Err(e) => { - error!("Error getting nonce for addr {:?}: {}", addr, e); - None - }, - }) - .collect(); - if nonces.is_empty() { - // all requests errored - errors += 1; - if errors > 5 { - return ERR!("Couldn't get nonce after 5 errored attempts, aborting"); - } - } else { - let max = nonces - .iter() - .map(|(n, _)| *n) - .max() - .expect("nonces should not be empty!"); - break Ok(( - max, - nonces - .into_iter() - .filter_map(|(n, web3)| if n == max { Some(web3) } else { None }) - .collect(), - )); - } - Timer::sleep(1.).await - } - }; - Box::new(Box::pin(fut).compat()) -} - fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { let one_gwei = U256::from(10u64.pow(9)); let percent = (num / U256::from(100)) * U256::from(percent); diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 797de35c52..8ee1935e26 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -563,9 +563,7 @@ fn test_nonce_several_urls() { let payment = coin.send_to_address(coin.my_address, 200000000.into()).wait().unwrap(); log!("{:?}", payment); - let new_nonce = get_addr_nonce(coin.my_address, block_on(coin.web3_instances.lock()).to_vec()) - .wait() - .unwrap(); + let new_nonce = coin.clone().get_addr_nonce(coin.my_address).wait().unwrap(); log!("{:?}", new_nonce); } @@ -841,7 +839,7 @@ fn test_withdraw_impl_manual_fee() { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); - get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); + EthCoin::get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -885,7 +883,7 @@ fn test_withdraw_impl_fee_details() { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); - get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); + EthCoin::get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); let withdraw_req = WithdrawRequest { amount: 1.into(), diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 139e93465a..ca25dfce8f 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -3,6 +3,7 @@ use super::*; use common::executor::AbortedError; use crypto::{CryptoCtxError, StandardHDCoinAddress}; use enum_derives::EnumFromTrait; +use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -411,6 +412,8 @@ async fn build_web3_instances( let transport = match uri.scheme_str() { Some("ws") | Some("wss") => { + const TMP_SOCKET_CONNECTION: Duration = Duration::from_secs(20); + let node = WebsocketTransportNode { uri: uri.clone(), gui_auth: eth_node.gui_auth, @@ -429,7 +432,9 @@ async fn build_web3_instances( // Temporarily start the connection loop (we close the connection once we have the client version below). // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or // dropping websocket support on parity nodes. - let fut = websocket_transport.clone().start_connection_loop(); + let fut = websocket_transport + .clone() + .start_connection_loop(Some(Instant::now() + TMP_SOCKET_CONNECTION)); let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); ctx.spawner().spawn_with_settings(fut, settings); @@ -456,19 +461,10 @@ async fn build_web3_instances( Ok(v) => v, Err(e) => { error!("Couldn't get client version for url {}: {}", eth_node.url, e); - - if let Web3Transport::Websocket(socket_transport) = web3.transport() { - socket_transport.stop_connection_loop().await; - }; - continue; }, }; - if let Web3Transport::Websocket(socket_transport) = web3.transport() { - socket_transport.stop_connection_loop().await; - }; - web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6b2675b9b9..eb47ec8d05 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -17,7 +17,7 @@ use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; -use instant::Duration; +use instant::{Duration, Instant}; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; use std::collections::HashMap; @@ -111,7 +111,7 @@ impl WebsocketTransport { } } - pub(crate) async fn start_connection_loop(self) { + pub(crate) async fn start_connection_loop(self, expires_at: Option) { let _guard = self.connection_guard.lock().await; const MAX_ATTEMPTS: u32 = 3; @@ -124,9 +124,8 @@ impl WebsocketTransport { max_attempts: u32, sleep_duration_on_failure: f64, ) -> tokio_tungstenite_wasm::Result { + let mut attempts = 0; loop { - let mut attempts = 0; - match tokio_tungstenite_wasm::connect(address.clone()).await { Ok(ws) => return Ok(ws), Err(e) => { @@ -162,6 +161,13 @@ impl WebsocketTransport { futures_util::select! { // KEEPALIVE HANDLING _ = keepalive_interval.next().fuse() => { + if let Some(expires_at) = expires_at { + if Instant::now() >= expires_at { + log::info!("Dropping temporary connection for {:?}", self.node.uri.to_string()); + break; + } + } + // Drop expired response notifier channels response_notifiers.clear_expired_entries(); @@ -271,7 +277,16 @@ impl WebsocketTransport { pub(crate) fn maybe_spawn_connection_loop(&self, coin: EthCoin) { // if we can acquire the lock here, it means connection loop is not alive if self.connection_guard.try_lock().is_some() { - let fut = self.clone().start_connection_loop(); + let fut = self.clone().start_connection_loop(None); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); + coin.spawner().spawn_with_settings(fut, settings); + } + } + + pub(crate) fn maybe_spawn_temporary_connection_loop(&self, coin: EthCoin, expires_at: Instant) { + // if we can acquire the lock here, it means connection loop is not alive + if self.connection_guard.try_lock().is_some() { + let fut = self.clone().start_connection_loop(Some(expires_at)); let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); coin.spawner().spawn_with_settings(fut, settings); } From 5c0606f980bf98d28fa973732cf370cd9b86cf24 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 19 Feb 2024 15:25:33 +0300 Subject: [PATCH 40/53] use incremental timeouts Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index eb47ec8d05..e0929575be 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -122,9 +122,11 @@ impl WebsocketTransport { async fn attempt_to_establish_socket_connection( address: String, max_attempts: u32, - sleep_duration_on_failure: f64, + mut sleep_duration_on_failure: f64, ) -> tokio_tungstenite_wasm::Result { + const MAX_SLEEP_DURATION: f64 = 32.0; let mut attempts = 0; + loop { match tokio_tungstenite_wasm::connect(address.clone()).await { Ok(ws) => return Ok(ws), @@ -135,7 +137,7 @@ impl WebsocketTransport { } Timer::sleep(sleep_duration_on_failure).await; - continue; + sleep_duration_on_failure = (sleep_duration_on_failure * 2.0).min(MAX_SLEEP_DURATION); }, }; } @@ -325,7 +327,7 @@ async fn send_request( response_notifier: notification_sender, })) .await - .expect("receiver channel must be alive"); + .map_err(|e| Error::Transport(TransportError::Message(e.to_string())))?; if let Ok(_ping) = notification_receiver.await { let response_map = unsafe { &mut *transport.responses.0 }; From 93589e8c35aa2cf53eea7a4624478d106aeb1d0e Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 19 Feb 2024 15:47:02 +0300 Subject: [PATCH 41/53] switch `info` log into `debug` Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index e0929575be..6771bd3657 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -165,7 +165,7 @@ impl WebsocketTransport { _ = keepalive_interval.next().fuse() => { if let Some(expires_at) = expires_at { if Instant::now() >= expires_at { - log::info!("Dropping temporary connection for {:?}", self.node.uri.to_string()); + log::debug!("Dropping temporary connection for {:?}", self.node.uri.to_string()); break; } } From c5177ea6301bdac142bb0ed63261cf5e98538ddc Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 20 Feb 2024 12:06:46 +0300 Subject: [PATCH 42/53] improve how we handle `last_request_failed` Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 5 ----- mm2src/coins/eth/web3_transport/mod.rs | 29 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 11707f3c81..9ecf974f2d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2558,7 +2558,6 @@ impl RpcCommonOps for EthCoin { Ok(Ok(_)) => { // Bring the live client to the front of rpc_clients clients.rotate_left(i); - client.web3.transport().set_last_request_failed(false); return Ok(client); }, Ok(Err(rpc_error)) => { @@ -2567,8 +2566,6 @@ impl RpcCommonOps for EthCoin { if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; }; - - client.web3.transport().set_last_request_failed(true); }, Err(timeout_error) => { debug!( @@ -2579,8 +2576,6 @@ impl RpcCommonOps for EthCoin { if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; }; - - client.web3.transport().set_last_request_failed(true); }, }; } diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 2968916fca..ded21de96e 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -54,7 +54,7 @@ impl Web3Transport { } } - pub fn set_last_request_failed(&self, val: bool) { + fn set_last_request_failed(&self, val: bool) { match self { Web3Transport::Http(http) => http.last_request_failed.store(val, Ordering::SeqCst), Web3Transport::Websocket(websocket) => websocket.last_request_failed.store(val, Ordering::SeqCst), @@ -91,12 +91,27 @@ impl Transport for Web3Transport { } fn send(&self, id: RequestId, request: Call) -> Self::Out { - match self { - Web3Transport::Http(http) => http.send(id, request), - Web3Transport::Websocket(websocket) => websocket.send(id, request), - #[cfg(target_arch = "wasm32")] - Web3Transport::Metamask(metamask) => metamask.send(id, request), - } + let selfi = self.clone(); + let fut = async move { + let result = match &selfi { + Web3Transport::Http(http) => http.send(id, request), + Web3Transport::Websocket(websocket) => websocket.send(id, request), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.send(id, request), + } + .await; + + if result.is_ok() { + selfi.set_last_request_failed(false); + } else { + println!("BASARISIZ"); + selfi.set_last_request_failed(true); + } + + result + }; + + Box::pin(fut) } } From 272a0b6fcc988aeea375df1b35d7c25012b414de Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 20 Feb 2024 12:41:10 +0300 Subject: [PATCH 43/53] seperate select functions controlled by callers Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/mod.rs | 1 - .../eth/web3_transport/websocket_transport.rs | 320 +++++++++++------- 2 files changed, 193 insertions(+), 128 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index ded21de96e..f74c1dfd44 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -104,7 +104,6 @@ impl Transport for Web3Transport { if result.is_ok() { selfi.set_last_request_failed(false); } else { - println!("BASARISIZ"); selfi.set_last_request_failed(true); } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6771bd3657..5dae50da88 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -30,6 +30,9 @@ use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; const REQUEST_TIMEOUT_AS_SEC: u64 = 10; +const MAX_ATTEMPTS: u32 = 3; +const SLEEP_DURATION: f64 = 1.; +const KEEPALIVE_DURATION: Duration = Duration::from_secs(10); #[derive(Clone, Debug)] pub(crate) struct WebsocketTransportNode { @@ -88,6 +91,13 @@ impl Drop for SafeMapPtr { unsafe impl Send for SafeMapPtr {} unsafe impl Sync for SafeMapPtr {} +enum OuterAction { + None, + Continue, + Break, + Return, +} + impl WebsocketTransport { pub(crate) fn with_event_handlers( node: WebsocketTransportNode, @@ -111,155 +121,212 @@ impl WebsocketTransport { } } - pub(crate) async fn start_connection_loop(self, expires_at: Option) { - let _guard = self.connection_guard.lock().await; - - const MAX_ATTEMPTS: u32 = 3; - const SLEEP_DURATION: f64 = 1.; + async fn handle_keepalive( + &self, + wsocket: &mut WebSocketStream, + response_notifiers: &mut ExpirableMap>, + expires_at: Option, + ) -> OuterAction { const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; - const KEEPALIVE_DURATION: Duration = Duration::from_secs(10); - - async fn attempt_to_establish_socket_connection( - address: String, - max_attempts: u32, - mut sleep_duration_on_failure: f64, - ) -> tokio_tungstenite_wasm::Result { - const MAX_SLEEP_DURATION: f64 = 32.0; - let mut attempts = 0; - - loop { - match tokio_tungstenite_wasm::connect(address.clone()).await { - Ok(ws) => return Ok(ws), - Err(e) => { - attempts += 1; - if attempts > max_attempts { - return Err(e); - } - Timer::sleep(sleep_duration_on_failure).await; - sleep_duration_on_failure = (sleep_duration_on_failure * 2.0).min(MAX_SLEEP_DURATION); - }, - }; + if let Some(expires_at) = expires_at { + if Instant::now() >= expires_at { + log::debug!("Dropping temporary connection for {:?}", self.node.uri.to_string()); + return OuterAction::Break; } } - // List of awaiting requests - let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); + // Drop expired response notifier channels + response_notifiers.clear_expired_entries(); - let mut wsocket = - match attempt_to_establish_socket_connection(self.node.uri.to_string(), MAX_ATTEMPTS, SLEEP_DURATION).await + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket + .send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())) + .await { - Ok(ws) => ws, + Ok(_) => { + should_continue = false; + break; + }, Err(e) => { - log::error!("Connection could not established for {}. Error {e}", self.node.uri); - return; + log::error!("{e}"); + should_continue = true; }, }; - let mut keepalive_interval = Ticker::new(KEEPALIVE_DURATION); - let mut req_rx = self.controller_channel.rx.lock().await; + Timer::sleep(SLEEP_DURATION).await; + } - loop { - futures_util::select! { - // KEEPALIVE HANDLING - _ = keepalive_interval.next().fuse() => { - if let Some(expires_at) = expires_at { - if Instant::now() >= expires_at { - log::debug!("Dropping temporary connection for {:?}", self.node.uri.to_string()); + if should_continue { + return OuterAction::Continue; + } + + OuterAction::None + } + + async fn handle_send_request( + &self, + request: Option, + wsocket: &mut WebSocketStream, + response_notifiers: &mut ExpirableMap>, + ) -> OuterAction { + match request { + Some(ControllerMessage::Request(WsRequest { + request_id, + serialized_request, + response_notifier, + })) => { + response_notifiers.insert( + request_id, + response_notifier, + Duration::from_secs(REQUEST_TIMEOUT_AS_SEC), + ); + + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket + .send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())) + .await + { + Ok(_) => { + should_continue = false; break; - } + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + }, } - // Drop expired response notifier channels - response_notifiers.clear_expired_entries(); - - let mut should_continue = Default::default(); - for _ in 0..MAX_ATTEMPTS { - match wsocket.send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())).await { - Ok(_) => { - should_continue = false; - break; - }, - Err(e) => { - log::error!("{e}"); - should_continue = true; + Timer::sleep(SLEEP_DURATION).await; + } + + if should_continue { + let _ = response_notifiers.remove(&request_id); + return OuterAction::Continue; + } + }, + Some(ControllerMessage::Close) => { + return OuterAction::Break; + }, + _ => {}, + } + + OuterAction::None + } + + async fn handle_response( + &self, + message: Option>, + response_notifiers: &mut ExpirableMap>, + ) -> OuterAction { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + return OuterAction::Continue; + } + + if let Some(id) = inc_event.get("id") { + let request_id = id.as_u64().unwrap_or_default() as usize; + + if let Some(notifier) = response_notifiers.remove(&request_id) { + let mut res_bytes: Vec = Vec::new(); + if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { + let response_map = unsafe { &mut *self.responses.0 }; + let _ = response_map.insert(request_id, res_bytes); + + notifier.send(()).expect("receiver channel must be alive"); } - }; + } + } + } + }, + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => return OuterAction::Continue, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => return OuterAction::Break, + Some(Err(e)) => { + log::error!("{e}"); + return OuterAction::Return; + }, + None => return OuterAction::Continue, + }; - Timer::sleep(SLEEP_DURATION).await; + OuterAction::None + } + + async fn attempt_to_establish_socket_connection( + &self, + max_attempts: u32, + mut sleep_duration_on_failure: f64, + ) -> tokio_tungstenite_wasm::Result { + const MAX_SLEEP_DURATION: f64 = 32.0; + let mut attempts = 0; + + loop { + match tokio_tungstenite_wasm::connect(self.node.uri.to_string()).await { + Ok(ws) => return Ok(ws), + Err(e) => { + attempts += 1; + if attempts > max_attempts { + return Err(e); } - if should_continue { - continue; + Timer::sleep(sleep_duration_on_failure).await; + sleep_duration_on_failure = (sleep_duration_on_failure * 2.0).min(MAX_SLEEP_DURATION); + }, + }; + } + } + + pub(crate) async fn start_connection_loop(self, expires_at: Option) { + let _guard = self.connection_guard.lock().await; + + // List of awaiting requests + let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); + + let mut wsocket = match self + .attempt_to_establish_socket_connection(MAX_ATTEMPTS, SLEEP_DURATION) + .await + { + Ok(ws) => ws, + Err(e) => { + log::error!("Connection could not established for {}. Error {e}", self.node.uri); + return; + }, + }; + + let mut keepalive_interval = Ticker::new(KEEPALIVE_DURATION); + let mut req_rx = self.controller_channel.rx.lock().await; + + loop { + futures_util::select! { + _ = keepalive_interval.next().fuse() => { + match self.handle_keepalive(&mut wsocket, &mut response_notifiers, expires_at).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, } } // SEND REQUESTS request = req_rx.next().fuse() => { - match request { - Some(ControllerMessage::Request(WsRequest { request_id, serialized_request, response_notifier })) => { - response_notifiers.insert(request_id, response_notifier, Duration::from_secs(REQUEST_TIMEOUT_AS_SEC)); - - let mut should_continue = Default::default(); - for _ in 0..MAX_ATTEMPTS { - match wsocket.send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())).await { - Ok(_) => { - should_continue = false; - break; - }, - Err(e) => { - log::error!("{e}"); - should_continue = true; - } - } - - Timer::sleep(SLEEP_DURATION).await; - } - - if should_continue { - let _ = response_notifiers.remove(&request_id); - continue; - } - }, - Some(ControllerMessage::Close) => { - break; - }, - _ => {}, + match self.handle_send_request(request, &mut wsocket, &mut response_notifiers).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, } } // HANDLE RESPONSES SENT FROM USER message = wsocket.next().fuse() => { - match message { - Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { - if let Ok(inc_event) = serde_json::from_str::(&inc_event) { - if !inc_event.is_object() { - continue; - } - - if let Some(id) = inc_event.get("id") { - let request_id = id.as_u64().unwrap_or_default() as usize; - - if let Some(notifier) = response_notifiers.remove(&request_id) { - let mut res_bytes: Vec = Vec::new(); - if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { - let response_map = unsafe { &mut *self.responses.0 }; - let _ = response_map.insert(request_id, res_bytes); - - notifier.send(()).expect("receiver channel must be alive"); - } - - } - } - } - }, - Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => continue, - Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => break, - Some(Err(e)) => { - log::error!("{e}"); - return; - }, - None => continue, + match self.handle_response(message, &mut response_notifiers).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, } } } @@ -277,18 +344,17 @@ impl WebsocketTransport { } pub(crate) fn maybe_spawn_connection_loop(&self, coin: EthCoin) { - // if we can acquire the lock here, it means connection loop is not alive - if self.connection_guard.try_lock().is_some() { - let fut = self.clone().start_connection_loop(None); - let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); - coin.spawner().spawn_with_settings(fut, settings); - } + self.maybe_spawn_connection_loop_inner(coin, None) } pub(crate) fn maybe_spawn_temporary_connection_loop(&self, coin: EthCoin, expires_at: Instant) { + self.maybe_spawn_connection_loop_inner(coin, Some(expires_at)) + } + + fn maybe_spawn_connection_loop_inner(&self, coin: EthCoin, expires_at: Option) { // if we can acquire the lock here, it means connection loop is not alive if self.connection_guard.try_lock().is_some() { - let fut = self.clone().start_connection_loop(Some(expires_at)); + let fut = self.clone().start_connection_loop(expires_at); let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); coin.spawner().spawn_with_settings(fut, settings); } From 0d6656b6c5be49798d9e3328e67d07d299425eb6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Tue, 20 Feb 2024 13:46:30 +0300 Subject: [PATCH 44/53] remove redundant comments Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 5dae50da88..e9d4181b3e 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -310,7 +310,6 @@ impl WebsocketTransport { } } - // SEND REQUESTS request = req_rx.next().fuse() => { match self.handle_send_request(request, &mut wsocket, &mut response_notifiers).await { OuterAction::None => {}, @@ -320,7 +319,6 @@ impl WebsocketTransport { } } - // HANDLE RESPONSES SENT FROM USER message = wsocket.next().fuse() => { match self.handle_response(message, &mut response_notifiers).await { OuterAction::None => {}, From 5e5c2e010798a4f6b12fba1f65030e292d182667 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 15:52:31 +0300 Subject: [PATCH 45/53] revert `Arc` on `web3_instances` Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 5 ++--- mm2src/coins/eth/eth_tests.rs | 21 ++++++++++----------- mm2src/coins/eth/eth_wasm_tests.rs | 2 +- mm2src/coins/eth/v2_activation.rs | 4 ++-- mm2src/coins/eth/web3_transport/mod.rs | 6 +----- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 9ecf974f2d..ffe3fe5b99 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -429,8 +429,7 @@ pub struct EthCoinImpl { swap_contract_address: Address, fallback_swap_contract: Option
, contract_supports_watchers: bool, - /// The separate web3 instances kept to get nonce, will replace the web3 completely soon - web3_instances: Arc>>, + web3_instances: AsyncMutex>, decimals: u8, gas_station_url: Option, gas_station_decimals: u8, @@ -5936,7 +5935,7 @@ pub async fn eth_coin_from_conf_and_request( gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy, - web3_instances: AsyncMutex::new(web3_instances).into(), + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(initial_history_state), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 8ee1935e26..b03f4708d8 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -146,7 +146,7 @@ fn eth_coin_from_keypair( fallback_swap_contract, contract_supports_watchers: false, ticker, - web3_instances: AsyncMutex::new(web3_instances).into(), + web3_instances: AsyncMutex::new(web3_instances), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -330,7 +330,7 @@ fn send_and_refund_erc20_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -418,7 +418,7 @@ fn send_and_refund_eth_payment() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -542,8 +542,7 @@ fn test_nonce_several_urls() { web3: web3_failing, is_parity: false, }, - ]) - .into(), + ]), decimals: 18, gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -599,7 +598,7 @@ fn test_wait_for_payment_spend_timeout() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -670,7 +669,7 @@ fn test_search_for_swap_tx_spend_was_spent() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -782,7 +781,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "BAT".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, @@ -1461,7 +1460,7 @@ fn test_message_hash() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1508,7 +1507,7 @@ fn test_sign_verify_message() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, @@ -1569,7 +1568,7 @@ fn test_eth_extract_secret() { fallback_swap_contract: None, contract_supports_watchers: false, ticker: "ETH".into(), - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: true }]), ctx: ctx.weak(), required_confirmations: 1.into(), chain_id: None, diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index f420d8a438..f0c94aadfd 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -37,7 +37,7 @@ async fn test_send() { swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), fallback_swap_contract: None, contract_supports_watchers: false, - web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]).into(), + web3_instances: AsyncMutex::new(vec![Web3Instance { web3, is_parity: false }]), decimals: 18, gas_station_url: None, gas_station_decimals: ETH_GAS_STATION_DECIMALS, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index ca25dfce8f..eb37dc2374 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -213,7 +213,7 @@ impl EthCoin { gas_station_url: self.gas_station_url.clone(), gas_station_decimals: self.gas_station_decimals, gas_station_policy: self.gas_station_policy.clone(), - web3_instances: AsyncMutex::new(web3_instances).into(), + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), ctx: self.ctx.clone(), required_confirmations, @@ -321,7 +321,7 @@ pub async fn eth_coin_from_conf_and_request_v2( gas_station_url: req.gas_station_url, gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), gas_station_policy: req.gas_station_policy, - web3_instances: AsyncMutex::new(web3_instances).into(), + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(HistorySyncState::NotEnabled), ctx: ctx.weak(), required_confirmations, diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index f74c1dfd44..67df6cfae3 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -101,11 +101,7 @@ impl Transport for Web3Transport { } .await; - if result.is_ok() { - selfi.set_last_request_failed(false); - } else { - selfi.set_last_request_failed(true); - } + selfi.set_last_request_failed(result.is_err()); result }; From a8440e85186e88104a6c699849aeadafdcabb5b4 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 20:01:39 +0300 Subject: [PATCH 46/53] create `eth_rpc` abstraction module Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 218 +++--------- mm2src/coins/eth/eth_rpc.rs | 443 +++++++++++++++++++++++++ mm2src/coins/eth/eth_tests.rs | 4 +- mm2src/coins/eth/web3_transport/mod.rs | 39 +-- 4 files changed, 488 insertions(+), 216 deletions(-) create mode 100644 mm2src/coins/eth/eth_rpc.rs diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index ffe3fe5b99..96f990211a 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -76,7 +76,7 @@ use std::time::Duration; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; -use web3_transport::{http_transport::HttpTransportNode, EthFeeHistoryNamespace, Web3Transport}; +use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; cfg_wasm32! { use crypto::MetamaskArc; @@ -108,6 +108,7 @@ use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, pub use rlp; mod eth_balance_events; +mod eth_rpc; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; mod web3_transport; @@ -620,9 +621,6 @@ async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransactionResult { let web3_tx = coin - .web3() - .await? - .eth() .transaction(TransactionId::Hash(tx_hash)) .await? .or_mm_err(|| RawTransactionError::HashNotExist(tx_hash.to_string()))?; @@ -754,7 +752,7 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = coin.web3().await?.eth().send_transaction(tx_to_send).await?; + let tx_hash = coin.send_transaction(tx_to_send).await?; let signed_tx = coin .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -1118,8 +1116,7 @@ impl SwapOps for EthCoin { match found { Some(event) => { let transaction = try_s!( - try_s!(selfi.web3().await) - .eth() + selfi .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await ); @@ -1675,12 +1672,7 @@ impl WatcherOps for EthCoin { let decimals = self.decimals; let fut = async move { - let tx_from_rpc = selfi - .web3() - .await? - .eth() - .transaction(TransactionId::Hash(tx.hash)) - .await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx)) @@ -2093,8 +2085,7 @@ impl MarketCoinOps for EthCoin { let coin = self.clone(); let fut = async move { - let result = try_s!(coin.web3().await) - .eth() + let result = coin .send_raw_transaction(bytes.into()) .await .map(|res| format!("{:02x}", res)) @@ -2111,9 +2102,7 @@ impl MarketCoinOps for EthCoin { let tx = tx.to_owned(); let fut = async move { - try_s!(coin.web3().await) - .eth() - .send_raw_transaction(tx.into()) + coin.send_raw_transaction(tx.into()) .await .map(|res| format!("{:02x}", res)) .map_err(|e| ERRL!("{}", e)) @@ -2289,11 +2278,7 @@ impl MarketCoinOps for EthCoin { if let Some(event) = found { if let Some(tx_hash) = event.transaction_hash { - let transaction = match try_tx_s!(selfi.web3().await) - .eth() - .transaction(TransactionId::Hash(tx_hash)) - .await - { + let transaction = match selfi.transaction(TransactionId::Hash(tx_hash)).await { Ok(Some(t)) => t, Ok(None) => { info!("Tx {} not found yet", tx_hash); @@ -2327,9 +2312,7 @@ impl MarketCoinOps for EthCoin { let coin = self.clone(); let fut = async move { - try_s!(coin.web3().await) - .eth() - .block_number() + coin.block_number() .await .map(|res| res.as_u64()) .map_err(|e| ERRL!("{}", e)) @@ -2476,7 +2459,7 @@ async fn sign_and_send_transaction_with_metamask( // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = try_tx_s!(try_tx_s!(coin.web3().await).eth().send_transaction(tx_to_send).await); + let tx_hash = try_tx_s!(try_tx_s!(coin.send_transaction(tx_to_send).await)); let maybe_signed_tx = try_tx_s!( coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -2607,13 +2590,7 @@ impl EthCoin { let coin = self.clone(); - let fut = async move { - try_s!(coin.web3().await) - .eth() - .logs(filter) - .await - .map_err(|e| ERRL!("{}", e)) - }; + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -2639,14 +2616,7 @@ impl EthCoin { let coin = self.clone(); - let fut = async move { - try_s!(coin.web3().await) - .trace() - .filter(filter.build()) - .await - .map_err(|e| ERRL!("{}", e)) - }; - + let fut = async move { coin.trace_filter(filter.build()).await.map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -2676,13 +2646,7 @@ impl EthCoin { let coin = self.clone(); - let fut = async move { - try_s!(coin.web3().await) - .eth() - .logs(filter.build()) - .await - .map_err(|e| ERRL!("{}", e)) - }; + let fut = async move { coin.logs(filter.build()).await.map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -2712,20 +2676,7 @@ impl EthCoin { }; } - let web3 = match self.web3().await { - Ok(t) => t, - Err(e) => { - ctx.log.log( - "", - &[&"tx_history", &self.ticker], - &ERRL!("Couldn't connect to client. Error: {} - retrying..", e), - ); - Timer::sleep(3.).await; - continue; - }, - }; - - let current_block = match web3.eth().block_number().await { + let current_block = match self.block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -2907,8 +2858,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); - let web3_tx = match web3 - .eth() + let web3_tx = match self .transaction(TransactionId::Hash(trace.transaction_hash.unwrap())) .await { @@ -2940,7 +2890,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); - let receipt = match web3.eth().transaction_receipt(trace.transaction_hash.unwrap()).await { + let receipt = match self.transaction_receipt(trace.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -2993,8 +2943,7 @@ impl EthCoin { } let raw = signed_tx_from_web3_tx(web3_tx).unwrap(); - let block = match web3 - .eth() + let block = match self .block(BlockId::Number(BlockNumber::Number(trace.block_number.into()))) .await { @@ -3077,20 +3026,7 @@ impl EthCoin { }; } - let web3 = match self.web3().await { - Ok(t) => t, - Err(e) => { - ctx.log.log( - "", - &[&"tx_history", &self.ticker], - &ERRL!("Couldn't connect to client. Error: {} - retrying..", e), - ); - Timer::sleep(3.).await; - continue; - }, - }; - - let current_block = match web3.eth().block_number().await { + let current_block = match self.block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -3292,8 +3228,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "client" => "ethereum", "method" => "tx_detail_by_hash"); - let web3_tx = match web3 - .eth() + let web3_tx = match self .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await { @@ -3327,7 +3262,7 @@ impl EthCoin { }, }; - let receipt = match web3.eth().transaction_receipt(event.transaction_hash.unwrap()).await { + let receipt = match self.transaction_receipt(event.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -3358,11 +3293,7 @@ impl EthCoin { None => None, }; let block_number = event.block_number.unwrap(); - let block = match web3 - .eth() - .block(BlockId::Number(BlockNumber::Number(block_number))) - .await - { + let block = match self.block(BlockId::Number(BlockNumber::Number(block_number))).await { Ok(Some(b)) => b, Ok(None) => { ctx.log.log( @@ -4085,12 +4016,7 @@ impl EthCoin { let coin = self.clone(); let fut = async move { match coin.coin_type { - EthCoinType::Eth => Ok(coin - .web3() - .await? - .eth() - .balance(address, Some(BlockNumber::Latest)) - .await?), + EthCoinType::Eth => Ok(coin.balance(address, Some(BlockNumber::Latest)).await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; @@ -4197,18 +4123,11 @@ impl EthCoin { Ok(owner_address) } - fn estimate_gas(&self, req: CallRequest) -> Box + Send> { + fn estimate_gas_wrapper(&self, req: CallRequest) -> Box + Send> { let coin = self.clone(); // always using None block number as old Geth version accept only single argument in this RPC - let fut = async move { - coin.web3() - .await - .map_err(|_| web3::Error::Unreachable)? - .eth() - .estimate_gas(req, None) - .await - }; + let fut = async move { coin.estimate_gas(req, None).await }; Box::new(fut.boxed().compat()) } @@ -4238,22 +4157,15 @@ impl EthCoin { gas_price: Some(gas_price), ..CallRequest::default() }; - coin.estimate_gas(estimate_gas_req).map_to_mm_fut(Web3RpcError::from) + coin.estimate_gas_wrapper(estimate_gas_req) + .map_to_mm_fut(Web3RpcError::from) })) } fn eth_balance(&self) -> BalanceFut { let coin = self.clone(); - let fut = async move { - coin.web3() - .await - .map_err(|_| web3::Error::Unreachable)? - .eth() - .balance(coin.my_address, Some(BlockNumber::Latest)) - .await - }; - + let fut = async move { coin.balance(coin.my_address, Some(BlockNumber::Latest)).await }; Box::new(fut.boxed().compat().map_to_mm_fut(BalanceError::from)) } @@ -4268,12 +4180,7 @@ impl EthCoin { ..CallRequest::default() }; - self.web3() - .await - .map_err(|_| web3::Error::Unreachable)? - .eth() - .call(request, Some(BlockId::Number(BlockNumber::Latest))) - .await + self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } fn allowance(&self, spender: Address) -> Web3RpcFut { @@ -4377,14 +4284,7 @@ impl EthCoin { let coin = self.clone(); - let fut = async move { - try_s!(coin.web3().await) - .eth() - .logs(filter) - .await - .map_err(|e| ERRL!("{}", e)) - }; - + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -4405,14 +4305,7 @@ impl EthCoin { let coin = self.clone(); - let fut = async move { - try_s!(coin.web3().await) - .eth() - .logs(filter) - .await - .map_err(|e| ERRL!("{}", e)) - }; - + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } @@ -4454,12 +4347,7 @@ impl EthCoin { ))); } - let tx_from_rpc = selfi - .web3() - .await? - .eth() - .transaction(TransactionId::Hash(tx.hash)) - .await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.hash)) })?; @@ -4749,12 +4637,7 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!( - try_s!(self.web3().await) - .eth() - .transaction(TransactionId::Hash(tx_hash)) - .await - ) { + let transaction = match try_s!(self.transaction(TransactionId::Hash(tx_hash)).await) { Some(t) => t, None => { return ERR!("Found ReceiverSpent event, but transaction {:02x} is missing", tx_hash) @@ -4779,12 +4662,7 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!( - try_s!(self.web3().await) - .eth() - .transaction(TransactionId::Hash(tx_hash)) - .await - ) { + let transaction = match try_s!(self.transaction(TransactionId::Hash(tx_hash)).await) { Some(t) => t, None => { return ERR!("Found SenderRefunded event, but transaction {:02x} is missing", tx_hash) @@ -4842,7 +4720,7 @@ impl EthCoin { None => None, }; - let eth_gas_price = match coin.web3().await?.eth().gas_price().await { + let eth_gas_price = match coin.gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); @@ -4850,11 +4728,7 @@ impl EthCoin { }, }; - let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3().await?.api(); - let eth_fee_history_price = match fee_history_namespace - .eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]) - .await - { + let eth_fee_history_price = match coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]).await { Ok(res) => res .base_fee_per_gas .first() @@ -4912,12 +4786,7 @@ impl EthCoin { ) -> Web3RpcResult> { let wait_until = wait_until_ms(wait_rpc_timeout_ms); while now_ms() < wait_until { - let maybe_tx = self - .web3() - .await? - .eth() - .transaction(TransactionId::Hash(tx_hash)) - .await?; + let maybe_tx = self.transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; return Ok(Some(signed_tx)); @@ -4946,7 +4815,7 @@ impl EthCoin { ))); } - let web3_receipt = match selfi.web3().await?.eth().transaction_receipt(payment_hash).await { + let web3_receipt = match selfi.transaction_receipt(payment_hash).await { Ok(r) => r, Err(e) => { error!( @@ -4994,7 +4863,7 @@ impl EthCoin { ))); } - match selfi.web3().await?.eth().block_number().await { + match selfi.block_number().await { Ok(current_block) => { if current_block >= block_number { break Ok(()); @@ -5326,7 +5195,7 @@ impl MmCoin for EthCoin { // Please note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. // Ideally we should determine the case when we have the insufficient balance and return `TradePreimageError::NotSufficientBalance` error. - let gas_limit = self.estimate_gas(estimate_gas_req).compat().await?; + let gas_limit = self.estimate_gas_wrapper(estimate_gas_req).compat().await?; let total_fee = gas_limit * gas_price; let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { @@ -5464,12 +5333,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let fut = async move { let expected_value = wei_from_big_decimal(&amount, coin.decimals)?; - let tx_from_rpc = coin - .web3() - .await? - .eth() - .transaction(TransactionId::Hash(fee_tx_hash)) - .await?; + let tx_from_rpc = coin.transaction(TransactionId::Hash(fee_tx_hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", fee_tx_hash)) @@ -6153,7 +6017,7 @@ async fn get_eth_gas_details( }; // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; + let gas_limit = eth_coin.estimate_gas_wrapper(estimate_gas_req).compat().await?; Ok((gas_limit, gas_price)) }, } diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs new file mode 100644 index 0000000000..737abeb3c9 --- /dev/null +++ b/mm2src/coins/eth/eth_rpc.rs @@ -0,0 +1,443 @@ +use super::web3_transport::FeeHistoryResult; +use super::{web3_transport::Web3Transport, EthCoin}; +use common::{custom_futures::timeout::FutureTimerExt, log::debug}; +use instant::Duration; +use serde_json::Value; +use web3::types::{Address, Block, BlockId, BlockNumber, Bytes, CallRequest, FeeHistory, Filter, Log, Proof, SyncState, + Trace, TraceFilter, Transaction, TransactionId, TransactionReceipt, TransactionRequest, Work, H256, + H520, H64, U256, U64}; +use web3::{helpers, Transport}; + +impl EthCoin { + pub async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { + let mut clients = self.web3_instances.lock().await; + + // try to find first live client + for (i, client) in clients.clone().into_iter().enumerate() { + let execute_fut = match client.web3.transport() { + Web3Transport::Http(http) => http.execute(method, params.clone()), + Web3Transport::Websocket(socket) => { + socket.maybe_spawn_connection_loop(self.clone()); + socket.execute(method, params.clone()) + }, + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.execute(methods, params), + }; + + match execute_fut.timeout(Duration::from_secs(15)).await { + Ok(Ok(r)) => { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(r); + }, + Ok(Err(rpc_error)) => { + debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + Err(timeout_error) => { + debug!( + "Client version timeout exceed on: {:?}. Error: {}", + &client, timeout_error + ); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + }; + } + + Err(web3::Error::Transport(web3::error::TransportError::Message( + "All the current rpc nodes are unavailable.".to_string(), + ))) + } +} + +impl EthCoin { + /// Get list of available accounts. + pub async fn accounts(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_accounts", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get current block number + pub async fn block_number(&self) -> Result { + self.try_rpc_send("eth_blockNumber", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Call a constant method of contract without changing the state of the blockchain. + pub async fn call(&self, req: CallRequest, block: Option) -> Result { + let req = helpers::serialize(&req); + let block = helpers::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into())); + + self.try_rpc_send("eth_call", vec![req, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get coinbase address + pub async fn coinbase(&self) -> Result { + self.try_rpc_send("eth_coinbase", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile LLL + pub async fn compile_lll(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileLLL", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile Solidity + pub async fn compile_solidity(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileSolidity", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile Serpent + pub async fn compile_serpent(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileSerpent", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Call a contract without changing the state of the blockchain to estimate gas usage. + pub async fn estimate_gas(&self, req: CallRequest, block: Option) -> Result { + let req = helpers::serialize(&req); + + let args = match block { + Some(block) => vec![req, helpers::serialize(&block)], + None => vec![req], + }; + + self.try_rpc_send("eth_estimateGas", args) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get current recommended gas price + pub async fn gas_price(&self) -> Result { + self.try_rpc_send("eth_gasPrice", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Returns a collection of historical gas information. This can be used for evaluating the max_fee_per_gas + /// and max_priority_fee_per_gas to send the future transactions. + pub async fn fee_history( + &self, + block_count: U256, + newest_block: BlockNumber, + reward_percentiles: Option>, + ) -> Result { + let block_count = helpers::serialize(&block_count); + let newest_block = helpers::serialize(&newest_block); + let reward_percentiles = helpers::serialize(&reward_percentiles); + + self.try_rpc_send("eth_feeHistory", vec![block_count, newest_block, reward_percentiles]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get balance of given address + pub async fn balance(&self, address: Address, block: Option) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getBalance", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get all logs matching a given filter object + pub async fn logs(&self, filter: Filter) -> Result, web3::Error> { + let filter = helpers::serialize(&filter); + self.try_rpc_send("eth_getLogs", vec![filter]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get block details with transaction hashes. + pub async fn block(&self, block: BlockId) -> Result>, web3::Error> { + let include_txs = helpers::serialize(&false); + + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockByHash", vec![hash, include_txs]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + self.try_rpc_send("eth_getBlockByNumber", vec![num, include_txs]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get block details with full transaction objects. + pub async fn block_with_txs(&self, block: BlockId) -> Result>, web3::Error> { + let include_txs = helpers::serialize(&true); + + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockByHash", vec![hash, include_txs]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + self.try_rpc_send("eth_getBlockByNumber", vec![num, include_txs]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get number of transactions in block + pub async fn block_transaction_count(&self, block: BlockId) -> Result, web3::Error> { + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockTransactionCountByHash", vec![hash]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + + self.try_rpc_send("eth_getBlockTransactionCountByNumber", vec![num]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get code under given address + pub async fn code(&self, address: Address, block: Option) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getCode", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get supported compilers + pub async fn compilers(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_getCompilers", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get chain id + pub async fn chain_id(&self) -> Result { + self.try_rpc_send("eth_chainId", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get available user accounts. This method is only available in the browser. With MetaMask, + /// this will cause the popup that prompts the user to allow or deny access to their accounts + /// to your app. + pub async fn request_accounts(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_requestAccounts", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get storage entry + pub async fn storage(&self, address: Address, idx: U256, block: Option) -> Result { + let address = helpers::serialize(&address); + let idx = helpers::serialize(&idx); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getStorageAt", vec![address, idx, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get nonce + pub async fn transaction_count(&self, address: Address, block: Option) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getTransactionCount", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get transaction + pub async fn transaction(&self, id: TransactionId) -> Result, web3::Error> { + let result = match id { + TransactionId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getTransactionByHash", vec![hash]) + }, + TransactionId::Block(BlockId::Hash(hash), index) => { + let hash = helpers::serialize(&hash); + let idx = helpers::serialize(&index); + self.try_rpc_send("eth_getTransactionByBlockHashAndIndex", vec![hash, idx]) + }, + TransactionId::Block(BlockId::Number(number), index) => { + let number = helpers::serialize(&number); + let idx = helpers::serialize(&index); + self.try_rpc_send("eth_getTransactionByBlockNumberAndIndex", vec![number, idx]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get transaction receipt + pub async fn transaction_receipt(&self, hash: H256) -> Result, web3::Error> { + let hash = helpers::serialize(&hash); + + self.try_rpc_send("eth_getTransactionReceipt", vec![hash]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get work package + pub async fn work(&self) -> Result { + self.try_rpc_send("eth_getWork", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get hash rate + pub async fn hashrate(&self) -> Result { + self.try_rpc_send("eth_hashrate", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get mining status + pub async fn mining(&self) -> Result { + self.try_rpc_send("eth_mining", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new block filter + pub async fn new_block_filter(&self) -> Result { + self.try_rpc_send("eth_newBlockFilter", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new pending transaction filter + pub async fn new_pending_transaction_filter(&self) -> Result { + self.try_rpc_send("eth_newPendingTransactionFilter", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new pending transaction filter + pub async fn protocol_version(&self) -> Result { + self.try_rpc_send("eth_protocolVersion", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Sends a rlp-encoded signed transaction + pub async fn send_raw_transaction(&self, rlp: Bytes) -> Result { + let rlp = helpers::serialize(&rlp); + self.try_rpc_send("eth_sendRawTransaction", vec![rlp]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Sends a transaction transaction + pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { + let tx = helpers::serialize(&tx); + self.try_rpc_send("eth_sendTransaction", vec![tx]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Signs a hash of given data + pub async fn sign(&self, address: Address, data: Bytes) -> Result { + let address = helpers::serialize(&address); + let data = helpers::serialize(&data); + self.try_rpc_send("eth_sign", vec![address, data]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Submit hashrate of external miner + pub async fn submit_hashrate(&self, rate: U256, id: H256) -> Result { + let rate = helpers::serialize(&rate); + let id = helpers::serialize(&id); + self.try_rpc_send("eth_submitHashrate", vec![rate, id]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Submit work of external miner + pub async fn submit_work(&self, nonce: H64, pow_hash: H256, mix_hash: H256) -> Result { + let nonce = helpers::serialize(&nonce); + let pow_hash = helpers::serialize(&pow_hash); + let mix_hash = helpers::serialize(&mix_hash); + self.try_rpc_send("eth_submitWork", vec![nonce, pow_hash, mix_hash]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get syncing status + pub async fn syncing(&self) -> Result { + self.try_rpc_send("eth_syncing", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Returns the account- and storage-values of the specified account including the Merkle-proof. + pub async fn proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> Result, web3::Error> { + let add = helpers::serialize(&address); + let ks = helpers::serialize(&keys); + let blk = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + self.try_rpc_send("eth_getProof", vec![add, ks, blk]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + pub async fn eth_fee_history( + &self, + count: U256, + block: BlockNumber, + reward_percentiles: &[f64], + ) -> Result { + let count = helpers::serialize(&count); + let block = helpers::serialize(&block); + let reward_percentiles = helpers::serialize(&reward_percentiles); + let params = vec![count, block, reward_percentiles]; + + self.try_rpc_send("eth_feeHistory", params) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Return traces matching the given filter + /// + /// See [TraceFilterBuilder](../types/struct.TraceFilterBuilder.html) + pub async fn trace_filter(&self, filter: TraceFilter) -> Result, web3::Error> { + let filter = helpers::serialize(&filter); + + self.try_rpc_send("trace_filter", vec![filter]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index b03f4708d8..6f0da6b396 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1013,7 +1013,7 @@ fn get_erc20_sender_trade_preimage() { .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(unsafe { ALLOWANCE.into() })))); EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoin::estimate_gas.mock_safe(|_, _| { + EthCoin::estimate_gas_wrapper.mock_safe(|_, _| { unsafe { ESTIMATE_GAS_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(APPROVE_GAS_LIMIT.into()))) }); @@ -1111,7 +1111,7 @@ fn test_get_fee_to_send_taker_fee() { const TRANSFER_GAS_LIMIT: u64 = 40_000; EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoin::estimate_gas + EthCoin::estimate_gas_wrapper .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(TRANSFER_GAS_LIMIT.into())))); // fee to send taker fee is `TRANSFER_GAS_LIMIT * gas_price` always. diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 67df6cfae3..1858dddfc4 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -6,9 +6,7 @@ use mm2_net::transport::GuiAuthValidationGenerator; use serde_json::Value as Json; use serde_json::Value; use std::sync::atomic::Ordering; -use web3::api::Namespace; -use web3::helpers::{self, to_string, CallFuture}; -use web3::types::BlockNumber; +use web3::helpers::to_string; use web3::{Error, RequestId, Transport}; use self::http_transport::AuthPayload; @@ -19,7 +17,7 @@ pub(crate) mod http_transport; #[cfg(target_arch = "wasm32")] pub(crate) mod metamask_transport; pub(crate) mod websocket_transport; -type Web3SendOut = BoxFuture<'static, Result>; +pub(crate) type Web3SendOut = BoxFuture<'static, Result>; #[derive(Clone, Debug)] pub(crate) enum Web3Transport { @@ -123,24 +121,6 @@ impl From for Web3Transport { fn from(metamask: metamask_transport::MetamaskTransport) -> Self { Web3Transport::Metamask(metamask) } } -/// eth_feeHistory support is missing even in the latest rust-web3 -/// It's the custom namespace implementing it -#[derive(Debug, Clone)] -pub struct EthFeeHistoryNamespace { - transport: T, -} - -impl Namespace for EthFeeHistoryNamespace { - fn new(transport: T) -> Self - where - Self: Sized, - { - Self { transport } - } - - fn transport(&self) -> &T { &self.transport } -} - #[derive(Debug, Deserialize)] pub struct FeeHistoryResult { #[serde(rename = "oldestBlock")] @@ -149,21 +129,6 @@ pub struct FeeHistoryResult { pub base_fee_per_gas: Vec, } -impl EthFeeHistoryNamespace { - pub fn eth_fee_history( - &self, - count: U256, - block: BlockNumber, - reward_percentiles: &[f64], - ) -> CallFuture { - let count = helpers::serialize(&count); - let block = helpers::serialize(&block); - let reward_percentiles = helpers::serialize(&reward_percentiles); - let params = vec![count, block, reward_percentiles]; - CallFuture::new(self.transport.execute("eth_feeHistory", params)) - } -} - /// Generates a signed message and inserts it into the request payload. pub(super) fn handle_gui_auth_payload( gui_auth_validation_generator: &Option, From 6a2ec70ca83361af8308e2cb20191a52e30bc57a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 20:06:05 +0300 Subject: [PATCH 47/53] add doc-comments to eth_rpc module Signed-off-by: onur-ozkan --- mm2src/coins/eth/eth_rpc.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 737abeb3c9..37312bc374 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -1,3 +1,7 @@ +//! This module serves as an abstraction layer for Ethereum RPCs. +//! Unlike the built-in functions in web3, this module dynamically +//! rotates through all transports in case of failures. + use super::web3_transport::FeeHistoryResult; use super::{web3_transport::Web3Transport, EthCoin}; use common::{custom_futures::timeout::FutureTimerExt, log::debug}; @@ -9,7 +13,7 @@ use web3::types::{Address, Block, BlockId, BlockNumber, Bytes, CallRequest, FeeH use web3::{helpers, Transport}; impl EthCoin { - pub async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { + async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { let mut clients = self.web3_instances.lock().await; // try to find first live client From b8d9c85ce516dde3f6447017dfab877acb75c78d Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 20:09:06 +0300 Subject: [PATCH 48/53] prefer `pub(crate)` Signed-off-by: onur-ozkan --- mm2src/coins/eth/eth_rpc.rs | 77 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 37312bc374..c647a753e4 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -60,23 +60,24 @@ impl EthCoin { } } +#[allow(dead_code)] impl EthCoin { /// Get list of available accounts. - pub async fn accounts(&self) -> Result, web3::Error> { + pub(crate) async fn accounts(&self) -> Result, web3::Error> { self.try_rpc_send("eth_accounts", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Get current block number - pub async fn block_number(&self) -> Result { + pub(crate) async fn block_number(&self) -> Result { self.try_rpc_send("eth_blockNumber", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Call a constant method of contract without changing the state of the blockchain. - pub async fn call(&self, req: CallRequest, block: Option) -> Result { + pub(crate) async fn call(&self, req: CallRequest, block: Option) -> Result { let req = helpers::serialize(&req); let block = helpers::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into())); @@ -86,14 +87,14 @@ impl EthCoin { } /// Get coinbase address - pub async fn coinbase(&self) -> Result { + pub(crate) async fn coinbase(&self) -> Result { self.try_rpc_send("eth_coinbase", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Compile LLL - pub async fn compile_lll(&self, code: String) -> Result { + pub(crate) async fn compile_lll(&self, code: String) -> Result { let code = helpers::serialize(&code); self.try_rpc_send("eth_compileLLL", vec![code]) .await @@ -101,7 +102,7 @@ impl EthCoin { } /// Compile Solidity - pub async fn compile_solidity(&self, code: String) -> Result { + pub(crate) async fn compile_solidity(&self, code: String) -> Result { let code = helpers::serialize(&code); self.try_rpc_send("eth_compileSolidity", vec![code]) .await @@ -109,7 +110,7 @@ impl EthCoin { } /// Compile Serpent - pub async fn compile_serpent(&self, code: String) -> Result { + pub(crate) async fn compile_serpent(&self, code: String) -> Result { let code = helpers::serialize(&code); self.try_rpc_send("eth_compileSerpent", vec![code]) .await @@ -117,7 +118,7 @@ impl EthCoin { } /// Call a contract without changing the state of the blockchain to estimate gas usage. - pub async fn estimate_gas(&self, req: CallRequest, block: Option) -> Result { + pub(crate) async fn estimate_gas(&self, req: CallRequest, block: Option) -> Result { let req = helpers::serialize(&req); let args = match block { @@ -131,7 +132,7 @@ impl EthCoin { } /// Get current recommended gas price - pub async fn gas_price(&self) -> Result { + pub(crate) async fn gas_price(&self) -> Result { self.try_rpc_send("eth_gasPrice", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) @@ -139,7 +140,7 @@ impl EthCoin { /// Returns a collection of historical gas information. This can be used for evaluating the max_fee_per_gas /// and max_priority_fee_per_gas to send the future transactions. - pub async fn fee_history( + pub(crate) async fn fee_history( &self, block_count: U256, newest_block: BlockNumber, @@ -155,7 +156,7 @@ impl EthCoin { } /// Get balance of given address - pub async fn balance(&self, address: Address, block: Option) -> Result { + pub(crate) async fn balance(&self, address: Address, block: Option) -> Result { let address = helpers::serialize(&address); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); @@ -165,7 +166,7 @@ impl EthCoin { } /// Get all logs matching a given filter object - pub async fn logs(&self, filter: Filter) -> Result, web3::Error> { + pub(crate) async fn logs(&self, filter: Filter) -> Result, web3::Error> { let filter = helpers::serialize(&filter); self.try_rpc_send("eth_getLogs", vec![filter]) .await @@ -173,7 +174,7 @@ impl EthCoin { } /// Get block details with transaction hashes. - pub async fn block(&self, block: BlockId) -> Result>, web3::Error> { + pub(crate) async fn block(&self, block: BlockId) -> Result>, web3::Error> { let include_txs = helpers::serialize(&false); let result = match block { @@ -191,7 +192,7 @@ impl EthCoin { } /// Get block details with full transaction objects. - pub async fn block_with_txs(&self, block: BlockId) -> Result>, web3::Error> { + pub(crate) async fn block_with_txs(&self, block: BlockId) -> Result>, web3::Error> { let include_txs = helpers::serialize(&true); let result = match block { @@ -209,7 +210,7 @@ impl EthCoin { } /// Get number of transactions in block - pub async fn block_transaction_count(&self, block: BlockId) -> Result, web3::Error> { + pub(crate) async fn block_transaction_count(&self, block: BlockId) -> Result, web3::Error> { let result = match block { BlockId::Hash(hash) => { let hash = helpers::serialize(&hash); @@ -226,7 +227,7 @@ impl EthCoin { } /// Get code under given address - pub async fn code(&self, address: Address, block: Option) -> Result { + pub(crate) async fn code(&self, address: Address, block: Option) -> Result { let address = helpers::serialize(&address); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); @@ -236,14 +237,14 @@ impl EthCoin { } /// Get supported compilers - pub async fn compilers(&self) -> Result, web3::Error> { + pub(crate) async fn compilers(&self) -> Result, web3::Error> { self.try_rpc_send("eth_getCompilers", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Get chain id - pub async fn chain_id(&self) -> Result { + pub(crate) async fn chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) @@ -252,14 +253,14 @@ impl EthCoin { /// Get available user accounts. This method is only available in the browser. With MetaMask, /// this will cause the popup that prompts the user to allow or deny access to their accounts /// to your app. - pub async fn request_accounts(&self) -> Result, web3::Error> { + pub(crate) async fn request_accounts(&self) -> Result, web3::Error> { self.try_rpc_send("eth_requestAccounts", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Get storage entry - pub async fn storage(&self, address: Address, idx: U256, block: Option) -> Result { + pub(crate) async fn storage(&self, address: Address, idx: U256, block: Option) -> Result { let address = helpers::serialize(&address); let idx = helpers::serialize(&idx); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); @@ -270,7 +271,7 @@ impl EthCoin { } /// Get nonce - pub async fn transaction_count(&self, address: Address, block: Option) -> Result { + pub(crate) async fn transaction_count(&self, address: Address, block: Option) -> Result { let address = helpers::serialize(&address); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); @@ -280,7 +281,7 @@ impl EthCoin { } /// Get transaction - pub async fn transaction(&self, id: TransactionId) -> Result, web3::Error> { + pub(crate) async fn transaction(&self, id: TransactionId) -> Result, web3::Error> { let result = match id { TransactionId::Hash(hash) => { let hash = helpers::serialize(&hash); @@ -302,7 +303,7 @@ impl EthCoin { } /// Get transaction receipt - pub async fn transaction_receipt(&self, hash: H256) -> Result, web3::Error> { + pub(crate) async fn transaction_receipt(&self, hash: H256) -> Result, web3::Error> { let hash = helpers::serialize(&hash); self.try_rpc_send("eth_getTransactionReceipt", vec![hash]) @@ -311,49 +312,49 @@ impl EthCoin { } /// Get work package - pub async fn work(&self) -> Result { + pub(crate) async fn work(&self) -> Result { self.try_rpc_send("eth_getWork", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Get hash rate - pub async fn hashrate(&self) -> Result { + pub(crate) async fn hashrate(&self) -> Result { self.try_rpc_send("eth_hashrate", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Get mining status - pub async fn mining(&self) -> Result { + pub(crate) async fn mining(&self) -> Result { self.try_rpc_send("eth_mining", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Start new block filter - pub async fn new_block_filter(&self) -> Result { + pub(crate) async fn new_block_filter(&self) -> Result { self.try_rpc_send("eth_newBlockFilter", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Start new pending transaction filter - pub async fn new_pending_transaction_filter(&self) -> Result { + pub(crate) async fn new_pending_transaction_filter(&self) -> Result { self.try_rpc_send("eth_newPendingTransactionFilter", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Start new pending transaction filter - pub async fn protocol_version(&self) -> Result { + pub(crate) async fn protocol_version(&self) -> Result { self.try_rpc_send("eth_protocolVersion", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Sends a rlp-encoded signed transaction - pub async fn send_raw_transaction(&self, rlp: Bytes) -> Result { + pub(crate) async fn send_raw_transaction(&self, rlp: Bytes) -> Result { let rlp = helpers::serialize(&rlp); self.try_rpc_send("eth_sendRawTransaction", vec![rlp]) .await @@ -361,7 +362,7 @@ impl EthCoin { } /// Sends a transaction transaction - pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { + pub(crate) async fn send_transaction(&self, tx: TransactionRequest) -> Result { let tx = helpers::serialize(&tx); self.try_rpc_send("eth_sendTransaction", vec![tx]) .await @@ -369,7 +370,7 @@ impl EthCoin { } /// Signs a hash of given data - pub async fn sign(&self, address: Address, data: Bytes) -> Result { + pub(crate) async fn sign(&self, address: Address, data: Bytes) -> Result { let address = helpers::serialize(&address); let data = helpers::serialize(&data); self.try_rpc_send("eth_sign", vec![address, data]) @@ -378,7 +379,7 @@ impl EthCoin { } /// Submit hashrate of external miner - pub async fn submit_hashrate(&self, rate: U256, id: H256) -> Result { + pub(crate) async fn submit_hashrate(&self, rate: U256, id: H256) -> Result { let rate = helpers::serialize(&rate); let id = helpers::serialize(&id); self.try_rpc_send("eth_submitHashrate", vec![rate, id]) @@ -387,7 +388,7 @@ impl EthCoin { } /// Submit work of external miner - pub async fn submit_work(&self, nonce: H64, pow_hash: H256, mix_hash: H256) -> Result { + pub(crate) async fn submit_work(&self, nonce: H64, pow_hash: H256, mix_hash: H256) -> Result { let nonce = helpers::serialize(&nonce); let pow_hash = helpers::serialize(&pow_hash); let mix_hash = helpers::serialize(&mix_hash); @@ -397,14 +398,14 @@ impl EthCoin { } /// Get syncing status - pub async fn syncing(&self) -> Result { + pub(crate) async fn syncing(&self) -> Result { self.try_rpc_send("eth_syncing", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } /// Returns the account- and storage-values of the specified account including the Merkle-proof. - pub async fn proof( + pub(crate) async fn proof( &self, address: Address, keys: Vec, @@ -418,7 +419,7 @@ impl EthCoin { .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } - pub async fn eth_fee_history( + pub(crate) async fn eth_fee_history( &self, count: U256, block: BlockNumber, @@ -437,7 +438,7 @@ impl EthCoin { /// Return traces matching the given filter /// /// See [TraceFilterBuilder](../types/struct.TraceFilterBuilder.html) - pub async fn trace_filter(&self, filter: TraceFilter) -> Result, web3::Error> { + pub(crate) async fn trace_filter(&self, filter: TraceFilter) -> Result, web3::Error> { let filter = helpers::serialize(&filter); self.try_rpc_send("trace_filter", vec![filter]) From 6b7267cb9039c33634a94be18d1dd28fc4e97bdd Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 20:13:17 +0300 Subject: [PATCH 49/53] update outdated logs Signed-off-by: onur-ozkan --- mm2src/coins/eth/eth_rpc.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index c647a753e4..aa8d873688 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -16,7 +16,6 @@ impl EthCoin { async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { let mut clients = self.web3_instances.lock().await; - // try to find first live client for (i, client) in clients.clone().into_iter().enumerate() { let execute_fut = match client.web3.transport() { Web3Transport::Http(http) => http.execute(method, params.clone()), @@ -35,17 +34,14 @@ impl EthCoin { return Ok(r); }, Ok(Err(rpc_error)) => { - debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); + debug!("Request on '{method}' failed. Error: {rpc_error}"); if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; }; }, Err(timeout_error) => { - debug!( - "Client version timeout exceed on: {:?}. Error: {}", - &client, timeout_error - ); + debug!("Timeout exceed for '{method}' request. Error: {timeout_error}",); if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { socket_transport.stop_connection_loop().await; @@ -54,9 +50,9 @@ impl EthCoin { }; } - Err(web3::Error::Transport(web3::error::TransportError::Message( - "All the current rpc nodes are unavailable.".to_string(), - ))) + Err(web3::Error::Transport(web3::error::TransportError::Message(format!( + "Request '{method}' failed due to not being able to find a living RPC client" + )))) } } @@ -260,7 +256,12 @@ impl EthCoin { } /// Get storage entry - pub(crate) async fn storage(&self, address: Address, idx: U256, block: Option) -> Result { + pub(crate) async fn storage( + &self, + address: Address, + idx: U256, + block: Option, + ) -> Result { let address = helpers::serialize(&address); let idx = helpers::serialize(&idx); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); @@ -271,7 +272,11 @@ impl EthCoin { } /// Get nonce - pub(crate) async fn transaction_count(&self, address: Address, block: Option) -> Result { + pub(crate) async fn transaction_count( + &self, + address: Address, + block: Option, + ) -> Result { let address = helpers::serialize(&address); let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); From df48248a22b4a4d96f0c0c28cbfb40f5f53026e7 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Wed, 21 Feb 2024 20:30:59 +0300 Subject: [PATCH 50/53] fix WASM error Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 2 +- mm2src/coins/eth/eth_rpc.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index a3e33186c0..4ac02cff04 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -2460,7 +2460,7 @@ async fn sign_and_send_transaction_with_metamask( // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = try_tx_s!(try_tx_s!(coin.send_transaction(tx_to_send).await)); + let tx_hash = try_tx_s!(coin.send_transaction(tx_to_send).await); let maybe_signed_tx = try_tx_s!( coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index aa8d873688..2b17bd72d3 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -24,7 +24,7 @@ impl EthCoin { socket.execute(method, params.clone()) }, #[cfg(target_arch = "wasm32")] - Web3Transport::Metamask(metamask) => metamask.execute(methods, params), + Web3Transport::Metamask(metamask) => metamask.execute(method, params.clone()), }; match execute_fut.timeout(Duration::from_secs(15)).await { From 665dc2f19201921e39895a457162938e2fab868c Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Thu, 22 Feb 2024 20:10:11 +0300 Subject: [PATCH 51/53] apply various improvements Signed-off-by: onur-ozkan --- mm2src/coins/eth.rs | 11 +++++++++-- mm2src/coins/eth/eth_rpc.rs | 4 +++- mm2src/coins/eth/v2_activation.rs | 9 +++++++-- mm2src/coins/eth/web3_transport/metamask_transport.rs | 5 +---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 4ac02cff04..27f1c10c3f 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,6 +21,7 @@ // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // use super::eth::Action::{Call, Create}; +use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; @@ -2535,7 +2536,7 @@ impl RpcCommonOps for EthCoin { .web3 .web3() .client_version() - .timeout(Duration::from_secs(15)) + .timeout(ETH_RPC_REQUEST_TIMEOUT) .await { Ok(Ok(_)) => { @@ -5738,11 +5739,17 @@ pub async fn eth_coin_from_conf_and_request( Web3Transport::Websocket(websocket_transport) }, - _ => { + Some("http") | Some("https") => { let node = HttpTransportNode { uri, gui_auth: false }; Web3Transport::new_http_with_event_handlers(node, event_handlers.clone()) }, + _ => { + return ERR!( + "Invalid node address '{}'. Only http(s) and ws(s) nodes are supported", + uri + ); + }, }; let web3 = Web3::new(transport); diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 2b17bd72d3..36b2c3221c 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -12,6 +12,8 @@ use web3::types::{Address, Block, BlockId, BlockNumber, Bytes, CallRequest, FeeH H520, H64, U256, U64}; use web3::{helpers, Transport}; +pub(crate) const ETH_RPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); + impl EthCoin { async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { let mut clients = self.web3_instances.lock().await; @@ -27,7 +29,7 @@ impl EthCoin { Web3Transport::Metamask(metamask) => metamask.execute(method, params.clone()), }; - match execute_fut.timeout(Duration::from_secs(15)).await { + match execute_fut.timeout(ETH_RPC_REQUEST_TIMEOUT).await { Ok(Ok(r)) => { // Bring the live client to the front of rpc_clients clients.rotate_left(i); diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index eb37dc2374..f340dec7f6 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -7,7 +7,7 @@ use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; -use v2_activation::web3_transport::websocket_transport::WebsocketTransport; +use web3_transport::websocket_transport::WebsocketTransport; #[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -440,7 +440,7 @@ async fn build_web3_instances( Web3Transport::Websocket(websocket_transport) }, - _ => { + Some("http") | Some("https") => { let node = HttpTransportNode { uri, gui_auth: eth_node.gui_auth, @@ -454,6 +454,11 @@ async fn build_web3_instances( event_handlers.clone(), ) }, + _ => { + return MmError::err(EthActivationV2Error::InvalidPayload(format!( + "Invalid node address '{uri}'. Only http(s) and ws(s) nodes are supported" + ))); + }, }; let web3 = Web3::new(transport); diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs index 4d5b87ad16..a586f9e7f9 100644 --- a/mm2src/coins/eth/web3_transport/metamask_transport.rs +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -66,10 +66,7 @@ impl MetamaskTransport { async fn send_impl(&self, id: RequestId, request: Call) -> Result { // Hold the mutex guard until the request is finished. let _rpc_lock = self.request_preparation().await?; - match self.inner.eip1193.send(id, request).await { - Ok(t) => Ok(t), - Err(e) => Err(e), - } + self.inner.eip1193.send(id, request).await } /// Ensures that the MetaMask wallet is targeted to [`EthConfig::chain_id`]. From 9de616c630fd946ae2b9e300dcd29f5c52105b1a Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 26 Feb 2024 13:10:36 +0300 Subject: [PATCH 52/53] send responses through notifier channel Signed-off-by: onur-ozkan --- .../eth/web3_transport/websocket_transport.rs | 64 +++++-------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index e9d4181b3e..1bdb3b2e91 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -1,11 +1,13 @@ -//! This module offers a transport layer for managing request-response style communication -//! with Ethereum nodes using websockets in a wait and lock-free manner (with unsafe raw-pointers). +//! This module offers a transport layer for managing request-response style communication with Ethereum +//! nodes using websockets that can work concurrently. +//! //! In comparison to HTTP transport, this approach proves to be much quicker (low-latency) and consumes //! less bandwidth. This efficiency is achieved by avoiding the handling of TCP handshakes (connection reusability) //! for each request. use super::handle_gui_auth_payload; use super::http_transport::de_rpc_response; +use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; use crate::eth::web3_transport::Web3SendOut; use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; use crate::{MmCoin, RpcTransportEventHandler}; @@ -20,7 +22,6 @@ use futures_util::{FutureExt, SinkExt, StreamExt}; use instant::{Duration, Instant}; use jsonrpc_core::Call; use mm2_net::transport::GuiAuthValidationGenerator; -use std::collections::HashMap; use std::sync::atomic::AtomicBool; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; @@ -29,7 +30,6 @@ use web3::error::{Error, TransportError}; use web3::helpers::to_string; use web3::{helpers::build_request, RequestId, Transport}; -const REQUEST_TIMEOUT_AS_SEC: u64 = 10; const MAX_ATTEMPTS: u32 = 3; const SLEEP_DURATION: f64 = 1.; const KEEPALIVE_DURATION: Duration = Duration::from_secs(10); @@ -47,7 +47,6 @@ pub struct WebsocketTransport { node: WebsocketTransportNode, event_handlers: Vec, pub(crate) gui_auth_validation_generator: Option, - responses: Arc, controller_channel: Arc, connection_guard: Arc>, } @@ -67,30 +66,9 @@ enum ControllerMessage { struct WsRequest { serialized_request: String, request_id: RequestId, - response_notifier: oneshot::Sender<()>, + response_notifier: oneshot::Sender>, } -/// A wrapper type for raw pointers used as a mutable Send & Sync HashMap reference. -/// -/// Safety notes: -/// -/// The implemented algorithm for socket request-response is already thread-safe, -/// so we don't care about race conditions. -/// -/// As for deallocations, see the `Drop` implementation below. -#[derive(Debug)] -struct SafeMapPtr(*mut HashMap>); - -impl Drop for SafeMapPtr { - fn drop(&mut self) { - // Let the compiler do the job. - let _ = unsafe { Box::from_raw(self.0) }; - } -} - -unsafe impl Send for SafeMapPtr {} -unsafe impl Sync for SafeMapPtr {} - enum OuterAction { None, Continue, @@ -108,7 +86,6 @@ impl WebsocketTransport { WebsocketTransport { node, event_handlers, - responses: Arc::new(SafeMapPtr(Box::into_raw(Default::default()))), request_id: Arc::new(AtomicUsize::new(1)), controller_channel: ControllerChannel { tx: Arc::new(AsyncMutex::new(req_tx)), @@ -124,7 +101,7 @@ impl WebsocketTransport { async fn handle_keepalive( &self, wsocket: &mut WebSocketStream, - response_notifiers: &mut ExpirableMap>, + response_notifiers: &mut ExpirableMap>>, expires_at: Option, ) -> OuterAction { const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; @@ -169,7 +146,7 @@ impl WebsocketTransport { &self, request: Option, wsocket: &mut WebSocketStream, - response_notifiers: &mut ExpirableMap>, + response_notifiers: &mut ExpirableMap>>, ) -> OuterAction { match request { Some(ControllerMessage::Request(WsRequest { @@ -180,7 +157,8 @@ impl WebsocketTransport { response_notifiers.insert( request_id, response_notifier, - Duration::from_secs(REQUEST_TIMEOUT_AS_SEC), + // Since request will be cancelled when timeout occurs, we are free to drop its state. + ETH_RPC_REQUEST_TIMEOUT + Duration::from_secs(3), ); let mut should_continue = Default::default(); @@ -219,7 +197,7 @@ impl WebsocketTransport { async fn handle_response( &self, message: Option>, - response_notifiers: &mut ExpirableMap>, + response_notifiers: &mut ExpirableMap>>, ) -> OuterAction { match message { Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { @@ -234,10 +212,7 @@ impl WebsocketTransport { if let Some(notifier) = response_notifiers.remove(&request_id) { let mut res_bytes: Vec = Vec::new(); if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { - let response_map = unsafe { &mut *self.responses.0 }; - let _ = response_map.insert(request_id, res_bytes); - - notifier.send(()).expect("receiver channel must be alive"); + notifier.send(res_bytes).expect("receiver channel must be alive"); } } } @@ -283,7 +258,7 @@ impl WebsocketTransport { let _guard = self.connection_guard.lock().await; // List of awaiting requests - let mut response_notifiers: ExpirableMap> = ExpirableMap::default(); + let mut response_notifiers: ExpirableMap>> = ExpirableMap::default(); let mut wsocket = match self .attempt_to_establish_socket_connection(MAX_ATTEMPTS, SLEEP_DURATION) @@ -336,9 +311,6 @@ impl WebsocketTransport { tx.send(ControllerMessage::Close) .await .expect("receiver channel must be alive"); - - let response_map = unsafe { &mut *self.responses.0 }; - response_map.clear(); } pub(crate) fn maybe_spawn_connection_loop(&self, coin: EthCoin) { @@ -381,7 +353,7 @@ async fn send_request( let mut tx = transport.controller_channel.tx.lock().await; - let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::<()>(); + let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::>(); event_handlers.on_outgoing_request(serialized_request.as_bytes()); @@ -393,13 +365,9 @@ async fn send_request( .await .map_err(|e| Error::Transport(TransportError::Message(e.to_string())))?; - if let Ok(_ping) = notification_receiver.await { - let response_map = unsafe { &mut *transport.responses.0 }; - if let Some(response) = response_map.remove(&request_id) { - event_handlers.on_incoming_response(&response); - - return de_rpc_response(response, &transport.node.uri.to_string()); - } + if let Ok(response) = notification_receiver.await { + event_handlers.on_incoming_response(&response); + return de_rpc_response(response, &transport.node.uri.to_string()); }; Err(Error::Transport(TransportError::Message(format!( From 537a020d2d0c81f63e854ca58ae4c160e4257ca6 Mon Sep 17 00:00:00 2001 From: onur-ozkan Date: Mon, 26 Feb 2024 14:41:31 +0300 Subject: [PATCH 53/53] clear outdated entries before sending response Signed-off-by: onur-ozkan --- mm2src/coins/eth/web3_transport/websocket_transport.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 1bdb3b2e91..f458aacc67 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -158,7 +158,7 @@ impl WebsocketTransport { request_id, response_notifier, // Since request will be cancelled when timeout occurs, we are free to drop its state. - ETH_RPC_REQUEST_TIMEOUT + Duration::from_secs(3), + ETH_RPC_REQUEST_TIMEOUT, ); let mut should_continue = Default::default(); @@ -207,6 +207,9 @@ impl WebsocketTransport { } if let Some(id) = inc_event.get("id") { + // just to ensure we don't have outdated entries + response_notifiers.clear_expired_entries(); + let request_id = id.as_u64().unwrap_or_default() as usize; if let Some(notifier) = response_notifiers.remove(&request_id) {