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

* add rough docs for most stuff

* cleanup

* bunch of docs

* bunch of docs

* gr
This commit is contained in:
Jayant Krishnamurthy 2023-08-02 19:25:30 -07:00 committed by GitHub
parent 175dfef5bd
commit cb49236a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 207 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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