Skip to content

Commit 71ce456

Browse files
committed
refactor(hermes): move rpc endpoints into submodules
1 parent b74df4f commit 71ce456

File tree

10 files changed

+540
-379
lines changed

10 files changed

+540
-379
lines changed

hermes/src/api/rest.rs

Lines changed: 33 additions & 379 deletions
Large diffs are not rendered by default.

hermes/src/api/rest/get_price_feed.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use {
2+
crate::{
3+
api::{
4+
rest::RestError,
5+
types::{
6+
PriceIdInput,
7+
RpcPriceFeed,
8+
},
9+
},
10+
doc_examples,
11+
store::types::{
12+
RequestTime,
13+
UnixTimestamp,
14+
},
15+
},
16+
anyhow::Result,
17+
axum::{
18+
extract::State,
19+
Json,
20+
},
21+
pyth_sdk::PriceIdentifier,
22+
serde_qs::axum::QsQuery,
23+
utoipa::IntoParams,
24+
};
25+
26+
#[derive(Debug, serde::Deserialize, IntoParams)]
27+
#[into_params(parameter_in=Query)]
28+
pub struct GetPriceFeedQueryParams {
29+
/// The id of the price feed to get an update for.
30+
id: PriceIdInput,
31+
32+
/// The unix timestamp in seconds. This endpoint will return the first update whose
33+
/// publish_time is >= the provided value.
34+
#[param(value_type = i64)]
35+
#[param(example = doc_examples::timestamp_example)]
36+
publish_time: UnixTimestamp,
37+
38+
/// If true, include the `metadata` field in the response with additional metadata about the
39+
/// price update.
40+
#[serde(default)]
41+
verbose: bool,
42+
43+
/// If true, include the binary price update in the `vaa` field of each returned feed. This
44+
/// binary data can be submitted to Pyth contracts to update the on-chain price.
45+
#[serde(default)]
46+
binary: bool,
47+
}
48+
49+
/// Get a price update for a price feed with a specific timestamp
50+
///
51+
/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp.
52+
#[utoipa::path(
53+
get,
54+
path = "/api/get_price_feed",
55+
responses(
56+
(status = 200, description = "Price update retrieved successfully", body = RpcPriceFeed)
57+
),
58+
params(
59+
GetPriceFeedQueryParams
60+
)
61+
)]
62+
pub async fn get_price_feed(
63+
State(state): State<crate::api::State>,
64+
QsQuery(params): QsQuery<GetPriceFeedQueryParams>,
65+
) -> Result<Json<RpcPriceFeed>, RestError> {
66+
let price_id: PriceIdentifier = params.id.into();
67+
68+
let price_feeds_with_update_data = state
69+
.store
70+
.get_price_feeds_with_update_data(
71+
vec![price_id],
72+
RequestTime::FirstAfter(params.publish_time),
73+
)
74+
.await
75+
.map_err(|_| RestError::UpdateDataNotFound)?;
76+
77+
Ok(Json(RpcPriceFeed::from_price_feed_update(
78+
price_feeds_with_update_data
79+
.price_feeds
80+
.into_iter()
81+
.next()
82+
.ok_or(RestError::UpdateDataNotFound)?,
83+
params.verbose,
84+
params.binary,
85+
)))
86+
}

