From 71ce45698b4580b97e90d1d20cad8c4493e2b799 Mon Sep 17 00:00:00 2001 From: Reisen Date: Mon, 21 Aug 2023 11:49:39 +0000 Subject: [PATCH] refactor(hermes): move rpc endpoints into submodules --- hermes/src/api/rest.rs | 412 ++-------------------- hermes/src/api/rest/get_price_feed.rs | 86 +++++ hermes/src/api/rest/get_vaa.rs | 98 +++++ hermes/src/api/rest/get_vaa_ccip.rs | 85 +++++ hermes/src/api/rest/index.rs | 20 ++ hermes/src/api/rest/latest_price_feeds.rs | 81 +++++ hermes/src/api/rest/latest_vaas.rs | 75 ++++ hermes/src/api/rest/live.rs | 11 + hermes/src/api/rest/price_feed_ids.rs | 36 ++ hermes/src/api/rest/ready.rs | 15 + 10 files changed, 540 insertions(+), 379 deletions(-) create mode 100644 hermes/src/api/rest/get_price_feed.rs create mode 100644 hermes/src/api/rest/get_vaa.rs create mode 100644 hermes/src/api/rest/get_vaa_ccip.rs create mode 100644 hermes/src/api/rest/index.rs create mode 100644 hermes/src/api/rest/latest_price_feeds.rs create mode 100644 hermes/src/api/rest/latest_vaas.rs create mode 100644 hermes/src/api/rest/live.rs create mode 100644 hermes/src/api/rest/price_feed_ids.rs create mode 100644 hermes/src/api/rest/ready.rs diff --git a/hermes/src/api/rest.rs b/hermes/src/api/rest.rs index 214d5fad..69ce3691 100644 --- a/hermes/src/api/rest.rs +++ b/hermes/src/api/rest.rs @@ -1,43 +1,33 @@ -use { - super::types::{ - PriceIdInput, - RpcPriceFeed, - RpcPriceIdentifier, - }, - crate::{ - doc_examples, - impl_deserialize_for_hex_string_wrapper, - store::types::{ - RequestTime, - UnixTimestamp, - }, - }, - anyhow::Result, - axum::{ - extract::State, - http::StatusCode, - response::{ - IntoResponse, - Response, - }, - Json, - }, - base64::{ - engine::general_purpose::STANDARD as base64_standard_engine, - Engine as _, - }, - derive_more::{ - Deref, - DerefMut, - }, - pyth_sdk::PriceIdentifier, - serde_qs::axum::QsQuery, - utoipa::{ - IntoParams, - ToSchema, +use axum::{ + http::StatusCode, + response::{ + IntoResponse, + Response, }, }; +mod get_price_feed; +mod get_vaa; +mod get_vaa_ccip; +mod index; +mod latest_price_feeds; +mod latest_vaas; +mod live; +mod price_feed_ids; +mod ready; + +pub use { + get_price_feed::*, + get_vaa::*, + get_vaa_ccip::*, + index::*, + latest_price_feeds::*, + latest_vaas::*, + live::*, + price_feed_ids::*, + ready::*, +}; + pub enum RestError { UpdateDataNotFound, CcipUpdateDataNotFound, @@ -51,13 +41,13 @@ impl IntoResponse for RestError { (StatusCode::NOT_FOUND, "Update data not found").into_response() } RestError::CcipUpdateDataNotFound => { - // Returning Bad Gateway error because CCIP expects a 5xx error if it needs to - // retry or try other endpoints. Bad Gateway seems the best choice here as this - // is not an internal error and could happen on two scenarios: + // Return "Bad Gateway" error because CCIP expects a 5xx error if it needs to retry + // or try other endpoints. "Bad Gateway" seems the best choice here as this is not + // an internal error and could happen on two scenarios: + // // 1. DB Api is not responding well (Bad Gateway is appropriate here) - // 2. Publish time is a few seconds before current time and a VAA - // Will be available in a few seconds. So we want the client to retry. - + // 2. Publish time is a few seconds before current time and a VAA Will be available + // in a few seconds. So we want the client to retry. (StatusCode::BAD_GATEWAY, "CCIP update data not found").into_response() } RestError::InvalidCCIPInput => { @@ -66,339 +56,3 @@ impl IntoResponse for RestError { } } } - -/// Get the set of price feed ids. -/// -/// Get all of the price feed ids for which price updates can be retrieved. -#[utoipa::path( - get, - path = "/api/price_feed_ids", - responses( - (status = 200, description = "Price feed ids retrieved successfully", body = Vec) - ), - params() -)] -pub async fn price_feed_ids( - State(state): State, -) -> Result>, RestError> { - let price_feed_ids = state - .store - .get_price_feed_ids() - .await - .iter() - .map(|id| RpcPriceIdentifier::from(&id)) - .collect(); - Ok(Json(price_feed_ids)) -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] -pub struct LatestVaasQueryParams { - /// Get the VAAs for these price feed ids. - /// Provide this parameter multiple times to retrieve multiple price updates, - /// ids[]=a12...&ids[]=b4c... - #[param( - rename = "ids[]", - example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" - )] - ids: Vec, -} - -/// Get VAAs for a set of price feed ids. -/// -/// Given a collection of price feed ids, retrieve the latest VAA for them. The returned -/// VAA(s) can be submitted to the Pyth contract to update the on-chain price -#[utoipa::path( - get, - path = "/api/latest_vaas", - responses( - (status = 200, description = "VAAs retrieved successfully", body = Vec, example=json!([doc_examples::vaa_example()])) - ), - params( - LatestVaasQueryParams - ) -)] -pub async fn latest_vaas( - State(state): State, - QsQuery(params): QsQuery, -) -> Result>, RestError> { - let price_ids: Vec = params.ids.into_iter().map(|id| id.into()).collect(); - let price_feeds_with_update_data = state - .store - .get_price_feeds_with_update_data(price_ids, RequestTime::Latest) - .await - .map_err(|_| RestError::UpdateDataNotFound)?; - Ok(Json( - price_feeds_with_update_data - .wormhole_merkle_update_data - .iter() - .map(|bytes| base64_standard_engine.encode(bytes)) // TODO: Support multiple - // encoding formats - .collect(), - )) -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] -pub struct LatestPriceFeedsQueryParams { - /// Get the most recent price update for these price feed ids. - /// Provide this parameter multiple times to retrieve multiple price updates, - /// ids[]=a12...&ids[]=b4c... - #[param( - rename = "ids[]", - example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" - )] - ids: Vec, - /// If true, include the `metadata` field in the response with additional metadata about - /// the price update. - #[serde(default)] - verbose: bool, - /// If true, include the binary price update in the `vaa` field of each returned feed. - /// This binary data can be submitted to Pyth contracts to update the on-chain price. - #[serde(default)] - binary: bool, -} - -/// Get the latest price updates by price feed id. -/// -/// Given a collection of price feed ids, retrieve the latest Pyth price for each price feed. -#[utoipa::path( - get, - path = "/api/latest_price_feeds", - responses( - (status = 200, description = "Price updates retrieved successfully", body = Vec) - ), - params( - LatestPriceFeedsQueryParams - ) -)] -pub async fn latest_price_feeds( - State(state): State, - QsQuery(params): QsQuery, -) -> Result>, RestError> { - let price_ids: Vec = params.ids.into_iter().map(|id| id.into()).collect(); - let price_feeds_with_update_data = state - .store - .get_price_feeds_with_update_data(price_ids, RequestTime::Latest) - .await - .map_err(|_| RestError::UpdateDataNotFound)?; - Ok(Json( - price_feeds_with_update_data - .price_feeds - .into_iter() - .map(|price_feed| { - RpcPriceFeed::from_price_feed_update(price_feed, params.verbose, params.binary) - }) - .collect(), - )) -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] -pub struct GetPriceFeedQueryParams { - /// The id of the price feed to get an update for. - id: PriceIdInput, - /// The unix timestamp in seconds. This endpoint will return the first update - /// whose publish_time is >= the provided value. - #[param(value_type = i64, example=doc_examples::timestamp_example)] - publish_time: UnixTimestamp, - /// If true, include the `metadata` field in the response with additional metadata about - /// the price update. - #[serde(default)] - verbose: bool, - /// If true, include the binary price update in the `vaa` field of each returned feed. - /// This binary data can be submitted to Pyth contracts to update the on-chain price. - #[serde(default)] - binary: bool, -} - -/// Get a price update for a price feed with a specific timestamp -/// -/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp. -#[utoipa::path( - get, - path = "/api/get_price_feed", - responses( - (status = 200, description = "Price update retrieved successfully", body = RpcPriceFeed) - ), - params( - GetPriceFeedQueryParams - ) -)] -pub async fn get_price_feed( - State(state): State, - QsQuery(params): QsQuery, -) -> Result, RestError> { - let price_id: PriceIdentifier = params.id.into(); - - let price_feeds_with_update_data = state - .store - .get_price_feeds_with_update_data( - vec![price_id], - RequestTime::FirstAfter(params.publish_time), - ) - .await - .map_err(|_| RestError::UpdateDataNotFound)?; - - Ok(Json(RpcPriceFeed::from_price_feed_update( - price_feeds_with_update_data - .price_feeds - .into_iter() - .next() - .ok_or(RestError::UpdateDataNotFound)?, - params.verbose, - params.binary, - ))) -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] -pub struct GetVaaQueryParams { - /// The id of the price feed to get an update for. - id: PriceIdInput, - /// The unix timestamp in seconds. This endpoint will return the first update - /// whose publish_time is >= the provided value. - #[param(value_type = i64, example=1690576641)] - publish_time: UnixTimestamp, -} - -#[derive(Debug, serde::Serialize, ToSchema)] -pub struct GetVaaResponse { - /// The VAA binary represented as a base64 string. - #[schema(example=doc_examples::vaa_example)] - vaa: String, - #[serde(rename = "publishTime")] - #[schema(value_type = i64, example=1690576641)] - publish_time: UnixTimestamp, -} - -/// Get a VAA for a price feed with a specific timestamp -/// -/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp. -#[utoipa::path( - get, - path = "/api/get_vaa", - responses( - (status = 200, description = "Price update retrieved successfully", body = GetVaaResponse), - (status = 404, description = "Price update not found", body = String) - ), - params( - GetVaaQueryParams - ) -)] -pub async fn get_vaa( - State(state): State, - QsQuery(params): QsQuery, -) -> Result, RestError> { - let price_id: PriceIdentifier = params.id.into(); - - let price_feeds_with_update_data = state - .store - .get_price_feeds_with_update_data( - vec![price_id], - RequestTime::FirstAfter(params.publish_time), - ) - .await - .map_err(|_| RestError::UpdateDataNotFound)?; - - let vaa = price_feeds_with_update_data - .wormhole_merkle_update_data - .get(0) - .map(|bytes| base64_standard_engine.encode(bytes)) - .ok_or(RestError::UpdateDataNotFound)?; - - let publish_time = price_feeds_with_update_data - .price_feeds - .get(0) - .ok_or(RestError::UpdateDataNotFound)? - .price_feed - .publish_time; - - Ok(Json(GetVaaResponse { vaa, publish_time })) -} - -#[derive(Debug, Clone, Deref, DerefMut, ToSchema)] -pub struct GetVaaCcipInput([u8; 40]); -impl_deserialize_for_hex_string_wrapper!(GetVaaCcipInput, 40); - -#[derive(Debug, serde::Deserialize, IntoParams)] -#[into_params(parameter_in=Query)] -pub struct GetVaaCcipQueryParams { - data: GetVaaCcipInput, -} - -#[derive(Debug, serde::Serialize, ToSchema)] -pub struct GetVaaCcipResponse { - data: String, // TODO: Use a typed wrapper for the hex output with leading 0x. -} - -/// Get a VAA for a price feed using CCIP -/// -/// This endpoint accepts a single argument which is a hex-encoded byte string of the following form: -/// ` ` -#[utoipa::path( - get, - path = "/api/get_vaa_ccip", - responses( - (status = 200, description = "Price update retrieved successfully", body = GetVaaCcipResponse) - ), - params( - GetVaaCcipQueryParams - ) -)] -pub async fn get_vaa_ccip( - State(state): State, - QsQuery(params): QsQuery, -) -> Result, RestError> { - let price_id: PriceIdentifier = PriceIdentifier::new( - params.data[0..32] - .try_into() - .map_err(|_| RestError::InvalidCCIPInput)?, - ); - let publish_time = UnixTimestamp::from_be_bytes( - params.data[32..40] - .try_into() - .map_err(|_| RestError::InvalidCCIPInput)?, - ); - - let price_feeds_with_update_data = state - .store - .get_price_feeds_with_update_data(vec![price_id], RequestTime::FirstAfter(publish_time)) - .await - .map_err(|_| RestError::CcipUpdateDataNotFound)?; - - let bytes = price_feeds_with_update_data - .wormhole_merkle_update_data - .get(0) // One price feed has only a single VAA as proof. - .ok_or(RestError::UpdateDataNotFound)?; - - Ok(Json(GetVaaCcipResponse { - data: format!("0x{}", hex::encode(bytes)), - })) -} - -pub async fn live() -> Response { - (StatusCode::OK, "OK").into_response() -} - -pub async fn ready(State(state): State) -> Response { - match state.store.is_ready().await { - true => (StatusCode::OK, "OK").into_response(), - false => (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable").into_response(), - } -} - -// This is the index page for the REST service. It will list all the available endpoints. -// TODO: Dynamically generate this list if possible. -pub async fn index() -> impl IntoResponse { - Json([ - "/live", - "/ready", - "/api/price_feed_ids", - "/api/latest_price_feeds?ids[]=&ids[]=&..(&verbose=true)(&binary=true)", - "/api/latest_vaas?ids[]=&ids[]=&...", - "/api/get_price_feed?id=&publish_time=(&verbose=true)(&binary=true)", - "/api/get_vaa?id=&publish_time=", - "/api/get_vaa_ccip?data=<0x+>", - ]) -} diff --git a/hermes/src/api/rest/get_price_feed.rs b/hermes/src/api/rest/get_price_feed.rs new file mode 100644 index 00000000..482d21af --- /dev/null +++ b/hermes/src/api/rest/get_price_feed.rs @@ -0,0 +1,86 @@ +use { + crate::{ + api::{ + rest::RestError, + types::{ + PriceIdInput, + RpcPriceFeed, + }, + }, + doc_examples, + store::types::{ + RequestTime, + UnixTimestamp, + }, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, + pyth_sdk::PriceIdentifier, + serde_qs::axum::QsQuery, + utoipa::IntoParams, +}; + +#[derive(Debug, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct GetPriceFeedQueryParams { + /// The id of the price feed to get an update for. + id: PriceIdInput, + + /// The unix timestamp in seconds. This endpoint will return the first update whose + /// publish_time is >= the provided value. + #[param(value_type = i64)] + #[param(example = doc_examples::timestamp_example)] + publish_time: UnixTimestamp, + + /// If true, include the `metadata` field in the response with additional metadata about the + /// price update. + #[serde(default)] + verbose: bool, + + /// If true, include the binary price update in the `vaa` field of each returned feed. This + /// binary data can be submitted to Pyth contracts to update the on-chain price. + #[serde(default)] + binary: bool, +} + +/// Get a price update for a price feed with a specific timestamp +/// +/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp. +#[utoipa::path( + get, + path = "/api/get_price_feed", + responses( + (status = 200, description = "Price update retrieved successfully", body = RpcPriceFeed) + ), + params( + GetPriceFeedQueryParams + ) +)] +pub async fn get_price_feed( + State(state): State, + QsQuery(params): QsQuery, +) -> Result, RestError> { + let price_id: PriceIdentifier = params.id.into(); + + let price_feeds_with_update_data = state + .store + .get_price_feeds_with_update_data( + vec![price_id], + RequestTime::FirstAfter(params.publish_time), + ) + .await + .map_err(|_| RestError::UpdateDataNotFound)?; + + Ok(Json(RpcPriceFeed::from_price_feed_update( + price_feeds_with_update_data + .price_feeds + .into_iter() + .next() + .ok_or(RestError::UpdateDataNotFound)?, + params.verbose, + params.binary, + ))) +} diff --git a/hermes/src/api/rest/get_vaa.rs b/hermes/src/api/rest/get_vaa.rs new file mode 100644 index 00000000..48288d55 --- /dev/null +++ b/hermes/src/api/rest/get_vaa.rs @@ -0,0 +1,98 @@ +use { + crate::{ + api::{ + rest::RestError, + types::PriceIdInput, + }, + doc_examples, + store::types::{ + RequestTime, + UnixTimestamp, + }, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, + base64::{ + engine::general_purpose::STANDARD as base64_standard_engine, + Engine as _, + }, + pyth_sdk::PriceIdentifier, + serde_qs::axum::QsQuery, + utoipa::{ + IntoParams, + ToSchema, + }, +}; + +#[derive(Debug, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct GetVaaQueryParams { + /// The ID of the price feed to get an update for. + id: PriceIdInput, + + /// The unix timestamp in seconds. This endpoint will return the first update whose + /// publish_time is >= the provided value. + #[param(value_type = i64)] + #[param(example = 1690576641)] + publish_time: UnixTimestamp, +} + +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct GetVaaResponse { + /// The VAA binary represented as a base64 string. + #[schema(example = doc_examples::vaa_example)] + vaa: String, + + #[serde(rename = "publishTime")] + #[schema(value_type = i64)] + #[schema(example = 1690576641)] + publish_time: UnixTimestamp, +} + +/// Get a VAA for a price feed with a specific timestamp +/// +/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp. +#[utoipa::path( + get, + path = "/api/get_vaa", + responses( + (status = 200, description = "Price update retrieved successfully", body = GetVaaResponse), + (status = 404, description = "Price update not found", body = String) + ), + params( + GetVaaQueryParams + ) +)] +pub async fn get_vaa( + State(state): State, + QsQuery(params): QsQuery, +) -> Result, RestError> { + let price_id: PriceIdentifier = params.id.into(); + + let price_feeds_with_update_data = state + .store + .get_price_feeds_with_update_data( + vec![price_id], + RequestTime::FirstAfter(params.publish_time), + ) + .await + .map_err(|_| RestError::UpdateDataNotFound)?; + + let vaa = price_feeds_with_update_data + .wormhole_merkle_update_data + .get(0) + .map(|bytes| base64_standard_engine.encode(bytes)) + .ok_or(RestError::UpdateDataNotFound)?; + + let publish_time = price_feeds_with_update_data + .price_feeds + .get(0) + .ok_or(RestError::UpdateDataNotFound)? + .price_feed + .publish_time; + + Ok(Json(GetVaaResponse { vaa, publish_time })) +} diff --git a/hermes/src/api/rest/get_vaa_ccip.rs b/hermes/src/api/rest/get_vaa_ccip.rs new file mode 100644 index 00000000..92daf843 --- /dev/null +++ b/hermes/src/api/rest/get_vaa_ccip.rs @@ -0,0 +1,85 @@ +use { + crate::{ + api::rest::RestError, + impl_deserialize_for_hex_string_wrapper, + store::types::{ + RequestTime, + UnixTimestamp, + }, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, + derive_more::{ + Deref, + DerefMut, + }, + pyth_sdk::PriceIdentifier, + serde_qs::axum::QsQuery, + utoipa::{ + IntoParams, + ToSchema, + }, +}; + +#[derive(Debug, Clone, Deref, DerefMut, ToSchema)] +pub struct GetVaaCcipInput([u8; 40]); +impl_deserialize_for_hex_string_wrapper!(GetVaaCcipInput, 40); + +#[derive(Debug, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct GetVaaCcipQueryParams { + data: GetVaaCcipInput, +} + +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct GetVaaCcipResponse { + data: String, // TODO: Use a typed wrapper for the hex output with leading 0x. +} + +/// Get a VAA for a price feed using CCIP +/// +/// This endpoint accepts a single argument which is a hex-encoded byte string of the following form: +/// ` ` +#[utoipa::path( + get, + path = "/api/get_vaa_ccip", + responses( + (status = 200, description = "Price update retrieved successfully", body = GetVaaCcipResponse) + ), + params( + GetVaaCcipQueryParams + ) +)] +pub async fn get_vaa_ccip( + State(state): State, + QsQuery(params): QsQuery, +) -> Result, RestError> { + let price_id: PriceIdentifier = PriceIdentifier::new( + params.data[0..32] + .try_into() + .map_err(|_| RestError::InvalidCCIPInput)?, + ); + let publish_time = UnixTimestamp::from_be_bytes( + params.data[32..40] + .try_into() + .map_err(|_| RestError::InvalidCCIPInput)?, + ); + + let price_feeds_with_update_data = state + .store + .get_price_feeds_with_update_data(vec![price_id], RequestTime::FirstAfter(publish_time)) + .await + .map_err(|_| RestError::CcipUpdateDataNotFound)?; + + let bytes = price_feeds_with_update_data + .wormhole_merkle_update_data + .get(0) // One price feed has only a single VAA as proof. + .ok_or(RestError::UpdateDataNotFound)?; + + Ok(Json(GetVaaCcipResponse { + data: format!("0x{}", hex::encode(bytes)), + })) +} diff --git a/hermes/src/api/rest/index.rs b/hermes/src/api/rest/index.rs new file mode 100644 index 00000000..96152aa4 --- /dev/null +++ b/hermes/src/api/rest/index.rs @@ -0,0 +1,20 @@ +use axum::{ + response::IntoResponse, + Json, +}; + +/// This is the index page for the REST service. It lists all the available endpoints. +/// +/// TODO: Dynamically generate this list if possible. +pub async fn index() -> impl IntoResponse { + Json([ + "/live", + "/ready", + "/api/price_feed_ids", + "/api/latest_price_feeds?ids[]=&ids[]=&..(&verbose=true)(&binary=true)", + "/api/latest_vaas?ids[]=&ids[]=&...", + "/api/get_price_feed?id=&publish_time=(&verbose=true)(&binary=true)", + "/api/get_vaa?id=&publish_time=", + "/api/get_vaa_ccip?data=<0x+>", + ]) +} diff --git a/hermes/src/api/rest/latest_price_feeds.rs b/hermes/src/api/rest/latest_price_feeds.rs new file mode 100644 index 00000000..d8cdc105 --- /dev/null +++ b/hermes/src/api/rest/latest_price_feeds.rs @@ -0,0 +1,81 @@ +use { + crate::{ + api::{ + rest::RestError, + types::{ + PriceIdInput, + RpcPriceFeed, + }, + }, + store::types::RequestTime, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, + pyth_sdk::PriceIdentifier, + serde_qs::axum::QsQuery, + utoipa::IntoParams, +}; + +#[derive(Debug, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct LatestPriceFeedsQueryParams { + /// Get the most recent price update for this set of price feed ids. + /// + /// This parameter can be provided multiple times to retrieve multiple price updates, + /// for example see the following query string: + /// + /// ``` + /// ?ids[]=a12...&ids[]=b4c... + /// ``` + #[param(rename = "ids[]")] + #[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")] + ids: Vec, + + /// If true, include the `metadata` field in the response with additional metadata about + /// the price update. + #[serde(default)] + verbose: bool, + + /// If true, include the binary price update in the `vaa` field of each returned feed. + /// This binary data can be submitted to Pyth contracts to update the on-chain price. + #[serde(default)] + binary: bool, +} + +/// Get the latest price updates by price feed id. +/// +/// Given a collection of price feed ids, retrieve the latest Pyth price for each price feed. +#[utoipa::path( + get, + path = "/api/latest_price_feeds", + responses( + (status = 200, description = "Price updates retrieved successfully", body = Vec) + ), + params( + LatestPriceFeedsQueryParams + ) +)] +pub async fn latest_price_feeds( + State(state): State, + QsQuery(params): QsQuery, +) -> Result>, RestError> { + let price_ids: Vec = params.ids.into_iter().map(|id| id.into()).collect(); + let price_feeds_with_update_data = state + .store + .get_price_feeds_with_update_data(price_ids, RequestTime::Latest) + .await + .map_err(|_| RestError::UpdateDataNotFound)?; + + Ok(Json( + price_feeds_with_update_data + .price_feeds + .into_iter() + .map(|price_feed| { + RpcPriceFeed::from_price_feed_update(price_feed, params.verbose, params.binary) + }) + .collect(), + )) +} diff --git a/hermes/src/api/rest/latest_vaas.rs b/hermes/src/api/rest/latest_vaas.rs new file mode 100644 index 00000000..b558e384 --- /dev/null +++ b/hermes/src/api/rest/latest_vaas.rs @@ -0,0 +1,75 @@ +use { + crate::{ + api::{ + rest::RestError, + types::PriceIdInput, + }, + doc_examples, + store::types::RequestTime, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, + base64::{ + engine::general_purpose::STANDARD as base64_standard_engine, + Engine as _, + }, + pyth_sdk::PriceIdentifier, + serde_qs::axum::QsQuery, + utoipa::IntoParams, +}; + +#[derive(Debug, serde::Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct LatestVaasQueryParams { + /// Get the VAAs for this set of price feed ids. + /// + /// This parameter can be provided multiple times to retrieve multiple price updates, + /// for example see the following query string: + /// + /// ``` + /// ?ids[]=a12...&ids[]=b4c... + /// ``` + #[param(rename = "ids[]")] + #[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")] + ids: Vec, +} + + +/// Get VAAs for a set of price feed ids. +/// +/// Given a collection of price feed ids, retrieve the latest VAA for each. The returned VAA(s) can +/// be submitted to the Pyth contract to update the on-chain price. If VAAs are not found for every +/// provided price ID the call will fail. +#[utoipa::path( + get, + path = "/api/latest_vaas", + params( + LatestVaasQueryParams + ), + responses( + (status = 200, description = "VAAs retrieved successfully", body = Vec, example=json!([doc_examples::vaa_example()])) + ), +)] +pub async fn latest_vaas( + State(state): State, + QsQuery(params): QsQuery, +) -> Result>, RestError> { + let price_ids: Vec = params.ids.into_iter().map(|id| id.into()).collect(); + let price_feeds_with_update_data = state + .store + .get_price_feeds_with_update_data(price_ids, RequestTime::Latest) + .await + .map_err(|_| RestError::UpdateDataNotFound)?; + + Ok(Json( + price_feeds_with_update_data + .wormhole_merkle_update_data + .iter() + .map(|bytes| base64_standard_engine.encode(bytes)) // TODO: Support multiple + // encoding formats + .collect(), + )) +} diff --git a/hermes/src/api/rest/live.rs b/hermes/src/api/rest/live.rs new file mode 100644 index 00000000..66343cc6 --- /dev/null +++ b/hermes/src/api/rest/live.rs @@ -0,0 +1,11 @@ +use axum::{ + http::StatusCode, + response::{ + IntoResponse, + Response, + }, +}; + +pub async fn live() -> Response { + (StatusCode::OK, "OK").into_response() +} diff --git a/hermes/src/api/rest/price_feed_ids.rs b/hermes/src/api/rest/price_feed_ids.rs new file mode 100644 index 00000000..0b5cb06d --- /dev/null +++ b/hermes/src/api/rest/price_feed_ids.rs @@ -0,0 +1,36 @@ +use { + crate::api::{ + rest::RestError, + types::RpcPriceIdentifier, + }, + anyhow::Result, + axum::{ + extract::State, + Json, + }, +}; + +/// Get the set of price feed IDs. +/// +/// This endpoint fetches all of the price feed IDs for which price updates can be retrieved. +#[utoipa::path( + get, + path = "/api/price_feed_ids", + params(), + responses( + (status = 200, description = "Price feed ids retrieved successfully", body = Vec) + ), +)] +pub async fn price_feed_ids( + State(state): State, +) -> Result>, RestError> { + let price_feed_ids = state + .store + .get_price_feed_ids() + .await + .iter() + .map(|id| RpcPriceIdentifier::from(&id)) + .collect(); + + Ok(Json(price_feed_ids)) +} diff --git a/hermes/src/api/rest/ready.rs b/hermes/src/api/rest/ready.rs new file mode 100644 index 00000000..1e466fab --- /dev/null +++ b/hermes/src/api/rest/ready.rs @@ -0,0 +1,15 @@ +use axum::{ + extract::State, + http::StatusCode, + response::{ + IntoResponse, + Response, + }, +}; + +pub async fn ready(State(state): State) -> Response { + match state.store.is_ready().await { + true => (StatusCode::OK, "OK").into_response(), + false => (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable").into_response(), + } +}