diff --git a/Cargo.lock b/Cargo.lock index 6d0447a4fb..2305004d7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -673,6 +673,7 @@ dependencies = [ "bytes 0.4.12", "chain", "common", + "derive_more", "dirs 1.0.5", "ethabi", "ethcore-transaction", @@ -702,6 +703,8 @@ dependencies = [ "rust-ini", "rustls 0.18.1", "script", + "ser_error", + "ser_error_derive", "serde", "serde_derive", "serde_json", @@ -735,6 +738,7 @@ dependencies = [ "cc", "chrono", "crossbeam", + "derive_more", "findshlibs", "fomat-macros 0.2.1", "fomat-macros 0.3.1", @@ -769,6 +773,8 @@ dependencies = [ "rand 0.7.3", "regex", "rusqlite", + "ser_error", + "ser_error_derive", "serde", "serde_bencode", "serde_bytes 0.11.5", @@ -2889,6 +2895,7 @@ dependencies = [ "crc", "crc32fast", "crossbeam", + "derive_more", "dirs 1.0.5", "either", "enum-primitive-derive", @@ -2925,6 +2932,8 @@ dependencies = [ "rmp-serde", "rpc", "script", + "ser_error", + "ser_error_derive", "serde", "serde_bencode", "serde_derive", @@ -4180,6 +4189,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" +[[package]] +name = "ser_error" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "ser_error_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote 1.0.7", + "ser_error", + "syn 1.0.33", +] + [[package]] name = "serde" version = "1.0.114" diff --git a/Cargo.toml b/Cargo.toml index c62fc1b342..09d24b6ac6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ common = { path = "mm2src/common" } crc = "1.8" crc32fast = { version = "1.2", features = ["std", "nightly"] } crossbeam = "0.7" +derive_more = "0.99" either = "1.6" ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } enum-primitive-derive = "0.1" @@ -98,6 +99,8 @@ serde = "1.0" serde_bencode = "0.2" serde_json = { version = "1.0", features = ["preserve_order"] } serde_derive = "1.0" +ser_error = { path = "mm2src/derives/ser_error" } +ser_error_derive = { path = "mm2src/derives/ser_error_derive" } serialization = { git = "https://github.com/artemii235/parity-bitcoin.git" } serialization_derive = { git = "https://github.com/artemii235/parity-bitcoin.git" } sp-trie = "2.0.0" @@ -142,6 +145,8 @@ members = [ "mm2src/floodsub", "mm2src/mm2_libp2p", "mm2src/gossipsub", + "mm2src/derives/ser_error", + "mm2src/derives/ser_error_derive", ] # The backtrace disables build.define("HAVE_DL_ITERATE_PHDR", "1"); for android which results in "unknown" function diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 6dbf1810e1..ce25caf660 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -17,6 +17,7 @@ byteorder = "1.3" bytes = "0.4" chain = { git = "https://github.com/artemii235/parity-bitcoin.git" } common = { path = "../common" } +derive_more = "0.99" ethabi = { git = "https://github.com/artemii235/ethabi" } ethcore-transaction = { git = "https://github.com/artemii235/parity-ethereum.git" } ethereum-types = { version = "0.4", default-features = false, features = ["std", "serialize"] } @@ -49,6 +50,8 @@ serde = "1.0" serde_derive = "1.0" serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serialization = { git = "https://github.com/artemii235/parity-bitcoin.git" } +ser_error = { path = "../derives/ser_error" } +ser_error_derive = { path = "../derives/ser_error_derive" } sha2 = "0.8" sha3 = "0.8" # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 352adc3757..6843f2c008 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -23,7 +23,9 @@ use bitcrypto::sha256; use common::custom_futures::TimedAsyncMutex; use common::executor::Timer; use common::mm_ctx::{MmArc, MmWeak}; +use common::mm_error::prelude::*; use common::{block_on, now_ms, slurp_url, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; +use derive_more::Display; use ethabi::{Contract, Token}; use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; use ethereum_types::{Address, H160, U256}; @@ -53,10 +55,12 @@ use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallReques TraceFilterBuilder, Transaction as Web3Transaction, TransactionId}; use web3::{self, Web3}; -use super::{CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MarketCoinOps, MmCoin, RpcClientType, RpcTransportEventHandler, - RpcTransportEventHandlerShared, SwapOps, TradeFee, TradePreimageError, TradePreimageValue, Transaction, - TransactionDetails, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawFee, WithdrawRequest}; +use super::{BalanceError, BalanceFut, CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NumConversError, NumConversResult, + RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SwapOps, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageValue, Transaction, TransactionDetails, + TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult}; pub use ethcore_transaction::SignedTransaction as SignedEthTx; pub use rlp; @@ -101,6 +105,99 @@ lazy_static! { pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); } +pub type Web3RpcFut = Box> + Send>; +pub type Web3RpcResult = Result>; + +#[derive(Debug, Display)] +pub enum Web3RpcError { + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl From for Web3RpcError { + fn from(e: serde_json::Error) -> Self { Web3RpcError::InvalidResponse(e.to_string()) } +} + +impl From for Web3RpcError { + fn from(e: web3::Error) -> Self { + let error_str = e.to_string(); + match e.kind() { + web3::ErrorKind::InvalidResponse(_) + | web3::ErrorKind::Decoder(_) + | web3::ErrorKind::Msg(_) + | web3::ErrorKind::Rpc(_) => Web3RpcError::InvalidResponse(error_str), + web3::ErrorKind::Transport(_) | web3::ErrorKind::Io(_) => Web3RpcError::Transport(error_str), + _ => Web3RpcError::Internal(error_str), + } + } +} + +impl From for Web3RpcError { + fn from(e: ethabi::Error) -> Web3RpcError { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + Web3RpcError::Internal(e.to_string()) + } +} + +impl From for WithdrawError { + fn from(e: ethabi::Error) -> Self { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + WithdrawError::InternalError(e.to_string()) + } +} + +impl From for WithdrawError { + fn from(e: web3::Error) -> Self { WithdrawError::Transport(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => WithdrawError::Transport(err), + Web3RpcError::Internal(internal) => WithdrawError::InternalError(internal), + } + } +} + +impl From for TradePreimageError { + fn from(e: web3::Error) -> Self { TradePreimageError::Transport(e.to_string()) } +} + +impl From for TradePreimageError { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => TradePreimageError::Transport(err), + Web3RpcError::Internal(internal) => TradePreimageError::InternalError(internal), + } + } +} + +impl From for TradePreimageError { + fn from(e: ethabi::Error) -> Self { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + TradePreimageError::InternalError(e.to_string()) + } +} + +impl From for BalanceError { + fn from(e: ethabi::Error) -> Self { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + BalanceError::Internal(e.to_string()) + } +} + +impl From for BalanceError { + fn from(e: web3::Error) -> Self { BalanceError::Transport(e.to_string()) } +} + #[derive(Debug, Deserialize, Serialize)] struct SavedTraces { /// ETH traces for my_address @@ -282,13 +379,13 @@ impl EthCoinImpl { } /// Get gas price - fn get_gas_price(&self) -> Box + Send> { + fn get_gas_price(&self) -> Web3RpcFut { let fut = if let Some(url) = &self.gas_station_url { Either01::A( GasStationData::get_gas_price(&url).map(|price| increase_by_percent_one_gwei(price, GAS_PRICE_PERCENT)), ) } else { - Either01::B(self.web3.eth().gas_price().map_err(|e| ERRL!("{}", e))) + Either01::B(self.web3.eth().gas_price().map_to_mm_fut(Web3RpcError::from)) }; Box::new(fut) } @@ -398,31 +495,47 @@ impl EthCoinImpl { } } -async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> Result { - let to_addr = try_s!(coin.address_from_str(&req.to)); - let my_balance = try_s!(coin.my_balance().compat().await); - let mut wei_amount = if req.max { - my_balance +async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { + let to_addr = coin + .address_from_str(&req.to) + .map_to_mm(WithdrawError::InvalidAddress)?; + let my_balance = coin.my_balance().compat().await?; + let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; + + let (mut wei_amount, dec_amount) = if req.max { + (my_balance, my_balance_dec.clone()) } else { - try_s!(wei_from_big_decimal(&req.amount, coin.decimals)) + let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; + (wei_amount, req.amount.clone()) }; if wei_amount > my_balance { - return ERR!("The amount {} to withdraw is larger than balance", req.amount); + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: my_balance_dec.clone(), + required: dec_amount, + }); }; let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { EthCoinType::Eth => (wei_amount, vec![], to_addr, coin.ticker()), EthCoinType::Erc20 { platform, token_addr } => { - let function = try_s!(ERC20_CONTRACT.function("transfer")); - let data = try_s!(function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])); + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; (0.into(), data, *token_addr, platform.as_str()) }, }; + let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; let (gas, gas_price) = match req.fee { - Some(WithdrawFee::EthGas { gas_price, gas }) => (gas.into(), try_s!(wei_from_big_decimal(&gas_price, 9))), - Some(_) => return ERR!("Unsupported input fee type"), + Some(WithdrawFee::EthGas { gas_price, gas }) => { + let gas_price = wei_from_big_decimal(&gas_price, 9)?; + (gas.into(), gas_price) + }, + Some(fee_policy) => { + let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); + return MmError::err(WithdrawError::InvalidFeePolicy(error)); + }, None => { - let gas_price = try_s!(coin.get_gas_price().compat().await); + let gas_price = coin.get_gas_price().compat().await?; // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH let eth_value_for_estimate = if req.max && coin.coin_type == EthCoinType::Eth { eth_value - gas_price * U256::from(21000) @@ -439,33 +552,34 @@ async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> Resul // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 gas_price: Some(gas_price), }; - let gas_fut = coin.estimate_gas(estimate_gas_req).compat(); - (try_s!(gas_fut.await), gas_price) + // 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 = coin.estimate_gas(estimate_gas_req).compat().await?; + (gas_limit, gas_price) }, }; let total_fee = gas * gas_price; if req.max && coin.coin_type == EthCoinType::Eth { if eth_value < total_fee || wei_amount < total_fee { - return ERR!("The value {} to withdraw is lower than fee {}", eth_value, total_fee); + return MmError::err(WithdrawError::AmountIsTooSmall { amount: eth_value_dec }); } eth_value -= total_fee; wei_amount -= total_fee; }; - let _nonce_lock = try_s!( - NONCE_LOCK - .lock(|_start, _now| { - if ctx.is_stopping() { - return ERR!("MM is stopping, aborting withdraw_impl in NONCE_LOCK"); - } - Ok(0.5) - }) - .await - ); + let _nonce_lock = NONCE_LOCK + .lock(|_start, _now| { + if ctx.is_stopping() { + let error = "MM is stopping, aborting withdraw_impl in NONCE_LOCK".to_owned(); + return MmError::err(WithdrawError::InternalError(error)); + } + Ok(0.5) + }) + .await?; let nonce_fut = get_addr_nonce(coin.my_address, coin.web3_instances.clone()).compat(); let nonce = match select(nonce_fut, Timer::sleep(30.)).await { - Either::Left((nonce_res, _)) => try_s!(nonce_res), - Either::Right(_) => return ERR!("Get address nonce timed out"), + Either::Left((nonce_res, _)) => nonce_res.map_to_mm(WithdrawError::Transport)?, + Either::Right(_) => return MmError::err(WithdrawError::Transport("Get address nonce timed out".to_owned())), }; let tx = UnSignedEthTx { nonce, @@ -478,20 +592,21 @@ async fn withdraw_impl(ctx: MmArc, coin: EthCoin, req: WithdrawRequest) -> Resul let signed = tx.sign(coin.key_pair.secret(), None); let bytes = rlp::encode(&signed); - let amount_decimal = try_s!(u256_to_big_decimal(wei_amount, coin.decimals)); + let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; let mut spent_by_me = amount_decimal.clone(); let received_by_me = if to_addr == coin.my_address { amount_decimal.clone() } else { 0.into() }; - let fee_details = try_s!(EthTxFeeDetails::new(gas, gas_price, fee_coin)); + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; if coin.coin_type == EthCoinType::Eth { spent_by_me += &fee_details.total_fee; } + let my_address = coin.my_address().map_to_mm(WithdrawError::InternalError)?; Ok(TransactionDetails { to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![try_s!(coin.my_address())], + from: vec![my_address], total_amount: amount_decimal, my_balance_change: &received_by_me - &spent_by_me, spent_by_me, @@ -887,11 +1002,11 @@ impl MarketCoinOps for EthCoin { fn my_address(&self) -> Result { Ok(checksum_address(&format!("{:#02x}", self.my_address))) } - fn my_balance(&self) -> Box + Send> { + fn my_balance(&self) -> BalanceFut { let decimals = self.decimals; let fut = self .my_balance() - .and_then(move |result| Ok(try_s!(u256_to_big_decimal(result, decimals)))) + .and_then(move |result| Ok(u256_to_big_decimal(result, decimals)?)) .map(|spendable| CoinBalance { spendable, unspendable: BigDecimal::from(0), @@ -899,10 +1014,10 @@ impl MarketCoinOps for EthCoin { Box::new(fut) } - fn base_coin_balance(&self) -> Box + Send> { + fn base_coin_balance(&self) -> BalanceFut { Box::new( self.eth_balance() - .and_then(move |result| Ok(try_s!(u256_to_big_decimal(result, 18)))), + .and_then(move |result| Ok(u256_to_big_decimal(result, 18)?)), ) } @@ -1239,7 +1354,7 @@ impl EthCoin { platform: _, token_addr, } => { - let allowance_fut = self.allowance(swap_contract_address); + let allowance_fut = self.allowance(swap_contract_address).map_err(|e| ERRL!("{}", e)); let function = try_fus!(SWAP_CONTRACT.function("erc20Payment")); let data = try_fus!(function.encode_input(&[ @@ -1255,7 +1370,7 @@ impl EthCoin { Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { if allowed < value { let balance_f = arc.my_balance(); - Box::new(balance_f.and_then(move |balance| { + Box::new(balance_f.map_err(|e| ERRL!("{}", e)).and_then(move |balance| { arc.approve(swap_contract_address, balance).and_then(move |_approved| { arc.sign_and_send_transaction( 0.into(), @@ -1426,41 +1541,41 @@ impl EthCoin { } } - fn my_balance(&self) -> Box + Send> { - match &self.coin_type { - EthCoinType::Eth => Box::new( - self.web3 + fn my_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + match coin.coin_type { + EthCoinType::Eth => Ok(coin + .web3 .eth() - .balance(self.my_address, Some(BlockNumber::Latest)) - .map_err(|e| ERRL!("{}", e)), - ), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => { - let function = try_fus!(ERC20_CONTRACT.function("balanceOf")); - let data = try_fus!(function.encode_input(&[Token::Address(self.my_address),])); - - let call_fut = self.call_request(*token_addr, None, Some(data.into())); - - Box::new(call_fut.and_then(move |res| { - let decoded = try_s!(function.decode_output(&res.0)); + .balance(coin.my_address, Some(BlockNumber::Latest)) + .compat() + .await?), + EthCoinType::Erc20 { ref token_addr, .. } => { + let function = ERC20_CONTRACT.function("balanceOf")?; + let data = function.encode_input(&[Token::Address(coin.my_address)])?; + let res = coin.call_request(*token_addr, None, Some(data.into())).compat().await?; + let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), - _ => ERR!("Expected U256 as balanceOf result but got {:?}", decoded), + _ => { + let error = format!("Expected U256 as balanceOf result but got {:?}", decoded); + MmError::err(BalanceError::InvalidResponse(error)) + }, } - })) - }, - } + }, + } + }; + Box::new(fut.boxed().compat()) } - fn eth_balance(&self) -> Box + Send> { + fn eth_balance(&self) -> BalanceFut { Box::new( self.web3 .eth() .balance(self.my_address, Some(BlockNumber::Latest)) - .map_err(|e| ERRL!("{}", e)), + .map_to_mm_fut(BalanceError::from), ) } @@ -1469,7 +1584,7 @@ impl EthCoin { to: Address, value: Option, data: Option, - ) -> impl Future { + ) -> impl Future { let request = CallRequest { from: Some(self.my_address), to, @@ -1479,35 +1594,34 @@ impl EthCoin { data, }; - self.web3 - .eth() - .call(request, Some(BlockNumber::Latest)) - .map_err(|e| ERRL!("{}", e)) + self.web3.eth().call(request, Some(BlockNumber::Latest)) } - fn allowance(&self, spender: Address) -> Box + Send + 'static> { - match &self.coin_type { - EthCoinType::Eth => panic!(), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => { - let function = try_fus!(ERC20_CONTRACT.function("allowance")); - let data = - try_fus!(function.encode_input(&[Token::Address(self.my_address), Token::Address(spender),])); - - let call_fut = self.call_request(*token_addr, None, Some(data.into())); + fn allowance(&self, spender: Address) -> Web3RpcFut { + let coin = self.clone(); + let fut = async move { + match coin.coin_type { + EthCoinType::Eth => MmError::err(Web3RpcError::Internal( + "'allowance' must not be called for ETH coin".to_owned(), + )), + EthCoinType::Erc20 { ref token_addr, .. } => { + let function = ERC20_CONTRACT.function("allowance")?; + let data = function.encode_input(&[Token::Address(coin.my_address), Token::Address(spender)])?; - Box::new(call_fut.and_then(move |res| { - let decoded = try_s!(function.decode_output(&res.0)); + let res = coin.call_request(*token_addr, None, Some(data.into())).compat().await?; + let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), - _ => ERR!("Expected U256 as allowance result but got {:?}", decoded), + _ => { + let error = format!("Expected U256 as allowance result but got {:?}", decoded); + MmError::err(Web3RpcError::InvalidResponse(error)) + }, } - })) - }, - } + }, + } + }; + Box::new(fut.boxed().compat()) } fn approve(&self, spender: Address, amount: U256) -> EthTxFut { @@ -1714,6 +1828,7 @@ impl EthCoin { Box::new( self.call_request(swap_contract_address, None, Some(data.into())) + .map_err(|e| ERRL!("{}", e)) .and_then(move |bytes| { let decoded_tokens = try_s!(function.decode_output(&bytes.0)); match decoded_tokens[2] { @@ -2424,11 +2539,11 @@ pub struct EthTxFeeDetails { } impl EthTxFeeDetails { - fn new(gas: U256, gas_price: U256, coin: &str) -> Result { + fn new(gas: U256, gas_price: U256, coin: &str) -> NumConversResult { let total_fee = gas * gas_price; // Fees are always paid in ETH, can use 18 decimals by default - let total_fee = try_s!(u256_to_big_decimal(total_fee, 18)); - let gas_price = try_s!(u256_to_big_decimal(gas_price, 18)); + let total_fee = u256_to_big_decimal(total_fee, 18)?; + let gas_price = u256_to_big_decimal(gas_price, 18)?; Ok(EthTxFeeDetails { coin: coin.to_owned(), @@ -2442,8 +2557,8 @@ impl EthTxFeeDetails { impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { - let ctx = try_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let ctx = try_f!(MmArc::from_weak(&self.ctx).or_mm_err(|| WithdrawError::InternalError("!ctx".to_owned()))); Box::new(Box::pin(withdraw_impl(ctx, self.clone(), req)).compat()) } @@ -2483,28 +2598,28 @@ impl MmCoin for EthCoin { fn get_trade_fee(&self) -> Box + Send> { let coin = self.clone(); - Box::new(self.get_gas_price().and_then(move |gas_price| { - let fee = gas_price * U256::from(150_000); - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, - }; - Ok(TradeFee { - coin: fee_coin.into(), - amount: try_s!(u256_to_big_decimal(fee, 18)).into(), - paid_from_trading_vol: false, - }) - })) + Box::new( + self.get_gas_price() + .map_err(|e| e.to_string()) + .and_then(move |gas_price| { + let fee = gas_price * U256::from(150_000); + let fee_coin = match &coin.coin_type { + EthCoinType::Eth => &coin.ticker, + EthCoinType::Erc20 { platform, .. } => platform, + }; + Ok(TradeFee { + coin: fee_coin.into(), + amount: try_s!(u256_to_big_decimal(fee, 18)).into(), + paid_from_trading_vol: false, + }) + }), + ) } - fn get_sender_trade_fee( - &self, - value: TradePreimageValue, - stage: FeeApproxStage, - ) -> Box + Send> { + fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let gas_price = try_map!(coin.get_gas_price().compat().await, TradePreimageError::Other); + let gas_price = coin.get_gas_price().compat().await?; let gas_price = increase_gas_price_by_stage(gas_price, &stage); let gas_limit = match coin.coin_type { EthCoinType::Eth => { @@ -2514,13 +2629,10 @@ impl MmCoin for EthCoin { EthCoinType::Erc20 { .. } => { let value = match value { TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { - try_map!(wei_from_big_decimal(&value, coin.decimals), TradePreimageError::Other) + wei_from_big_decimal(&value, coin.decimals)? }, }; - let allowed = try_map!( - coin.allowance(coin.swap_contract_address).compat().await, - TradePreimageError::Other - ); + let allowed = coin.allowance(coin.swap_contract_address).compat().await?; if allowed < value { // this gas_limit includes gas for `approve`, `erc20Payment` and `senderRefund` contract calls U256::from(300_000 + APPROVE_GAS_LIMIT) @@ -2532,7 +2644,7 @@ impl MmCoin for EthCoin { }; let total_fee = gas_limit * gas_price; - let amount = try_map!(u256_to_big_decimal(total_fee, 18), TradePreimageError::Other); + let amount = u256_to_big_decimal(total_fee, 18)?; let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, @@ -2546,16 +2658,13 @@ impl MmCoin for EthCoin { Box::new(fut.boxed().compat()) } - fn get_receiver_trade_fee( - &self, - stage: FeeApproxStage, - ) -> Box + Send> { + fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let gas_price = try_map!(coin.get_gas_price().compat().await, TradePreimageError::Other); + let gas_price = coin.get_gas_price().compat().await?; let gas_price = increase_gas_price_by_stage(gas_price, &stage); let total_fee = gas_price * U256::from(150_000); - let amount = try_map!(u256_to_big_decimal(total_fee, 18), TradePreimageError::Other); + let amount = u256_to_big_decimal(total_fee, 18)?; let fee_coin = match &coin.coin_type { EthCoinType::Eth => &coin.ticker, EthCoinType::Erc20 { platform, .. } => platform, @@ -2573,13 +2682,10 @@ impl MmCoin for EthCoin { &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> Box + Send> { + ) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let dex_fee_amount = try_map!( - wei_from_big_decimal(&dex_fee_amount, coin.decimals), - TradePreimageError::Other - ); + let dex_fee_amount = wei_from_big_decimal(&dex_fee_amount, coin.decimals)?; // pass the dummy params let to_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY) @@ -2587,16 +2693,13 @@ impl MmCoin for EthCoin { let (eth_value, data, call_addr, fee_coin) = match &coin.coin_type { EthCoinType::Eth => (dex_fee_amount, Vec::new(), &to_addr, &coin.ticker), EthCoinType::Erc20 { platform, token_addr } => { - let function = try_map!(ERC20_CONTRACT.function("transfer"), TradePreimageError::Other); - let data = try_map!( - function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)]), - TradePreimageError::Other - ); + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)])?; (0.into(), data, token_addr, platform) }, }; - let gas_price = try_map!(coin.get_gas_price().compat().await, TradePreimageError::Other); + let gas_price = coin.get_gas_price().compat().await?; let gas_price = increase_gas_price_by_stage(gas_price, &stage); let estimate_gas_req = CallRequest { value: Some(eth_value), @@ -2611,12 +2714,9 @@ 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 = try_map!( - coin.estimate_gas(estimate_gas_req).compat().await, - TradePreimageError::Other - ); + let gas_limit = coin.estimate_gas(estimate_gas_req).compat().await?; let total_fee = gas_limit * gas_price; - let amount = try_map!(u256_to_big_decimal(total_fee, 18), TradePreimageError::Other); + let amount = u256_to_big_decimal(total_fee, 18)?; Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), @@ -2686,12 +2786,12 @@ fn display_u256_with_decimal_point(number: U256, decimals: u8) -> String { string.trim_end_matches('0').into() } -pub fn u256_to_big_decimal(number: U256, decimals: u8) -> Result { +pub fn u256_to_big_decimal(number: U256, decimals: u8) -> NumConversResult { let string = display_u256_with_decimal_point(number, decimals); - Ok(try_s!(string.parse())) + Ok(string.parse::()?) } -pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> Result { +pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResult { let mut amount = amount.to_string(); let dot = amount.find(|c| c == '.'); let decimals = decimals as usize; @@ -2707,7 +2807,9 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> Result Box + Send> { + fn get_gas_price(uri: &str) -> Web3RpcFut { let uri = uri.to_owned(); let fut = async move { slurp_url(&uri).await }; - Box::new(fut.boxed().compat().and_then(|res| -> Result { - if res.0 != StatusCode::OK { - return ERR!("Gas price request failed with status code {}", res.0); - } + Box::new( + fut.boxed() + .compat() + .map_to_mm_fut(Web3RpcError::Transport) + .and_then(|res| -> Web3RpcResult { + if res.0 != StatusCode::OK { + let error = format!("Gas price request failed with status code {}", res.0); + return MmError::err(Web3RpcError::Transport(error)); + } - let result: GasStationData = try_s!(json::from_slice(&res.2)); - Ok(result.average_gwei()) - })) + let result: GasStationData = json::from_slice(&res.2)?; + Ok(result.average_gwei()) + }), + ) } } diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 9971044267..8e3559c7e6 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -861,7 +861,11 @@ fn test_get_fee_to_send_taker_fee() { /// Some ERC20 tokens return the `error: -32016, message: \"The execution failed due to an exception.\"` error /// if the balance is insufficient. /// So [`EthCoin::get_fee_to_send_taker_fee`] must return [`TradePreimageError::NotSufficientBalance`]. +/// +/// Please note this test doesn't work correctly now, +/// because as of now [`EthCoin::get_fee_to_send_taker_fee`] doesn't process the `Exception` web3 error correctly. #[test] +#[ignore] fn test_get_fee_to_send_taker_fee_insufficient_balance() { const DEX_FEE_AMOUNT: u64 = 100_000_000_000; @@ -875,10 +879,13 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); + let error = coin + .get_fee_to_send_taker_fee(dex_fee_amount.clone(), FeeApproxStage::WithoutApprox) + .wait() + .unwrap_err(); + log!((error)); assert!( - coin.get_fee_to_send_taker_fee(dex_fee_amount.clone(), FeeApproxStage::WithoutApprox) - .wait() - .is_err(), + matches!(error.get_inner(), TradePreimageError::NotSufficientBalance {..}), "Expected TradePreimageError::NotSufficientBalance" ); } diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index e02d07da8e..9531fa8fad 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -29,19 +29,22 @@ #[macro_use] extern crate lazy_static; #[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_json; +#[macro_use] extern crate ser_error_derive; use async_trait::async_trait; -use bigdecimal::BigDecimal; +use bigdecimal::{BigDecimal, ParseBigDecimalError}; use common::executor::{spawn, Timer}; use common::mm_ctx::{from_ctx, MmArc}; +use common::mm_error::prelude::*; use common::mm_metrics::MetricsWeak; use common::mm_number::MmNumber; -use common::{block_on, calc_total_pages, now_ms, rpc_err_response, rpc_response, HyRes, TraceSource, Traceable}; +use common::{block_on, calc_total_pages, now_ms, rpc_err_response, rpc_response, HttpStatusCode, HyRes}; +use derive_more::Display; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures01::Future; use gstuff::slurp; -use http::Response; +use http::{Response, StatusCode}; use rpc::v1::types::Bytes as BytesJson; use serde::{Deserialize, Deserializer}; use serde_json::{self as json, Value as Json}; @@ -63,23 +66,45 @@ macro_rules! try_fus { }; } +macro_rules! try_f { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(e) => return Box::new(futures01::future::err(e)), + } + }; +} + #[doc(hidden)] #[cfg(test)] pub mod coins_tests; pub mod eth; -use self::eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; +use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; + pub mod utxo; -use self::utxo::qtum::{self, qtum_coin_from_conf_and_request, QtumCoin}; -use self::utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; -use self::utxo::{GenerateTransactionError, UtxoFeeDetails, UtxoTx}; +use utxo::qtum::{self, qtum_coin_from_conf_and_request, QtumCoin}; +use utxo::utxo_common::big_decimal_from_sat_unsigned; +use utxo::utxo_standard::{utxo_standard_coin_from_conf_and_request, UtxoStandardCoin}; +use utxo::{GenerateTxError, UtxoFeeDetails, UtxoTx}; + pub mod qrc20; use qrc20::{qrc20_coin_from_conf_and_request, Qrc20Coin, Qrc20FeeDetails}; + #[doc(hidden)] #[allow(unused_variables)] pub mod test_coin; pub use test_coin::TestCoin; +pub type BalanceResult = Result>; +pub type BalanceFut = Box> + Send>; +pub type NumConversResult = Result>; +pub type WithdrawResult = Result>; +pub type WithdrawFut = Box> + Send>; +pub type TradePreimageResult = Result>; +pub type TradePreimageFut = Box> + Send>; +pub type CoinFindResult = Result>; + pub trait Transaction: fmt::Debug + 'static { /// Raw transaction bytes of the transaction fn tx_hex(&self) -> Vec; @@ -259,14 +284,14 @@ pub trait MarketCoinOps { fn my_address(&self) -> Result; - fn my_balance(&self) -> Box + Send>; + fn my_balance(&self) -> BalanceFut; - fn my_spendable_balance(&self) -> Box + Send> { + fn my_spendable_balance(&self) -> BalanceFut { Box::new(self.my_balance().map(|CoinBalance { spendable, .. }| spendable)) } /// Base coin balance for tokens, e.g. ETH balance in ERC20 case - fn base_coin_balance(&self) -> Box + Send>; + fn base_coin_balance(&self) -> BalanceFut; /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send>; @@ -303,7 +328,7 @@ pub trait MarketCoinOps { fn min_trading_vol(&self) -> MmNumber; } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] #[serde(tag = "type")] pub enum WithdrawFee { UtxoFixed { @@ -461,43 +486,217 @@ pub enum TradePreimageValue { UpperBound(BigDecimal), } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum TradePreimageError { - NotSufficientBalance(String), - Other(String), + #[display( + fmt = "Not enough {} to preimage the trade: available {}, required at least {}", + coin, + available, + required + )] + NotSufficientBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + #[display(fmt = "The amount {} is too small", amount)] + AmountIsTooSmall { amount: BigDecimal }, + #[display(fmt = "The max available amount {} is too small", amount)] + UpperBoundAmountIsTooSmall { amount: BigDecimal }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl From for TradePreimageError { + fn from(e: NumConversError) -> Self { TradePreimageError::InternalError(e.to_string()) } +} + +impl TradePreimageError { + /// Construct [`TradePreimageError`] from [`GenerateTxError`] using additional `coin` and `decimals`. + pub fn from_generate_tx_error( + gen_tx_err: GenerateTxError, + coin: String, + decimals: u8, + is_upper_bound: bool, + ) -> TradePreimageError { + match gen_tx_err { + GenerateTxError::EmptyUtxoSet { required } => { + let required = big_decimal_from_sat_unsigned(required, decimals); + TradePreimageError::NotSufficientBalance { + coin, + available: BigDecimal::from(0), + required, + } + }, + GenerateTxError::EmptyOutputs => TradePreimageError::InternalError(gen_tx_err.to_string()), + GenerateTxError::OutputValueLessThanDust { value, .. } => { + let amount = big_decimal_from_sat_unsigned(value, decimals); + if is_upper_bound { + TradePreimageError::UpperBoundAmountIsTooSmall { amount } + } else { + TradePreimageError::AmountIsTooSmall { amount } + } + }, + GenerateTxError::DeductFeeFromOutputFailed { + output_value, required, .. + } => { + let available = big_decimal_from_sat_unsigned(output_value, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + TradePreimageError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::NotEnoughUtxos { sum_utxos, required } => { + let available = big_decimal_from_sat_unsigned(sum_utxos, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + TradePreimageError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::Transport(e) => TradePreimageError::Transport(e), + GenerateTxError::Internal(e) => TradePreimageError::InternalError(e), + } + } } -impl fmt::Display for TradePreimageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +/// The reason of unsuccessful conversion of two internal numbers, e.g. `u64` from `BigNumber`. +#[derive(Debug, Display)] +pub struct NumConversError(String); + +impl From for NumConversError { + fn from(e: ParseBigDecimalError) -> Self { NumConversError::new(e.to_string()) } +} + +impl NumConversError { + pub fn new(description: String) -> NumConversError { NumConversError(description) } + + pub fn description(&self) -> &str { &self.0 } +} + +#[derive(Debug, Display, PartialEq)] +pub enum BalanceError { + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), +} + +impl From for BalanceError { + fn from(e: NumConversError) -> Self { BalanceError::Internal(e.to_string()) } +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum WithdrawError { + #[display( + fmt = "Not enough {} to withdraw: available {}, required at least {}", + coin, + available, + required + )] + NotSufficientBalance { + coin: String, + available: BigDecimal, + required: BigDecimal, + }, + #[display(fmt = "Balance is zero")] + ZeroBalanceToWithdrawMax, + #[display(fmt = "The amount {} is too small", amount)] + AmountIsTooSmall { amount: BigDecimal }, + #[display(fmt = "Invalid address: {}", _0)] + InvalidAddress(String), + #[display(fmt = "Invalid fee policy: {}", _0)] + InvalidFeePolicy(String), + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for WithdrawError { + fn status_code(&self) -> StatusCode { match self { - TradePreimageError::NotSufficientBalance(e) => write!(f, "Not sufficient balance: {}", e), - TradePreimageError::Other(e) => write!(f, "{}", e), + WithdrawError::NotSufficientBalance { .. } + | WithdrawError::ZeroBalanceToWithdrawMax + | WithdrawError::AmountIsTooSmall { .. } + | WithdrawError::InvalidAddress(_) + | WithdrawError::InvalidFeePolicy(_) + | WithdrawError::NoSuchCoin { .. } => StatusCode::BAD_REQUEST, + WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } -impl Traceable for TradePreimageError { - fn trace(self, source: TraceSource) -> Self { - match self { - TradePreimageError::NotSufficientBalance(e) => { - TradePreimageError::NotSufficientBalance(source.with_msg(&e)) - }, - TradePreimageError::Other(e) => TradePreimageError::Other(source.with_msg(&e)), +impl From for WithdrawError { + fn from(e: NumConversError) -> Self { WithdrawError::InternalError(e.to_string()) } +} + +impl From for WithdrawError { + fn from(e: BalanceError) -> Self { + match e { + BalanceError::Transport(error) | BalanceError::InvalidResponse(error) => WithdrawError::Transport(error), + BalanceError::Internal(internal) => WithdrawError::InternalError(internal), } } } -impl From for TradePreimageError { - fn from(e: GenerateTransactionError) -> Self { +impl From for WithdrawError { + fn from(e: CoinFindError) -> Self { match e { - GenerateTransactionError::EmptyUtxoSet => { - TradePreimageError::NotSufficientBalance(GenerateTransactionError::EmptyUtxoSet.to_string()) + CoinFindError::NoSuchCoin { coin } => WithdrawError::NoSuchCoin { coin }, + } + } +} + +impl WithdrawError { + /// Construct [`WithdrawError`] from [`GenerateTxError`] using additional `coin` and `decimals`. + pub fn from_generate_tx_error(gen_tx_err: GenerateTxError, coin: String, decimals: u8) -> WithdrawError { + match gen_tx_err { + GenerateTxError::EmptyUtxoSet { required } => { + let required = big_decimal_from_sat_unsigned(required, decimals); + WithdrawError::NotSufficientBalance { + coin, + available: BigDecimal::from(0), + required, + } + }, + GenerateTxError::EmptyOutputs => WithdrawError::InternalError(gen_tx_err.to_string()), + GenerateTxError::OutputValueLessThanDust { value, .. } => { + let amount = big_decimal_from_sat_unsigned(value, decimals); + WithdrawError::AmountIsTooSmall { amount } }, - GenerateTransactionError::NotSufficientBalance { description } - | GenerateTransactionError::DeductFeeFromOutputFailed { description } => { - TradePreimageError::NotSufficientBalance(description) + GenerateTxError::DeductFeeFromOutputFailed { + output_value, required, .. + } => { + let available = big_decimal_from_sat_unsigned(output_value, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + WithdrawError::NotSufficientBalance { + coin, + available, + required, + } + }, + GenerateTxError::NotEnoughUtxos { sum_utxos, required } => { + let available = big_decimal_from_sat_unsigned(sum_utxos, decimals); + let required = big_decimal_from_sat_unsigned(required, decimals); + WithdrawError::NotSufficientBalance { + coin, + available, + required, + } }, - e => TradePreimageError::Other(e.to_string()), + GenerateTxError::Transport(e) => WithdrawError::Transport(e), + GenerateTxError::Internal(e) => WithdrawError::InternalError(e), } } } @@ -518,7 +717,7 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { coin_conf["wallet_only"].as_bool().unwrap_or(false) } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send>; + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; /// Maximum number of digits after decimal point used to denominate integer coin units (satoshis, wei, etc.) fn decimals(&self) -> u8; @@ -582,24 +781,17 @@ pub trait MmCoin: SwapOps + MarketCoinOps + fmt::Debug + Send + Sync + 'static { fn get_trade_fee(&self) -> Box + Send>; /// Get fee to be paid by sender per whole swap using the sending value and check if the wallet has sufficient balance to pay the fee. - fn get_sender_trade_fee( - &self, - value: TradePreimageValue, - stage: FeeApproxStage, - ) -> Box + Send>; + fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut; /// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee. - fn get_receiver_trade_fee( - &self, - stage: FeeApproxStage, - ) -> Box + Send>; + fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut; /// Get transaction fee the Taker has to pay to send a `TakerFee` transaction and check if the wallet has sufficient balance to pay the fee. fn get_fee_to_send_taker_fee( &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> Box + Send>; + ) -> TradePreimageFut; /// required transaction confirmations number to ensure double-spend safety fn required_confirmations(&self) -> u64; @@ -945,6 +1137,22 @@ pub async fn lp_coinfind(ctx: &MmArc, ticker: &str) -> Result Ok(coins.get(ticker).cloned()) } +#[derive(Display)] +pub enum CoinFindError { + #[display(fmt = "No such coin: {}", coin)] + NoSuchCoin { coin: String }, +} + +pub async fn lp_coinfind_or_err(ctx: &MmArc, ticker: &str) -> CoinFindResult { + match lp_coinfind(ctx, ticker).await { + Ok(Some(coin)) => Ok(coin), + Ok(None) => MmError::err(CoinFindError::NoSuchCoin { + coin: ticker.to_owned(), + }), + Err(e) => panic!("Unexpected error: {}", e), + } +} + #[derive(Deserialize)] struct ConvertAddressReq { coin: String, @@ -1010,17 +1218,9 @@ pub async fn validate_address(ctx: MmArc, req: Json) -> Result> Ok(try_s!(Response::builder().body(body))) } -pub async fn withdraw(ctx: MmArc, req: Json) -> Result>, String> { - let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); - let coin = match lp_coinfind(&ctx, &ticker).await { - Ok(Some(t)) => t, - Ok(None) => return ERR!("No such coin: {}", ticker), - Err(err) => return ERR!("!lp_coinfind({}): {}", ticker, err), - }; - let withdraw_req: WithdrawRequest = try_s!(json::from_value(req)); - let res = try_s!(coin.withdraw(withdraw_req).compat().await); - let body = try_s!(json::to_vec(&res)); - Ok(try_s!(Response::builder().body(body))) +pub async fn withdraw(ctx: MmArc, req: WithdrawRequest) -> WithdrawResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + coin.withdraw(req).compat().await } pub async fn send_raw_transaction(ctx: MmArc, req: Json) -> Result>, String> { diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index ca2e4794ef..302ef60ee6 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -2,24 +2,28 @@ use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; use crate::utxo::qtum::QtumBasedCoin; -use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps}; +use crate::utxo::rpc_clients::{ElectrumClient, NativeClient, UnspentInfo, UtxoRpcClientEnum, UtxoRpcClientOps, + UtxoRpcError, UtxoRpcResult}; use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_inputs_signed_by_pub}; -use crate::utxo::{qtum, sign_tx, ActualTxFee, AdditionalTxData, FeePolicy, GenerateTransactionError, +use crate::utxo::{qtum, sign_tx, ActualTxFee, AdditionalTxData, FeePolicy, GenerateTxError, GenerateTxResult, RecentlySpentOutPoints, UtxoCoinBuilder, UtxoCoinFields, UtxoCommonOps, UtxoTx, VerboseTransactionFrom, UTXO_LOCK}; -use crate::{CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, - TradePreimageError, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut, - ValidateAddressResult, WithdrawFee, WithdrawRequest}; +use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, + MmCoin, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawError, WithdrawFee, + WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bigdecimal::BigDecimal; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; +use common::block_on; use common::executor::Timer; use common::jsonrpc_client::{JsonRpcClient, JsonRpcError, JsonRpcRequest, RpcRes}; use common::log::{error, warn}; use common::mm_ctx::MmArc; +use common::mm_error::prelude::*; use common::mm_number::MmNumber; -use common::{block_on, Traceable}; +use derive_more::Display; use ethabi::{Function, Token}; use ethereum_types::{H160, U256}; use futures::compat::Future01CompatExt; @@ -59,6 +63,8 @@ const QRC20_PAYMENT_SENT_TOPIC: &str = "ccc9c05183599bd3135da606eaaf535daffe256e const QRC20_RECEIVER_SPENT_TOPIC: &str = "36c177bcb01c6d568244f05261e2946c8c977fa50822f3fa098c470770ee1f3e"; const QRC20_SENDER_REFUNDED_TOPIC: &str = "1797d500133f8e427eb9da9523aa4a25cb40f50ebc7dbda3c7c81778973f35ba"; +pub type Qrc20ABIResult = Result>; + struct Qrc20CoinBuilder<'a> { ctx: &'a MmArc, ticker: &'a str, @@ -175,23 +181,6 @@ impl UtxoCoinBuilder for Qrc20CoinBuilder<'_> { } } -macro_rules! stringify_gen_tx_error { - ($selff: expr, $exp: expr) => { - match $exp { - GenerateTransactionError::EmptyUtxoSet => ERRL!( - "Not enough {} to Pay Fee: {}", - $selff.platform, - GenerateTransactionError::EmptyUtxoSet - ), - GenerateTransactionError::NotSufficientBalance { description } - | GenerateTransactionError::DeductFeeFromOutputFailed { description } => { - ERRL!("Not enough {} to Pay Fee: {}", $selff.platform, description) - }, - e => ERRL!("{}", e), - } - }; -} - pub async fn qrc20_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -313,6 +302,39 @@ struct GenerateQrc20TxResult { gas_fee: u64, } +#[derive(Debug, Display)] +pub enum Qrc20ABIError { + #[display(fmt = "Invalid QRC20 ABI params: {}", _0)] + InvalidParams(String), + #[display(fmt = "QRC20 ABI error: {}", _0)] + ABIError(String), +} + +impl From for Qrc20ABIError { + fn from(e: ethabi::Error) -> Qrc20ABIError { Qrc20ABIError::ABIError(e.to_string()) } +} + +impl From for TradePreimageError { + fn from(e: Qrc20ABIError) -> Self { + // `Qrc20ABIError` is always an internal error + TradePreimageError::InternalError(e.to_string()) + } +} + +impl From for WithdrawError { + fn from(e: Qrc20ABIError) -> Self { + // `Qrc20ABIError` is always an internal error + WithdrawError::InternalError(e.to_string()) + } +} + +impl From for UtxoRpcError { + fn from(e: Qrc20ABIError) -> Self { + // `Qrc20ABIError` is always an internal error + UtxoRpcError::Internal(e.to_string()) + } +} + impl Qrc20Coin { /// `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), /// or should be sum of gas fee of all contract calls. @@ -331,10 +353,13 @@ impl Qrc20Coin { // Move over all QRC20 tokens should share the same cache with each other and base QTUM coin let _utxo_lock = UTXO_LOCK.lock().await; + let platform = self.platform.clone(); + let decimals = self.utxo.decimals; let GenerateQrc20TxResult { signed, .. } = self .generate_qrc20_transaction(outputs) .await - .map_err(|e| stringify_gen_tx_error!(self, e))?; + .mm_err(|e| WithdrawError::from_generate_tx_error(e, platform, decimals)) + .map_err(|e| ERRL!("{}", e))?; let _tx = try_s!(self.utxo.rpc_client.send_transaction(&signed).compat().await); Ok(signed.into()) } @@ -344,11 +369,8 @@ impl Qrc20Coin { async fn generate_qrc20_transaction( &self, contract_outputs: Vec, - ) -> Result { - let (unspents, _) = try_map!( - self.ordered_mature_unspents(&self.utxo.my_address).await, - GenerateTransactionError::Other - ); + ) -> Result> { + let (unspents, _) = self.ordered_mature_unspents(&self.utxo.my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -363,16 +385,15 @@ impl Qrc20Coin { .generate_transaction(unspents, outputs, fee_policy, tx_fee, Some(gas_fee)) .await?; let prev_script = ScriptBuilder::build_p2pkh(&self.utxo.my_address.hash); - let signed = try_map!( - sign_tx( - unsigned, - &self.utxo.key_pair, - prev_script, - self.utxo.conf.signature_version, - self.utxo.conf.fork_id - ), - GenerateTransactionError::Other - ); + let signed = sign_tx( + unsigned, + &self.utxo.key_pair, + prev_script, + self.utxo.conf.signature_version, + self.utxo.conf.fork_id, + ) + .map_to_mm(GenerateTxError::Internal)?; + let miner_fee = data.fee_amount + data.unused_change.unwrap_or_default(); Ok(GenerateQrc20TxResult { signed, @@ -387,17 +408,12 @@ impl Qrc20Coin { amount: U256, gas_limit: u64, gas_price: u64, - ) -> Result { - let function = try_s!(eth::ERC20_CONTRACT.function("transfer")); - let params = try_s!(function.encode_input(&[Token::Address(to_addr), Token::Uint(amount)])); + ) -> Qrc20ABIResult { + let function = eth::ERC20_CONTRACT.function("transfer")?; + let params = function.encode_input(&[Token::Address(to_addr), Token::Uint(amount)])?; - let script_pubkey = try_s!(generate_contract_call_script_pubkey( - ¶ms, - gas_limit, - gas_price, - &self.contract_address, - )) - .to_bytes(); + let script_pubkey = + generate_contract_call_script_pubkey(¶ms, gas_limit, gas_price, &self.contract_address)?.to_bytes(); Ok(ContractCallOutput { value: OUTPUT_QTUM_AMOUNT, @@ -411,7 +427,7 @@ impl Qrc20Coin { &self, contract_outputs: Vec, stage: &FeeApproxStage, - ) -> Result { + ) -> TradePreimageResult { let decimals = self.as_ref().decimals; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -422,8 +438,7 @@ impl Qrc20Coin { let fee_policy = FeePolicy::SendExact; let miner_fee = UtxoCommonOps::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, Some(gas_fee), stage) - .await - .trace(source!())?; + .await?; let gas_fee = big_decimal_from_sat(gas_fee as i64, decimals); Ok(miner_fee + gas_fee) } @@ -435,7 +450,7 @@ impl UtxoCommonOps for Qrc20Coin { /// Get only QTUM transaction fee. async fn get_tx_fee(&self) -> Result { utxo_common::get_tx_fee(&self.utxo).await } - async fn get_htlc_spend_fee(&self) -> Result { utxo_common::get_htlc_spend_fee(self).await } + async fn get_htlc_spend_fee(&self) -> UtxoRpcResult { utxo_common::get_htlc_spend_fee(self).await } fn addresses_from_script(&self, script: &Script) -> Result, String> { utxo_common::addresses_from_script(&self.utxo.conf, script) @@ -453,7 +468,7 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::address_from_str(&self.utxo.conf, address) } - async fn get_current_mtp(&self) -> Result { utxo_common::get_current_mtp(&self.utxo).await } + async fn get_current_mtp(&self) -> UtxoRpcResult { utxo_common::get_current_mtp(&self.utxo).await } fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { self.is_qtum_unspent_mature(output) } @@ -465,7 +480,7 @@ impl UtxoCommonOps for Qrc20Coin { fee_policy: FeePolicy, fee: Option, gas_fee: Option, - ) -> Result<(TransactionInputSigner, AdditionalTxData), GenerateTransactionError> { + ) -> GenerateTxResult { utxo_common::generate_transaction(self, utxos, outputs, fee_policy, fee, gas_fee).await } @@ -474,7 +489,7 @@ impl UtxoCommonOps for Qrc20Coin { unsigned: TransactionInputSigner, data: AdditionalTxData, my_script_pub: ScriptBytes, - ) -> Result<(TransactionInputSigner, AdditionalTxData), String> { + ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub).await } @@ -501,7 +516,7 @@ impl UtxoCommonOps for Qrc20Coin { async fn ordered_mature_unspents<'a>( &'a self, address: &Address, - ) -> Result<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>), String> { + ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { utxo_common::ordered_mature_unspents(self, address).await } @@ -521,7 +536,7 @@ impl UtxoCommonOps for Qrc20Coin { async fn list_unspent_ordered<'a>( &'a self, address: &Address, - ) -> Result<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>), String> { + ) -> UtxoRpcResult<(Vec, AsyncMutexGuard<'a, RecentlySpentOutPoints>)> { utxo_common::ordered_mature_unspents(self, address).await } @@ -531,7 +546,7 @@ impl UtxoCommonOps for Qrc20Coin { fee_policy: FeePolicy, gas_fee: Option, stage: &FeeApproxStage, - ) -> Result { + ) -> TradePreimageResult { utxo_common::preimage_trade_fee_required_to_send_outputs(self, outputs, fee_policy, gas_fee, stage).await } @@ -832,33 +847,39 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> Result { utxo_common::my_address(self) } - fn my_balance(&self) -> Box + Send> { + fn my_balance(&self) -> BalanceFut { let my_address = self.my_addr_as_contract_addr(); - let params = &[Token::Address(my_address)]; + let params = [Token::Address(my_address)]; let contract_address = self.contract_address; let decimals = self.utxo.decimals; - let fut = self - .utxo - .rpc_client - .rpc_contract_call(ViewContractCallType::BalanceOf, &contract_address, params) - .map_err(|e| ERRL!("{}", e)) - .and_then(move |tokens| match tokens.first() { - Some(Token::Uint(bal)) => u256_to_big_decimal(*bal, decimals), - Some(_) => ERR!(r#"Expected Uint as "balanceOf" result but got {:?}"#, tokens), - None => ERR!(r#"Expected Uint as "balanceOf" result but got nothing"#), - }) - .map(|spendable| CoinBalance { + let coin = self.clone(); + let fut = async move { + let tokens = coin + .utxo + .rpc_client + .rpc_contract_call(ViewContractCallType::BalanceOf, &contract_address, ¶ms) + .compat() + .await?; + let spendable = match tokens.first() { + Some(Token::Uint(bal)) => u256_to_big_decimal(*bal, decimals)?, + _ => { + let error = format!("Expected U256 as balanceOf result but got {:?}", tokens); + return MmError::err(BalanceError::InvalidResponse(error)); + }, + }; + Ok(CoinBalance { spendable, unspendable: BigDecimal::from(0), - }); - Box::new(fut) + }) + }; + Box::new(fut.boxed().compat()) } - fn base_coin_balance(&self) -> Box + Send> { + fn base_coin_balance(&self) -> BalanceFut { let selfi = self.clone(); let fut = async move { - let CoinBalance { spendable, .. } = try_s!(selfi.qtum_balance().await); + let CoinBalance { spendable, .. } = selfi.qtum_balance().await?; Ok(spendable) }; Box::new(fut.boxed().compat()) @@ -925,7 +946,7 @@ impl MarketCoinOps for Qrc20Coin { impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } - fn withdraw(&self, req: WithdrawRequest) -> Box + Send> { + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) } @@ -958,11 +979,7 @@ impl MmCoin for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn get_sender_trade_fee( - &self, - value: TradePreimageValue, - stage: FeeApproxStage, - ) -> Box + Send> { + fn get_sender_trade_fee(&self, value: TradePreimageValue, stage: FeeApproxStage) -> TradePreimageFut { let selfi = self.clone(); let decimals = self.utxo.decimals; let fut = async move { @@ -975,46 +992,38 @@ impl MmCoin for Qrc20Coin { let my_balance = U256::max_value(); let value = match value { TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { - try_map!(wei_from_big_decimal(&value, decimals), TradePreimageError::Other) + wei_from_big_decimal(&value, decimals)? }, }; let erc20_payment_fee = { - let erc20_payment_outputs = try_map!( - selfi - .generate_swap_payment_outputs( - my_balance, - swap_id.clone(), - value, - timelock, - secret_hash.clone(), - receiver_addr, - selfi.swap_contract_address, - ) - .await, - TradePreimageError::Other - ); + let erc20_payment_outputs = selfi + .generate_swap_payment_outputs( + my_balance, + swap_id.clone(), + value, + timelock, + secret_hash.clone(), + receiver_addr, + selfi.swap_contract_address, + ) + .await?; selfi .preimage_trade_fee_required_to_send_outputs(erc20_payment_outputs, &stage) - .await - .trace(source!())? + .await? }; let sender_refund_fee = { - let sender_refund_output = try_map!( - selfi.sender_refund_output( - &selfi.swap_contract_address, - swap_id, - value, - secret_hash, - receiver_addr - ), - TradePreimageError::Other - ); + let sender_refund_output = selfi.sender_refund_output( + &selfi.swap_contract_address, + swap_id, + value, + secret_hash, + receiver_addr, + )?; selfi .preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) - .await - .trace(source!())? + .await? }; let total_fee = erc20_payment_fee + sender_refund_fee; @@ -1027,10 +1036,7 @@ impl MmCoin for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn get_receiver_trade_fee( - &self, - stage: FeeApproxStage, - ) -> Box + Send> { + fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { let selfi = self.clone(); let fut = async move { // pass the dummy params @@ -1041,10 +1047,8 @@ impl MmCoin for Qrc20Coin { // get the max available value that we can pass into the contract call params // see `generate_contract_call_script_pubkey` let value = u64::max_value().into(); - let output = try_map!( - selfi.receiver_spend_output(&selfi.swap_contract_address, swap_id, value, secret, sender_addr), - TradePreimageError::Other - ); + let output = + selfi.receiver_spend_output(&selfi.swap_contract_address, swap_id, value, secret, sender_addr)?; let total_fee = selfi .preimage_trade_fee_required_to_send_outputs(vec![output], &stage) @@ -1062,25 +1066,19 @@ impl MmCoin for Qrc20Coin { &self, dex_fee_amount: BigDecimal, stage: FeeApproxStage, - ) -> Box + Send> { + ) -> TradePreimageFut { let selfi = self.clone(); let fut = async move { - let amount = try_map!( - wei_from_big_decimal(&dex_fee_amount, selfi.utxo.decimals), - TradePreimageError::Other - ); + let amount = wei_from_big_decimal(&dex_fee_amount, selfi.utxo.decimals)?; // pass the dummy params let to_addr = H160::default(); - let transfer_output = try_map!( - selfi.transfer_output(to_addr, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT), - TradePreimageError::Other - ); + let transfer_output = + selfi.transfer_output(to_addr, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)?; let total_fee = selfi .preimage_trade_fee_required_to_send_outputs(vec![transfer_output], &stage) - .await - .trace(source!())?; + .await?; Ok(TradeFee { coin: selfi.platform.clone(), amount: total_fee.into(), @@ -1132,65 +1130,73 @@ pub struct Qrc20FeeDetails { total_gas_fee: BigDecimal, } -async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> Result { - let to_addr = try_s!(UtxoAddress::from_str(&req.to)); +async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult { + let to_addr = UtxoAddress::from_str(&req.to) + .map_err(|e| e.to_string()) + .map_to_mm(WithdrawError::InvalidAddress)?; let conf = &coin.utxo.conf; let is_p2pkh = to_addr.prefix == conf.pub_addr_prefix && to_addr.t_addr_prefix == conf.pub_t_addr_prefix; let is_p2sh = to_addr.prefix == conf.p2sh_addr_prefix && to_addr.t_addr_prefix == conf.p2sh_t_addr_prefix && conf.segwit; if !is_p2pkh && !is_p2sh { - return ERR!("Address {} has invalid format", to_addr); + let error = "Expected either P2PKH or P2SH".to_owned(); + return MmError::err(WithdrawError::InvalidAddress(error)); } let _utxo_lock = UTXO_LOCK.lock().await; - let qrc20_balance = try_s!(coin.my_spendable_balance().compat().await); + let qrc20_balance = coin.my_spendable_balance().compat().await?; // the qrc20_amount_sat is used only within smart contract calls let (qrc20_amount_sat, qrc20_amount) = if req.max { - let amount = try_s!(wei_from_big_decimal(&qrc20_balance, coin.utxo.decimals)); + let amount = wei_from_big_decimal(&qrc20_balance, coin.utxo.decimals)?; if amount.is_zero() { - return ERR!("Balance is 0"); + return MmError::err(WithdrawError::ZeroBalanceToWithdrawMax); } (amount, qrc20_balance.clone()) } else { - let amount_sat = try_s!(wei_from_big_decimal(&req.amount, coin.utxo.decimals)); + let amount_sat = wei_from_big_decimal(&req.amount, coin.utxo.decimals)?; if amount_sat.is_zero() { - return ERR!("The amount {} is too small", req.amount); + return MmError::err(WithdrawError::AmountIsTooSmall { + amount: req.amount.clone(), + }); } if req.amount > qrc20_balance { - return ERR!( - "The amount {} to withdraw is larger than balance {}", - req.amount, - qrc20_balance - ); + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker().to_owned(), + available: qrc20_balance, + required: req.amount, + }); } (amount_sat, req.amount) }; let (gas_limit, gas_price) = match req.fee { Some(WithdrawFee::Qrc20Gas { gas_limit, gas_price }) => (gas_limit, gas_price), - Some(_) => return ERR!("Unsupported input fee type"), + Some(fee_policy) => { + let error = format!("Expected 'Qrc20Gas' fee type, found {:?}", fee_policy); + return MmError::err(WithdrawError::InvalidFeePolicy(error)); + }, None => (QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT), }; - let transfer_output = try_s!(coin.transfer_output( + // [`Qrc20Coin::transfer_output`] shouldn't fail if the arguments are correct + let transfer_output = coin.transfer_output( qtum::contract_addr_from_utxo_addr(to_addr.clone()), qrc20_amount_sat, gas_limit, - gas_price - )); + gas_price, + )?; let outputs = vec![transfer_output]; let GenerateQrc20TxResult { signed, miner_fee, gas_fee, - } = coin - .generate_qrc20_transaction(outputs) - .await - .map_err(|e| stringify_gen_tx_error!(coin, e))?; + } = coin.generate_qrc20_transaction(outputs).await.mm_err(|gen_tx_error| { + WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform.clone(), coin.utxo.decimals) + })?; let received_by_me = if to_addr == coin.utxo.my_address { qrc20_amount.clone() @@ -1198,8 +1204,11 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> Result for UtxoRpcError { + fn from(e: ethabi::Error) -> Self { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + UtxoRpcError::Internal(e.to_string()) + } +} + pub mod for_tests { use super::*; @@ -360,7 +369,7 @@ pub trait Qrc20RpcOps { func: ViewContractCallType, contract_addr: &H160, tokens: &[Token], - ) -> Box, Error = String> + Send>; + ) -> UtxoRpcFut>; fn token_decimals(&self, token_address: &H160) -> Box + Send>; } @@ -378,19 +387,24 @@ impl Qrc20RpcOps for UtxoRpcClientEnum { func: ViewContractCallType, contract_addr: &H160, tokens: &[Token], - ) -> Box, Error = String> + Send> { + ) -> UtxoRpcFut> { let function = func.as_function().clone(); - let params = try_fus!(function.encode_input(tokens)); + let params = try_f!(function.encode_input(tokens).map_to_mm(UtxoRpcError::from)); let contract_addr = contract_addr_into_rpc_format(contract_addr); - let fut = match self { - UtxoRpcClientEnum::Native(native) => native.call_contract(&contract_addr, params.into()), - UtxoRpcClientEnum::Electrum(electrum) => electrum.blockchain_contract_call(&contract_addr, params.into()), + let rpc_client = self.clone(); + let fut = async move { + let fut = match rpc_client { + UtxoRpcClientEnum::Native(native) => native.call_contract(&contract_addr, params.into()), + UtxoRpcClientEnum::Electrum(electrum) => { + electrum.blockchain_contract_call(&contract_addr, params.into()) + }, + }; + let result = fut.compat().await?; + let decoded = function.decode_output(&result.execution_result.output)?; + Ok(decoded) }; - let fut = fut - .map_err(|e| ERRL!("{}", e)) - .and_then(move |result| Ok(try_s!(function.decode_output(&result.execution_result.output)))); - Box::new(fut) + Box::new(fut.boxed().compat()) } fn token_decimals(&self, token_address: &H160) -> Box + Send> { diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index af27b11e69..430762fac9 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -7,15 +7,17 @@ pub fn generate_contract_call_script_pubkey( gas_limit: u64, gas_price: u64, contract_address: &[u8], -) -> Result { +) -> Qrc20ABIResult