Refactor price_info (#6003)
* refactor PriceInfo to use Fetch and reuse the client * forget Fetch future to keep it running in the background * update Debug message for price_info::Client * wrap underlying errors in price_info client * use debug_struct in price_info client debug implementation * use global fetch service in price_info client * rename gas_pricer parameter in RunCmd * move price_info to its own crate * fix price_info tests * replace rustc_serialize with serde_json in price_info * add documentation for price_info * remove unused rustc-serialize dependency from ethcore * fix price_info formatting * re-export fetch crate in price_info * remove unused cfg attributes in price_info * add tests for price_info
This commit is contained in:
		
							parent
							
								
									fec4ccbbb8
								
							
						
					
					
						commit
						c7af702270
					
				
							
								
								
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -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" | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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), | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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 <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| 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<F> { | ||||
| 	set_price: F, | ||||
| 	channel: mpsc::Sender<()>, | ||||
| } | ||||
| 
 | ||||
| impl<F> Drop for SetPriceHandler<F> { | ||||
| 	fn drop(&mut self) { | ||||
| 		let _ = self.channel.send(()); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl<F: Fn(PriceInfo) + Sync + Send + 'static> Handler<HttpStream> for SetPriceHandler<F> { | ||||
| 	fn on_request(&mut self, _: &mut Request) -> Next { Next::read().timeout(Duration::from_secs(3)) } | ||||
| 	fn on_request_writable(&mut self, _: &mut Encoder<HttpStream>) -> 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<HttpStream>) -> 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<F: Fn(PriceInfo) + Sync + Send + 'static> SetPriceHandler<F> { | ||||
| 	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<F: Fn(PriceInfo) + Sync + Send + 'static>(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<F: Fn(PriceInfo) + Send + Sync + 'static>(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); | ||||
| } | ||||
| @ -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"), | ||||
|  | ||||
| @ -366,4 +366,3 @@ fn main() { | ||||
| 		process::exit(main_direct(can_restart)); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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<GasPricer> 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 | ||||
| 				) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @ -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<Mode>, | ||||
| @ -480,9 +480,12 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> 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<RotatingLogger>) -> 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<BlockChainClient>)), | ||||
|  | ||||
							
								
								
									
										16
									
								
								price-info/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								price-info/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -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 <admin@parity.io>"] | ||||
| 
 | ||||
| [dependencies] | ||||
| fetch = { path = "../util/fetch" } | ||||
| futures = "0.1" | ||||
| log = "0.3" | ||||
| serde_json = "1.0" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| ethcore-util = { path = "../util" } | ||||
							
								
								
									
										31
									
								
								price-info/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								price-info/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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 <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| #![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::*; | ||||
							
								
								
									
										222
									
								
								price-info/src/price_info.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								price-info/src/price_info.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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 <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| 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<io::Error> for Error { | ||||
| 	fn from(err: io::Error) -> Self { Error::Io(err) } | ||||
| } | ||||
| 
 | ||||
| impl From<fetch::Error> 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<F = FetchClient> { | ||||
| 	api_endpoint: String, | ||||
| 	fetch: F, | ||||
| } | ||||
| 
 | ||||
| impl<F> fmt::Debug for Client<F> { | ||||
| 	fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||||
| 		fmt.debug_struct("price_info::Client") | ||||
| 		   .field("api_endpoint", &self.api_endpoint) | ||||
| 		   .finish() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl<F> cmp::PartialEq for Client<F> { | ||||
| 	fn eq(&self, other: &Client<F>) -> bool { | ||||
| 		self.api_endpoint == other.api_endpoint | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| impl<F: Fetch> Client<F> { | ||||
| 	/// Creates a new instance of the `Client` given a `fetch::Client`.
 | ||||
| 	pub fn new(fetch: F) -> Client<F> { | ||||
| 		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<G: Fn(PriceInfo) + Sync + Send + 'static>(&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<Value, _> = 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<String>, Arc<Mutex<u64>>); | ||||
| 	impl Fetch for FakeFetch { | ||||
| 		type Result = FutureResult<fetch::Response, fetch::Error>; | ||||
| 		fn new() -> Result<Self, fetch::Error> 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<F, I, E>(&self, f: F) where | ||||
| 			F: Future<Item=I, Error=E> + Send + 'static, | ||||
| 			I: Send + 'static, | ||||
| 			E: Send + 'static { | ||||
| 			let _ = f.wait(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fn price_info_ok(response: &str) -> Client<FakeFetch> { | ||||
| 		Client::new(FakeFetch(Some(response.to_owned()), Default::default())) | ||||
| 	} | ||||
| 
 | ||||
| 	fn price_info_not_found() -> Client<FakeFetch> { | ||||
| 		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); | ||||
| 	} | ||||
| } | ||||
| @ -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<F, I, E>(&self, _: F) where | ||||
| 		F: Future<Item=I, Error=E> + 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<F, I, E>(&self, f: F) where | ||||
| 		F: Future<Item=I, Error=E> + 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); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user