add /v2/updates/price/latest endpoint (#1225)

* add /v2/updates/price/latest endpoint

* Update data type in BinaryPriceUpdate struct

* support compressed update data

* Update API endpoint in index.rs

* Update hermes/src/api/types.rs

Co-authored-by: Ali Behjati <bahjatia@gmail.com>

* move to v2 module and address comments

* address more comments

* address more comments

---------

Co-authored-by: Ali Behjati <bahjatia@gmail.com>
This commit is contained in:
Daniel Chew 2024-01-19 11:53:19 +00:00 committed by GitHub
parent be8447324c
commit 69e4fee501
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 202 additions and 0 deletions

View File

@ -121,6 +121,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
rest::latest_price_feeds,
rest::latest_vaas,
rest::price_feed_ids,
rest::latest_price_updates,
),
components(
schemas(
@ -152,6 +153,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
.route("/api/latest_price_feeds", get(rest::latest_price_feeds))
.route("/api/latest_vaas", get(rest::latest_vaas))
.route("/api/price_feed_ids", get(rest::price_feed_ids))
.route("/v2/updates/price/latest", get(rest::latest_price_updates))
.route("/live", get(rest::live))
.route("/ready", get(rest::ready))
.route("/ws", get(ws::ws_route_handler))

View File

@ -19,6 +19,7 @@ mod latest_vaas;
mod live;
mod price_feed_ids;
mod ready;
mod v2;
pub use {
get_price_feed::*,
@ -30,6 +31,7 @@ pub use {
live::*,
price_feed_ids::*,
ready::*,
v2::latest_price_updates::*,
};
pub enum RestError {

View File

@ -16,5 +16,6 @@ pub async fn index() -> impl IntoResponse {
"/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>>",
"/v2/updates/price/latest?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
])
}

View File

@ -0,0 +1,129 @@
use {
crate::{
aggregate::RequestTime,
api::{
rest::{
verify_price_ids_exist,
RestError,
},
types::{
BinaryPriceUpdate,
EncodingType,
ParsedPriceUpdate,
PriceIdInput,
PriceUpdate,
},
},
},
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 LatestPriceUpdatesQueryParams {
/// 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 parsed price update in the `parsed` field of each returned feed.
#[serde(default)]
encoding: EncodingType,
/// If true, include the parsed price update in the `parsed` field of each returned feed.
#[serde(default = "default_true")]
parsed: bool,
}
fn default_true() -> bool {
true
}
/// 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 = "/v2/updates/price/latest",
responses(
(status = 200, description = "Price updates retrieved successfully", body = Vec<PriceUpdate>),
(status = 404, description = "Price ids not found", body = String)
),
params(
LatestPriceUpdatesQueryParams
)
)]
pub async fn latest_price_updates(
State(state): State<crate::api::ApiState>,
QsQuery(params): QsQuery<LatestPriceUpdatesQueryParams>,
) -> Result<Json<Vec<PriceUpdate>>, RestError> {
let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
verify_price_ids_exist(&state, &price_ids).await?;
let price_feeds_with_update_data = crate::aggregate::get_price_feeds_with_update_data(
&*state.state,
&price_ids,
RequestTime::Latest,
)
.await
.map_err(|e| {
tracing::warn!(
"Error getting price feeds {:?} with update data: {:?}",
price_ids,
e
);
RestError::UpdateDataNotFound
})?;
let price_update_data = price_feeds_with_update_data.update_data;
let encoded_data: Vec<String> = price_update_data
.into_iter()
.map(|data| match params.encoding {
EncodingType::Base64 => base64_standard_engine.encode(data),
EncodingType::Hex => hex::encode(data),
})
.collect();
let binary_price_update = BinaryPriceUpdate {
encoding: params.encoding,
data: encoded_data,
};
let parsed_price_updates: Option<Vec<ParsedPriceUpdate>> = if params.parsed {
Some(
price_feeds_with_update_data
.price_feeds
.into_iter()
.map(|price_feed| price_feed.into())
.collect(),
)
} else {
None
};
let compressed_price_update = PriceUpdate {
binary: binary_price_update,
parsed: parsed_price_updates,
};
Ok(Json(vec![compressed_price_update]))
}

View File

@ -0,0 +1 @@
pub mod latest_price_updates;

View File

@ -58,6 +58,16 @@ pub struct RpcPriceFeedMetadata {
pub prev_publish_time: Option<UnixTimestamp>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct RpcPriceFeedMetadataV2 {
#[schema(value_type = Option<u64>, example=85480034)]
pub slot: Option<Slot>,
#[schema(value_type = Option<i64>, example=doc_examples::timestamp_example)]
pub proof_available_time: Option<UnixTimestamp>,
#[schema(value_type = Option<i64>, example=doc_examples::timestamp_example)]
pub prev_publish_time: Option<UnixTimestamp>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct RpcPriceFeed {
pub id: RpcPriceIdentifier,
@ -179,3 +189,60 @@ impl RpcPriceIdentifier {
RpcPriceIdentifier(id.to_bytes())
}
}
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, serde::Serialize)]
pub enum EncodingType {
#[default]
#[serde(rename = "hex")]
Hex,
#[serde(rename = "base64")]
Base64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct BinaryPriceUpdate {
pub encoding: EncodingType,
pub data: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct ParsedPriceUpdate {
pub id: String,
pub price: RpcPrice,
pub ema_price: RpcPrice,
pub metadata: RpcPriceFeedMetadataV2,
}
impl From<PriceFeedUpdate> for ParsedPriceUpdate {
fn from(price_feed_update: PriceFeedUpdate) -> Self {
let price_feed = price_feed_update.price_feed;
Self {
id: price_feed.id.to_string(),
price: RpcPrice {
price: price_feed.get_price_unchecked().price,
conf: price_feed.get_price_unchecked().conf,
expo: price_feed.get_price_unchecked().expo,
publish_time: price_feed.get_price_unchecked().publish_time,
},
ema_price: RpcPrice {
price: price_feed.get_ema_price_unchecked().price,
conf: price_feed.get_ema_price_unchecked().conf,
expo: price_feed.get_ema_price_unchecked().expo,
publish_time: price_feed.get_ema_price_unchecked().publish_time,
},
metadata: RpcPriceFeedMetadataV2 {
proof_available_time: price_feed_update.received_at,
slot: price_feed_update.slot,
prev_publish_time: price_feed_update.prev_publish_time,
},
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct PriceUpdate {
pub binary: BinaryPriceUpdate,
#[serde(skip_serializing_if = "Option::is_none")]
pub parsed: Option<Vec<ParsedPriceUpdate>>,
}