From 29cce2bfb3a8951266e5f129eef5987654240473 Mon Sep 17 00:00:00 2001 From: Olof Blomqvist Date: Thu, 23 May 2024 19:30:21 +0200 Subject: [PATCH] some api experimentation and update topology types --- Cargo.toml | 2 + README.md | 8 +- src/main.rs | 50 ++++---- src/modules/marlowe/mod.rs | 1 - src/modules/marlowe/restapi/mod.rs | 193 +++++++++++++++++++++++++++++ src/modules/marlowe/state.rs | 3 +- src/types.rs | 13 +- 7 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 src/modules/marlowe/restapi/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 962bfd0..103198c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,8 @@ futures = "0.3.28" bytes = "1.5.0" chrono = "0.4.31" rand = "0.8.5" +utoipa = "4.2.0" +utoipa-swagger-ui = "6.0.0" [features] debug = [] diff --git a/README.md b/README.md index 07d1c98..b573779 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,16 @@ This is an experimental indexer for Marlowe contracts on the Cardano blockchain, ```bash # Connect to a local node (defaults to CARDANO_NODE_SOCKET_PATH) - cargo run socket-sync --network=preprod + cargo run -- socket-sync --network=preprod # Connect to a local node with specified path - cargo run socket-sync --network=preprod -- "\\.\pipe\cardano-node-preprod" + cargo run -- socket-sync --network=preprod -- "\\.\pipe\cardano-node-preprod" # Connect to a random remote node - cargo run tcp-sync --network=preprod + cargo run -- tcp-sync --network=preprod # Connect to a specific remote node - cargo run tcp-sync --network=preprod -- 192.168.1.122:3000 + cargo run -- tcp-sync --network=preprod -- 192.168.1.122:3000 ``` 4. Open localhost:8000 in a browser diff --git a/src/main.rs b/src/main.rs index 204847a..5fb085d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,29 +172,35 @@ async fn main() -> Result<()> { runtime.block_on(ccs.run()) }); - // Enable all modules to be reached thru their own gql filters - let routes = - graphiql.or(filters) - .recover(|err: Rejection| async move { - if let Some(GraphQLBadRequest(err)) = err.find() { - return Ok::<_, std::convert::Infallible>(warp::reply::with_status( - err.to_string(), - warp::hyper::StatusCode::BAD_REQUEST, - )); - } - tracing::warn!("Invalid Request: {:?}",err); - Ok(warp::reply::with_status( - "INTERNAL_SERVER_ERROR".to_string(), - warp::hyper::StatusCode::INTERNAL_SERVER_ERROR, - )) - }); - - - tokio::spawn(warp::serve(routes).run(([0, 0, 0, 0], opt.graphql_listen_port))); + + //let rest_api_routes = crate::modules::marlowe::restapi::routes(global_state_arc.clone()); + + // Combine the routes, REST API routes first + let routes = + //rest_api_routes + graphiql .or(filters) + + .recover(|err: Rejection| async move { + if let Some(GraphQLBadRequest(err)) = err.find() { + return Ok::<_, std::convert::Infallible>(warp::reply::with_status( + err.to_string(), + warp::hyper::StatusCode::BAD_REQUEST, + )); + } + tracing::warn!("Invalid Request: {:?}", err); + Ok(warp::reply::with_status( + "INTERNAL_SERVER_ERROR".to_string(), + warp::hyper::StatusCode::INTERNAL_SERVER_ERROR, + )) + }); + + + tokio::spawn( + warp::serve(routes) + .run(([0, 0, 0, 0], opt.graphql_listen_port))); worker_handle.join().expect("Couldn't join on the associated thread"); - tracing::info!("Application stopping gracefully."); @@ -236,8 +242,8 @@ async fn resolve_addr(network_name:&NetworkArg) -> anyhow::Result { let decoded_data : TopologyData = serde_json::de::from_str(&body)?; - if let Some(p) = decoded_data.producers.first() { - Ok(format!("{}:{}",p.addr,p.port)) + if let Some(p) = decoded_data.bootstrap_peers.first() { + Ok(format!("{}:{}",p.address,p.port)) } else { Err(anyhow::Error::msg(format!("Found no producers in the response from {url}"))) } diff --git a/src/modules/marlowe/mod.rs b/src/modules/marlowe/mod.rs index 17b9c63..c0e31db 100644 --- a/src/modules/marlowe/mod.rs +++ b/src/modules/marlowe/mod.rs @@ -4,7 +4,6 @@ use crate::core::lib::slot_to_posix_time_with_specific_magic; use marlowe_lang::semantics::{ContractSemantics, MachineState}; use tokio::sync::mpsc::UnboundedReceiver; use tracing::info; - pub mod state; pub mod worker; pub mod graphql; diff --git a/src/modules/marlowe/restapi/mod.rs b/src/modules/marlowe/restapi/mod.rs new file mode 100644 index 0000000..db951d9 --- /dev/null +++ b/src/modules/marlowe/restapi/mod.rs @@ -0,0 +1,193 @@ +// not sure i even want to have rest endpoints yet - this is just here for experimentation + +use std::{collections::HashMap, sync::Arc}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use warp::{filters::BoxedFilter, reject::Rejection, reply::Reply, Filter}; +use crate::{modules::ModulesStates, state::GlobalState}; + +use utoipa::{ToSchema, OpenApi}; + +#[derive(ToSchema)] +struct Contract { } + + +#[derive(OpenApi)] +#[openapi( + paths( + get_contract_handler, + echo_handler + ), + components(schemas(SearchQuery, Contract)), + tags( + (name = "Marlowe", description = "Operations with Marlowe contracts") + ) +)] +struct ApiDoc; + +#[derive(Serialize, Deserialize)] +struct JsonResponse { + message: String, +} + +#[derive(Deserialize, ToSchema)] +struct SearchQuery { + search: String, + page: Option, +} + +#[derive(Debug)] +struct CustomError { + message: String, +} + +impl warp::reject::Reject for CustomError {} + +// Custom error type for not found contracts +#[derive(Debug)] +struct ContractNotFoundError; + +impl warp::reject::Reject for ContractNotFoundError {} + +async fn handle_rejection(err: warp::reject::Rejection) -> Result { + if let Some(custom_error) = err.find::() { + let json = warp::reply::json(&HashMap::from([("error", custom_error.message.clone())])); + Ok(warp::reply::with_status(json, StatusCode::BAD_REQUEST)) + } else { + // Fallback error response + let json = warp::reply::json(&HashMap::from([("error", format!("{err:?}"))])); + Ok(warp::reply::with_status(json, StatusCode::INTERNAL_SERVER_ERROR)) + } +} + +pub fn routes(state: Arc>) -> BoxedFilter<(impl Reply,)> { + let state_for_hello = state.clone(); + + let hello = warp::path("hello") + .and(warp::get()) + .and(warp::query::()) + .and_then(move |query: SearchQuery| { + let state = state_for_hello.clone(); + async move { + + let data = state.sub_state.marlowe_state + .get_by_shortid_from_mem_cache(query.search).await + .ok_or_else(|| warp::reject::custom(ContractNotFoundError))?; + + let last_tx = data.transitions.last() + .ok_or_else(|| warp::reject::custom(CustomError { message: "No transactions found".to_string() }))?; + + let c = last_tx.datum.as_ref() + .ok_or_else(|| warp::reject::custom(CustomError { message: "No datum found".to_string() }))?; + + let jj = serde_json::to_value(&c) + .map_err(|_| warp::reject::custom(CustomError { message: "Serialization error".to_string() }))?; + + Ok::<_, warp::Rejection>(warp::reply::json(&jj)) + } + }); + + let echo = warp::path("echo") + .and(warp::path::param()) + .and_then(echo_handler); + + let routes = hello.or(echo) + ;//.recover(handle_rejection); + + + let api_doc_route = warp::path("api-doc.json") + .and(warp::get()) + .map(|| warp::reply::json(&ApiDoc::openapi())); + + let config = Arc::new(utoipa_swagger_ui::Config::new(["/api-doc.json","/api-doc1.json", "/api-doc2.json"])); + let swagger_ui = warp::path("swagger-ui") + .and(warp::get()) + .and(warp::path::full()) + .and(warp::path::tail()) + .and(warp::any().map(move || config.clone())) + .and_then(serve_swagger); + + api_doc_route.or(swagger_ui).or(routes).boxed() + +} + +async fn serve_swagger( + full_path: warp::filters::path::FullPath, + tail: warp::filters::path::Tail, + config: Arc>, +) -> Result, Rejection> { + if full_path.as_str() == "/swagger-ui" { + return Ok(Box::new(warp::redirect::found(warp::hyper::http::Uri::from_static( + "/swagger-ui/", + )))); + } + + let path = tail.as_str(); + match utoipa_swagger_ui::serve(path, config) { + Ok(file) => { + if let Some(file) = file { + let mut r = warp::reply::Response::new(file.bytes.into()); + r.headers_mut().append("Content-Type", file.content_type.parse().unwrap()); + Ok(Box::new(r)) + } else { + Ok(Box::new(StatusCode::NOT_FOUND)) + } + } + Err(error) => { + let body : String = error.to_string(); + let mut r = warp::reply::Response::new(body.into()); + *r.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + Ok(Box::new(r)) + }, + } +} + + + +// Annotated hello handler for OpenAPI +#[utoipa::path( + get, + path = "/echo", + responses( + (status = 200, description = "yay"), + (status = 404, description = "boo") + ), + params( + ("param" = String, Query, description = "hmmm") + ) +)] + +async fn echo_handler(param: String) -> Result { + Ok(format!("Echo: {}", param)) +} + +// Annotated hello handler for OpenAPI +#[utoipa::path( + get, + path = "/get_contract", + responses( + (status = 200, description = "Contract data fetched successfully", body = Contract), + (status = 404, description = "Contract not found") + ), + params( + ("search" = String, Query, description = "Search query parameter"), + ("page" = Option, Query, description = "Optional page number") + ) +)] + +async fn get_contract_handler(query: SearchQuery, state: Arc>) -> Result { + let data = state.sub_state.marlowe_state + .get_by_shortid_from_mem_cache(query.search).await + .ok_or_else(|| warp::reject::custom(CustomError { message: "BOO THAT CONTRACT WAS NOT FOUND".to_string() }))?; + + let last_tx = data.transitions.last() + .ok_or_else(|| warp::reject::custom(CustomError { message: "No transactions found".to_string() }))?; + + let c = last_tx.datum.as_ref() + .ok_or_else(|| warp::reject::custom(CustomError { message: "No datum found".to_string() }))?; + + let jj = serde_json::to_value(&c) + .map_err(|_| warp::reject::custom(CustomError { message: "Serialization error".to_string() }))?; + + Ok::<_, warp::Rejection>(warp::reply::json(&jj)) +} \ No newline at end of file diff --git a/src/modules/marlowe/state.rs b/src/modules/marlowe/state.rs index 00582bf..a2d00f6 100644 --- a/src/modules/marlowe/state.rs +++ b/src/modules/marlowe/state.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use crossterm::terminal; use marlowe_lang::plutus_data; use marlowe_lang::plutus_data::{ToPlutusDataDerive, FromPlutusDataDerive}; +use serde::Deserialize; use sled::IVec; use tokio::sync::RwLock; use tracing::debug; @@ -46,7 +47,7 @@ impl Contract { } } -#[derive(Clone,Debug,ToPlutusDataDerive,FromPlutusDataDerive)] +#[derive(Clone,Debug,ToPlutusDataDerive,FromPlutusDataDerive,Deserialize)] pub struct GraphQLHackForu64 { data : String } diff --git a/src/types.rs b/src/types.rs index d15d853..d7539c7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,18 +1,15 @@ #[derive(serde::Deserialize)] pub struct ProducerInfo { - #[serde(alias = "Addr")] - pub addr : String, + #[serde(alias = "address")] + pub address : String, - #[serde(alias = "Port")] + #[serde(alias = "port")] #[allow(dead_code)] pub port : u16, - - #[serde(alias = "Continent")] - #[allow(dead_code)] pub continent : Option } #[derive(serde::Deserialize)] pub struct TopologyData { - #[serde(alias = "Producers")] - pub producers : Vec + #[serde(alias = "bootstrapPeers")] + pub bootstrap_peers : Vec } \ No newline at end of file