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);