refactor(hermes): move rpc endpoints into submodules
This commit is contained in:
parent
b74df4ff17
commit
71ce45698b
|
@ -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>>",
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)))
|
||||
}
|
|
@ -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 }))
|
||||
}
|
|
@ -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)),
|
||||
}))
|
||||
}
|
|
@ -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>>",
|
||||
])
|
||||
}
|
|
@ -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(),
|
||||
))
|
||||
}
|
|
@ -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(),
|
||||
))
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{
|
||||
IntoResponse,
|
||||
Response,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn live() -> Response {
|
||||
(StatusCode::OK, "OK").into_response()
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue