Browse Source

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>
Daniel Chew 1 năm trước cách đây
mục cha
commit
69e4fee501

+ 2 - 0
hermes/src/api.rs

@@ -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))

+ 2 - 0
hermes/src/api/rest.rs

@@ -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 {

+ 1 - 0
hermes/src/api/rest/index.rs

@@ -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)",
     ])
 }

+ 129 - 0
hermes/src/api/rest/v2/latest_price_updates.rs

@@ -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]))
+}

+ 1 - 0
hermes/src/api/rest/v2/mod.rs

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

+ 67 - 0
hermes/src/api/types.rs

@@ -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>>,
+}