refactor(hermes): move rpc endpoints into submodules

This commit is contained in:
Reisen 2023-08-21 11:49:39 +00:00 committed by Reisen
parent b74df4ff17
commit 71ce45698b
10 changed files with 540 additions and 379 deletions

View File

@ -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<RpcPriceIdentifier>)
),
params()
)]
pub async fn price_feed_ids(
State(state): State<super::State>,
) -> Result<Json<Vec<RpcPriceIdentifier>>, 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<PriceIdInput>,
}
/// 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<String>, example=json!([doc_examples::vaa_example()]))
),
params(
LatestVaasQueryParams
)
)]
pub async fn latest_vaas(
State(state): State<super::State>,
QsQuery(params): QsQuery<LatestVaasQueryParams>,
) -> Result<Json<Vec<String>>, RestError> {
let price_ids: Vec<PriceIdentifier> = 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<PriceIdInput>,
/// 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<RpcPriceFeed>)
),
params(
LatestPriceFeedsQueryParams
)
)]
pub async fn latest_price_feeds(
State(state): State<super::State>,
QsQuery(params): QsQuery<LatestPriceFeedsQueryParams>,
) -> Result<Json<Vec<RpcPriceFeed>>, RestError> {
let price_ids: Vec<PriceIdentifier> = 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<super::State>,
QsQuery(params): QsQuery<GetPriceFeedQueryParams>,
) -> Result<Json<RpcPriceFeed>, 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<super::State>,
QsQuery(params): QsQuery<GetVaaQueryParams>,
) -> Result<Json<GetVaaResponse>, 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:
/// `<price feed id (32 bytes> <publish time as unix timestamp (8 bytes, big endian)>`
#[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<super::State>,
QsQuery(params): QsQuery<GetVaaCcipQueryParams>,
) -> Result<Json<GetVaaCcipResponse>, 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<super::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[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&verbose=true)(&binary=true)",
"/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&...",
"/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
"/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
])
}

View File

@ -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<crate::api::State>,
QsQuery(params): QsQuery<GetPriceFeedQueryParams>,
) -> Result<Json<RpcPriceFeed>, 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,
)))
}

View File

@ -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<crate::api::State>,
QsQuery(params): QsQuery<GetVaaQueryParams>,
) -> Result<Json<GetVaaResponse>, 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 }))
}

View File

@ -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:
/// `<price feed id (32 bytes> <publish time as unix timestamp (8 bytes, big endian)>`
#[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<crate::api::State>,
QsQuery(params): QsQuery<GetVaaCcipQueryParams>,
) -> Result<Json<GetVaaCcipResponse>, 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)),
}))
}

View File

@ -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[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&verbose=true)(&binary=true)",
"/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&...",
"/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
"/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
])
}

View File

@ -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<PriceIdInput>,
/// 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<RpcPriceFeed>)
),
params(
LatestPriceFeedsQueryParams
)
)]
pub async fn latest_price_feeds(
State(state): State<crate::api::State>,
QsQuery(params): QsQuery<LatestPriceFeedsQueryParams>,
) -> Result<Json<Vec<RpcPriceFeed>>, RestError> {
let price_ids: Vec<PriceIdentifier> = 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(),
))
}

View File

@ -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<PriceIdInput>,
}
/// 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<String>, example=json!([doc_examples::vaa_example()]))
),
)]
pub async fn latest_vaas(
State(state): State<crate::api::State>,
QsQuery(params): QsQuery<LatestVaasQueryParams>,
) -> Result<Json<Vec<String>>, RestError> {
let price_ids: Vec<PriceIdentifier> = 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(),
))
}

View File

@ -0,0 +1,11 @@
use axum::{
http::StatusCode,
response::{
IntoResponse,
Response,
},
};
pub async fn live() -> Response {
(StatusCode::OK, "OK").into_response()
}

View File

@ -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<RpcPriceIdentifier>)
),
)]
pub async fn price_feed_ids(
State(state): State<crate::api::State>,
) -> Result<Json<Vec<RpcPriceIdentifier>>, RestError> {
let price_feed_ids = state
.store
.get_price_feed_ids()
.await
.iter()
.map(|id| RpcPriceIdentifier::from(&id))
.collect();
Ok(Json(price_feed_ids))
}

View File

@ -0,0 +1,15 @@
use axum::{
extract::State,
http::StatusCode,
response::{
IntoResponse,
Response,
},
};
pub async fn ready(State(state): State<crate::api::State>) -> Response {
match state.store.is_ready().await {
true => (StatusCode::OK, "OK").into_response(),
false => (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable").into_response(),
}
}