From cb49236a58366b1315c375698d9d29ad656c1124 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Wed, 2 Aug 2023 19:25:30 -0700 Subject: [PATCH] [hermes] Document most of the endpoints (#997) * add rough docs for most stuff * cleanup * bunch of docs * bunch of docs * gr --- hermes/src/api.rs | 9 ++- hermes/src/api/rest.rs | 127 +++++++++++++++++++++++++++++++++----- hermes/src/api/types.rs | 96 ++++++++++++++++++++++++---- hermes/src/store/types.rs | 4 ++ 4 files changed, 207 insertions(+), 29 deletions(-) diff --git a/hermes/src/api.rs b/hermes/src/api.rs index 65f36e0b..360c6743 100644 --- a/hermes/src/api.rs +++ b/hermes/src/api.rs @@ -46,9 +46,14 @@ pub async fn run(store: Arc, 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, 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 diff --git a/hermes/src/api/rest.rs b/hermes/src/api/rest.rs index e84d9241..be488fac 100644 --- a/hermes/src/api/rest.rs +++ b/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) + ), + params() +)] pub async fn price_feed_ids( State(state): State, -) -> Result>, RestError> { - let price_feeds = state.store.get_price_feed_ids().await; - Ok(Json(price_feeds)) +) -> Result>, 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, } - +/// 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) + ), + params( + LatestVaasQueryParams + ) +)] pub async fn latest_vaas( State(state): State, QsQuery(params): QsQuery, @@ -124,7 +165,7 @@ pub struct LatestPriceFeedsQueryParams { get, path = "/api/latest_price_feeds", responses( - (status = 200, description = "Price updates retrieved successfully", body = [Vec]) + (status = 200, description = "Price updates retrieved successfully", body = Vec) ), 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, QsQuery(params): QsQuery, @@ -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, QsQuery(params): QsQuery, @@ -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: +/// ` ` +#[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, QsQuery(params): QsQuery, diff --git a/hermes/src/api/types.rs b/hermes/src/api/types.rs index 9b4b58b0..6229fd06 100644 --- a/hermes/src/api/types.rs +++ b/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, - /// Vaa binary represented in base64. + /// The VAA binary represented as a base64 string. #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Option, 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, @@ -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()) + } +} diff --git a/hermes/src/store/types.rs b/hermes/src/store/types.rs index e072c386..01572316 100644 --- a/hermes/src/store/types.rs +++ b/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)]