-
Notifications
You must be signed in to change notification settings - Fork 300
feat(hermes): create latest TWAP endpoint #2131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
2f54446
9cb3bbd
09e02ba
cd51ee1
63a3b59
6c866a9
d098703
9592a7e
fafb9f8
1e55df1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
use { | ||
crate::{ | ||
api::{ | ||
rest::{validate_price_ids, RestError}, | ||
types::{BinaryUpdate, EncodingType, ParsedPriceFeedTwap, PriceIdInput, TwapsResponse}, | ||
ApiState, | ||
}, | ||
state::aggregate::{Aggregates, RequestTime}, | ||
}, | ||
anyhow::Result, | ||
axum::{ | ||
extract::{Path, State}, | ||
Json, | ||
}, | ||
base64::{engine::general_purpose::STANDARD as base64_standard_engine, Engine as _}, | ||
pyth_sdk::{DurationInSeconds, PriceIdentifier, UnixTimestamp}, | ||
serde::Deserialize, | ||
serde_qs::axum::QsQuery, | ||
utoipa::IntoParams, | ||
}; | ||
|
||
#[derive(Debug, Deserialize, IntoParams)] | ||
#[into_params(parameter_in=Path)] | ||
pub struct LatestTwapsPathParams { | ||
/// The time window in seconds over which to calculate the TWAP, ending at the current time. | ||
/// For example, a value of 300 would return the most recent 5 minute TWAP. | ||
/// Must be greater than 0 and less than or equal to 600 seconds (10 minutes). | ||
#[param(example = "300")] | ||
#[serde(deserialize_with = "validate_twap_window")] | ||
window_seconds: u64, | ||
} | ||
|
||
#[derive(Debug, Deserialize, IntoParams)] | ||
#[into_params(parameter_in=Query)] | ||
pub struct LatestTwapsQueryParams { | ||
/// Get the most recent TWAP (time weighted average price) for this set of price feed ids. | ||
/// The `binary` data contains the signed start & end cumulative price updates needed to calculate | ||
/// the TWAPs on-chain. The `parsed` data contains the calculated TWAPs. | ||
/// | ||
/// 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>, | ||
|
||
/// Optional encoding type. If true, return the price update in the encoding specified by the encoding parameter. Default is `hex`. | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
#[serde(default)] | ||
encoding: EncodingType, | ||
|
||
/// If true, include the calculated TWAP in the `parsed` field of each returned feed. Default is `true`. | ||
#[serde(default = "default_true")] | ||
parsed: bool, | ||
|
||
/// If true, invalid price IDs in the `ids` parameter are ignored. Only applicable to the v2 APIs. Default is `false`. | ||
#[serde(default)] | ||
ignore_invalid_price_ids: bool, | ||
} | ||
|
||
fn validate_twap_window<'de, D>(deserializer: D) -> Result<DurationInSeconds, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
use serde::de::Error; | ||
let seconds = DurationInSeconds::deserialize(deserializer)?; | ||
if seconds == 0 || seconds > 600 { | ||
return Err(D::Error::custom( | ||
"twap_window_seconds must be in range (0, 600]", | ||
)); | ||
} | ||
Ok(seconds) | ||
} | ||
fn default_true() -> bool { | ||
true | ||
} | ||
|
||
/// Get the latest TWAP by price feed id with a custom time window. | ||
/// | ||
/// Given a collection of price feed ids, retrieve the latest Pyth TWAP price for each price feed. | ||
#[utoipa::path( | ||
get, | ||
path = "/v2/updates/twap/{window_seconds}/latest", | ||
responses( | ||
(status = 200, description = "TWAPs retrieved successfully", body = TwapsResponse), | ||
(status = 404, description = "Price ids not found", body = String) | ||
), | ||
params( | ||
LatestTwapsPathParams, | ||
LatestTwapsQueryParams | ||
) | ||
)] | ||
pub async fn latest_twaps<S>( | ||
State(state): State<ApiState<S>>, | ||
Path(path_params): Path<LatestTwapsPathParams>, | ||
QsQuery(params): QsQuery<LatestTwapsQueryParams>, | ||
) -> Result<Json<TwapsResponse>, RestError> | ||
where | ||
S: Aggregates, | ||
{ | ||
let price_id_inputs: Vec<PriceIdentifier> = | ||
params.ids.into_iter().map(|id| id.into()).collect(); | ||
let price_ids: Vec<PriceIdentifier> = | ||
validate_price_ids(&state, &price_id_inputs, params.ignore_invalid_price_ids).await?; | ||
|
||
// Collect start and end bounds for the TWAP window | ||
let window_seconds = path_params.window_seconds as i64; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you want this to be an i64 in the params to avoid this conversion here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i made |
||
let current_time = std::time::SystemTime::now() | ||
.duration_since(std::time::UNIX_EPOCH) | ||
.unwrap() | ||
.as_secs() as UnixTimestamp; | ||
let start_time = current_time - window_seconds; | ||
|
||
// Calculate the average | ||
let twaps_with_update_data = Aggregates::get_twaps_with_update_data( | ||
&*state.state, | ||
&price_ids, | ||
RequestTime::FirstAfter(start_time), | ||
RequestTime::Latest, | ||
) | ||
.await | ||
.map_err(|e| { | ||
tracing::warn!( | ||
"Error getting TWAPs for price IDs {:?} with update data: {:?}", | ||
price_ids, | ||
e | ||
); | ||
RestError::UpdateDataNotFound | ||
})?; | ||
|
||
let twap_update_data = twaps_with_update_data.update_data; | ||
let binary: Vec<BinaryUpdate> = twap_update_data | ||
.into_iter() | ||
.map(|data_vec| { | ||
let encoded_data = data_vec | ||
.into_iter() | ||
.map(|data| match params.encoding { | ||
EncodingType::Base64 => base64_standard_engine.encode(data), | ||
EncodingType::Hex => hex::encode(data), | ||
}) | ||
.collect(); | ||
BinaryUpdate { | ||
encoding: params.encoding, | ||
data: encoded_data, | ||
} | ||
}) | ||
.collect(); | ||
|
||
let parsed: Option<Vec<ParsedPriceFeedTwap>> = if params.parsed { | ||
Some( | ||
twaps_with_update_data | ||
.twaps | ||
.into_iter() | ||
.map(Into::into) | ||
.collect(), | ||
) | ||
} else { | ||
None | ||
}; | ||
|
||
let twap_resp = TwapsResponse { binary, parsed }; | ||
Ok(Json(twap_resp)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
pub mod latest_price_updates; | ||
pub mod latest_publisher_stake_caps; | ||
pub mod latest_twaps; | ||
pub mod price_feeds_metadata; | ||
pub mod sse; | ||
pub mod timestamp_price_updates; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
use { | ||
super::doc_examples, | ||
crate::state::aggregate::{PriceFeedUpdate, PriceFeedsWithUpdateData, Slot, UnixTimestamp}, | ||
crate::state::aggregate::{ | ||
PriceFeedTwap, PriceFeedUpdate, PriceFeedsWithUpdateData, Slot, UnixTimestamp, | ||
}, | ||
anyhow::Result, | ||
base64::{engine::general_purpose::STANDARD as base64_standard_engine, Engine as _}, | ||
borsh::{BorshDeserialize, BorshSerialize}, | ||
derive_more::{Deref, DerefMut}, | ||
pyth_sdk::{Price, PriceFeed, PriceIdentifier}, | ||
rust_decimal::Decimal, | ||
serde::{Deserialize, Serialize}, | ||
std::{ | ||
collections::BTreeMap, | ||
|
@@ -140,7 +143,7 @@ pub struct RpcPrice { | |
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)] | ||
#[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). | ||
|
@@ -244,6 +247,50 @@ impl From<PriceFeedUpdate> for ParsedPriceUpdate { | |
} | ||
} | ||
} | ||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] | ||
pub struct ParsedPriceFeedTwap { | ||
pub id: RpcPriceIdentifier, | ||
/// The start unix timestamp of the window | ||
pub start_timestamp: i64, | ||
/// The end unix timestamp of the window | ||
pub end_timestamp: i64, | ||
/// The calculated time weighted average price over the window | ||
pub twap: RpcPrice, | ||
/// The % of slots where the network was down over the TWAP window. | ||
/// A value of zero indicates no slots were missed over the window, and | ||
/// a value of one indicates that every slot was missed over the window. | ||
/// This is a float value stored as a string to avoid precision loss. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update this comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is actually being returned as a string (utoipa's default behavior for Decimal) |
||
#[serde(with = "pyth_sdk::utils::as_string")] | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
#[schema(value_type = String, example="0.00001")] | ||
pub down_slots_ratio: Decimal, | ||
} | ||
impl From<PriceFeedTwap> for ParsedPriceFeedTwap { | ||
fn from(pft: PriceFeedTwap) -> Self { | ||
Self { | ||
id: RpcPriceIdentifier::from(pft.id), | ||
start_timestamp: pft.start_timestamp, | ||
end_timestamp: pft.end_timestamp, | ||
twap: RpcPrice { | ||
price: pft.twap.price, | ||
conf: pft.twap.conf, | ||
expo: pft.twap.expo, | ||
publish_time: pft.twap.publish_time, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. publish_time seems redundant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah fair. i reused |
||
}, | ||
down_slots_ratio: pft.down_slots_ratio, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] | ||
pub struct TwapsResponse { | ||
/// Each BinaryUpdate contains the start & end cumulative price updates used to | ||
/// calculate a given price feed's TWAP. | ||
pub binary: Vec<BinaryUpdate>, | ||
|
||
/// The calculated TWAPs for each price ID | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
pub parsed: Option<Vec<ParsedPriceFeedTwap>>, | ||
} | ||
|
||
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, Clone, ToSchema)] | ||
pub struct ParsedPublisherStakeCapsUpdate { | ||
|
Uh oh!
There was an error while loading. Please reload this page.