Skip to content

Commit

Permalink
some api experimentation and update topology types
Browse files Browse the repository at this point in the history
  • Loading branch information
OlofBlomqvist committed May 23, 2024
1 parent a65a9f6 commit 29cce2b
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 36 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 28 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");

Expand Down Expand Up @@ -236,8 +242,8 @@ async fn resolve_addr(network_name:&NetworkArg) -> anyhow::Result<String> {

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}")))
}
Expand Down
1 change: 0 additions & 1 deletion src/modules/marlowe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
193 changes: 193 additions & 0 deletions src/modules/marlowe/restapi/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u32>,
}

#[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<impl Reply, std::convert::Infallible> {
if let Some(custom_error) = err.find::<CustomError>() {
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<GlobalState<ModulesStates>>) -> BoxedFilter<(impl Reply,)> {
let state_for_hello = state.clone();

let hello = warp::path("hello")
.and(warp::get())
.and(warp::query::<SearchQuery>())
.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<utoipa_swagger_ui::Config<'static>>,
) -> Result<Box<dyn Reply + 'static>, 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<impl Reply, warp::reject::Rejection> {
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<u32>, Query, description = "Optional page number")
)
)]

async fn get_contract_handler(query: SearchQuery, state: Arc<GlobalState<ModulesStates>>) -> Result<impl Reply, warp::reject::Rejection> {
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))
}
3 changes: 2 additions & 1 deletion src/modules/marlowe/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,7 +47,7 @@ impl Contract {
}
}

#[derive(Clone,Debug,ToPlutusDataDerive,FromPlutusDataDerive)]
#[derive(Clone,Debug,ToPlutusDataDerive,FromPlutusDataDerive,Deserialize)]
pub struct GraphQLHackForu64 {
data : String
}
Expand Down
13 changes: 5 additions & 8 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -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<String>
}

#[derive(serde::Deserialize)]
pub struct TopologyData {
#[serde(alias = "Producers")]
pub producers : Vec<ProducerInfo>
#[serde(alias = "bootstrapPeers")]
pub bootstrap_peers : Vec<ProducerInfo>
}

0 comments on commit 29cce2b

Please sign in to comment.