Skip to content

Commit c951ff6

Browse files
ali-bahjatiReisen
andauthored
[hermes] Improve latest feeds rest api (#752)
* [hermes] Improve latest feeds rest api This change adds verbose and binary option to latest_price_feeds endpoint. Unfortunately it exposes many internal information which required touching different components to expose batch_vaa specific information. The code is now coupled to batch_vaa and we need to refactor it when we add other proof types (and eventually remove it when it get deprecated). * Update hermes/src/network/rpc/rest.rs Co-authored-by: Reisen <[email protected]> * Update hermes/src/network/rpc/rest.rs Co-authored-by: Reisen <[email protected]> * Fix merge --------- Co-authored-by: Reisen <[email protected]>
1 parent 3ad3a46 commit c951ff6

File tree

6 files changed

+126
-71
lines changed

6 files changed

+126
-71
lines changed

hermes/src/network/rpc.rs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use {
1414
};
1515

1616
mod rest;
17+
mod rpc_price_feed;
1718

1819
#[derive(Clone)]
1920
pub struct State {

hermes/src/network/rpc/rest.rs

+35-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
use {
2+
super::rpc_price_feed::{
3+
RpcPriceFeed,
4+
RpcPriceFeedMetadata,
5+
},
26
crate::store::RequestTime,
37
crate::{
48
impl_deserialize_for_hex_string_wrapper,
@@ -23,10 +27,7 @@ use {
2327
Deref,
2428
DerefMut,
2529
},
26-
pyth_sdk::{
27-
PriceFeed,
28-
PriceIdentifier,
29-
},
30+
pyth_sdk::PriceIdentifier,
3031
};
3132

3233
/// PriceIdInput is a wrapper around a 32-byte hex string.
@@ -93,8 +94,8 @@ pub async fn latest_vaas(
9394
.map_err(|_| RestError::UpdateDataNotFound)?;
9495
Ok(Json(
9596
price_feeds_with_update_data
96-
.update_data
9797
.batch_vaa
98+
.update_data
9899
.iter()
99100
.map(|vaa_bytes| base64_standard_engine.encode(vaa_bytes)) // TODO: Support multiple
100101
// encoding formats
@@ -104,22 +105,42 @@ pub async fn latest_vaas(
104105

105106
#[derive(Debug, serde::Deserialize)]
106107
pub struct LatestPriceFeedsQueryParams {
107-
ids: Vec<PriceIdInput>,
108+
ids: Vec<PriceIdInput>,
109+
#[serde(default)]
110+
verbose: bool,
111+
#[serde(default)]
112+
binary: bool,
108113
}
109114

110115
pub async fn latest_price_feeds(
111116
State(state): State<super::State>,
112117
Query(params): Query<LatestPriceFeedsQueryParams>,
113-
) -> Result<Json<Vec<PriceFeed>>, RestError> {
118+
) -> Result<Json<Vec<RpcPriceFeed>>, RestError> {
114119
let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
115120
let price_feeds_with_update_data = state
116121
.store
117122
.get_price_feeds_with_update_data(price_ids, RequestTime::Latest)
118123
.map_err(|_| RestError::UpdateDataNotFound)?;
119124
Ok(Json(
120125
price_feeds_with_update_data
121-
.price_feeds
126+
.batch_vaa
127+
.price_infos
122128
.into_values()
129+
.map(|price_info| {
130+
let mut rpc_price_feed: RpcPriceFeed = price_info.price_feed.into();
131+
rpc_price_feed.metadata = params.verbose.then_some(RpcPriceFeedMetadata {
132+
emitter_chain: price_info.emitter_chain,
133+
sequence_number: price_info.sequence_number,
134+
attestation_time: price_info.attestation_time,
135+
price_service_receive_time: price_info.receive_time,
136+
});
137+
138+
rpc_price_feed.vaa = params
139+
.binary
140+
.then_some(base64_standard_engine.encode(&price_info.vaa_bytes));
141+
142+
rpc_price_feed
143+
})
123144
.collect(),
124145
))
125146
}
@@ -152,20 +173,18 @@ pub async fn get_vaa(
152173
.map_err(|_| RestError::UpdateDataNotFound)?;
153174

154175
let vaa = price_feeds_with_update_data
155-
.update_data
156176
.batch_vaa
177+
.update_data
157178
.get(0)
158179
.map(|vaa_bytes| base64_standard_engine.encode(vaa_bytes))
159180
.ok_or(RestError::UpdateDataNotFound)?;
160181

161182
let publish_time = price_feeds_with_update_data
162-
.price_feeds
183+
.batch_vaa
184+
.price_infos
163185
.get(&price_id)
164-
.map(|price_feed| price_feed.get_price_unchecked().publish_time)
186+
.map(|price_info| price_info.publish_time)
165187
.ok_or(RestError::UpdateDataNotFound)?;
166-
let publish_time: UnixTimestamp = publish_time
167-
.try_into()
168-
.map_err(|_| RestError::UpdateDataNotFound)?;
169188

170189
Ok(Json(GetVaaResponse { vaa, publish_time }))
171190
}
@@ -197,13 +216,11 @@ pub async fn get_vaa_ccip(
197216
.map_err(|_| RestError::CcipUpdateDataNotFound)?;
198217

199218
let vaa = price_feeds_with_update_data
200-
.update_data
201219
.batch_vaa
220+
.update_data
202221
.get(0) // One price feed has only a single VAA as proof.
203222
.ok_or(RestError::UpdateDataNotFound)?;
204223

205-
// FIXME: We should return 5xx when the vaa is not found and 4xx when the price id is not there
206-
207224
Ok(Json(GetVaaCcipResponse {
208225
data: format!("0x{}", hex::encode(vaa)),
209226
}))
@@ -221,7 +238,7 @@ pub async fn index() -> impl IntoResponse {
221238
Json([
222239
"/live",
223240
"/api/price_feed_ids",
224-
"/api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..",
241+
"/api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&verbose=true)(&binary=true)",
225242
"/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&...",
226243
"/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
227244
"/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use {
2+
crate::store::UnixTimestamp,
3+
pyth_sdk::{
4+
Price,
5+
PriceFeed,
6+
PriceIdentifier,
7+
},
8+
};
9+
10+
type Base64String = String;
11+
12+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13+
pub struct RpcPriceFeedMetadata {
14+
pub emitter_chain: u16,
15+
pub attestation_time: UnixTimestamp,
16+
pub sequence_number: u64,
17+
pub price_service_receive_time: UnixTimestamp,
18+
}
19+
20+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21+
pub struct RpcPriceFeed {
22+
pub id: PriceIdentifier,
23+
pub price: Price,
24+
pub ema_price: Price,
25+
pub metadata: Option<RpcPriceFeedMetadata>,
26+
/// Vaa binary represented in base64.
27+
pub vaa: Option<Base64String>,
28+
}
29+
30+
impl From<PriceFeed> for RpcPriceFeed {
31+
fn from(price_feed: PriceFeed) -> Self {
32+
Self {
33+
id: price_feed.id,
34+
price: price_feed.get_price_unchecked(),
35+
ema_price: price_feed.get_ema_price_unchecked(),
36+
metadata: None,
37+
vaa: None,
38+
}
39+
}
40+
}

hermes/src/store.rs

+14-30
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
use {
2-
self::storage::Storage,
3-
anyhow::Result,
4-
pyth_sdk::{
5-
PriceFeed,
6-
PriceIdentifier,
7-
},
8-
serde::{
9-
Deserialize,
10-
Serialize,
11-
},
12-
std::{
13-
collections::HashMap,
14-
sync::Arc,
2+
self::{
3+
proof::batch_vaa::PriceInfosWithUpdateData,
4+
storage::Storage,
155
},
6+
anyhow::Result,
7+
pyth_sdk::PriceIdentifier,
8+
std::sync::Arc,
169
};
1710

1811
mod proof;
@@ -30,19 +23,8 @@ pub enum Update {
3023
Vaa(Vec<u8>),
3124
}
3225

33-
#[derive(Clone, Default, Serialize, Deserialize)]
34-
pub struct UpdateData {
35-
pub batch_vaa: Vec<Vec<u8>>,
36-
}
37-
38-
// TODO: A price feed might not have update data in all different
39-
// formats. For example, Batch VAA and Merkle updates will result
40-
// in different price feeds. We need to figure out how to handle
41-
// it properly.
42-
#[derive(Clone, Default)]
4326
pub struct PriceFeedsWithUpdateData {
44-
pub price_feeds: HashMap<PriceIdentifier, PriceFeed>,
45-
pub update_data: UpdateData,
27+
pub batch_vaa: PriceInfosWithUpdateData,
4628
}
4729

4830
pub type State = Arc<Box<dyn Storage>>;
@@ -75,11 +57,13 @@ impl Store {
7557
price_ids: Vec<PriceIdentifier>,
7658
request_time: RequestTime,
7759
) -> Result<PriceFeedsWithUpdateData> {
78-
proof::batch_vaa::get_price_feeds_with_update_data(
79-
self.state.clone(),
80-
price_ids,
81-
request_time,
82-
)
60+
Ok(PriceFeedsWithUpdateData {
61+
batch_vaa: proof::batch_vaa::get_price_infos_with_update_data(
62+
self.state.clone(),
63+
price_ids,
64+
request_time,
65+
)?,
66+
})
8367
}
8468

8569
pub fn get_price_feed_ids(&self) -> Vec<PriceIdentifier> {

hermes/src/store/proof/batch_vaa.rs

+36-19
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ use {
44
Key,
55
StorageData,
66
},
7-
PriceFeedsWithUpdateData,
87
RequestTime,
98
State,
109
UnixTimestamp,
11-
UpdateData,
1210
},
1311
anyhow::{
1412
anyhow,
@@ -24,38 +22,59 @@ use {
2422
PriceAttestation,
2523
PriceStatus,
2624
},
27-
std::collections::{
28-
HashMap,
29-
HashSet,
25+
std::{
26+
collections::{
27+
HashMap,
28+
HashSet,
29+
},
30+
time::{
31+
SystemTime,
32+
UNIX_EPOCH,
33+
},
3034
},
3135
wormhole::VAA,
3236
};
3337

3438
// TODO: We need to add more metadata to this struct.
3539
#[derive(Clone, Default, PartialEq, Debug)]
3640
pub struct PriceInfo {
37-
pub price_feed: PriceFeed,
38-
pub vaa_bytes: Vec<u8>,
39-
pub publish_time: UnixTimestamp,
41+
pub price_feed: PriceFeed,
42+
pub vaa_bytes: Vec<u8>,
43+
pub publish_time: UnixTimestamp,
44+
pub emitter_chain: u16,
45+
pub attestation_time: UnixTimestamp,
46+
pub receive_time: UnixTimestamp,
47+
pub sequence_number: u64,
4048
}
4149

50+
#[derive(Clone, Default)]
51+
pub struct PriceInfosWithUpdateData {
52+
pub price_infos: HashMap<PriceIdentifier, PriceInfo>,
53+
pub update_data: Vec<Vec<u8>>,
54+
}
4255

4356
pub fn store_vaa_update(state: State, vaa_bytes: Vec<u8>) -> Result<()> {
4457
// FIXME: Vaa bytes might not be a valid Pyth BatchUpdate message nor originate from Our emitter.
4558
// We should check that.
59+
// FIXME: We receive multiple vaas for the same update (due to different signedVAAs). We need
60+
// to drop them.
4661
let vaa = VAA::from_bytes(&vaa_bytes)?;
4762
let batch_price_attestation = BatchPriceAttestation::deserialize(vaa.payload.as_slice())
4863
.map_err(|_| anyhow!("Failed to deserialize VAA"))?;
4964

5065
for price_attestation in batch_price_attestation.price_attestations {
51-
let price_feed = price_attestation_to_price_feed(price_attestation);
66+
let price_feed = price_attestation_to_price_feed(price_attestation.clone());
5267

5368
let publish_time = price_feed.get_price_unchecked().publish_time.try_into()?;
5469

5570
let price_info = PriceInfo {
5671
price_feed,
5772
vaa_bytes: vaa_bytes.clone(),
5873
publish_time,
74+
emitter_chain: vaa.emitter_chain.into(),
75+
attestation_time: price_attestation.attestation_time.try_into()?,
76+
receive_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
77+
sequence_number: vaa.sequence,
5978
};
6079

6180
let key = Key::BatchVaa(price_feed.id);
@@ -65,32 +84,30 @@ pub fn store_vaa_update(state: State, vaa_bytes: Vec<u8>) -> Result<()> {
6584
}
6685

6786

68-
pub fn get_price_feeds_with_update_data(
87+
pub fn get_price_infos_with_update_data(
6988
state: State,
7089
price_ids: Vec<PriceIdentifier>,
7190
request_time: RequestTime,
72-
) -> Result<PriceFeedsWithUpdateData> {
73-
let mut price_feeds = HashMap::new();
91+
) -> Result<PriceInfosWithUpdateData> {
92+
let mut price_infos = HashMap::new();
7493
let mut vaas: HashSet<Vec<u8>> = HashSet::new();
7594
for price_id in price_ids {
7695
let key = Key::BatchVaa(price_id);
7796
let maybe_data = state.get(key, request_time.clone())?;
7897

7998
match maybe_data {
8099
Some(StorageData::BatchVaa(price_info)) => {
81-
price_feeds.insert(price_info.price_feed.id, price_info.price_feed);
82-
vaas.insert(price_info.vaa_bytes);
100+
vaas.insert(price_info.vaa_bytes.clone());
101+
price_infos.insert(price_id, price_info);
83102
}
84103
None => {
85104
return Err(anyhow!("No price feed found for price id: {:?}", price_id));
86105
}
87106
}
88107
}
89-
let update_data = UpdateData {
90-
batch_vaa: vaas.into_iter().collect(),
91-
};
92-
Ok(PriceFeedsWithUpdateData {
93-
price_feeds,
108+
let update_data: Vec<Vec<u8>> = vaas.into_iter().collect();
109+
Ok(PriceInfosWithUpdateData {
110+
price_infos,
94111
update_data,
95112
})
96113
}

hermes/src/store/storage.rs

-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ use {
55
UnixTimestamp,
66
},
77
anyhow::Result,
8-
derive_more::{
9-
Deref,
10-
DerefMut,
11-
},
128
pyth_sdk::PriceIdentifier,
139
};
1410

0 commit comments

Comments
 (0)