Skip to content

Commit 9fd9e17

Browse files
authored
[hermes] add /v2/price_feeds endpoint (#1277)
* initial stab * fix comments * add filter feature * fix deprecated warnings * use cache * Update price_feeds API query * Update PriceFeedsQueryParams struct in price_feeds.rs * fix merge conflict * fix default value * add tracing info * fix comment * address comments * change var name * refactor * refactor * refactor * refactor * undo changes in cache.rs * undo changes in aggregate.rs * address comments * address comments * address comments and improve fetching data speed * address comments * address comments * bump * change chunk size * change function name * address comment * address comments * address comments * address comments * Remove debug print statement * address comments and add to openapi
1 parent eaaa74a commit 9fd9e17

File tree

14 files changed

+1364
-369
lines changed

14 files changed

+1364
-369
lines changed

hermes/Cargo.lock

Lines changed: 1031 additions & 344 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hermes/Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hermes"
3-
version = "0.5.2"
3+
version = "0.5.3"
44
description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle."
55
edition = "2021"
66

@@ -30,6 +30,7 @@ nonzero_ext = { version = "0.3.0" }
3030
prometheus-client = { version = "0.21.2" }
3131
prost = { version = "0.12.1" }
3232
pyth-sdk = { version = "0.8.0" }
33+
pyth-sdk-solana = { version = "0.9.0" }
3334
pythnet-sdk = { path = "../pythnet/pythnet_sdk/", version = "2.0.0", features = ["strum"] }
3435
rand = { version = "0.8.5" }
3536
reqwest = { version = "0.11.14", features = ["blocking", "json"] }
@@ -50,9 +51,9 @@ utoipa-swagger-ui = { version = "3.1.4", features = ["axum"] }
5051
wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
5152

5253
# We are bound to this Solana version in order to match pyth-oracle.
53-
solana-client = { version = "=1.13.3" }
54-
solana-sdk = { version = "=1.13.3" }
55-
solana-account-decoder = { version = "=1.13.3" }
54+
solana-client = { version = "=1.16.19" }
55+
solana-sdk = { version = "=1.16.19" }
56+
solana-account-decoder = { version = "=1.16.19" }
5657

5758

5859
[build-dependencies]

hermes/src/aggregate.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ where
431431

432432
pub async fn is_ready(state: &State) -> bool {
433433
let metadata = state.aggregate_state.read().await;
434+
let price_feeds_metadata = state.price_feeds_metadata.read().await;
434435

435436
let has_completed_recently = match metadata.latest_completed_update_at.as_ref() {
436437
Some(latest_completed_update_time) => {
@@ -449,7 +450,9 @@ pub async fn is_ready(state: &State) -> bool {
449450
_ => false,
450451
};
451452

452-
has_completed_recently && is_not_behind
453+
let is_metadata_loaded = !price_feeds_metadata.is_empty();
454+
455+
has_completed_recently && is_not_behind && is_metadata_loaded
453456
}
454457

455458
#[cfg(test)]

hermes/src/api.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
123123
rest::price_feed_ids,
124124
rest::latest_price_updates,
125125
rest::timestamp_price_updates,
126+
rest::price_feeds_metadata,
126127
),
127128
components(
128129
schemas(
@@ -139,6 +140,8 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
139140
types::BinaryPriceUpdate,
140141
types::ParsedPriceUpdate,
141142
types::RpcPriceFeedMetadataV2,
143+
types::PriceFeedMetadata,
144+
types::AssetType
142145
)
143146
),
144147
tags(
@@ -164,6 +167,7 @@ pub async fn run(opts: RunOptions, state: ApiState) -> Result<()> {
164167
"/v2/updates/price/:publish_time",
165168
get(rest::timestamp_price_updates),
166169
)
170+
.route("/v2/price_feeds", get(rest::price_feeds_metadata))
167171
.route("/live", get(rest::live))
168172
.route("/ready", get(rest::ready))
169173
.route("/ws", get(ws::ws_route_handler))

hermes/src/api/rest.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub use {
3333
ready::*,
3434
v2::{
3535
latest_price_updates::*,
36+
price_feeds_metadata::*,
3637
timestamp_price_updates::*,
3738
},
3839
};
@@ -43,6 +44,7 @@ pub enum RestError {
4344
CcipUpdateDataNotFound,
4445
InvalidCCIPInput,
4546
PriceIdsNotFound { missing_ids: Vec<PriceIdentifier> },
47+
RpcConnectionError { message: String },
4648
}
4749

4850
impl IntoResponse for RestError {
@@ -80,6 +82,9 @@ impl IntoResponse for RestError {
8082
)
8183
.into_response()
8284
}
85+
RestError::RpcConnectionError { message } => {
86+
(StatusCode::INTERNAL_SERVER_ERROR, message).into_response()
87+
}
8388
}
8489
}
8590
}

