Bläddra i källkod

[hermes] Document most of the endpoints (#997)

* add rough docs for most stuff

* cleanup

* bunch of docs

* bunch of docs

* gr
Jayant Krishnamurthy 2 år sedan
förälder
incheckning
cb49236a58
4 ändrade filer med 207 tillägg och 29 borttagningar
  1. 7 2
      hermes/src/api.rs
  2. 113 14
      hermes/src/api/rest.rs
  3. 83 13
      hermes/src/api/types.rs
  4. 4 0
      hermes/src/store/types.rs

+ 7 - 2
hermes/src/api.rs

@@ -46,9 +46,14 @@ pub async fn run(store: Arc<Store>, mut update_rx: Receiver<()>, rpc_addr: Strin
     #[openapi(
     paths(
       rest::latest_price_feeds,
+      rest::latest_vaas,
+      rest::get_price_feed,
+      rest::get_vaa,
+      rest::get_vaa_ccip,
+      rest::price_feed_ids,
     ),
     components(
-      schemas(types::RpcPriceFeedMetadata, types::RpcPriceFeed, types::PriceIdInput)
+      schemas(types::RpcPriceFeedMetadata, types::RpcPriceFeed, types::RpcPrice, types::RpcPriceIdentifier, types::PriceIdInput, rest::GetVaaResponse, rest::GetVaaCcipResponse, rest::GetVaaCcipInput)
     ),
     tags(
       (name = "hermes", description = "Pyth Real-Time Pricing API")
@@ -78,7 +83,7 @@ pub async fn run(store: Arc<Store>, mut update_rx: Receiver<()>, rpc_addr: Strin
         .layer(CorsLayer::permissive())
         // non-strict mode permits escaped [] in URL parameters.
         // 5 is the allowed depth (also the default value for this parameter).
-        .layer(Extension(QsQueryConfig::new(false)));
+        .layer(Extension(QsQueryConfig::new(5, false)));
 
 
     // Call dispatch updates to websocket every 1 seconds

+ 113 - 14
hermes/src/api/rest.rs

@@ -2,6 +2,7 @@ use {
     super::types::{
         PriceIdInput,
         RpcPriceFeed,
+        RpcPriceIdentifier,
     },
     crate::{
         impl_deserialize_for_hex_string_wrapper,
@@ -30,8 +31,10 @@ use {
     },
     pyth_sdk::PriceIdentifier,
     serde_qs::axum::QsQuery,
-    std::collections::HashSet,
-    utoipa::IntoParams,
+    utoipa::{
+        IntoParams,
+        ToSchema,
+    },
 };
 
 pub enum RestError {
@@ -63,19 +66,57 @@ 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<HashSet<PriceIdentifier>>, RestError> {
-    let price_feeds = state.store.get_price_feed_ids().await;
-    Ok(Json(price_feeds))
+) -> 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)]
+#[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>)
+  ),
+  params(
+    LatestVaasQueryParams
+  )
+)]
 pub async fn latest_vaas(
     State(state): State<super::State>,
     QsQuery(params): QsQuery<LatestVaasQueryParams>,
@@ -124,7 +165,7 @@ pub struct LatestPriceFeedsQueryParams {
   get,
   path = "/api/latest_price_feeds",
   responses(
-    (status = 200, description = "Price updates retrieved successfully", body = [Vec<RpcPriceFeed>])
+    (status = 200, description = "Price updates retrieved successfully", body = Vec<RpcPriceFeed>)
   ),
   params(
     LatestPriceFeedsQueryParams
@@ -151,16 +192,38 @@ pub async fn latest_price_feeds(
     ))
 }
 
-#[derive(Debug, serde::Deserialize)]
+#[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=1690576641)]
     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>,
@@ -187,19 +250,40 @@ pub async fn get_price_feed(
     )))
 }
 
-#[derive(Debug, serde::Deserialize)]
+#[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)]
+#[derive(Debug, serde::Serialize, ToSchema)]
 pub struct GetVaaResponse {
+    /// The VAA binary represented as a base64 string.
     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>,
@@ -231,20 +315,35 @@ pub async fn get_vaa(
     Ok(Json(GetVaaResponse { vaa, publish_time }))
 }
 
-#[derive(Debug, Clone, Deref, DerefMut)]
+#[derive(Debug, Clone, Deref, DerefMut, ToSchema)]
 pub struct GetVaaCcipInput([u8; 40]);
 impl_deserialize_for_hex_string_wrapper!(GetVaaCcipInput, 40);
 
-#[derive(Debug, serde::Deserialize)]
+#[derive(Debug, serde::Deserialize, IntoParams)]
+#[into_params(parameter_in=Query)]
 pub struct GetVaaCcipQueryParams {
     data: GetVaaCcipInput,
 }
 
-#[derive(Debug, serde::Serialize)]
+#[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>,

+ 83 - 13
hermes/src/api/types.rs

@@ -11,14 +11,16 @@ use {
         engine::general_purpose::STANDARD as base64_standard_engine,
         Engine as _,
     },
+    borsh::{
+        BorshDeserialize,
+        BorshSerialize,
+    },
     derive_more::{
         Deref,
         DerefMut,
     },
-    pyth_sdk::{
-        Price,
-        PriceIdentifier,
-    },
+    hex::FromHexError,
+    pyth_sdk::PriceIdentifier,
     utoipa::ToSchema,
     wormhole_sdk::Chain,
 };