hermes/src/api/rest/get_vaa.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use {
2+
crate::{
3+
api::{
4+
rest::RestError,
5+
types::PriceIdInput,
6+
},
7+
doc_examples,
8+
store::types::{
9+
RequestTime,
10+
UnixTimestamp,
11+
},
12+
},
13+
anyhow::Result,
14+
axum::{
15+
extract::State,
16+
Json,
17+
},
18+
base64::{
19+
engine::general_purpose::STANDARD as base64_standard_engine,
20+
Engine as _,
21+
},
22+
pyth_sdk::PriceIdentifier,
23+
serde_qs::axum::QsQuery,
24+
utoipa::{
25+
IntoParams,
26+
ToSchema,
27+
},
28+
};
29+
30+
#[derive(Debug, serde::Deserialize, IntoParams)]
31+
#[into_params(parameter_in=Query)]
32+
pub struct GetVaaQueryParams {
33+
/// The ID of the price feed to get an update for.
34+
id: PriceIdInput,
35+
36+
/// The unix timestamp in seconds. This endpoint will return the first update whose
37+
/// publish_time is >= the provided value.
38+
#[param(value_type = i64)]
39+
#[param(example = 1690576641)]
40+
publish_time: UnixTimestamp,
41+
}
42+
43+
#[derive(Debug, serde::Serialize, ToSchema)]
44+
pub struct GetVaaResponse {
45+
/// The VAA binary represented as a base64 string.
46+
#[schema(example = doc_examples::vaa_example)]
47+
vaa: String,
48+
49+
#[serde(rename = "publishTime")]
50+
#[schema(value_type = i64)]
51+
#[schema(example = 1690576641)]
52+
publish_time: UnixTimestamp,
53+
}
54+
55+
/// Get a VAA for a price feed with a specific timestamp
56+
///
57+
/// Given a price feed id and timestamp, retrieve the Pyth price update closest to that timestamp.
58+
#[utoipa::path(
59+
get,
60+
path = "/api/get_vaa",
61+
responses(
62+
(status = 200, description = "Price update retrieved successfully", body = GetVaaResponse),
63+
(status = 404, description = "Price update not found", body = String)
64+
),
65+
params(
66+
GetVaaQueryParams
67+
)
68+
)]
69+
pub async fn get_vaa(
70+
State(state): State<crate::api::State>,
71+
QsQuery(params): QsQuery<GetVaaQueryParams>,
72+
) -> Result<Json<GetVaaResponse>, RestError> {
73+
let price_id: PriceIdentifier = params.id.into();
74+
75+
let price_feeds_with_update_data = state
76+
.store
77+
.get_price_feeds_with_update_data(
78+
vec![price_id],
79+
RequestTime::FirstAfter(params.publish_time),
80+
)
81+
.await
82+
.map_err(|_| RestError::UpdateDataNotFound)?;
83+
84+
let vaa = price_feeds_with_update_data
85+
.wormhole_merkle_update_data
86+
.get(0)
87+
.map(|bytes| base64_standard_engine.encode(bytes))
88+
.ok_or(RestError::UpdateDataNotFound)?;
89+
90+
let publish_time = price_feeds_with_update_data
91+
.price_feeds
92+
.get(0)
93+
.ok_or(RestError::UpdateDataNotFound)?
94+
.price_feed
95+
.publish_time;
96+
97+
Ok(Json(GetVaaResponse { vaa, publish_time }))
98+
}

hermes/src/api/rest/get_vaa_ccip.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use {
2+
crate::{
3+
api::rest::RestError,
4+
impl_deserialize_for_hex_string_wrapper,
5+
store::types::{
6+
RequestTime,
7+
UnixTimestamp,
8+
},
9+
},
10+
anyhow::Result,
11+
axum::{
12+
extract::State,
13+
Json,
14+
},
15+
derive_more::{
16+
Deref,
17+
DerefMut,
18+
},
19+
pyth_sdk::PriceIdentifier,
20+
serde_qs::axum::QsQuery,
21+
utoipa::{
22+
IntoParams,
23+
ToSchema,
24+
},
25+
};
26+
27+
#[derive(Debug, Clone, Deref, DerefMut, ToSchema)]
28+
pub struct GetVaaCcipInput([u8; 40]);
29+
impl_deserialize_for_hex_string_wrapper!(GetVaaCcipInput, 40);
30+
31+
#[derive(Debug, serde::Deserialize, IntoParams)]
32+
#[into_params(parameter_in=Query)]
33+
pub struct GetVaaCcipQueryParams {
34+
data: GetVaaCcipInput,
35+
}
36+
37+
#[derive(Debug, serde::Serialize, ToSchema)]
38+
pub struct GetVaaCcipResponse {
39+
data: String, // TODO: Use a typed wrapper for the hex output with leading 0x.
40+
}
41+
42+
/// Get a VAA for a price feed using CCIP
43+
///
44+
/// This endpoint accepts a single argument which is a hex-encoded byte string of the following form:
45+
/// `<price feed id (32 bytes> <publish time as unix timestamp (8 bytes, big endian)>`
46+
#[utoipa::path(
47+
get,
48+
path = "/api/get_vaa_ccip",
49+
responses(
50+
(status = 200, description = "Price update retrieved successfully", body = GetVaaCcipResponse)
51+
),
52+
params(
53+
GetVaaCcipQueryParams
54+
)
55+
)]
56+
pub async fn get_vaa_ccip(
57+
State(state): State<crate::api::State>,
58+
QsQuery(params): QsQuery<GetVaaCcipQueryParams>,
59+
) -> Result<Json<GetVaaCcipResponse>, RestError> {
60+
let price_id: PriceIdentifier = PriceIdentifier::new(
61+
params.data[0..32]
62+
.try_into()
63+
.map_err(|_| RestError::InvalidCCIPInput)?,
64+
);
65+
let publish_time = UnixTimestamp::from_be_bytes(
66+
params.data[32..40]
67+
.try_into()
68+
.map_err(|_| RestError::InvalidCCIPInput)?,
69+
);
70+
71+
let price_feeds_with_update_data = state
72+
.store
73+
.get_price_feeds_with_update_data(vec![price_id], RequestTime::FirstAfter(publish_time))
74+
.await
75+
.map_err(|_| RestError::CcipUpdateDataNotFound)?;
76+
77+
let bytes = price_feeds_with_update_data
78+
.wormhole_merkle_update_data
79+
.get(0) // One price feed has only a single VAA as proof.
80+
.ok_or(RestError::UpdateDataNotFound)?;
81+
82+
Ok(Json(GetVaaCcipResponse {
83+
data: format!("0x{}", hex::encode(bytes)),
84+
}))
85+
}