hermes/src/api/rest/index.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ pub async fn index() -> impl IntoResponse {
1818
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
1919
"/v2/updates/price/latest?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
2020
"/v2/updates/price/<timestamp>?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
21+
"/v2/price_feeds?(query=btc)(&asset_type=crypto|equity|fx|metal|rates)",
2122
])
2223
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod latest_price_updates;
2+
pub mod price_feeds_metadata;
23
pub mod timestamp_price_updates;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use {
2+
crate::{
3+
api::{
4+
rest::RestError,
5+
types::{
6+
AssetType,
7+
PriceFeedMetadata,
8+
},
9+
},
10+
price_feeds_metadata::get_price_feeds_metadata,
11+
},
12+
anyhow::Result,
13+
axum::{
14+
extract::State,
15+
Json,
16+
},
17+
serde::Deserialize,
18+
serde_qs::axum::QsQuery,
19+
utoipa::IntoParams,
20+
};
21+
22+
23+
#[derive(Debug, Deserialize, IntoParams)]
24+
#[into_params(parameter_in=Query)]
25+
pub struct PriceFeedsMetadataQueryParams {
26+
/// Optional query parameter. If provided, the results will be filtered to all price feeds whose symbol contains the query string. Query string is case insensitive.
27+
#[param(example = "bitcoin")]
28+
query: Option<String>,
29+
30+
/// Optional query parameter. If provided, the results will be filtered by asset type. Possible values are crypto, equity, fx, metal, rates. Filter string is case insensitive.
31+
#[param(example = "crypto")]
32+
asset_type: Option<AssetType>,
33+
}
34+
35+
/// Get the set of price feeds.
36+
///
37+
/// This endpoint fetches all price feeds from the Pyth network. It can be filtered by asset type
38+
/// and query string.
39+
#[utoipa::path(
40+
get,
41+
path = "/v2/price_feeds",
42+
responses(
43+
(status = 200, description = "Price feeds metadata retrieved successfully", body = Vec<RpcPriceIdentifier>)
44+
),
45+
params(
46+
PriceFeedsMetadataQueryParams
47+
)
48+
)]
49+
pub async fn price_feeds_metadata(
50+
State(state): State<crate::api::ApiState>,
51+
QsQuery(params): QsQuery<PriceFeedsMetadataQueryParams>,
52+
) -> Result<Json<Vec<PriceFeedMetadata>>, RestError> {
53+
let price_feeds_metadata =
54+
get_price_feeds_metadata(&*state.state, params.query, params.asset_type)
55+
.await
56+
.map_err(|e| {
57+
tracing::warn!("RPC connection error: {}", e);
58+
RestError::RpcConnectionError {
59+
message: format!("RPC connection error: {}", e),
60+
}
61+
})?;
62+
63+
Ok(Json(price_feeds_metadata))
64+
}

