From 61e29ac16600d7dbf0d2de816c03af11129a0665 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Thu, 27 Jul 2023 11:29:56 -0700 Subject: [PATCH] [hermes] add utoipa for API docs (#990) * [hermes] add utoipa for docs * fix build * format --- hermes/Cargo.lock | 158 ++++++++++++++++++++++++++++++++++++++++ hermes/Cargo.toml | 2 + hermes/build.rs | 23 +++++- hermes/src/api.rs | 25 +++++++ hermes/src/api/rest.rs | 20 ++++- hermes/src/api/types.rs | 15 +++- 6 files changed, 238 insertions(+), 5 deletions(-) diff --git a/hermes/Cargo.lock b/hermes/Cargo.lock index cec1f59f..2c307403 100644 --- a/hermes/Cargo.lock +++ b/hermes/Cargo.lock @@ -1173,6 +1173,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1183,6 +1192,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1781,6 +1801,8 @@ dependencies = [ "strum", "tokio", "tower-http", + "utoipa", + "utoipa-swagger-ui", "wormhole-sdk", ] @@ -2070,6 +2092,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", + "serde", ] [[package]] @@ -2969,6 +2992,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4252,6 +4285,41 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.31", + "rust-embed-utils", + "shellexpand", + "syn 2.0.26", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2 0.10.7", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -4414,6 +4482,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.22" @@ -4727,6 +4804,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -6057,6 +6143,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -6157,6 +6252,47 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utoipa" +version = "3.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c624186f22e625eb8faa777cb33d34cd595aa16d1742aa1d8b6cf35d3e4dda9" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ce5f21ca77e010f5283fa791c6ab892c68b3668a1bdc6b7ac6cf978f5d5b30" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.66", + "quote 1.0.31", + "regex", + "syn 2.0.26", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4602d7100d3cfd8a086f30494e68532402ab662fa366c9d201d677e33cee138d" +dependencies = [ + "axum", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "utoipa", + "zip", +] + [[package]] name = "value-bag" version = "1.4.1" @@ -6193,6 +6329,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -6669,6 +6815,18 @@ dependencies = [ "syn 2.0.26", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/hermes/Cargo.toml b/hermes/Cargo.toml index b1a13d07..1d14b7d2 100644 --- a/hermes/Cargo.toml +++ b/hermes/Cargo.toml @@ -59,6 +59,8 @@ structopt = { version = "0.3.26" } strum = { version = "0.24.1", features = ["derive"] } tokio = { version = "1.26.0", features = ["full"] } tower-http = { version = "0.4.0", features = ["cors"] } +utoipa = { version = "3.4.0", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "3.1.4", features = ["axum"] } wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" } [patch.crates-io] diff --git a/hermes/build.rs b/hermes/build.rs index d582733a..00afb273 100644 --- a/hermes/build.rs +++ b/hermes/build.rs @@ -75,6 +75,8 @@ fn main() { .output() .expect("failed to generate protobuf definitions"); + let rust_target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + // Build the Go library. let mut cmd = Command::new("go"); cmd.arg("build") @@ -84,11 +86,28 @@ fn main() { .arg("src/network/p2p.go") .arg("src/network/p2p.pb.go"); + // Cross-compile the Go binary based on the Rust target architecture + match &*rust_target_arch { + "x86_64" => { + // CGO_ENABLED required for building amd64 on mac os + cmd.env("GOARCH", "amd64").env("CGO_ENABLED", "1"); + } + "aarch64" => { + cmd.env("GOARCH", "arm64"); + } + // Add other target architectures as needed. + _ => {} + } + + // Tell Rust to link our Go library at compile time. println!("cargo:rustc-link-search=native={out_var}"); println!("cargo:rustc-link-lib=static=pythnet"); println!("cargo:rustc-link-lib=resolv"); - let status = cmd.status().unwrap(); - assert!(status.success()); + let go_build_output = cmd.output().expect("Failed to execute Go build command"); + if !go_build_output.status.success() { + let error_message = String::from_utf8_lossy(&go_build_output.stderr); + panic!("Go build failed:\n{}", error_message); + } } diff --git a/hermes/src/api.rs b/hermes/src/api.rs index c6758efe..728373c1 100644 --- a/hermes/src/api.rs +++ b/hermes/src/api.rs @@ -12,6 +12,16 @@ use { sync::mpsc::Receiver, }, tower_http::cors::CorsLayer, + utoipa::{ + openapi::security::{ + ApiKey, + ApiKeyValue, + SecurityScheme, + }, + Modify, + OpenApi, + }, + utoipa_swagger_ui::SwaggerUi, }; mod rest; @@ -38,12 +48,27 @@ impl State { /// Currently this is based on Axum due to the simplicity and strong ecosystem support for the /// packages they are based on (tokio & hyper). pub async fn run(store: Arc, mut update_rx: Receiver<()>, rpc_addr: String) -> Result<()> { + #[derive(OpenApi)] + #[openapi( + paths( + rest::latest_price_feeds, + ), + components( + schemas(types::RpcPriceFeedMetadata, types::RpcPriceFeed) + ), + tags( + (name = "hermes", description = "Pyth Real-Time Pricing API") + ) + )] + struct ApiDoc; + let state = State::new(store); // Initialize Axum Router. Note the type here is a `Router` due to the use of the // `with_state` method which replaces `Body` with `State` in the type signature. let app = Router::new(); let app = app + .merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi())) .route("/", get(rest::index)) .route("/live", get(rest::live)) .route("/ready", get(rest::ready)) diff --git a/hermes/src/api/rest.rs b/hermes/src/api/rest.rs index c0f22f79..a8f3080d 100644 --- a/hermes/src/api/rest.rs +++ b/hermes/src/api/rest.rs @@ -31,6 +31,7 @@ use { pyth_sdk::PriceIdentifier, serde_qs::axum::QsQuery, std::collections::HashSet, + utoipa::IntoParams, }; pub enum RestError { @@ -95,15 +96,32 @@ pub async fn latest_vaas( )) } -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, serde::Deserialize, IntoParams)] pub struct LatestPriceFeedsQueryParams { + #[param(value_type = String)] ids: Vec, #[serde(default)] + #[param(value_type = Option, required = false, nullable = true)] verbose: bool, #[serde(default)] + #[param(value_type = Option, required = false, nullable = true)] binary: bool, } +/// Get the latest prices by price feed ids. +/// +/// Get the latest price updates for a provided collection of price feed ids. +/// +#[utoipa::path( + get, + path = "/api/latest_price_feeds", + responses( + (status = 200, description = "Price feeds retrieved successfully", body = [Vec]) + ), + params( + LatestPriceFeedsQueryParams + ) +)] pub async fn latest_price_feeds( State(state): State, QsQuery(params): QsQuery, diff --git a/hermes/src/api/types.rs b/hermes/src/api/types.rs index 4371452a..d9b84f7f 100644 --- a/hermes/src/api/types.rs +++ b/hermes/src/api/types.rs @@ -19,6 +19,14 @@ use { Price, PriceIdentifier, }, + utoipa::{ + openapi::{ + RefOr, + Schema, + }, + IntoParams, + ToSchema, + }, wormhole_sdk::Chain, }; @@ -40,14 +48,16 @@ impl From for PriceIdentifier { type Base64String = String; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RpcPriceFeedMetadata { + #[schema(value_type = u64)] pub slot: Slot, pub emitter_chain: u16, + #[schema(value_type = i64)] pub price_service_receive_time: UnixTimestamp, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct RpcPriceFeed { pub id: PriceIdentifier, pub price: Price, @@ -56,6 +66,7 @@ pub struct RpcPriceFeed { pub metadata: Option, /// Vaa binary represented in base64. #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = Option)] pub vaa: Option, }