diff --git a/Cargo.lock b/Cargo.lock index 6f204eb30..7e1db98f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,11 +475,11 @@ dependencies = [ "native-contracts 0.1.0", "num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "price-info 1.7.0", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.2.0", "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "stats 0.1.0", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", @@ -667,7 +667,7 @@ dependencies = [ "ethcrypto 0.1.0", "ethkey 0.2.0", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "native-contracts 0.1.0", @@ -900,7 +900,7 @@ name = "fetch" version = "0.1.0" dependencies = [ "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -933,10 +933,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "futures-cpupool" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1741,7 +1740,7 @@ dependencies = [ "ethsync 1.8.0", "fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1785,7 +1784,7 @@ dependencies = [ "ethcore-util 1.8.0", "fetch 0.1.0", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "jsonrpc-http-server 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1897,7 +1896,7 @@ dependencies = [ "evm 0.1.0", "fetch 0.1.0", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "jsonrpc-http-server 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "jsonrpc-ipc-server 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", @@ -2131,6 +2130,17 @@ dependencies = [ "difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "price-info" +version = "1.7.0" +dependencies = [ + "ethcore-util 1.7.0", + "fetch 0.1.0", + "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "primal" version = "0.2.3" @@ -3164,7 +3174,7 @@ dependencies = [ "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" "checksum foreign-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e4056b9bd47f8ac5ba12be771f77a0dae796d1bbaaf5fd0b9c2d38b69b8a29d" "checksum futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8e51e7f9c150ba7fd4cee9df8bf6ea3dea5b63b68955ddad19ccd35b71dcfb4d" -"checksum futures-cpupool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bb982bb25cd8fa5da6a8eb3a460354c984ff1113da82bcb4f0b0862b5795db82" +"checksum futures-cpupool 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a283c84501e92cade5ea673a2a7ca44f71f209ccdd302a3e0896f50083d2c5ff" "checksum gcc 0.3.51 (registry+https://github.com/rust-lang/crates.io-index)" = "120d07f202dcc3f72859422563522b66fe6463a4c513df062874daad05f85f0a" "checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685" diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index c629c3aca..71e329201 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -44,11 +44,11 @@ lru-cache = "0.1.0" native-contracts = { path = "native_contracts" } num = "0.1" num_cpus = "1.2" +price-info = { path = "../price-info" } rand = "0.3" rlp = { path = "../util/rlp" } rust-crypto = "0.2.34" rustc-hex = "1.0" -rustc-serialize = "0.3" semver = "0.6" stats = { path = "../util/stats" } time = "0.1" diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index af7922c20..89f9d2e57 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -98,10 +98,10 @@ extern crate lru_cache; extern crate native_contracts; extern crate num_cpus; extern crate num; +extern crate price_info; extern crate rand; extern crate rlp; extern crate rustc_hex; -extern crate rustc_serialize; extern crate semver; extern crate stats; extern crate time; diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index 4b7671946..80971355b 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -33,9 +33,10 @@ use miner::{MinerService, MinerStatus, TransactionQueue, RemovalReason, Transact AccountDetails, TransactionOrigin}; use miner::banning_queue::{BanningTransactionQueue, Threshold}; use miner::work_notify::{WorkPoster, NotifyWork}; -use miner::price_info::PriceInfo; use miner::local_transactions::{Status as LocalTransactionStatus}; use miner::service_transaction_checker::ServiceTransactionChecker; +use price_info::{Client as PriceInfoClient, PriceInfo}; +use price_info::fetch::Client as FetchClient; use header::BlockNumber; /// Different possible definitions for pending transaction set. @@ -154,6 +155,7 @@ pub struct GasPriceCalibratorOptions { pub struct GasPriceCalibrator { options: GasPriceCalibratorOptions, next_calibration: Instant, + price_info: PriceInfoClient, } impl GasPriceCalibrator { @@ -163,7 +165,7 @@ impl GasPriceCalibrator { let usd_per_tx = self.options.usd_per_tx; trace!(target: "miner", "Getting price info"); - PriceInfo::get(move |price: PriceInfo| { + self.price_info.get(move |price: PriceInfo| { trace!(target: "miner", "Price info arrived: {:?}", price); let usd_per_eth = price.ethusd; let wei_per_usd: f32 = 1.0e18 / usd_per_eth; @@ -189,10 +191,11 @@ pub enum GasPricer { impl GasPricer { /// Create a new Calibrated `GasPricer`. - pub fn new_calibrated(options: GasPriceCalibratorOptions) -> GasPricer { + pub fn new_calibrated(options: GasPriceCalibratorOptions, fetch: FetchClient) -> GasPricer { GasPricer::Calibrated(GasPriceCalibrator { options: options, next_calibration: Instant::now(), + price_info: PriceInfoClient::new(fetch), }) } diff --git a/ethcore/src/miner/mod.rs b/ethcore/src/miner/mod.rs index 78c16ab88..1c07f4fab 100644 --- a/ethcore/src/miner/mod.rs +++ b/ethcore/src/miner/mod.rs @@ -45,7 +45,6 @@ mod banning_queue; mod external; mod local_transactions; mod miner; -mod price_info; mod service_transaction_checker; mod transaction_queue; mod work_notify; diff --git a/ethcore/src/miner/price_info.rs b/ethcore/src/miner/price_info.rs deleted file mode 100644 index 29994afb4..000000000 --- a/ethcore/src/miner/price_info.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2015-2017 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -use rustc_serialize::json::Json; -use std::thread; -use std::io::Read; -use std::time::Duration; -use std::str::FromStr; -use std::sync::mpsc; -use hyper::client::{Handler, Request, Response, Client}; -use hyper::{Url, Next, Encoder, Decoder}; -use hyper::net::HttpStream; - -#[derive(Debug)] -pub struct PriceInfo { - pub ethusd: f32, -} - -pub struct SetPriceHandler { - set_price: F, - channel: mpsc::Sender<()>, -} - -impl Drop for SetPriceHandler { - fn drop(&mut self) { - let _ = self.channel.send(()); - } -} - -impl Handler for SetPriceHandler { - fn on_request(&mut self, _: &mut Request) -> Next { Next::read().timeout(Duration::from_secs(3)) } - fn on_request_writable(&mut self, _: &mut Encoder) -> Next { Next::read().timeout(Duration::from_secs(3)) } - fn on_response(&mut self, _: Response) -> Next { Next::read().timeout(Duration::from_secs(3)) } - - fn on_response_readable(&mut self, r: &mut Decoder) -> Next { - let mut body = String::new(); - let info = r.read_to_string(&mut body) - .map_err(|e| format!("Unable to read response: {:?}", e)) - .and_then(|_| self.process_response(&body)); - - if let Err(e) = info { - warn!("Failed to auto-update latest ETH price: {:?}", e); - } - Next::end() - } -} - -impl SetPriceHandler { - fn process_response(&self, body: &str) -> Result<(), String> { - let json = Json::from_str(body).map_err(|e| format!("Invalid JSON returned: {:?}", e))?; - let obj = json.find_path(&["result", "ethusd"]).ok_or("USD price not found".to_owned())?; - let ethusd = match *obj { - Json::String(ref s) => FromStr::from_str(s).ok(), - _ => None, - }.ok_or("Unexpected price format.".to_owned())?; - - (self.set_price)(PriceInfo { - ethusd: ethusd, - }); - Ok(()) - } -} - -impl PriceInfo { - pub fn get(set_price: F) { - thread::spawn(move || { - let url = FromStr::from_str("http://api.etherscan.io/api?module=stats&action=ethprice") - .expect("string known to be a valid URL; qed"); - - if let Err(e) = Self::request(url, set_price) { - warn!("Failed to auto-update latest ETH price: {:?}", e); - } - }); - } - - fn request(url: Url, set_price: F) -> Result<(), String> { - let (tx, rx) = mpsc::channel(); - let client = Client::new().map_err(|e| format!("Unable to start client: {:?}", e))?; - - client.request( - url, - SetPriceHandler { - set_price: set_price, - channel: tx, - }, - ).map_err(|_| "Request failed.".to_owned())?; - - // Wait for exit - let _ = rx.recv().map_err(|e| format!("Request interrupted: {:?}", e))?; - client.close(); - - Ok(()) - } -} - -#[test] #[ignore] -fn should_get_price_info() { - use std::sync::Arc; - use std::time::Duration; - use ethcore_logger::init_log; - use util::{Condvar, Mutex}; - - init_log(); - let done = Arc::new((Mutex::new(PriceInfo { ethusd: 0f32 }), Condvar::new())); - let rdone = done.clone(); - - PriceInfo::get(move |price| { let mut p = rdone.0.lock(); *p = price; rdone.1.notify_one(); }); - let mut p = done.0.lock(); - let t = done.1.wait_for(&mut p, Duration::from_millis(10000)); - assert!(!t.timed_out()); - assert!(p.ethusd != 0f32); -} diff --git a/parity/configuration.rs b/parity/configuration.rs index ee06b6b46..ea6f00a81 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -333,7 +333,7 @@ impl Configuration { let verifier_settings = self.verifier_settings(); // Special presets are present for the dev chain. - let (gas_pricer, miner_options) = match spec { + let (gas_pricer_conf, miner_options) = match spec { SpecType::Dev => (GasPricerConfig::Fixed(0.into()), self.miner_options(0)?), _ => (self.gas_pricer_config()?, self.miner_options(self.args.flag_reseal_min_period)?), }; @@ -356,7 +356,7 @@ impl Configuration { net_conf: net_conf, network_id: network_id, acc_conf: self.accounts_config()?, - gas_pricer: gas_pricer, + gas_pricer_conf: gas_pricer_conf, miner_extras: self.miner_extras()?, stratum: self.stratum_options()?, update_policy: update_policy, @@ -1309,7 +1309,7 @@ mod tests { public_node: false, warp_sync: true, acc_conf: Default::default(), - gas_pricer: Default::default(), + gas_pricer_conf: Default::default(), miner_extras: Default::default(), update_policy: UpdatePolicy { enable_downloading: true, require_consensus: true, filter: UpdateFilter::Critical, track: ReleaseTrack::Unknown, path: default_hypervisor_path() }, mode: Default::default(), @@ -1604,7 +1604,7 @@ mod tests { let conf = parse(&args); match conf.into_command().unwrap().cmd { Cmd::Run(c) => { - assert_eq!(c.gas_pricer, GasPricerConfig::Fixed(0.into())); + assert_eq!(c.gas_pricer_conf, GasPricerConfig::Fixed(0.into())); assert_eq!(c.miner_options.reseal_min_period, Duration::from_millis(0)); }, _ => panic!("Should be Cmd::Run"), diff --git a/parity/main.rs b/parity/main.rs index c35cdb6ad..72579be74 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -366,4 +366,3 @@ fn main() { process::exit(main_direct(can_restart)); } } - diff --git a/parity/params.rs b/parity/params.rs index 40181f0c0..3054db48f 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -22,6 +22,7 @@ use ethcore::spec::Spec; use ethcore::ethereum; use ethcore::client::Mode; use ethcore::miner::{GasPricer, GasPriceCalibratorOptions}; +use hash_fetch::fetch::Client as FetchClient; use user_defaults::UserDefaults; #[derive(Debug, PartialEq)] @@ -226,15 +227,18 @@ impl Default for GasPricerConfig { } } -impl Into for GasPricerConfig { - fn into(self) -> GasPricer { - match self { +impl GasPricerConfig { + pub fn to_gas_pricer(&self, fetch: FetchClient) -> GasPricer { + match *self { GasPricerConfig::Fixed(u) => GasPricer::Fixed(u), GasPricerConfig::Calibrated { usd_per_tx, recalibration_period, .. } => { - GasPricer::new_calibrated(GasPriceCalibratorOptions { - usd_per_tx: usd_per_tx, - recalibration_period: recalibration_period, - }) + GasPricer::new_calibrated( + GasPriceCalibratorOptions { + usd_per_tx: usd_per_tx, + recalibration_period: recalibration_period, + }, + fetch + ) } } } diff --git a/parity/run.rs b/parity/run.rs index 4e7a16376..15e09d74a 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -88,7 +88,7 @@ pub struct RunCmd { pub warp_sync: bool, pub public_node: bool, pub acc_conf: AccountsConfig, - pub gas_pricer: GasPricerConfig, + pub gas_pricer_conf: GasPricerConfig, pub miner_extras: MinerExtras, pub update_policy: UpdatePolicy, pub mode: Option, @@ -480,9 +480,12 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R // prepare account provider let account_provider = Arc::new(prepare_account_provider(&cmd.spec, &cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?); + // fetch service + let fetch = FetchClient::new().map_err(|e| format!("Error starting fetch client: {:?}", e))?; + // create miner - let initial_min_gas_price = cmd.gas_pricer.initial_min(); - let miner = Miner::new(cmd.miner_options, cmd.gas_pricer.into(), &spec, Some(account_provider.clone())); + let initial_min_gas_price = cmd.gas_pricer_conf.initial_min(); + let miner = Miner::new(cmd.miner_options, cmd.gas_pricer_conf.to_gas_pricer(fetch.clone()), &spec, Some(account_provider.clone())); miner.set_author(cmd.miner_extras.author); miner.set_gas_floor_target(cmd.miner_extras.gas_floor_target); miner.set_gas_ceil_target(cmd.miner_extras.gas_ceil_target); @@ -637,9 +640,6 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc) -> R // spin up event loop let event_loop = EventLoop::spawn(); - // fetch service - let fetch = FetchClient::new().map_err(|e| format!("Error starting fetch client: {:?}", e))?; - // the updater service let updater = Updater::new( Arc::downgrade(&(service.client() as Arc)), diff --git a/price-info/Cargo.toml b/price-info/Cargo.toml new file mode 100644 index 000000000..b0226df8e --- /dev/null +++ b/price-info/Cargo.toml @@ -0,0 +1,16 @@ +[package] +description = "Fetch current ETH price" +homepage = "http://parity.io" +license = "GPL-3.0" +name = "price-info" +version = "1.7.0" +authors = ["Parity Technologies "] + +[dependencies] +fetch = { path = "../util/fetch" } +futures = "0.1" +log = "0.3" +serde_json = "1.0" + +[dev-dependencies] +ethcore-util = { path = "../util" } diff --git a/price-info/src/lib.rs b/price-info/src/lib.rs new file mode 100644 index 000000000..ec6fcfb5d --- /dev/null +++ b/price-info/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +#![warn(missing_docs)] + +//! A simple client to get the current ETH price using an external API. + +extern crate futures; +extern crate serde_json; + +#[macro_use] +extern crate log; + +pub extern crate fetch; + +mod price_info; + +pub use price_info::*; diff --git a/price-info/src/price_info.rs b/price-info/src/price_info.rs new file mode 100644 index 000000000..36ca033d2 --- /dev/null +++ b/price-info/src/price_info.rs @@ -0,0 +1,222 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use std::cmp; +use std::fmt; +use std::io; +use std::io::Read; +use std::str::FromStr; + +use fetch; +use fetch::{Client as FetchClient, Fetch}; +use futures::Future; +use serde_json; +use serde_json::Value; + +/// Current ETH price information. +#[derive(Debug)] +pub struct PriceInfo { + /// Current ETH price in USD. + pub ethusd: f32, +} + +/// Price info error. +#[derive(Debug)] +pub enum Error { + /// The API returned an unexpected status code or content. + UnexpectedResponse(&'static str, String), + /// There was an error when trying to reach the API. + Fetch(fetch::Error), + /// IO error when reading API response. + Io(io::Error), +} + +impl From for Error { + fn from(err: io::Error) -> Self { Error::Io(err) } +} + +impl From for Error { + fn from(err: fetch::Error) -> Self { Error::Fetch(err) } +} + +/// A client to get the current ETH price using an external API. +pub struct Client { + api_endpoint: String, + fetch: F, +} + +impl fmt::Debug for Client { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("price_info::Client") + .field("api_endpoint", &self.api_endpoint) + .finish() + } +} + +impl cmp::PartialEq for Client { + fn eq(&self, other: &Client) -> bool { + self.api_endpoint == other.api_endpoint + } +} + +impl Client { + /// Creates a new instance of the `Client` given a `fetch::Client`. + pub fn new(fetch: F) -> Client { + let api_endpoint = "http://api.etherscan.io/api?module=stats&action=ethprice".to_owned(); + Client { api_endpoint, fetch } + } + + /// Gets the current ETH price and calls `set_price` with the result. + pub fn get(&self, set_price: G) { + self.fetch.forget(self.fetch.fetch(&self.api_endpoint) + .map_err(|err| Error::Fetch(err)) + .and_then(move |mut response| { + let mut result = String::new(); + response.read_to_string(&mut result)?; + + if response.is_success() { + let value: Result = serde_json::from_str(&result); + if let Ok(v) = value { + let obj = v.pointer("/result/ethusd").and_then(|obj| { + match *obj { + Value::String(ref s) => FromStr::from_str(s).ok(), + _ => None, + } + }); + + if let Some(ethusd) = obj { + set_price(PriceInfo { ethusd }); + return Ok(()); + } + } + } + + let status = response.status().canonical_reason().unwrap_or("unknown"); + Err(Error::UnexpectedResponse(status, result)) + }) + .map_err(|err| { + warn!("Failed to auto-update latest ETH price: {:?}", err); + err + }) + ); + } +} + +#[cfg(test)] +mod test { + extern crate ethcore_util as util; + + use self::util::Mutex; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + use fetch; + use fetch::Fetch; + use futures; + use futures::future::{Future, FutureResult}; + use price_info::Client; + + #[derive(Clone)] + struct FakeFetch(Option, Arc>); + impl Fetch for FakeFetch { + type Result = FutureResult; + fn new() -> Result where Self: Sized { Ok(FakeFetch(None, Default::default())) } + fn fetch_with_abort(&self, url: &str, _abort: fetch::Abort) -> Self::Result { + assert_eq!(url, "http://api.etherscan.io/api?module=stats&action=ethprice"); + let mut val = self.1.lock(); + *val = *val + 1; + if let Some(ref response) = self.0 { + let data = ::std::io::Cursor::new(response.clone()); + futures::future::ok(fetch::Response::from_reader(data)) + } else { + futures::future::ok(fetch::Response::not_found()) + } + } + + // this guarantees that the calls to price_info::Client::get will block for execution + fn forget(&self, f: F) where + F: Future + Send + 'static, + I: Send + 'static, + E: Send + 'static { + let _ = f.wait(); + } + } + + fn price_info_ok(response: &str) -> Client { + Client::new(FakeFetch(Some(response.to_owned()), Default::default())) + } + + fn price_info_not_found() -> Client { + Client::new(FakeFetch::new().unwrap()) + } + + #[test] + fn should_get_price_info() { + // given + let response = r#"{ + "status": "1", + "message": "OK", + "result": { + "ethbtc": "0.0891", + "ethbtc_timestamp": "1499894236", + "ethusd": "209.55", + "ethusd_timestamp": "1499894229" + } + }"#; + + let price_info = price_info_ok(response); + + // when + price_info.get(|price| { + + // then + assert_eq!(price.ethusd, 209.55); + }); + } + + #[test] + fn should_not_call_set_price_if_response_is_malformed() { + // given + let response = "{}"; + + let price_info = price_info_ok(response); + let b = Arc::new(AtomicBool::new(false)); + + // when + let bb = b.clone(); + price_info.get(move |_| { + bb.store(true, Ordering::Relaxed); + }); + + // then + assert_eq!(b.load(Ordering::Relaxed), false); + } + + #[test] + fn should_not_call_set_price_if_response_is_invalid() { + // given + let price_info = price_info_not_found(); + let b = Arc::new(AtomicBool::new(false)); + + // when + let bb = b.clone(); + price_info.get(move |_| { + bb.store(true, Ordering::Relaxed); + }); + + // then + assert_eq!(b.load(Ordering::Relaxed), false); + } +} diff --git a/util/fetch/src/client.rs b/util/fetch/src/client.rs index 4aa85bd34..64193639a 100644 --- a/util/fetch/src/client.rs +++ b/util/fetch/src/client.rs @@ -61,6 +61,14 @@ pub trait Fetch: Clone + Send + Sync + 'static { f.boxed() } + /// Spawn the future in context of this `Fetch` thread pool as "fire and forget", i.e. dropping this future without + /// canceling the underlying future. + /// Implementation is optional. + fn forget(&self, _: F) where + F: Future + Send + 'static, + I: Send + 'static, + E: Send + 'static {} + /// Fetch URL and get a future for the result. /// Supports aborting the request in the middle of execution. fn fetch_with_abort(&self, url: &str, abort: Abort) -> Self::Result; @@ -149,6 +157,14 @@ impl Fetch for Client { self.pool.spawn(f).boxed() } + fn forget(&self, f: F) where + F: Future + Send + 'static, + I: Send + 'static, + E: Send + 'static, + { + self.pool.spawn(f).forget() + } + fn fetch_with_abort(&self, url: &str, abort: Abort) -> Self::Result { debug!(target: "fetch", "Fetching from: {:?}", url);