hermes/src/api/rest/index.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use axum::{
2+
response::IntoResponse,
3+
Json,
4+
};
5+
6+
/// This is the index page for the REST service. It lists all the available endpoints.
7+
///
8+
/// TODO: Dynamically generate this list if possible.
9+
pub async fn index() -> impl IntoResponse {
10+
Json([
11+
"/live",
12+
"/ready",
13+
"/api/price_feed_ids",
14+
"/api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&verbose=true)(&binary=true)",
15+
"/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&...",
16+
"/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
17+
"/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
18+
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
19+
])
20+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use {
2+
crate::{
3+
api::{
4+
rest::RestError,
5+
types::{
6+
PriceIdInput,
7+
RpcPriceFeed,
8+
},
9+
},
10+
store::types::RequestTime,
11+
},
12+
anyhow::Result,
13+
axum::{
14+
extract::State,
15+
Json,
16+
},
17+
pyth_sdk::PriceIdentifier,
18+
serde_qs::axum::QsQuery,
19+
utoipa::IntoParams,
20+
};
21+
22+
#[derive(Debug, serde::Deserialize, IntoParams)]
23+
#[into_params(parameter_in=Query)]
24+
pub struct LatestPriceFeedsQueryParams {
25+
/// Get the most recent price update for this set of price feed ids.
26+
///
27+
/// This parameter can be provided multiple times to retrieve multiple price updates,
28+
/// for example see the following query string:
29+
///
30+
/// ```
31+
/// ?ids[]=a12...&ids[]=b4c...
32+
/// ```
33+
#[param(rename = "ids[]")]
34+
#[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")]
35+
ids: Vec<PriceIdInput>,
36+
37+
/// If true, include the `metadata` field in the response with additional metadata about
38+
/// the price update.
39+
#[serde(default)]
40+
verbose: bool,
41+
42+
/// If true, include the binary price update in the `vaa` field of each returned feed.
43+
/// This binary data can be submitted to Pyth contracts to update the on-chain price.
44+
#[serde(default)]
45+
binary: bool,
46+
}
47+
48+
/// Get the latest price updates by price feed id.
49+
///
50+
/// Given a collection of price feed ids, retrieve the latest Pyth price for each price feed.
51+
#[utoipa::path(
52+
get,
53+
path = "/api/latest_price_feeds",
54+
responses(
55+
(status = 200, description = "Price updates retrieved successfully", body = Vec<RpcPriceFeed>)
56+
),
57+
params(
58+
LatestPriceFeedsQueryParams
59+
)
60+
)]
61+
pub async fn latest_price_feeds(
62+
State(state): State<crate::api::State>,
63+
QsQuery(params): QsQuery<LatestPriceFeedsQueryParams>,
64+
) -> Result<Json<Vec<RpcPriceFeed>>, RestError> {
65+
let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
66+
let price_feeds_with_update_data = state
67+
.store
68+
.get_price_feeds_with_update_data(price_ids, RequestTime::Latest)
69+
.await
70+
.map_err(|_| RestError::UpdateDataNotFound)?;
71+
72+
Ok(Json(
73+
price_feeds_with_update_data
74+
.price_feeds
75+
.into_iter()
76+
.map(|price_feed| {
77+
RpcPriceFeed::from_price_feed_update(price_feed, params.verbose, params.binary)
78+
})
79+
.collect(),
80+
))
81+
}

0 commit comments

Comments
 (0)