hermes/src/api/types.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ use {
2828
Deserialize,
2929
Serialize,
3030
},
31+
std::{
32+
collections::BTreeMap,
33+
fmt::{
34+
Display,
35+
Formatter,
36+
Result as FmtResult,
37+
},
38+
},
3139
utoipa::ToSchema,
3240
wormhole_sdk::Chain,
3341
};
@@ -52,7 +60,7 @@ impl From<PriceIdInput> for PriceIdentifier {
5260

5361
type Base64String = String;
5462

55-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
63+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
5664
pub struct RpcPriceFeedMetadata {
5765
#[schema(value_type = Option<u64>, example=85480034)]
5866
pub slot: Option<Slot>,
@@ -64,7 +72,7 @@ pub struct RpcPriceFeedMetadata {
6472
pub prev_publish_time: Option<UnixTimestamp>,
6573
}
6674

67-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
75+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
6876
pub struct RpcPriceFeedMetadataV2 {
6977
#[schema(value_type = Option<u64>, example=85480034)]
7078
pub slot: Option<Slot>,
@@ -74,7 +82,7 @@ pub struct RpcPriceFeedMetadataV2 {
7482
pub prev_publish_time: Option<UnixTimestamp>,
7583
}
7684

77-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
85+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
7886
pub struct RpcPriceFeed {
7987
pub id: RpcPriceIdentifier,
8088
pub price: RpcPrice,
@@ -142,8 +150,8 @@ impl RpcPriceFeed {
142150
Eq,
143151
BorshSerialize,
144152
BorshDeserialize,
145-
serde::Serialize,
146-
serde::Deserialize,
153+
Serialize,
154+
Deserialize,
147155
ToSchema,
148156
)]
149157
pub struct RpcPrice {
@@ -178,8 +186,8 @@ pub struct RpcPrice {
178186
Hash,
179187
BorshSerialize,
180188
BorshDeserialize,
181-
serde::Serialize,
182-
serde::Deserialize,
189+
Serialize,
190+
Deserialize,
183191
ToSchema,
184192
)]
185193
#[repr(C)]
@@ -204,7 +212,7 @@ impl From<PriceIdentifier> for RpcPriceIdentifier {
204212
}
205213
}
206214

207-
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, serde::Serialize, ToSchema)]
215+
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, ToSchema)]
208216
pub enum EncodingType {
209217
#[default]
210218
#[serde(rename = "hex")]
@@ -222,13 +230,13 @@ impl EncodingType {
222230
}
223231
}
224232

225-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
233+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
226234
pub struct BinaryPriceUpdate {
227235
pub encoding: EncodingType,
228236
pub data: Vec<String>,
229237
}
230238

231-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
239+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
232240
pub struct ParsedPriceUpdate {
233241
pub id: RpcPriceIdentifier,
234242
pub price: RpcPrice,
@@ -263,7 +271,7 @@ impl From<PriceFeedUpdate> for ParsedPriceUpdate {
263271
}
264272
}
265273

266-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
274+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
267275
pub struct PriceUpdate {
268276
pub binary: BinaryPriceUpdate,
269277
#[serde(skip_serializing_if = "Option::is_none")]
@@ -316,3 +324,27 @@ impl TryFrom<PriceUpdate> for PriceFeedsWithUpdateData {
316324
})
317325
}
318326
}
327+
328+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
329+
pub struct PriceFeedMetadata {
330+
pub id: PriceIdentifier,
331+
// BTreeMap is used to automatically sort the keys to ensure consistent ordering of attributes in the JSON response.
332+
// This enhances user experience by providing a predictable structure, avoiding confusion from varying orders in different responses.
333+
pub attributes: BTreeMap<String, String>,
334+
}
335+
336+
#[derive(Debug, Serialize, Deserialize, PartialEq, ToSchema)]
337+
#[serde(rename_all = "lowercase")]
338+
pub enum AssetType {
339+
Crypto,
340+
FX,
341+
Equity,
342+
Metals,
343+
Rates,
344+
}
345+
346+
impl Display for AssetType {
347+
fn fmt(&self, f: &mut Formatter) -> FmtResult {
348+
write!(f, "{:?}", self)
349+
}
350+
}

hermes/src/config/pythnet.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use clap::Args;
1+
use {
2+
clap::Args,
3+
solana_sdk::pubkey::Pubkey,
4+
};
5+
6+
const DEFAULT_PYTHNET_MAPPING_ADDR: &str = "AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J";
27

38
#[derive(Args, Clone, Debug)]
49
#[command(next_help_heading = "Pythnet Options")]
@@ -13,4 +18,10 @@ pub struct Options {
1318
#[arg(long = "pythnet-http-addr")]
1419
#[arg(env = "PYTHNET_HTTP_ADDR")]
1520
pub http_addr: String,
21+
22+
/// Pyth mapping account address.
23+
#[arg(long = "mapping-address")]
24+
#[arg(default_value = DEFAULT_PYTHNET_MAPPING_ADDR)]
25+
#[arg(env = "MAPPING_ADDRESS")]
26+
pub mapping_addr: Pubkey,
1627
}

0 commit comments

Comments
 (0)