// Copyright 2018 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. //! Controller for wallet.. instantiates and handles listeners (or single-run //! invocations) as needed. //! Still experimental use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter}; use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig}; use core::core; use core::core::Transaction; use failure::ResultExt; use futures::future::{err, ok}; use futures::{Future, Stream}; use hyper::{Body, Request, Response, StatusCode}; use keychain::Keychain; use libtx::slate::Slate; use libwallet::api::{APIForeign, APIOwner}; use libwallet::types::{ CbData, NodeClient, OutputData, SendTXArgs, TxLogEntry, WalletBackend, WalletInfo, }; use libwallet::{Error, ErrorKind}; use serde::{Deserialize, Serialize}; use serde_json; use std::collections::HashMap; use std::marker::PhantomData; use std::net::SocketAddr; use std::sync::Arc; use url::form_urlencoded; use util::secp::pedersen; use util::to_base64; use util::Mutex; /// Instantiate wallet Owner API for a single-use (command line) call /// Return a function containing a loaded API context to call pub fn owner_single_use(wallet: Arc>, f: F) -> Result<(), Error> where T: WalletBackend, F: FnOnce(&mut APIOwner) -> Result<(), Error>, C: NodeClient, K: Keychain, { f(&mut APIOwner::new(wallet.clone()))?; Ok(()) } /// Instantiate wallet Foreign API for a single-use (command line) call /// Return a function containing a loaded API context to call pub fn foreign_single_use(wallet: Arc>, f: F) -> Result<(), Error> where T: WalletBackend, F: FnOnce(&mut APIForeign) -> Result<(), Error>, C: NodeClient, K: Keychain, { f(&mut APIForeign::new(wallet.clone()))?; Ok(()) } /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls pub fn owner_listener( wallet: Arc>, addr: &str, api_secret: Option, tls_config: Option, ) -> Result<(), Error> where T: WalletBackend + Send + Sync + 'static, OwnerAPIHandler: Handler, C: NodeClient + 'static, K: Keychain + 'static, { let api_handler = OwnerAPIHandler::new(wallet); let mut router = Router::new(); if api_secret.is_some() { let api_basic_auth = "Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap())); let basic_realm = "Basic realm=GrinOwnerAPI".to_string(); let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(api_basic_auth, basic_realm)); router.add_middleware(basic_auth_middleware); } router .add_route("/v1/wallet/owner/**", Arc::new(api_handler)) .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; let mut apis = ApiServer::new(); info!("Starting HTTP Owner API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let api_thread = apis.start(socket_addr, router, tls_config) .context(ErrorKind::GenericError( "API thread failed to start".to_string(), ))?; api_thread .join() .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) } /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls pub fn foreign_listener( wallet: Arc>, addr: &str, tls_config: Option, ) -> Result<(), Error> where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { let api_handler = ForeignAPIHandler::new(wallet); let mut router = Router::new(); router .add_route("/v1/wallet/foreign/**", Arc::new(api_handler)) .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; let mut apis = ApiServer::new(); info!("Starting HTTP Foreign API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let api_thread = apis.start(socket_addr, router, tls_config) .context(ErrorKind::GenericError( "API thread failed to start".to_string(), ))?; api_thread .join() .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) } type WalletResponseFuture = Box, Error = Error> + Send>; /// API Handler/Wrapper for owner functions pub struct OwnerAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Wallet instance pub wallet: Arc>, phantom: PhantomData, phantom_c: PhantomData, } impl OwnerAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Create a new owner API handler for GET methods pub fn new(wallet: Arc>) -> OwnerAPIHandler { OwnerAPIHandler { wallet, phantom: PhantomData, phantom_c: PhantomData, } } pub fn retrieve_outputs( &self, req: &Request, api: APIOwner, ) -> Result<(bool, Vec<(OutputData, pedersen::Commitment)>), Error> { let mut update_from_node = false; let mut id = None; let mut show_spent = false; let params = parse_params(req); if let Some(_) = params.get("refresh") { update_from_node = true; } if let Some(_) = params.get("show_spent") { show_spent = true; } if let Some(ids) = params.get("tx_id") { if let Some(x) = ids.first() { id = Some(x.parse().unwrap()); } } api.retrieve_outputs(show_spent, update_from_node, id) } pub fn retrieve_txs( &self, req: &Request, api: APIOwner, ) -> Result<(bool, Vec), Error> { let mut tx_id = None; let mut tx_slate_id = None; let mut update_from_node = false; let params = parse_params(req); if let Some(_) = params.get("refresh") { update_from_node = true; } if let Some(ids) = params.get("id") { if let Some(x) = ids.first() { tx_id = Some(x.parse().unwrap()); } } if let Some(tx_slate_ids) = params.get("tx_id") { if let Some(x) = tx_slate_ids.first() { tx_slate_id = Some(x.parse().unwrap()); } } api.retrieve_txs(update_from_node, tx_id, tx_slate_id) } pub fn retrieve_stored_tx( &self, req: &Request, api: APIOwner, ) -> Result<(bool, Option), Error> { let params = parse_params(req); if let Some(id_string) = params.get("id") { match id_string[0].parse() { Ok(id) => match api.retrieve_txs(true, Some(id), None) { Ok((_, txs)) => Ok((txs[0].confirmed, txs[0].get_stored_tx())), Err(e) => { error!("retrieve_stored_tx: failed with error: {}", e); Err(e) } }, Err(e) => { error!("retrieve_stored_tx: could not parse id: {}", e); Err(ErrorKind::TransactionDumpError( "retrieve_stored_tx: cannot dump transaction. Could not parse id in request.", ).into()) } } } else { Err(ErrorKind::TransactionDumpError( "retrieve_stored_tx: Cannot retrieve transaction. Missing id param in request.", ).into()) } } pub fn retrieve_summary_info( &self, req: &Request, mut api: APIOwner, ) -> Result<(bool, WalletInfo), Error> { let mut minimum_confirmations = 1; // TODO - default needed here let params = parse_params(req); let update_from_node = params.get("refresh").is_some(); if let Some(confs) = params.get("minimum_confirmations") { if let Some(x) = confs.first() { minimum_confirmations = x.parse().unwrap(); } } api.retrieve_summary_info(update_from_node, minimum_confirmations) } pub fn node_height( &self, _req: &Request, mut api: APIOwner, ) -> Result<(u64, bool), Error> { api.node_height() } fn handle_get_request(&self, req: &Request) -> Result, Error> { let api = APIOwner::new(self.wallet.clone()); Ok(match req .uri() .path() .trim_right_matches("/") .rsplit("/") .next() .unwrap() { "retrieve_outputs" => json_response(&self.retrieve_outputs(req, api)?), "retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?), "node_height" => json_response(&self.node_height(req, api)?), "retrieve_txs" => json_response(&self.retrieve_txs(req, api)?), "retrieve_stored_tx" => json_response(&self.retrieve_stored_tx(req, api)?), _ => response(StatusCode::BAD_REQUEST, ""), }) } pub fn issue_send_tx( &self, req: Request, mut api: APIOwner, ) -> Box + Send> { Box::new(parse_body(req).and_then(move |args: SendTXArgs| { let result = api.initiate_tx( None, args.amount, args.minimum_confirmations, args.max_outputs, args.num_change_outputs, args.selection_strategy_is_use_all, args.message, ); let (mut slate, lock_fn) = match result { Ok(s) => { info!( "Tx created: {} grin to {} (strategy '{}')", core::amount_to_hr_string(args.amount, false), &args.dest, args.selection_strategy_is_use_all, ); s } Err(e) => { error!("Tx not created: {}", e); match e.kind() { // user errors, don't backtrace ErrorKind::NotEnoughFunds { .. } => {} ErrorKind::FeeDispute { .. } => {} ErrorKind::FeeExceedsAmount { .. } => {} _ => { // otherwise give full dump error!("Backtrace: {}", e.backtrace().unwrap()); } }; return Err(e); } }; if args.method == "http" { let adapter = HTTPWalletCommAdapter::new(); slate = adapter.send_tx_sync(&args.dest, &slate)?; api.tx_lock_outputs(&slate, lock_fn)?; api.finalize_tx(&mut slate)?; } else if args.method == "file" { let adapter = FileWalletCommAdapter::new(); adapter.send_tx_async(&args.dest, &slate)?; api.tx_lock_outputs(&slate, lock_fn)?; } else { error!("unsupported payment method: {}", args.method); return Err(ErrorKind::ClientCallback("unsupported payment method"))?; } Ok(slate) })) } pub fn finalize_tx( &self, req: Request, mut api: APIOwner, ) -> Box + Send> { Box::new( parse_body(req).and_then(move |mut slate| match api.finalize_tx(&mut slate) { Ok(_) => ok(slate.clone()), Err(e) => { error!("finalize_tx: failed with error: {}", e); err(e) } }), ) } pub fn cancel_tx( &self, req: Request, mut api: APIOwner, ) -> Box + Send> { let params = parse_params(&req); if let Some(id_string) = params.get("id") { Box::new(match id_string[0].parse() { Ok(id) => match api.cancel_tx(Some(id), None) { Ok(_) => ok(()), Err(e) => { error!("cancel_tx: failed with error: {}", e); err(e) } }, Err(e) => { error!("cancel_tx: could not parse id: {}", e); err(ErrorKind::TransactionCancellationError( "cancel_tx: cannot cancel transaction. Could not parse id in request.", ).into()) } }) } else if let Some(tx_id_string) = params.get("tx_id") { Box::new(match tx_id_string[0].parse() { Ok(tx_id) => match api.cancel_tx(None, Some(tx_id)) { Ok(_) => ok(()), Err(e) => { error!("cancel_tx: failed with error: {}", e); err(e) } }, Err(e) => { error!("cancel_tx: could not parse tx_id: {}", e); err(ErrorKind::TransactionCancellationError( "cancel_tx: cannot cancel transaction. Could not parse tx_id in request.", ).into()) } }) } else { Box::new(err(ErrorKind::TransactionCancellationError( "cancel_tx: Cannot cancel transaction. Missing id or tx_id param in request.", ).into())) } } pub fn post_tx( &self, req: Request, api: APIOwner, ) -> Box + Send> { let params = match req.uri().query() { Some(query_string) => form_urlencoded::parse(query_string.as_bytes()) .into_owned() .fold(HashMap::new(), |mut hm, (k, v)| { hm.entry(k).or_insert(vec![]).push(v); hm }), None => HashMap::new(), }; let fluff = params.get("fluff").is_some(); Box::new( parse_body(req).and_then(move |slate| match api.post_tx(&slate, fluff) { Ok(_) => ok(()), Err(e) => { error!("post_tx: failed with error: {}", e); err(e) } }), ) } fn handle_post_request(&self, req: Request) -> WalletResponseFuture { let api = APIOwner::new(self.wallet.clone()); match req .uri() .path() .trim_right_matches("/") .rsplit("/") .next() .unwrap() { "issue_send_tx" => Box::new( self.issue_send_tx(req, api) .and_then(|slate| ok(json_response_pretty(&slate))), ), "finalize_tx" => Box::new( self.finalize_tx(req, api) .and_then(|slate| ok(json_response_pretty(&slate))), ), "cancel_tx" => Box::new( self.cancel_tx(req, api) .and_then(|_| ok(response(StatusCode::OK, ""))), ), "post_tx" => Box::new( self.post_tx(req, api) .and_then(|_| ok(response(StatusCode::OK, ""))), ), _ => Box::new(err(ErrorKind::GenericError( "Unknown error handling post request".to_owned(), ).into())), } } } impl Handler for OwnerAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { fn get(&self, req: Request) -> ResponseFuture { match self.handle_get_request(&req) { Ok(r) => Box::new(ok(r)), Err(e) => { error!("Request Error: {:?}", e); Box::new(ok(create_error_response(e))) } } } fn post(&self, req: Request) -> ResponseFuture { Box::new( self.handle_post_request(req) .and_then(|r| ok(r)) .or_else(|e| { error!("Request Error: {:?}", e); ok(create_error_response(e)) }), ) } fn options(&self, _req: Request) -> ResponseFuture { Box::new(ok(create_ok_response("{}"))) } } /// API Handler/Wrapper for foreign functions pub struct ForeignAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Wallet instance pub wallet: Arc>, phantom: PhantomData, phantom_c: PhantomData, } impl ForeignAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// create a new api handler pub fn new(wallet: Arc>) -> ForeignAPIHandler { ForeignAPIHandler { wallet, phantom: PhantomData, phantom_c: PhantomData, } } fn build_coinbase( &self, req: Request, mut api: APIForeign, ) -> Box + Send> { Box::new(parse_body(req).and_then(move |block_fees| api.build_coinbase(&block_fees))) } fn receive_tx( &self, req: Request, mut api: APIForeign, ) -> Box + Send> { Box::new(parse_body(req).and_then( //TODO: No way to insert a message from the params move |mut slate| match api.receive_tx(&mut slate, None, None) { Ok(_) => ok(slate.clone()), Err(e) => { error!("receive_tx: failed with error: {}", e); err(e) } }, )) } fn handle_request(&self, req: Request) -> WalletResponseFuture { let api = *APIForeign::new(self.wallet.clone()); match req .uri() .path() .trim_right_matches("/") .rsplit("/") .next() .unwrap() { "build_coinbase" => Box::new( self.build_coinbase(req, api) .and_then(|res| ok(json_response(&res))), ), "receive_tx" => Box::new( self.receive_tx(req, api) .and_then(|res| ok(json_response(&res))), ), _ => Box::new(ok(response(StatusCode::BAD_REQUEST, "unknown action"))), } } } impl Handler for ForeignAPIHandler where T: WalletBackend + Send + Sync + 'static, C: NodeClient + Send + Sync + 'static, K: Keychain + 'static, { fn post(&self, req: Request) -> ResponseFuture { Box::new(self.handle_request(req).and_then(|r| ok(r)).or_else(|e| { error!("Request Error: {:?}", e); ok(create_error_response(e)) })) } } // Utility to serialize a struct into JSON and produce a sensible Response // out of it. fn json_response(s: &T) -> Response where T: Serialize, { match serde_json::to_string(s) { Ok(json) => response(StatusCode::OK, json), Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), } } // pretty-printed version of above fn json_response_pretty(s: &T) -> Response where T: Serialize, { match serde_json::to_string_pretty(s) { Ok(json) => response(StatusCode::OK, json), Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), } } fn create_error_response(e: Error) -> Response { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("access-control-allow-origin", "*") .header("access-control-allow-headers", "Content-Type") .body(format!("{}", e).into()) .unwrap() } fn create_ok_response(json: &str) -> Response { Response::builder() .status(StatusCode::OK) .header("access-control-allow-origin", "*") .body(json.to_string().into()) .unwrap() } fn response>(status: StatusCode, text: T) -> Response { Response::builder() .status(status) .header("access-control-allow-origin", "*") .body(text.into()) .unwrap() } fn parse_params(req: &Request) -> HashMap> { match req.uri().query() { Some(query_string) => form_urlencoded::parse(query_string.as_bytes()) .into_owned() .fold(HashMap::new(), |mut hm, (k, v)| { hm.entry(k).or_insert(vec![]).push(v); hm }), None => HashMap::new(), } } fn parse_body(req: Request) -> Box + Send> where for<'de> T: Deserialize<'de> + Send + 'static, { Box::new( req.into_body() .concat2() .map_err(|_| ErrorKind::GenericError("Failed to read request".to_owned()).into()) .and_then(|body| match serde_json::from_reader(&body.to_vec()[..]) { Ok(obj) => ok(obj), Err(e) => { err(ErrorKind::GenericError(format!("Invalid request body: {}", e)).into()) } }), ) }