@@ -33,7 +35,7 @@ use {
 ///
 /// See https://pyth.network/developers/price-feed-ids for a list of all price feed ids.
 #[derive(Debug, Clone, Deref, DerefMut, ToSchema)]
-#[schema(value_type=String)]
+#[schema(value_type=String, example="63f341689d98a12ef60a5cff1d7f85c70a9e17bf1575f0e7c0b2512d48b1c8b3")]
 pub struct PriceIdInput([u8; 32]);
 // TODO: Use const generics instead of macro.
 impl_deserialize_for_hex_string_wrapper!(PriceIdInput, 32);
@@ -58,13 +60,12 @@ pub struct RpcPriceFeedMetadata {
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
 pub struct RpcPriceFeed {
-    #[schema(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")]
-    pub id:        PriceIdentifier,
-    pub price:     Price,
-    pub ema_price: Price,
+    pub id:        RpcPriceIdentifier,
+    pub price:     RpcPrice,
+    pub ema_price: RpcPrice,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub metadata:  Option<RpcPriceFeedMetadata>,
-    /// Vaa binary represented in base64.
+    /// The VAA binary represented as a base64 string.
     #[serde(skip_serializing_if = "Option::is_none")]
     #[schema(value_type = Option<String>, example="UE5BVQEAAAADuAEAAAADDQC1H7meY5fTed0FsykIb8dt+7nKpbuzfvU2DplDi+dcUl8MC+UIkS65+rkiq+zmNBxE2gaxkBkjdIicZ/fBo+X7AAEqp+WtlWb84np8jJfLpuQ2W+l5KXTigsdAhz5DyVgU3xs+EnaIZxBwcE7EKzjMam+V9rlRy0CGsiQ1kjqqLzfAAQLsoVO0Vu5gVmgc8XGQ7xYhoz36rsBgMjG+e3l/B01esQi/KzPuBf/Ar8Sg5aSEOvEU0muSDb+KIr6d8eEC+FtcAAPZEaBSt4ysXVL84LUcJemQD3SiG30kOfUpF8o7/wI2M2Jf/LyCsbKEQUyLtLbZqnJBSfZJR5AMsrnHDqngMLEGAAY4UDG9GCpRuPvg8hOlsrXuPP3zq7yVPqyG0SG+bNo8rEhP5b1vXlHdG4bZsutX47d5VZ6xnFROKudx3T3/fnWUAQgAU1+kUFc3e0ZZeX1dLRVEryNIVyxMQIcxWwdey+jlIAYowHRM0fJX3Scs80OnT/CERwh5LMlFyU1w578NqxW+AQl2E/9fxjgUTi8crOfDpwsUsmOWw0+Q5OUGhELv/2UZoHAjsaw9OinWUggKACo4SdpPlHYldoWF+J2yGWOW+F4iAQre4c+ocb6a9uSWOnTldFkioqhd9lhmV542+VonCvuy4Tu214NP+2UNd/4Kk3KJCf3iziQJrCBeLi1cLHdLUikgAQtvRFR/nepcF9legl+DywAkUHi5/1MNjlEQvlHyh2XbMiS85yu7/9LgM6Sr+0ukfZY5mSkOcvUkpHn+T+Nw/IrQAQ7lty5luvKUmBpI3ITxSmojJ1aJ0kj/dc0ZcQk+/qo0l0l3/eRLkYjw5j+MZKA8jEubrHzUCke98eSoj8l08+PGAA+DAKNtCwNZe4p6J1Ucod8Lo5RKFfA84CPLVyEzEPQFZ25U9grUK6ilF4GhEia/ndYXLBt3PGW3qa6CBBPM7rH3ABGAyYEtUwzB4CeVedA5o6cKpjRkIebqDNSOqltsr+w7kXdfFVtsK2FMGFZNt5rbpIR+ppztoJ6eOKHmKmi9nQ99ARKkTxRErOs9wJXNHaAuIRV38o1pxRrlQRzGsRuKBqxcQEpC8OPFpyKYcp6iD5l7cO/gRDTamLFyhiUBwKKMP07FAWTEJv8AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAAGp0GAUFVV1YAAAAAAAUYUmIAACcQBsfKUtr4PgZbIXRxRESU79PjE4IBAFUA5i32yLSoX+GmfbRNwS3l2zMPesZrctxliv7fD0pBW0MAAAKqqMJFwAAAAAAqE/NX////+AAAAABkxCb7AAAAAGTEJvoAAAKqIcWxYAAAAAAlR5m4CP/mPsh1IezjYpDlJ4GRb5q4fTs2LjtyO6M0XgVimrIQ4kSh1qg7JKW4gbGkyRntVFR9JO/GNd3FPDit0BK6M+JzXh/h12YNCz9wxlZTvXrNtWNbzqT+91pvl5cphhSPMfAHyEzTPaGR9tKDy9KNu56pmhaY32d2vfEWQmKo22guegeR98oDxs67MmnUraco46a3zEnac2Bm80pasUgMO24=")]
     pub vaa:       Option<Base64String>,
@@ -81,14 +82,14 @@ impl RpcPriceFeed {
         let price_feed_message = price_feed_update.price_feed;
 
         Self {
-            id:        PriceIdentifier::new(price_feed_message.feed_id),
-            price:     Price {
+            id:        RpcPriceIdentifier::new(price_feed_message.feed_id),
+            price:     RpcPrice {
                 price:        price_feed_message.price,
                 conf:         price_feed_message.conf,
                 expo:         price_feed_message.exponent,
                 publish_time: price_feed_message.publish_time,
             },
-            ema_price: Price {
+            ema_price: RpcPrice {
                 price:        price_feed_message.ema_price,
                 conf:         price_feed_message.ema_conf,
                 expo:         price_feed_message.exponent,
@@ -105,3 +106,72 @@ impl RpcPriceFeed {
         }
     }
 }
+
+/// A price with a degree of uncertainty at a certain time, represented as a price +- a confidence
+/// interval.
+///
+/// The confidence interval roughly corresponds to the standard error of a normal distribution.
+/// Both the price and confidence are stored in a fixed-point numeric representation, `x *
+/// 10^expo`, where `expo` is the exponent. For example:
+#[derive(
+    Clone,
+    Copy,
+    Default,
+    Debug,
+    PartialEq,
+    Eq,
+    BorshSerialize,
+    BorshDeserialize,
+    serde::Serialize,
+    serde::Deserialize,
+    ToSchema,
+)]
+pub struct RpcPrice {
+    /// The price itself, stored as a string to avoid precision loss
+    #[serde(with = "pyth_sdk::utils::as_string")]
+    #[schema(value_type = String, example="2920679499999")]
+    pub price:        i64,
+    /// The confidence interval associated with the price, stored as a string to avoid precision loss
+    #[serde(with = "pyth_sdk::utils::as_string")]
+    #[schema(value_type = String, example="509500001")]
+    pub conf:         u64,
+    /// The exponent associated with both the price and confidence interval. Multiply those values
+    /// by `10^expo` to get the real value.
+    #[schema(example=-8)]
+    pub expo:         i32,
+    /// When the price was published. The `publish_time` is a unix timestamp, i.e., the number of
+    /// seconds since the Unix epoch (00:00:00 UTC on 1 Jan 1970).
+    #[schema(value_type = i64, example=1690576641)]
+    pub publish_time: UnixTimestamp,
+}
+
+
+#[derive(
+    Copy,
+    Clone,
+    Debug,
+    Default,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Hash,
+    BorshSerialize,
+    BorshDeserialize,
+    serde::Serialize,
+    serde::Deserialize,
+    ToSchema,
+)]
+#[repr(C)]
+#[schema(value_type = String, example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")]
+pub struct RpcPriceIdentifier(#[serde(with = "hex")] [u8; 32]);
+
+impl RpcPriceIdentifier {
+    pub fn new(bytes: [u8; 32]) -> RpcPriceIdentifier {
+        RpcPriceIdentifier(bytes)
+    }
+
+    pub fn from(id: &PriceIdentifier) -> RpcPriceIdentifier {
+        RpcPriceIdentifier(id.to_bytes().clone())
+    }
+}

+ 4 - 0
hermes/src/store/types.rs

@@ -10,6 +10,10 @@ pub struct ProofSet {
 }
 
 pub type Slot = u64;
+
+/// The number of seconds since the Unix epoch (00:00:00 UTC on 1 Jan 1970). The timestamp is
+/// always positive, but represented as a signed integer because that's the standard on Unix
+/// systems and allows easy subtraction to compute durations.
 pub type UnixTimestamp = i64;
 
 #[derive(Clone, PartialEq, Eq, Debug)]