Skip to content

Commit

Permalink
Electrum extended error msg #611 (#614)
Browse files Browse the repository at this point in the history
* Add host:port to RPC electrum error #611

* Add coin name to RPC electrum error #611

* Change order of imports

* Add ignore attribute for the unstable on MacOS mm2::mm2_tests::test_multiple_buy_sell_no_delay test

* Refactor the test_electrum_rpc_client_error test
* Remove ignore attribute
* Add comparing of expected and actual error strings
  • Loading branch information
sergeyboyko0791 authored Apr 15, 2020
1 parent d7e666a commit 7d10c66
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 28 deletions.
3 changes: 3 additions & 0 deletions mm2src/coins/coins_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::utxo::rpc_clients::NativeClientImpl;

pub fn test_list_unspent() {
let client = NativeClientImpl {
coin_ticker: "RICK".into(),
uri: "http://127.0.0.1:10271".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
};
Expand All @@ -15,6 +16,7 @@ pub fn test_list_unspent() {

pub fn test_get_block_count() {
let client = NativeClientImpl {
coin_ticker: "RICK".into(),
uri: "http://127.0.0.1:10271".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
};
Expand All @@ -24,6 +26,7 @@ pub fn test_get_block_count() {

pub fn test_import_address() {
let client = NativeClientImpl {
coin_ticker: "RICK".into(),
uri: "http://127.0.0.1:10271".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
};
Expand Down
7 changes: 4 additions & 3 deletions mm2src/coins/utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1516,12 +1516,12 @@ impl MmCoin for UtxoCoin {
Ok(value) => value,
Err(e) => {
match &e.error {
JsonRpcErrorType::Transport(e) | JsonRpcErrorType::Parse(e) => {
JsonRpcErrorType::Transport(e) | JsonRpcErrorType::Parse(_, e) => {
ctx.log.log("", &[&"tx_history", &self.ticker], &ERRL!("Error {} on scripthash_get_history, retrying", e));
thread::sleep(Duration::from_secs(10));
continue;
},
JsonRpcErrorType::Response(err) => {
JsonRpcErrorType::Response(_addr, err) => {
if *err == history_too_large {
ctx.log.log("", &[&"tx_history", &self.ticker], &ERRL!("Got `history too large`, stopping further attempts to retrieve it"));
*unwrap!(self.history_sync_state.lock()) = HistorySyncState::Error(json!({
Expand Down Expand Up @@ -1885,6 +1885,7 @@ pub async fn utxo_coin_from_conf_and_request(
None => try_s!(conf["rpcport"].as_u64().ok_or(ERRL!("Rpc port is not set neither in `coins` file nor in native daemon config"))) as u16,
};
let client = Arc::new(NativeClientImpl {
coin_ticker: ticker.to_string(),
uri: fomat!("http://127.0.0.1:"(rpc_port)),
auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)),
});
Expand All @@ -1898,7 +1899,7 @@ pub async fn utxo_coin_from_conf_and_request(
let mut servers: Vec<ElectrumRpcRequest> = try_s!(json::from_value(req["servers"].clone()));
let mut rng = small_rng();
servers.as_mut_slice().shuffle(&mut rng);
let mut client = ElectrumClientImpl::new();
let mut client = ElectrumClientImpl::new(ticker.to_string());
for server in servers.iter() {
match client.add_server(server) {
Ok(_) => (),
Expand Down
62 changes: 52 additions & 10 deletions mm2src/coins/utxo/rpc_clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use common::{StringError};
use common::wio::{slurp_req};
use common::executor::{spawn, Timer};
use common::custom_futures::{join_all_sequential, select_ok_sequential};
use common::jsonrpc_client::{JsonRpcClient, JsonRpcResponseFut, JsonRpcRequest, JsonRpcResponse, RpcRes};
use common::jsonrpc_client::{JsonRpcClient, JsonRpcRemoteAddr, JsonRpcResponseFut, JsonRpcRequest, JsonRpcResponse, RpcRes};
use futures01::{Future, Poll, Sink, Stream};
use futures01::future::{Either, loop_fn, Loop, select_ok};
use futures01::sync::{mpsc, oneshot};
Expand Down Expand Up @@ -254,6 +254,8 @@ pub enum EstimateFeeMethod {
/// This description will be updated with more info
#[derive(Clone, Debug)]
pub struct NativeClientImpl {
/// Name of coin the rpc client is intended to work with
pub coin_ticker: String,
/// The uri to send requests to
pub uri: String,
/// Value of Authorization header, e.g. "Basic base64(user:password)"
Expand All @@ -264,13 +266,33 @@ pub struct NativeClientImpl {
pub struct NativeClient(pub Arc<NativeClientImpl>);
impl Deref for NativeClient {type Target = NativeClientImpl; fn deref (&self) -> &NativeClientImpl {&*self.0}}

/// The trait provides methods to generate the JsonRpcClient instance info such as name of coin.
pub trait UtxoJsonRpcClientInfo: JsonRpcClient {
/// Name of coin the rpc client is intended to work with
fn coin_name(&self) -> &str;

/// Generate client info from coin name
fn client_info(&self) -> String {
format!("coin: {}", self.coin_name())
}
}

impl UtxoJsonRpcClientInfo for NativeClientImpl {
fn coin_name(&self) -> &str {
self.coin_ticker.as_str()
}
}

impl JsonRpcClient for NativeClientImpl {
fn version(&self) -> &'static str { "1.0" }

fn next_id(&self) -> String { "0".into() }

fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) }

fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut {
let request_body = try_fus!(json::to_string(&request));
let uri = self.uri.clone();

let http_request = try_fus!(
Request::builder()
Expand All @@ -279,17 +301,19 @@ impl JsonRpcClient for NativeClientImpl {
AUTHORIZATION,
self.auth.clone()
)
.uri(self.uri.clone())
.uri(uri.clone())
.body(Vec::from(request_body))
);
Box::new(slurp_req(http_request).then(move |result| -> Result<JsonRpcResponse, String> {
Box::new(slurp_req(http_request).then(move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> {
let res = try_s!(result);
let body = try_s!(std::str::from_utf8(&res.2));
if res.0 != StatusCode::OK {
return ERR!("Rpc request {:?} failed with HTTP status code {}, response body: {}",
request, res.0, body);
}
Ok(try_s!(json::from_str(body)))

let response = try_s!(json::from_str(body));
Ok((uri.into(), response))
}))
}
}
Expand Down Expand Up @@ -753,6 +777,7 @@ impl Drop for ElectrumConnection {

#[derive(Debug)]
pub struct ElectrumClientImpl {
coin_ticker: String,
connections: Vec<ElectrumConnection>,
next_id: AtomicU64,
}
Expand All @@ -761,11 +786,16 @@ pub struct ElectrumClientImpl {
async fn electrum_request_multi(
client: ElectrumClient,
request: JsonRpcRequest,
) -> Result<JsonRpcResponse, String> {
) -> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> {
let mut futures = vec![];
for connection in client.connections.iter() {
let connection_addr = connection.addr.clone();
match &*connection.tx.lock().await {
Some(tx) => futures.push(electrum_request(request.clone(), tx.clone(), connection.responses.clone())),
Some(tx) => {
let fut = electrum_request(request.clone(), tx.clone(), connection.responses.clone())
.map(|response| (JsonRpcRemoteAddr(connection_addr), response));
futures.push(fut)
},
None => (),
}
}
Expand Down Expand Up @@ -797,7 +827,7 @@ pub extern fn electrum_replied (ri: i32, id: i32) {
/// I haven't looked into this because we'll probably use a websocket or Java implementation instead.
#[cfg(not(feature = "native"))]
async fn electrum_request_multi (client: ElectrumClient, request: JsonRpcRequest)
-> Result<JsonRpcResponse, String> {
-> Result<(JsonRpcRemoteAddr, JsonRpcResponse), String> {
use futures::future::{select, Either};
use std::mem::MaybeUninit;
use std::os::raw::c_char;
Expand All @@ -806,6 +836,8 @@ async fn electrum_request_multi (client: ElectrumClient, request: JsonRpcRequest
let req = try_s! (json::to_string (&request));
let id: i32 = try_s! (request.id.parse());
let mut jres: Option<JsonRpcResponse> = None;
// address of server from which an Rpc response was received
let mut remote_address = JsonRpcRemoteAddr::default();

for connection in client.connections.iter() {
let (tx, rx) = futures::channel::oneshot::channel();
Expand Down Expand Up @@ -834,11 +866,12 @@ async fn electrum_request_multi (client: ElectrumClient, request: JsonRpcRequest
result: res,
error: Json::Null
});
remote_address = JsonRpcRemoteAddr(connection.addr.clone());
// server.ping must be sent to all servers to keep all connections alive
if request.method != "server.ping" {break}
}
let jres = try_s! (jres.ok_or ("!jres"));
Ok (jres)
Ok ((remote_address, jres))
}

impl ElectrumClientImpl {
Expand All @@ -865,13 +898,21 @@ impl Deref for ElectrumClient {type Target = ElectrumClientImpl; fn deref (&self

const BLOCKCHAIN_HEADERS_SUB_ID: &'static str = "blockchain.headers.subscribe";

impl UtxoJsonRpcClientInfo for ElectrumClient {
fn coin_name(&self) -> &str {
self.coin_ticker.as_str()
}
}

impl JsonRpcClient for ElectrumClient {
fn version(&self) -> &'static str { "2.0" }

fn next_id(&self) -> String {
self.next_id.fetch_add(1, AtomicOrdering::Relaxed).to_string()
}

fn client_info(&self) -> String { UtxoJsonRpcClientInfo::client_info(self) }

fn transport(&self, request: JsonRpcRequest) -> JsonRpcResponseFut {
Box::new(electrum_request_multi(self.clone(), request).boxed().compat())
}
Expand Down Expand Up @@ -1063,8 +1104,9 @@ impl UtxoRpcClientOps for ElectrumClient {

#[cfg_attr(test, mockable)]
impl ElectrumClientImpl {
pub fn new() -> ElectrumClientImpl {
pub fn new(coin_ticker: String) -> ElectrumClientImpl {
ElectrumClientImpl {
coin_ticker,
connections: vec![],
next_id: 0.into(),
}
Expand Down Expand Up @@ -1378,7 +1420,7 @@ fn electrum_request(
request: JsonRpcRequest,
tx: mpsc::Sender<Vec<u8>>,
responses: Arc<AsyncMutex<HashMap<String, async_oneshot::Sender<JsonRpcResponse>>>>
) -> JsonRpcResponseFut {
) -> Box<dyn Future<Item=JsonRpcResponse, Error=String> + Send + 'static> {
let send_fut = async move {
let mut json = try_s!(json::to_string(&request));
// Electrum request and responses must end with \n
Expand Down
43 changes: 34 additions & 9 deletions mm2src/coins/utxo/utxo_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ use crate::utxo::rpc_clients::{ElectrumProtocol, ListSinceBlockRes};
use futures::future::join_all;
use mocktopus::mocking::*;
use super::*;
use rpc::v1::types::H256 as H256Json;

const TEST_COIN_NAME: &'static str = "ETOMIC";

fn electrum_client_for_test(servers: &[&str]) -> UtxoRpcClientEnum {
let mut client = ElectrumClientImpl::new();
let mut client = ElectrumClientImpl::new(TEST_COIN_NAME.into());
for server in servers {
client.add_server(&ElectrumRpcRequest {
url: server.to_string(),
Expand Down Expand Up @@ -66,7 +69,7 @@ fn utxo_coin_for_test(rpc_client: UtxoRpcClientEnum, force_seed: Option<&str>) -
p2sh_t_addr_prefix: 0,
pub_addr_prefix: 60,
pub_t_addr_prefix: 0,
ticker: "ETOMIC".into(),
ticker: TEST_COIN_NAME.into(),
wif_prefix: 0,
tx_fee: TxFee::Fixed(1000),
version_group_id: 0x892f2085,
Expand Down Expand Up @@ -246,6 +249,7 @@ fn test_sat_from_big_decimal() {
#[test]
fn test_wait_for_payment_spend_timeout_native() {
let client = NativeClientImpl {
coin_ticker: "RICK".into(),
uri: "http://127.0.0.1:10271".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
};
Expand Down Expand Up @@ -273,7 +277,7 @@ fn test_wait_for_payment_spend_timeout_electrum() {
MockResult::Return(Box::new(futures01::future::ok(None)))
});

let client = ElectrumClientImpl::new();
let client = ElectrumClientImpl::new(TEST_COIN_NAME.into());
let client = UtxoRpcClientEnum::Electrum(ElectrumClient(Arc::new(client)));
let coin = utxo_coin_for_test(client, None);
let transaction = unwrap!(hex::decode("01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000"));
Expand Down Expand Up @@ -337,6 +341,7 @@ fn test_withdraw_impl_set_fixed_fee() {
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -346,7 +351,7 @@ fn test_withdraw_impl_set_fixed_fee() {
let withdraw_req = WithdrawRequest {
amount: 1.into(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: false,
fee: Some(WithdrawFee::UtxoFixed { amount: "0.1".parse().unwrap() }),
};
Expand All @@ -365,6 +370,7 @@ fn test_withdraw_impl_sat_per_kb_fee() {
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -374,7 +380,7 @@ fn test_withdraw_impl_sat_per_kb_fee() {
let withdraw_req = WithdrawRequest {
amount: 1.into(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: false,
fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap() }),
};
Expand All @@ -396,6 +402,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() {
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -405,7 +412,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() {
let withdraw_req = WithdrawRequest {
amount: "9.9789".parse().unwrap(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: false,
fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap() }),
};
Expand All @@ -429,6 +436,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee()
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -438,7 +446,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee()
let withdraw_req = WithdrawRequest {
amount: "9.9789".parse().unwrap(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: false,
fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.09999999".parse().unwrap() }),
};
Expand All @@ -462,6 +470,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() {
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -471,7 +480,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() {
let withdraw_req = WithdrawRequest {
amount: "9.97939455".parse().unwrap(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: false,
fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap() }),
};
Expand All @@ -486,6 +495,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() {
});

let client = NativeClient(Arc::new(NativeClientImpl {
coin_ticker: TEST_COIN_NAME.into(),
uri: "http://127.0.0.1".to_owned(),
auth: fomat!("Basic " (base64_encode("user481805103:pass97a61c8d048bcf468c6c39a314970e557f57afd1d8a5edee917fb29bafb3a43371", URL_SAFE))),
}));
Expand All @@ -495,7 +505,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() {
let withdraw_req = WithdrawRequest {
amount: 0.into(),
to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(),
coin: "ETOMIC".to_string(),
coin: TEST_COIN_NAME.into(),
max: true,
fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap() }),
};
Expand Down Expand Up @@ -601,3 +611,18 @@ fn get_tx_details_coinbase_transaction() {

block_on(fut);
}

#[test]
fn test_electrum_rpc_client_error() {
let client = electrum_client_for_test(&["electrum1.cipig.net:10060"]);

let empty_hash = H256Json::default();
let err = unwrap_err!(client.get_verbose_transaction(empty_hash).wait());

// use the static string instead because the actual error message cannot be obtain
// by serde_json serialization
let expected = r#"JsonRpcError { client_info: "coin: ETOMIC", request: JsonRpcRequest { jsonrpc: "2.0", id: "0", method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({\'code\': -5, \'message\': \'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.\'})")})) }"#;
let actual = format!("{}", err);

assert_eq!(expected, actual);
}
Loading

0 comments on commit 7d10c66

Please sign in to comment.