Skip to content
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

feat: update hermes to support publisher stake caps #1866

Merged
merged 13 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/hermes/server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/hermes/server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hermes"
version = "0.5.17"
version = "0.6.0"
description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle."
edition = "2021"

Expand Down
7 changes: 6 additions & 1 deletion apps/hermes/server/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ where
rest::latest_vaas,
rest::price_feed_ids,
rest::latest_price_updates,
rest::latest_publisher_stake_caps,
rest::timestamp_price_updates,
rest::price_feeds_metadata,
rest::price_stream_sse_handler,
Expand All @@ -127,7 +128,7 @@ where
types::RpcPriceIdentifier,
types::EncodingType,
types::PriceUpdate,
types::BinaryPriceUpdate,
types::BinaryUpdate,
types::ParsedPriceUpdate,
types::RpcPriceFeedMetadataV2,
types::PriceFeedMetadata,
Expand Down Expand Up @@ -158,6 +159,10 @@ where
get(rest::price_stream_sse_handler),
)
.route("/v2/updates/price/latest", get(rest::latest_price_updates))
.route(
"/v2/updates/publisher_stake_caps/latest",
get(rest::latest_publisher_stake_caps),
)
.route(
"/v2/updates/price/:publish_time",
get(rest::timestamp_price_updates),
Expand Down
1 change: 1 addition & 0 deletions apps/hermes/server/src/api/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub use {
ready::*,
v2::{
latest_price_updates::*,
latest_publisher_stake_caps::*,
price_feeds_metadata::*,
sse::*,
timestamp_price_updates::*,
Expand Down
4 changes: 2 additions & 2 deletions apps/hermes/server/src/api/rest/v2/latest_price_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {
RestError,
},
types::{
BinaryPriceUpdate,
BinaryUpdate,
EncodingType,
ParsedPriceUpdate,
PriceIdInput,
Expand Down Expand Up @@ -108,7 +108,7 @@ where
EncodingType::Hex => hex::encode(data),
})
.collect();
let binary_price_update = BinaryPriceUpdate {
let binary_price_update = BinaryUpdate {
encoding: params.encoding,
data: encoded_data,
};
Expand Down
114 changes: 114 additions & 0 deletions apps/hermes/server/src/api/rest/v2/latest_publisher_stake_caps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use {
crate::{
api::{
rest::RestError,
types::{
BinaryUpdate,
EncodingType,
ParsedPublisherStakeCapsUpdate,
},
ApiState,
},
state::Aggregates,
},
anyhow::Result,
axum::{
extract::State,
Json,
},
base64::{
engine::general_purpose::STANDARD as base64_standard_engine,
Engine as _,
},
serde::{
Deserialize,
Serialize,
},
serde_qs::axum::QsQuery,
utoipa::{
IntoParams,
ToSchema,
},
};


#[derive(Debug, Deserialize, IntoParams)]
#[into_params(parameter_in=Query)]
pub struct LatestPublisherStakeCapsUpdateData {
/// Get the most recent publisher stake caps update data.

/// Optional encoding type. If true, return the message in the encoding specified by the encoding parameter. Default is `hex`.
#[serde(default)]
encoding: EncodingType,

/// If true, include the parsed update in the `parsed` field of each returned feed. Default is `true`.
#[serde(default = "default_true")]
parsed: bool,
}

fn default_true() -> bool {
true
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct LatestPublisherStakeCapsUpdateDataResponse {
pub binary: BinaryUpdate,
#[serde(skip_serializing_if = "Option::is_none")]
pub parsed: Option<Vec<ParsedPublisherStakeCapsUpdate>>,
}

/// Get the most recent publisher stake caps update data.
#[utoipa::path(
get,
path = "/v2/updates/publisher_stake_caps/latest",
responses(
(status = 200, description = "Publisher stake caps update data retrieved succesfully", body = Vec<PriceFeedMetadata>)
),
params(
LatestPublisherStakeCapsUpdateData
)
)]
pub async fn latest_publisher_stake_caps<S>(
State(state): State<ApiState<S>>,
QsQuery(params): QsQuery<LatestPublisherStakeCapsUpdateData>,
) -> Result<Json<LatestPublisherStakeCapsUpdateDataResponse>, RestError>
where
S: Aggregates,
{
let state = &*state.state;
let publisher_stake_caps_with_update_data =
Aggregates::get_latest_publisher_stake_caps_with_update_data(state)
.await
.map_err(|e| {
tracing::warn!(
"Error getting publisher stake caps with update data: {:?}",
e
);
RestError::UpdateDataNotFound
})?;

let encoded_data: Vec<String> = publisher_stake_caps_with_update_data
.update_data
.into_iter()
.map(|data| match params.encoding {
EncodingType::Base64 => base64_standard_engine.encode(data),
EncodingType::Hex => hex::encode(data),
})
.collect();

let binary = BinaryUpdate {
encoding: params.encoding,
data: encoded_data,
};

let parsed: Option<Vec<ParsedPublisherStakeCapsUpdate>> = if params.parsed {
Some(publisher_stake_caps_with_update_data.publisher_stake_caps)
} else {
None
};

Ok(Json(LatestPublisherStakeCapsUpdateDataResponse {
binary,
parsed,
}))
}
1 change: 1 addition & 0 deletions apps/hermes/server/src/api/rest/v2/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod latest_price_updates;
pub mod latest_publisher_stake_caps;
pub mod price_feeds_metadata;
pub mod sse;
pub mod timestamp_price_updates;
4 changes: 2 additions & 2 deletions apps/hermes/server/src/api/rest/v2/sse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {
RestError,
},
types::{
BinaryPriceUpdate,
BinaryUpdate,
EncodingType,
ParsedPriceUpdate,
PriceIdInput,
Expand Down Expand Up @@ -211,7 +211,7 @@ where
.into_iter()
.map(|data| encoding.encode_str(&data))
.collect();
let binary_price_update = BinaryPriceUpdate {
let binary_price_update = BinaryUpdate {
encoding,
data: encoded_data,
};
Expand Down
4 changes: 2 additions & 2 deletions apps/hermes/server/src/api/rest/v2/timestamp_price_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use {
RestError,
},
types::{
BinaryPriceUpdate,
BinaryUpdate,
EncodingType,
ParsedPriceUpdate,
PriceIdInput,
Expand Down Expand Up @@ -123,7 +123,7 @@ where
.into_iter()
.map(|data| query_params.encoding.encode_str(&data))
.collect();
let binary_price_update = BinaryPriceUpdate {
let binary_price_update = BinaryUpdate {
encoding: query_params.encoding,
data: encoded_data,
};
Expand Down
18 changes: 16 additions & 2 deletions apps/hermes/server/src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use {
Deserialize,
Serialize,
},
solana_sdk::pubkey::Pubkey,
std::{
collections::BTreeMap,
fmt::{
Expand All @@ -40,6 +41,7 @@ use {
wormhole_sdk::Chain,
};


/// A price id is a 32-byte hex string, optionally prefixed with "0x".
/// Price ids are case insensitive.
///
Expand Down Expand Up @@ -231,7 +233,7 @@ impl EncodingType {
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct BinaryPriceUpdate {
pub struct BinaryUpdate {
pub encoding: EncodingType,
pub data: Vec<String>,
}
Expand Down Expand Up @@ -271,9 +273,21 @@ impl From<PriceFeedUpdate> for ParsedPriceUpdate {
}
}

#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, Clone)]
pub struct ParsedPublisherStakeCapsUpdate {
pub publisher_stake_caps: Vec<ParsedPublisherStakeCap>,
}

#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, Clone)]
pub struct ParsedPublisherStakeCap {
#[serde(with = "pyth_sdk::utils::as_string")]
pub publisher: Pubkey,
pub cap: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PriceUpdate {
pub binary: BinaryPriceUpdate,
pub binary: BinaryUpdate,
#[serde(skip_serializing_if = "Option::is_none")]
pub parsed: Option<Vec<ParsedPriceUpdate>>,
}
Expand Down
52 changes: 51 additions & 1 deletion apps/hermes/server/src/state/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use {
WormholeMerkleState,
},
crate::{
api::types::{
ParsedPublisherStakeCap,
ParsedPublisherStakeCapsUpdate,
},
network::wormhole::VaaBytes,
state::{
benchmarks::Benchmarks,
Expand Down Expand Up @@ -45,6 +49,7 @@ use {
messages::{
Message,
MessageType,
PUBLISHER_STAKE_CAPS_MESSAGE_FEED_ID,
},
wire::{
from_slice,
Expand All @@ -55,6 +60,7 @@ use {
},
},
serde::Serialize,
solana_sdk::pubkey::Pubkey,
std::{
collections::HashSet,
time::Duration,
Expand All @@ -68,7 +74,6 @@ use {
},
wormhole_sdk::Vaa,
};

pub mod metrics;
pub mod wormhole_merkle;

Expand Down Expand Up @@ -215,6 +220,12 @@ pub struct PriceFeedsWithUpdateData {
pub update_data: Vec<Vec<u8>>,
}

#[derive(Debug, PartialEq)]
pub struct PublisherStakeCapsWithUpdateData {
pub publisher_stake_caps: Vec<ParsedPublisherStakeCapsUpdate>,
pub update_data: Vec<Vec<u8>>,
}

#[derive(Debug, Serialize)]
pub struct ReadinessMetadata {
pub has_completed_recently: bool,
Expand Down Expand Up @@ -242,6 +253,9 @@ where
price_ids: &[PriceIdentifier],
request_time: RequestTime,
) -> Result<PriceFeedsWithUpdateData>;
async fn get_latest_publisher_stake_caps_with_update_data(
&self,
) -> Result<PublisherStakeCapsWithUpdateData>;
}

/// Allow downcasting State into CacheState for functions that depend on the `Cache` service.
Expand Down Expand Up @@ -403,10 +417,46 @@ where
}
}

async fn get_latest_publisher_stake_caps_with_update_data(
&self,
) -> Result<PublisherStakeCapsWithUpdateData> {
let messages = self
.fetch_message_states(
vec![PUBLISHER_STAKE_CAPS_MESSAGE_FEED_ID],
RequestTime::Latest,
MessageStateFilter::Only(MessageType::PublisherStakeCapsMessage),
)
.await?;

let publisher_stake_caps = messages
.iter()
.map(|message_state| match message_state.message.clone() {
Message::PublisherStakeCapsMessage(message) => Ok(ParsedPublisherStakeCapsUpdate {
publisher_stake_caps: message
.caps
.iter()
.map(|cap| ParsedPublisherStakeCap {
publisher: Pubkey::from(cap.publisher),
cap: cap.cap,
})
.collect(),
}),
_ => Err(anyhow!("Invalid message state type")),
})
.collect::<Result<Vec<_>>>()?;

let update_data = construct_update_data(messages.into_iter().map(|m| m.into()).collect())?;
Ok(PublisherStakeCapsWithUpdateData {
publisher_stake_caps,
update_data,
})
}

async fn get_price_feed_ids(&self) -> HashSet<PriceIdentifier> {
guibescos marked this conversation as resolved.
Show resolved Hide resolved
Cache::message_state_keys(self)
.await
.iter()
.filter(|key| key.feed_id != PUBLISHER_STAKE_CAPS_MESSAGE_FEED_ID)
.map(|key| PriceIdentifier::new(key.feed_id))
.collect()
}
Expand Down
Loading