Skip to content

Commit 69e4fee

Browse files
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 <[email protected]> * move to v2 module and address comments * address more comments * address more comments --------- Co-authored-by: Ali Behjati <[email protected]>
1 parent be84473 commit 69e4fee

File tree

6 files changed

+202
-0
lines changed

6 files changed

+202
-0
lines changed

hermes/src/api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
121121
rest::latest_price_feeds,
122122
rest::latest_vaas,
123123
rest::price_feed_ids,
124+
rest::latest_price_updates,
124125
),
125126
components(
126127
schemas(
@@ -152,6 +153,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
152153
.route("/api/latest_price_feeds", get(rest::latest_price_feeds))
153154
.route("/api/latest_vaas", get(rest::latest_vaas))
154155
.route("/api/price_feed_ids", get(rest::price_feed_ids))
156+
.route("/v2/updates/price/latest", get(rest::latest_price_updates))
155157
.route("/live", get(rest::live))
156158
.route("/ready", get(rest::ready))
157159
.route("/ws", get(ws::ws_route_handler))

hermes/src/api/rest.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod latest_vaas;
1919
mod live;
2020
mod price_feed_ids;
2121
mod ready;
22+
mod v2;
2223

2324
pub use {
2425
get_price_feed::*,
@@ -30,6 +31,7 @@ pub use {
3031
live::*,
3132
price_feed_ids::*,
3233
ready::*,
34+
v2::latest_price_updates::*,
3335
};
3436

3537
pub enum RestError {

hermes/src/api/rest/index.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ pub async fn index() -> impl IntoResponse {
1616
"/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
1717
"/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
1818
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
19+
"/v2/updates/price/latest?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
1920
])
2021
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use {
2+
crate::{
3+
aggregate::RequestTime,
4+
api::{
5+
rest::{
6+
verify_price_ids_exist,
7+
RestError,
8+
},
9+
types::{
10+
BinaryPriceUpdate,
11+
EncodingType,
12+
ParsedPriceUpdate,
13+
PriceIdInput,
14+
PriceUpdate,
15+
},
16+
},
17+
},
18+
anyhow::Result,
19+
axum::{
20+
extract::State,
21+
Json,
22+
},
23+
base64::{
24+
engine::general_purpose::STANDARD as base64_standard_engine,
25+
Engine as _,
26+
},
27+
pyth_sdk::PriceIdentifier,
28+
serde_qs::axum::QsQuery,
29+
utoipa::IntoParams,
30+
};
31+
32+
33+
#[derive(Debug, serde::Deserialize, IntoParams)]
34+
#[into_params(parameter_in=Query)]
35+
pub struct LatestPriceUpdatesQueryParams {
36+
/// Get the most recent price update for this set of price feed ids.
37+
///
38+
/// This parameter can be provided multiple times to retrieve multiple price updates,
39+
/// for example see the following query string:
40+
///
41+
/// ```
42+
/// ?ids[]=a12...&ids[]=b4c...
43+
/// ```
44+
#[param(rename = "ids[]")]
45+
#[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")]
46+
ids: Vec<PriceIdInput>,
47+
48+
/// If true, include the parsed price update in the `parsed` field of each returned feed.
49+
#[serde(default)]
50+
encoding: EncodingType,
51+
52+
/// If true, include the parsed price update in the `parsed` field of each returned feed.
53+
#[serde(default = "default_true")]
54+
parsed: bool,
55+
}
56+
57+
fn default_true() -> bool {
58+
true
59+
}
60+
61+
/// Get the latest price updates by price feed id.
62+
///
63+
/// Given a collection of price feed ids, retrieve the latest Pyth price for each price feed.
64+
#[utoipa::path(
65+
get,
66+
path = "/v2/updates/price/latest",
67+
responses(
68+
(status = 200, description = "Price updates retrieved successfully", body = Vec<PriceUpdate>),
69+
(status = 404, description = "Price ids not found", body = String)
70+
),
71+
params(
72+
LatestPriceUpdatesQueryParams
73+
)
74+
)]
75+
pub async fn latest_price_updates(
76+
State(state): State<crate::api::ApiState>,
77+
QsQuery(params): QsQuery<LatestPriceUpdatesQueryParams>,
78+
) -> Result<Json<Vec<PriceUpdate>>, RestError> {
79+
let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
80+
81+
verify_price_ids_exist(&state, &price_ids).await?;
82+
83+
let price_feeds_with_update_data = crate::aggregate::get_price_feeds_with_update_data(
84+
&*state.state,
85+
&price_ids,
86+
RequestTime::Latest,
87+
)
88+
.await
89+
.map_err(|e| {
90+
tracing::warn!(
91+
"Error getting price feeds {:?} with update data: {:?}",
92+
price_ids,
93+
e
94+
);
95+
RestError::UpdateDataNotFound
96+
})?;
97+
98+
let price_update_data = price_feeds_with_update_data.update_data;
99+
let encoded_data: Vec<String> = price_update_data
100+
.into_iter()
101+
.map(|data| match params.encoding {
102+
EncodingType::Base64 => base64_standard_engine.encode(data),
103+
EncodingType::Hex => hex::encode(data),
104+
})
105+
.collect();
106+
let binary_price_update = BinaryPriceUpdate {
107+
encoding: params.encoding,
108+
data: encoded_data,
109+
};
110+
let parsed_price_updates: Option<Vec<ParsedPriceUpdate>> = if params.parsed {
111+
Some(
112+
price_feeds_with_update_data
113+
.price_feeds
114+
.into_iter()
115+
.map(|price_feed| price_feed.into())
116+
.collect(),
117+
)
118+
} else {
119+
None
120+
};
121+
122+
let compressed_price_update = PriceUpdate {
123+
binary: binary_price_update,
124+
parsed: parsed_price_updates,
125+
};
126+
127+
128+
Ok(Json(vec![compressed_price_update]))
129+
}

hermes/src/api/rest/v2/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod latest_price_updates;

hermes/src/api/types.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ pub struct RpcPriceFeedMetadata {
5858
pub prev_publish_time: Option<UnixTimestamp>,
5959
}
6060

61+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
62+
pub struct RpcPriceFeedMetadataV2 {
63+
#[schema(value_type = Option<u64>, example=85480034)]
64+
pub slot: Option<Slot>,
65+
#[schema(value_type = Option<i64>, example=doc_examples::timestamp_example)]
66+
pub proof_available_time: Option<UnixTimestamp>,
67+
#[schema(value_type = Option<i64>, example=doc_examples::timestamp_example)]
68+
pub prev_publish_time: Option<UnixTimestamp>,
69+
}
70+
6171
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
6272
pub struct RpcPriceFeed {
6373
pub id: RpcPriceIdentifier,
@@ -179,3 +189,60 @@ impl RpcPriceIdentifier {
179189
RpcPriceIdentifier(id.to_bytes())
180190
}
181191
}
192+
193+
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, serde::Serialize)]
194+
pub enum EncodingType {
195+
#[default]
196+
#[serde(rename = "hex")]
197+
Hex,
198+
#[serde(rename = "base64")]
199+
Base64,
200+
}
201+
202+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
203+
pub struct BinaryPriceUpdate {
204+
pub encoding: EncodingType,
205+
pub data: Vec<String>,
206+
}
207+
208+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
209+
pub struct ParsedPriceUpdate {
210+
pub id: String,
211+
pub price: RpcPrice,
212+
pub ema_price: RpcPrice,
213+
pub metadata: RpcPriceFeedMetadataV2,
214+
}
215+
216+
impl From<PriceFeedUpdate> for ParsedPriceUpdate {
217+
fn from(price_feed_update: PriceFeedUpdate) -> Self {
218+
let price_feed = price_feed_update.price_feed;
219+
220+
Self {
221+
id: price_feed.id.to_string(),
222+
price: RpcPrice {
223+
price: price_feed.get_price_unchecked().price,
224+
conf: price_feed.get_price_unchecked().conf,
225+
expo: price_feed.get_price_unchecked().expo,
226+
publish_time: price_feed.get_price_unchecked().publish_time,
227+
},
228+
ema_price: RpcPrice {
229+
price: price_feed.get_ema_price_unchecked().price,
230+
conf: price_feed.get_ema_price_unchecked().conf,
231+
expo: price_feed.get_ema_price_unchecked().expo,
232+
publish_time: price_feed.get_ema_price_unchecked().publish_time,
233+
},
234+
metadata: RpcPriceFeedMetadataV2 {
235+
proof_available_time: price_feed_update.received_at,
236+
slot: price_feed_update.slot,
237+
prev_publish_time: price_feed_update.prev_publish_time,
238+
},
239+
}
240+
}
241+
}
242+
243+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
244+
pub struct PriceUpdate {
245+
pub binary: BinaryPriceUpdate,
246+
#[serde(skip_serializing_if = "Option::is_none")]
247+
pub parsed: Option<Vec<ParsedPriceUpdate>>,
248+
}

0 commit comments

Comments
 (0)