diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 000000000..07c3987a1 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "grin_api" +version = "0.1.0" +authors = ["Ignotus Peverell "] + +[dependencies] +grin_chain = { path = "../chain" } + +iron = "~0.5.1" +log = "~0.3" +router = "~0.5.1" +serde = "~0.9.10" +serde_derive = "~0.9.10" +serde_json = "~0.9.8" diff --git a/api/rustfmt.toml b/api/rustfmt.toml new file mode 100644 index 000000000..e26e77f1d --- /dev/null +++ b/api/rustfmt.toml @@ -0,0 +1,3 @@ +hard_tabs = true +wrap_comments = true +write_mode = "Overwrite" diff --git a/api/src/endpoints.rs b/api/src/endpoints.rs new file mode 100644 index 000000000..8854e5b62 --- /dev/null +++ b/api/src/endpoints.rs @@ -0,0 +1,65 @@ +// Copyright 2016 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// pub struct HashID(pub [u8; 32]); +// +// impl FromStr for HashId { +// type Err = ; +// +// fn from_str(s: &str) -> Result { +// } +// } + +use std::net::ToSocketAddrs; +use std::sync::Arc; +use std::thread; + +use iron::method::Method; + +use chain::{self, Tip}; +use rest::*; + +/// ApiEndpoint implementation for the blockchain. Exposes the current chain +/// state as a simple JSON object. +#[derive(Clone)] +pub struct ChainApi { + /// data store access + chain_store: Arc, +} + +impl ApiEndpoint for ChainApi { + type ID = String; + type T = Tip; + + fn methods(&self) -> Vec { + vec![Method::Get] + } + + fn get(&self, id: String) -> ApiResult { + self.chain_store.head().map_err(|e| ApiError::Internal(e.to_string())) + } +} + +/// Start all server REST APIs. Just register all of them on a ApiServer +/// instance and runs the corresponding HTTP server. +pub fn start_rest_apis(addr: String, chain_store: Arc) { + + thread::spawn(move || { + let mut apis = ApiServer::new("/v1".to_string()); + apis.register_endpoint("/chain".to_string(), ChainApi { chain_store: chain_store }); + apis.start(&addr[..]).unwrap_or_else(|e| { + error!("Failed to start API HTTP server: {}.", e); + }); + }); +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 000000000..c56e43e98 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,29 @@ +// Copyright 2016 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate grin_chain as chain; + +#[macro_use] +extern crate log; +extern crate iron; +extern crate router; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +mod endpoints; +mod rest; + +pub use endpoints::start_rest_apis; diff --git a/api/src/rest.rs b/api/src/rest.rs new file mode 100644 index 000000000..89b42546e --- /dev/null +++ b/api/src/rest.rs @@ -0,0 +1,275 @@ +// Copyright 2016 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! RESTful API server to easily expose services as RESTful JSON/HTTP endpoints. +//! Fairly constrained on what the service API must look like by design. +//! +//! To use it, just have your service(s) implement the ApiEndpoint trait and +//! register them on a ApiServer. + +use std::error::Error; +use std::fmt::{self, Display, Debug, Formatter}; +use std::io::Read; +use std::net::ToSocketAddrs; +use std::ops::Index; +use std::string::ToString; +use std::str::FromStr; + +use iron::{Iron, Request, Response, IronResult, IronError, status, headers}; +use iron::method::Method; +use iron::modifiers::Header; +use iron::middleware::Handler; +use router::Router; +use serde::{Serialize, Deserialize}; +use serde_json; + +/// Errors that can be returned by an ApiEndpoint implementation. +#[derive(Debug)] +pub enum ApiError { + Internal(String), + Argument(String), +} + +impl Display for ApiError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + ApiError::Argument(ref s) => write!(f, "Bad arguments: {}", s), + ApiError::Internal(ref s) => write!(f, "Internal error: {}", s), + } + } +} + +impl Error for ApiError { + fn description(&self) -> &str { + match *self { + ApiError::Argument(_) => "Bad arguments.", + ApiError::Internal(_) => "Internal error.", + } + } +} + +impl From for IronError { + fn from(e: ApiError) -> IronError { + match e { + ApiError::Argument(_) => IronError::new(e, status::Status::BadRequest), + ApiError::Internal(_) => IronError::new(e, status::Status::InternalServerError), + } + } +} + +pub type ApiResult = ::std::result::Result; + +/// Trait to implement to expose a service as a RESTful HTTP endpoint. Each +/// method corresponds to a specific relative URL and HTTP method following +/// basic REST principles: +/// +/// * create: POST / +/// * get: GET /:id +/// * update: PUT /:id +/// * delete: DELETE /:id +/// +/// The methods method defines which operation the endpoint implements, they're +/// all optional by default. It also allows the framework to automatically +/// define the OPTIONS HTTP method. +/// +/// The type accepted by create and update, and returned by get, must implement +/// the serde Serialize and Deserialize traits. The identifier type returned by +/// create and accepted by all other methods must have a string representation. +pub trait ApiEndpoint: Clone + Send + Sync + 'static { + type ID: ToString + FromStr; + type T: Serialize + Deserialize; + + fn methods(&self) -> Vec; + + #[allow(unused_variables)] + fn create(&self, o: Self::T) -> ApiResult { + unimplemented!() + } + + #[allow(unused_variables)] + fn delete(&self, id: Self::ID) -> ApiResult<()> { + unimplemented!() + } + + #[allow(unused_variables)] + fn update(&self, id: Self::ID, o: Self::T) -> ApiResult<()> { + unimplemented!() + } + + #[allow(unused_variables)] + fn get(&self, id: Self::ID) -> ApiResult { + unimplemented!() + } +} + +// Wrapper required to define the implementation below, Rust doesn't let us +// define the parametric implementation for trait from another crate. +struct ApiWrapper(E); + +impl Handler for ApiWrapper + where E: ApiEndpoint, + <::ID as FromStr>::Err: Debug + Send + Error +{ + fn handle(&self, req: &mut Request) -> IronResult { + match req.method { + Method::Get => { + let res = self.0.get(extract_param(req, "id")?)?; + let res_json = serde_json::to_string(&res) + .map_err(|e| IronError::new(e, status::InternalServerError))?; + Ok(Response::with((status::Ok, res_json))) + } + Method::Put => { + let id = extract_param(req, "id")?; + let t: E::T = serde_json::from_reader(req.body.by_ref()) + .map_err(|e| IronError::new(e, status::BadRequest))?; + self.0.update(id, t)?; + Ok(Response::with(status::NoContent)) + } + Method::Delete => { + let id = extract_param(req, "id")?; + self.0.delete(id)?; + Ok(Response::with(status::NoContent)) + } + Method::Post => { + let t: E::T = serde_json::from_reader(req.body.by_ref()) + .map_err(|e| IronError::new(e, status::BadRequest))?; + let id = self.0.create(t)?; + Ok(Response::with((status::Created, id.to_string()))) + } + _ => Ok(Response::with(status::MethodNotAllowed)), + } + } +} + +fn extract_param(req: &mut Request, param: &'static str) -> IronResult + where ID: ToString + FromStr, + ::Err: Debug + Send + Error + 'static +{ + + let id = req.extensions.get::().unwrap().find(param).unwrap_or(""); + id.parse::().map_err(|e| IronError::new(e, status::BadRequest)) +} + +/// HTTP server allowing the registration of ApiEndpoint implementations. +pub struct ApiServer { + root: String, + router: Router, +} + +impl ApiServer { + /// Creates a new ApiServer that will serve ApiEndpoint implementations + /// under the root URL. + pub fn new(root: String) -> ApiServer { + ApiServer { + root: root, + router: Router::new(), + } + } + + /// Starts the ApiServer at the provided address. + pub fn start(self, addr: A) -> Result<(), String> { + Iron::new(self.router).http(addr).map(|_| ()).map_err(|e| e.to_string()) + } + + /// Register a new API endpoint, providing a relative URL for the new + /// endpoint. + pub fn register_endpoint(&mut self, subpath: String, endpoint: E) + where E: ApiEndpoint, + <::ID as FromStr>::Err: Debug + Send + Error + { + + assert_eq!(subpath.chars().nth(0).unwrap(), '/'); + + // declare a route for each method actually implemented by the endpoint + let route_postfix = &subpath[1..]; + let root = self.root.clone() + &subpath; + for m in endpoint.methods() { + let full_path = match m { + Method::Get => root.clone() + "/:id", + Method::Put => root.clone() + "/:id", + Method::Delete => root.clone() + "/:id", + Method::Post => root.clone(), + _ => panic!(format!("Unsupported method: {}.", m)), + }; + self.router.route(m.clone(), + full_path, + ApiWrapper(endpoint.clone()), + m.to_string() + "_" + route_postfix); + } + + // support for the HTTP Options method by differentiating what's on the + // root resource vs the id resource + let (root_opts, sub_opts) = endpoint.methods().iter().fold((vec![], vec![]), + |mut acc, m| { + if *m == Method::Post { + acc.0.push(m.clone()); + } else { + acc.1.push(m.clone()); + } + acc + }); + self.router.options(root.clone(), + move |_: &mut Request| { + Ok(Response::with((status::Ok, + Header(headers::Allow(root_opts.clone()))))) + }, + "option_".to_string() + route_postfix); + self.router.options(root.clone() + "/:id", + move |_: &mut Request| { + Ok(Response::with((status::Ok, + Header(headers::Allow(sub_opts.clone()))))) + }, + "option_id_".to_string() + route_postfix); + } +} + + +#[cfg(test)] +mod test { + use super::*; + use rest::*; + + #[derive(Serialize, Deserialize)] + pub struct Animal { + name: String, + legs: u32, + lethal: bool, + } + + #[derive(Clone)] + pub struct TestApi; + + impl ApiEndpoint for TestApi { + type ID = String; + type T = Animal; + + fn methods(&self) -> Vec { + vec![Method::Get] + } + + fn get(&self, name: String) -> Result { + Ok(Animal { + name: name, + legs: 4, + lethal: false, + }) + } + } + + #[test] + fn req_chain_json() { + let mut apis = ApiServer::new("/v1".to_string()); + apis.register_endpoint("/animal".to_string(), TestApi); + } +} diff --git a/chain/Cargo.toml b/chain/Cargo.toml index 3d99666cf..021b9de8b 100644 --- a/chain/Cargo.toml +++ b/chain/Cargo.toml @@ -7,6 +7,8 @@ authors = ["Ignotus Peverell "] bitflags = "^0.7.0" byteorder = "^0.5" log = "^0.3" +serde = "~0.9.10" +serde_derive = "~0.9.10" time = "^0.1" grin_core = { path = "../core" } diff --git a/chain/src/lib.rs b/chain/src/lib.rs index 184caf51f..03f966941 100644 --- a/chain/src/lib.rs +++ b/chain/src/lib.rs @@ -25,6 +25,9 @@ extern crate bitflags; extern crate byteorder; #[macro_use] extern crate log; +extern crate serde; +#[macro_use] +extern crate serde_derive; extern crate time; extern crate grin_core as core; diff --git a/chain/src/store.rs b/chain/src/store.rs index 9448590c1..ac80d79e0 100644 --- a/chain/src/store.rs +++ b/chain/src/store.rs @@ -84,8 +84,8 @@ impl ChainStore for ChainKVStore { } fn save_block_header(&self, bh: &BlockHeader) -> Result<(), Error> { - self.db.put_ser( - &to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..], bh) + self.db.put_ser(&to_key(BLOCK_HEADER_PREFIX, &mut bh.hash().to_vec())[..], + bh) } fn get_header_by_height(&self, height: u64) -> Result { diff --git a/chain/src/types.rs b/chain/src/types.rs index 9f7f22cf0..e5e3acd1e 100644 --- a/chain/src/types.rs +++ b/chain/src/types.rs @@ -24,7 +24,7 @@ use core::ser; /// blockchain tree. References the max height and the latest and previous /// blocks /// for convenience and the total difficulty. -#[derive(Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Tip { /// Height of the tip (max height of the fork) pub height: u64, diff --git a/core/Cargo.toml b/core/Cargo.toml index 2ece352d8..bb989ac15 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,6 +8,8 @@ byteorder = "^0.5" num-bigint = "^0.1.35" rust-crypto = "^0.2" rand = "^0.3" +serde = "~0.9.10" +serde_derive = "~0.9.10" time = "^0.1" tiny-keccak = "1.1" diff --git a/core/src/core/hash.rs b/core/src/core/hash.rs index 440c350ee..d5d0a3188 100644 --- a/core/src/core/hash.rs +++ b/core/src/core/hash.rs @@ -27,7 +27,7 @@ pub const ZERO_HASH: Hash = Hash([0; 32]); /// A hash to uniquely (or close enough) identify one of the main blockchain /// constructs. Used pervasively for blocks, transactions and ouputs. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)] pub struct Hash(pub [u8; 32]); impl fmt::Display for Hash { diff --git a/core/src/core/target.rs b/core/src/core/target.rs index 31197e91b..c1d5f2ba5 100644 --- a/core/src/core/target.rs +++ b/core/src/core/target.rs @@ -17,9 +17,12 @@ //! the related difficulty, defined as the maximum target divided by the hash. use byteorder::{ByteOrder, BigEndian}; +use std::fmt; +use std::error::Error; use std::ops::Add; use bigint::BigUint; +use serde::{Serialize, Serializer, Deserialize, Deserializer, de}; use core::hash::Hash; use ser::{self, Reader, Writer, Writeable, Readable}; @@ -78,3 +81,38 @@ impl Readable for Difficulty { Ok(Difficulty { num: BigUint::from_bytes_be(&data[..]) }) } } + +impl Serialize for Difficulty { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(self.num.to_str_radix(10).as_str()) + } +} + +impl Deserialize for Difficulty { + fn deserialize(deserializer: D) -> Result + where D: Deserializer + { + deserializer.deserialize_i32(DiffVisitor) + } +} + +struct DiffVisitor; + +impl de::Visitor for DiffVisitor { + type Value = Difficulty; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a difficulty") + } + + fn visit_str(self, s: &str) -> Result + where E: de::Error + { + let bigui = BigUint::parse_bytes(s.as_bytes(), 10).ok_or_else(|| { + de::Error::invalid_value(de::Unexpected::Str(s), &"a value number") + })?; + Ok(Difficulty { num: bigui }) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index a7f2f3307..31da509ee 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,6 +26,9 @@ extern crate crypto; extern crate num_bigint as bigint; extern crate rand; extern crate secp256k1zkp as secp; +extern crate serde; +#[macro_use] +extern crate serde_derive; extern crate time; extern crate tiny_keccak; diff --git a/grin/Cargo.toml b/grin/Cargo.toml index 299cd267d..737f5a4f0 100644 --- a/grin/Cargo.toml +++ b/grin/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Ignotus Peverell "] [dependencies] +grin_api = { path = "../api" } grin_chain = { path = "../chain" } grin_core = { path = "../core" } grin_store = { path = "../store" } diff --git a/grin/src/lib.rs b/grin/src/lib.rs index 13487115b..184000468 100644 --- a/grin/src/lib.rs +++ b/grin/src/lib.rs @@ -32,6 +32,7 @@ extern crate time; extern crate tokio_core; extern crate tokio_timer; +extern crate grin_api as api; extern crate grin_chain as chain; #[macro_use] extern crate grin_core as core; diff --git a/grin/src/server.rs b/grin/src/server.rs index 7e816e566..70f09f7c8 100644 --- a/grin/src/server.rs +++ b/grin/src/server.rs @@ -24,6 +24,7 @@ use futures::{future, Future}; use tokio_core::reactor; use adapters::{NetToChainAdapter, ChainToNetAdapter}; +use api; use chain; use chain::ChainStore; use core; @@ -69,6 +70,9 @@ pub struct ServerConfig { /// Directory under which the rocksdb stores will be created pub db_root: String, + /// Network address for the Rest API HTTP server. + pub api_http_addr: String, + /// Allows overriding the default cuckoo cycle size pub cuckoo_size: u8, @@ -86,6 +90,7 @@ impl Default for ServerConfig { fn default() -> ServerConfig { ServerConfig { db_root: ".grin".to_string(), + api_http_addr: "127.0.0.1:13415".to_string(), cuckoo_size: 0, capabilities: p2p::FULL_NODE, seeding_type: Seeding::None, @@ -150,6 +155,8 @@ impl Server { evt_handle.spawn(server.start(evt_handle.clone()).map_err(|_| ())); + api::start_rest_apis(config.api_http_addr.clone(), chain_store.clone()); + warn!("Grin server started."); Ok(Server { config: config, diff --git a/p2p/src/conn.rs b/p2p/src/conn.rs index f86e3cc37..119aca7c7 100644 --- a/p2p/src/conn.rs +++ b/p2p/src/conn.rs @@ -13,8 +13,7 @@ // limitations under the License. //! Provides a connection wrapper that handles the lower level tasks in sending -//! or -//! receiving data from the TCP socket, as well as dealing with timeouts. +//! or receiving data from the TCP socket, as well as dealing with timeouts. use std::iter; use std::ops::Deref;