diff --git a/Cargo.lock b/Cargo.lock index b95f060418..dd07317670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,7 @@ dependencies = [ "penumbra-proto", "prost", "rand 0.8.5", + "regex", "serde", "sha2 0.10.8", "tempfile", @@ -811,7 +812,9 @@ dependencies = [ "hex", "ibc-proto", "ibc-types", + "indexmap 2.4.0", "insta", + "is_sorted", "maplit", "matchit", "paste", @@ -936,6 +939,8 @@ dependencies = [ "hex", "indenter", "itertools 0.12.1", + "maplit", + "pbjson-types", "predicates", "prost", "rlp", @@ -4544,6 +4549,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_sorted" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357376465c37db3372ef6a00585d336ed3d0f11d4345eef77ebcb05865392b21" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" diff --git a/charts/sequencer/Chart.yaml b/charts/sequencer/Chart.yaml index cd718ae1dc..54b04be575 100644 --- a/charts/sequencer/Chart.yaml +++ b/charts/sequencer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.0 +version: 1.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. diff --git a/charts/sequencer/files/cometbft/config/genesis.json b/charts/sequencer/files/cometbft/config/genesis.json index 9320326e2d..f76d4f3f2d 100644 --- a/charts/sequencer/files/cometbft/config/genesis.json +++ b/charts/sequencer/files/cometbft/config/genesis.json @@ -119,9 +119,24 @@ {{- if $index }},{{- end }} {{ include "sequencer.address" $value }} {{- end }} - ] + ], {{- if not .Values.global.dev }} {{- else }} + "connect": { + "marketMap": { + "marketMap": { + "markets": {} + }, + "params": { + "marketAuthorities": [], + "admin": "{{ .Values.genesis.marketAdminAddress }}" + } + }, + "oracle": { + "currencyPairGenesis": [], + "nextId": "0" + } + } {{- end}} }, "chain_id": "{{ .Values.genesis.chainId }}", @@ -142,7 +157,16 @@ }, "version": { "app": "0" + }, + {{- if not .Values.global.dev }} + "abci": { + "vote_extensions_enable_height": "0" } + {{- else }} + "abci": { + "vote_extensions_enable_height": "1" + } + {{- end}} }, "genesis_time": "{{ .Values.genesis.genesisTime }}", "initial_height": "0", diff --git a/charts/sequencer/templates/configmaps.yaml b/charts/sequencer/templates/configmaps.yaml index 89f9deedea..f666257a1d 100644 --- a/charts/sequencer/templates/configmaps.yaml +++ b/charts/sequencer/templates/configmaps.yaml @@ -73,7 +73,10 @@ data: OTEL_EXPORTER_OTLP_HEADERS: "{{ .Values.sequencer.otel.otlpHeaders }}" OTEL_EXPORTER_OTLP_TRACE_HEADERS: "{{ .Values.sequencer.otel.traceHeaders }}" OTEL_SERVICE_NAME: "{{ tpl .Values.sequencer.otel.serviceName . }}" + ASTRIA_SEQUENCER_ORACLE_GRPC_ADDR: "http://127.0.0.1:{{ .Values.ports.oracleGrpc }}" + ASTRIA_SEQUENCER_ORACLE_CLIENT_TIMEOUT_MILLISECONDS: "{{ .Values.sequencer.oracle.clientTimeout }}" {{- if not .Values.global.dev }} {{- else }} + ASTRIA_SEQUENCER_NO_ORACLE: "true" {{- end }} --- diff --git a/charts/sequencer/values.yaml b/charts/sequencer/values.yaml index 80bd795258..8d01e06b7d 100644 --- a/charts/sequencer/values.yaml +++ b/charts/sequencer/values.yaml @@ -34,6 +34,7 @@ genesis: base: "astria" ibcCompat: "astriacompat" authoritySudoAddress: "" + marketAdminAddress: "" allowedFeeAssets: [] # - nria ibc: @@ -108,6 +109,8 @@ sequencer: mempool: parked: maxTxCount: 200 + oracle: + clientTimeout: 1000 metrics: enabled: false otel: @@ -271,9 +274,11 @@ ports: cometbftRpc: 26657 cometbftMetrics: 26660 sequencerABCI: 26658 + # note: the oracle sidecar also uses 8080 by default but can be changed with --port sequencerGrpc: 8080 relayerRpc: 2450 sequencerMetrics: 9000 + oracleGrpc: 8081 # ServiceMonitor configuration serviceMonitor: diff --git a/crates/astria-core/Cargo.toml b/crates/astria-core/Cargo.toml index a7a45d5ba0..18e164fedb 100644 --- a/crates/astria-core/Cargo.toml +++ b/crates/astria-core/Cargo.toml @@ -36,6 +36,7 @@ penumbra-ibc = { workspace = true } penumbra-proto = { workspace = true } prost = { workspace = true } rand = { workspace = true } +regex = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } sha2 = { workspace = true } tendermint = { workspace = true } diff --git a/crates/astria-core/src/connect/abci.rs b/crates/astria-core/src/connect/abci.rs new file mode 100644 index 0000000000..0394cec7fb --- /dev/null +++ b/crates/astria-core/src/connect/abci.rs @@ -0,0 +1,74 @@ +pub mod v2 { + use bytes::Bytes; + use indexmap::IndexMap; + + use crate::{ + connect::types::v2::{ + CurrencyPairId, + Price, + }, + generated::connect::abci::v2 as raw, + }; + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct OracleVoteExtensionError(#[from] OracleVoteExtensionErrorKind); + + #[derive(Debug, thiserror::Error)] + #[error("failed to validate connect.abci.v2.OracleVoteExtension")] + enum OracleVoteExtensionErrorKind { + #[error("failed decoding price value in .prices field for key `{id}`")] + DecodePrice { + id: u64, + source: crate::connect::types::v2::DecodePriceError, + }, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct OracleVoteExtension { + pub prices: IndexMap, + } + + impl OracleVoteExtension { + /// Converts an on-wire [`raw::OracleVoteExtension`] to a validated domain type + /// [`OracleVoteExtension`]. + /// + /// # Errors + /// Returns an error if a value in the `.prices` map could not be validated. + pub fn try_from_raw( + raw: raw::OracleVoteExtension, + ) -> Result { + let prices = raw + .prices + .into_iter() + .map(|(id, price)| { + let price = Price::try_from(price).map_err(|source| { + OracleVoteExtensionErrorKind::DecodePrice { + id, + source, + } + })?; + Ok::<_, OracleVoteExtensionErrorKind>((CurrencyPairId::new(id), price)) + }) + .collect::>()?; + Ok(Self { + prices, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::OracleVoteExtension { + fn encode_price(input: Price) -> Bytes { + Bytes::copy_from_slice(&input.get().to_be_bytes()) + } + + raw::OracleVoteExtension { + prices: self + .prices + .into_iter() + .map(|(id, price)| (id.get(), encode_price(price))) + .collect(), + } + } + } +} diff --git a/crates/astria-core/src/connect/market_map.rs b/crates/astria-core/src/connect/market_map.rs new file mode 100644 index 0000000000..19b9249294 --- /dev/null +++ b/crates/astria-core/src/connect/market_map.rs @@ -0,0 +1,580 @@ +pub mod v2 { + use std::str::FromStr; + + use indexmap::IndexMap; + + use crate::{ + connect::types::v2::{ + CurrencyPair, + CurrencyPairError, + }, + generated::connect::marketmap::v2 as raw, + primitive::v1::{ + Address, + AddressError, + }, + Protobuf, + }; + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::GenesisState", into = "raw::GenesisState") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct GenesisState { + pub market_map: MarketMap, + pub last_updated: u64, + pub params: Params, + } + + impl TryFrom for GenesisState { + type Error = GenesisStateError; + + fn try_from(raw: raw::GenesisState) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::GenesisState { + fn from(genesis_state: GenesisState) -> Self { + genesis_state.into_raw() + } + } + + impl Protobuf for GenesisState { + type Error = GenesisStateError; + type Raw = raw::GenesisState; + + fn try_from_raw_ref(raw: &raw::GenesisState) -> Result { + Self::try_from_raw(raw.clone()) + } + + /// Converts from a raw protobuf `GenesisState` to a native `GenesisState`. + /// + /// # Errors + /// + /// - if the `market_map` field is missing + /// - if the `market_map` field is invalid + /// - if the `params` field is missing + /// - if the `params` field is invalid + fn try_from_raw(raw: raw::GenesisState) -> Result { + let Some(market_map) = raw + .market_map + .map(MarketMap::try_from_raw) + .transpose() + .map_err(GenesisStateError::invalid_market_map)? + else { + return Err(GenesisStateError::missing_market_map()); + }; + let last_updated = raw.last_updated; + let Some(params) = raw + .params + .map(Params::try_from_raw) + .transpose() + .map_err(GenesisStateError::invalid_params)? + else { + return Err(GenesisStateError::missing_params()); + }; + Ok(Self { + market_map, + last_updated, + params, + }) + } + + fn to_raw(&self) -> raw::GenesisState { + self.clone().into_raw() + } + + #[must_use] + fn into_raw(self) -> raw::GenesisState { + raw::GenesisState { + market_map: Some(self.market_map.into_raw()), + last_updated: self.last_updated, + params: Some(self.params.into_raw()), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct GenesisStateError(GenesisStateErrorKind); + + impl GenesisStateError { + #[must_use] + pub fn missing_market_map() -> Self { + Self(GenesisStateErrorKind::MissingMarketMap) + } + + #[must_use] + pub fn invalid_market_map(err: MarketMapError) -> Self { + Self(GenesisStateErrorKind::MarketMapParseError(err)) + } + + #[must_use] + pub fn missing_params() -> Self { + Self(GenesisStateErrorKind::MissingParams) + } + + #[must_use] + pub fn invalid_params(err: ParamsError) -> Self { + Self(GenesisStateErrorKind::ParamsParseError(err)) + } + } + + #[derive(Debug, thiserror::Error)] + enum GenesisStateErrorKind { + #[error("missing market map")] + MissingMarketMap, + #[error("failed to parse market map")] + MarketMapParseError(#[from] MarketMapError), + #[error("missing params")] + MissingParams, + #[error("failed to parse params")] + ParamsParseError(#[from] ParamsError), + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::Params", into = "raw::Params") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Params { + pub market_authorities: Vec
, + pub admin: Address, + } + + impl TryFrom for Params { + type Error = ParamsError; + + fn try_from(raw: raw::Params) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::Params { + fn from(params: Params) -> Self { + params.into_raw() + } + } + + impl Params { + /// Converts from a raw protobuf `Params` to a native `Params`. + /// + /// # Errors + /// + /// - if any of the `market_authorities` addresses are invalid + /// - if the `admin` address is invalid + pub fn try_from_raw(raw: raw::Params) -> Result { + let market_authorities = raw + .market_authorities + .into_iter() + .map(|s| Address::from_str(&s)) + .collect::, _>>() + .map_err(ParamsError::market_authority_parse_error)?; + let admin = raw.admin.parse().map_err(ParamsError::admin_parse_error)?; + Ok(Self { + market_authorities, + admin, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::Params { + raw::Params { + market_authorities: self + .market_authorities + .into_iter() + .map(|a| a.to_string()) + .collect(), + admin: self.admin.to_string(), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct ParamsError(ParamsErrorKind); + + impl ParamsError { + #[must_use] + pub fn market_authority_parse_error(err: AddressError) -> Self { + Self(ParamsErrorKind::MarketAuthorityParseError(err)) + } + + #[must_use] + pub fn admin_parse_error(err: AddressError) -> Self { + Self(ParamsErrorKind::AdminParseError(err)) + } + } + + #[derive(Debug, thiserror::Error)] + pub enum ParamsErrorKind { + #[error("failed to parse market authority address")] + MarketAuthorityParseError(#[source] AddressError), + #[error("failed to parse admin address")] + AdminParseError(#[source] AddressError), + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::Market", into = "raw::Market") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Market { + pub ticker: Ticker, + pub provider_configs: Vec, + } + + impl TryFrom for Market { + type Error = MarketError; + + fn try_from(raw: raw::Market) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::Market { + fn from(market: Market) -> Self { + market.into_raw() + } + } + + impl Market { + /// Converts from a raw protobuf `Market` to a native `Market`. + /// + /// # Errors + /// + /// - if the `ticker` field is missing + /// - if the `ticker` field is invalid + /// - if any of the `provider_configs` are invalid + pub fn try_from_raw(raw: raw::Market) -> Result { + let Some(ticker) = raw + .ticker + .map(Ticker::try_from_raw) + .transpose() + .map_err(MarketError::invalid_ticker)? + else { + return Err(MarketError::missing_ticker()); + }; + + let provider_configs = raw + .provider_configs + .into_iter() + .map(ProviderConfig::try_from_raw) + .collect::, _>>() + .map_err(MarketError::invalid_provider_config)?; + Ok(Self { + ticker, + provider_configs, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::Market { + raw::Market { + ticker: Some(self.ticker.into_raw()), + provider_configs: self + .provider_configs + .into_iter() + .map(ProviderConfig::into_raw) + .collect(), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct MarketError(MarketErrorKind); + + impl MarketError { + #[must_use] + pub fn missing_ticker() -> Self { + Self(MarketErrorKind::MissingTicker) + } + + #[must_use] + pub fn invalid_ticker(err: TickerError) -> Self { + Self(MarketErrorKind::TickerParseError(err)) + } + + #[must_use] + pub fn invalid_provider_config(err: ProviderConfigError) -> Self { + Self(MarketErrorKind::ProviderConfigParseError(err)) + } + } + + #[derive(Debug, thiserror::Error)] + enum MarketErrorKind { + #[error("missing ticker")] + MissingTicker, + #[error("failed to parse ticker")] + TickerParseError(#[from] TickerError), + #[error("failed to parse provider config")] + ProviderConfigParseError(#[from] ProviderConfigError), + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::Ticker", into = "raw::Ticker") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Ticker { + pub currency_pair: CurrencyPair, + pub decimals: u64, + pub min_provider_count: u64, + pub enabled: bool, + pub metadata_json: String, + } + + impl TryFrom for Ticker { + type Error = TickerError; + + fn try_from(raw: raw::Ticker) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::Ticker { + fn from(ticker: Ticker) -> Self { + ticker.into_raw() + } + } + + impl Ticker { + /// Converts from a raw protobuf `Ticker` to a native `Ticker`. + /// + /// # Errors + /// + /// - if the `currency_pair` field is missing + /// - if the `currency_pair` field is invalid + pub fn try_from_raw(raw: raw::Ticker) -> Result { + let currency_pair = raw + .currency_pair + .ok_or_else(|| TickerError::field_not_set("currency_pair"))? + .try_into() + .map_err(TickerError::invalid_currency_pair)?; + + Ok(Self { + currency_pair, + decimals: raw.decimals, + min_provider_count: raw.min_provider_count, + enabled: raw.enabled, + metadata_json: raw.metadata_json, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::Ticker { + raw::Ticker { + currency_pair: Some(self.currency_pair.into_raw()), + decimals: self.decimals, + min_provider_count: self.min_provider_count, + enabled: self.enabled, + metadata_json: self.metadata_json, + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct TickerError(#[from] TickerErrorKind); + + impl TickerError { + #[must_use] + fn field_not_set(name: &'static str) -> Self { + TickerErrorKind::FieldNotSet { + name, + } + .into() + } + + #[must_use] + fn invalid_currency_pair(source: CurrencyPairError) -> Self { + TickerErrorKind::InvalidCurrencyPair { + source, + } + .into() + } + } + + #[derive(Debug, thiserror::Error)] + #[error("failed validating wire type `{}`", raw::Ticker::full_name())] + enum TickerErrorKind { + #[error("required field not set: .{name}")] + FieldNotSet { name: &'static str }, + #[error("field `.currency_pair` was invalid")] + InvalidCurrencyPair { source: CurrencyPairError }, + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::ProviderConfig", into = "raw::ProviderConfig") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ProviderConfig { + pub name: String, + pub off_chain_ticker: String, + pub normalize_by_pair: CurrencyPair, + pub invert: bool, + pub metadata_json: String, + } + + impl TryFrom for ProviderConfig { + type Error = ProviderConfigError; + + fn try_from(raw: raw::ProviderConfig) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::ProviderConfig { + fn from(provider_config: ProviderConfig) -> Self { + provider_config.into_raw() + } + } + + impl ProviderConfig { + /// Converts from a raw protobuf `ProviderConfig` to a native `ProviderConfig`. + /// + /// # Errors + /// + /// - if the `normalize_by_pair` field is missing + pub fn try_from_raw(raw: raw::ProviderConfig) -> Result { + let normalize_by_pair = raw + .normalize_by_pair + .ok_or_else(|| ProviderConfigError::field_not_set("normalize_by_pair"))? + .try_into() + .map_err(ProviderConfigError::invalid_normalize_by_pair)?; + Ok(Self { + name: raw.name, + off_chain_ticker: raw.off_chain_ticker, + normalize_by_pair, + invert: raw.invert, + metadata_json: raw.metadata_json, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::ProviderConfig { + raw::ProviderConfig { + name: self.name, + off_chain_ticker: self.off_chain_ticker, + normalize_by_pair: Some(self.normalize_by_pair.into_raw()), + invert: self.invert, + metadata_json: self.metadata_json, + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct ProviderConfigError(#[from] ProviderConfigErrorKind); + + impl ProviderConfigError { + #[must_use] + fn field_not_set(name: &'static str) -> Self { + ProviderConfigErrorKind::FieldNotSet { + name, + } + .into() + } + + fn invalid_normalize_by_pair(source: CurrencyPairError) -> Self { + ProviderConfigErrorKind::InvalidNormalizeByPair { + source, + } + .into() + } + } + + #[derive(Debug, thiserror::Error)] + #[error("failed validating wire type `{}`", raw::ProviderConfig::full_name())] + enum ProviderConfigErrorKind { + #[error("required field not set: .{name}")] + FieldNotSet { name: &'static str }, + #[error("field `.normalize_by_pair` was invalid")] + InvalidNormalizeByPair { source: CurrencyPairError }, + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::MarketMap", into = "raw::MarketMap") + )] + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct MarketMap { + pub markets: IndexMap, + } + + impl TryFrom for MarketMap { + type Error = MarketMapError; + + fn try_from(raw: raw::MarketMap) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::MarketMap { + fn from(market_map: MarketMap) -> Self { + market_map.into_raw() + } + } + + impl MarketMap { + /// Converts from a raw protobuf `MarketMap` to a native `MarketMap`. + /// + /// # Errors + /// + /// - if any of the markets are invalid + /// - if any of the market names are invalid + pub fn try_from_raw(raw: raw::MarketMap) -> Result { + let mut markets = IndexMap::new(); + for (k, v) in raw.markets { + let market = Market::try_from_raw(v) + .map_err(|e| MarketMapError::invalid_market(k.clone(), e))?; + markets.insert(k, market); + } + Ok(Self { + markets, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::MarketMap { + let markets = self + .markets + .into_iter() + .map(|(k, v)| (k, v.into_raw())) + .collect(); + raw::MarketMap { + markets, + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct MarketMapError(MarketMapErrorKind); + + impl MarketMapError { + #[must_use] + pub fn invalid_market(name: String, err: MarketError) -> Self { + Self(MarketMapErrorKind::InvalidMarket { + name, + source: err, + }) + } + } + + #[derive(Debug, thiserror::Error)] + enum MarketMapErrorKind { + #[error("invalid market `{name}`")] + InvalidMarket { name: String, source: MarketError }, + } +} diff --git a/crates/astria-core/src/connect/mod.rs b/crates/astria-core/src/connect/mod.rs new file mode 100644 index 0000000000..62eac09078 --- /dev/null +++ b/crates/astria-core/src/connect/mod.rs @@ -0,0 +1,5 @@ +pub mod abci; +pub mod market_map; +pub mod oracle; +pub mod service; +pub mod types; diff --git a/crates/astria-core/src/connect/oracle.rs b/crates/astria-core/src/connect/oracle.rs new file mode 100644 index 0000000000..8cd723c542 --- /dev/null +++ b/crates/astria-core/src/connect/oracle.rs @@ -0,0 +1,422 @@ +pub mod v2 { + use pbjson_types::Timestamp; + + use crate::{ + connect::types::v2::{ + CurrencyPair, + CurrencyPairError, + CurrencyPairId, + CurrencyPairNonce, + ParsePriceError, + Price, + }, + generated::connect::oracle::v2 as raw, + Protobuf, + }; + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::QuotePrice", into = "raw::QuotePrice") + )] + #[derive(Debug, Clone)] + pub struct QuotePrice { + pub price: Price, + pub block_timestamp: Timestamp, + pub block_height: u64, + } + + impl TryFrom for QuotePrice { + type Error = QuotePriceError; + + fn try_from(raw: raw::QuotePrice) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::QuotePrice { + fn from(quote_price: QuotePrice) -> Self { + quote_price.into_raw() + } + } + + impl QuotePrice { + /// Converts from a raw protobuf `QuotePrice` to a native `QuotePrice`. + /// + /// # Errors + /// + /// - if the `price` field is invalid + /// - if the `block_timestamp` field is missing + pub fn try_from_raw(raw: raw::QuotePrice) -> Result { + let price = raw.price.parse().map_err(QuotePriceError::parse_price)?; + let Some(block_timestamp) = raw.block_timestamp else { + return Err(QuotePriceError::missing_block_timestamp()); + }; + let block_height = raw.block_height; + Ok(Self { + price, + block_timestamp, + block_height, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::QuotePrice { + raw::QuotePrice { + price: self.price.to_string(), + block_timestamp: Some(self.block_timestamp), + block_height: self.block_height, + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct QuotePriceError(QuotePriceErrorKind); + + impl QuotePriceError { + #[must_use] + fn parse_price(source: ParsePriceError) -> Self { + Self(QuotePriceErrorKind::Price { + source, + }) + } + + #[must_use] + fn missing_block_timestamp() -> Self { + Self(QuotePriceErrorKind::MissingBlockTimestamp) + } + } + + #[derive(Debug, thiserror::Error)] + #[error("failed to validate wire type `{}`", raw::QuotePrice::full_name())] + enum QuotePriceErrorKind { + #[error("failed to parse `price` field")] + Price { source: ParsePriceError }, + #[error("missing block timestamp")] + MissingBlockTimestamp, + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::CurrencyPairState", into = "raw::CurrencyPairState") + )] + #[derive(Debug, Clone)] + pub struct CurrencyPairState { + pub price: QuotePrice, + pub nonce: CurrencyPairNonce, + pub id: CurrencyPairId, + } + + impl TryFrom for CurrencyPairState { + type Error = CurrencyPairStateError; + + fn try_from(raw: raw::CurrencyPairState) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::CurrencyPairState { + fn from(currency_pair_state: CurrencyPairState) -> Self { + currency_pair_state.into_raw() + } + } + + impl CurrencyPairState { + /// Converts from a raw protobuf `CurrencyPairState` to a native `CurrencyPairState`. + /// + /// # Errors + /// + /// - if the `price` field is missing + /// - if the `price` field is invalid + pub fn try_from_raw(raw: raw::CurrencyPairState) -> Result { + let Some(price) = raw + .price + .map(QuotePrice::try_from_raw) + .transpose() + .map_err(CurrencyPairStateError::quote_price_parse_error)? + else { + return Err(CurrencyPairStateError::missing_price()); + }; + let nonce = CurrencyPairNonce::new(raw.nonce); + let id = CurrencyPairId::new(raw.id); + Ok(Self { + price, + nonce, + id, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::CurrencyPairState { + raw::CurrencyPairState { + price: Some(self.price.into_raw()), + nonce: self.nonce.get(), + id: self.id.get(), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct CurrencyPairStateError(CurrencyPairStateErrorKind); + + impl CurrencyPairStateError { + #[must_use] + fn missing_price() -> Self { + Self(CurrencyPairStateErrorKind::MissingPrice) + } + + #[must_use] + fn quote_price_parse_error(err: QuotePriceError) -> Self { + Self(CurrencyPairStateErrorKind::QuotePriceParseError(err)) + } + } + + #[derive(Debug, thiserror::Error)] + enum CurrencyPairStateErrorKind { + #[error("missing price")] + MissingPrice, + #[error("failed to parse quote price")] + QuotePriceParseError(#[source] QuotePriceError), + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + try_from = "raw::CurrencyPairGenesis", + into = "raw::CurrencyPairGenesis" + ) + )] + #[derive(Debug, Clone)] + pub struct CurrencyPairGenesis { + pub currency_pair: CurrencyPair, + pub currency_pair_price: QuotePrice, + pub id: CurrencyPairId, + pub nonce: CurrencyPairNonce, + } + + impl TryFrom for CurrencyPairGenesis { + type Error = CurrencyPairGenesisError; + + fn try_from(raw: raw::CurrencyPairGenesis) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::CurrencyPairGenesis { + fn from(currency_pair_genesis: CurrencyPairGenesis) -> Self { + currency_pair_genesis.into_raw() + } + } + + impl CurrencyPairGenesis { + #[must_use] + pub fn currency_pair(&self) -> &CurrencyPair { + &self.currency_pair + } + + #[must_use] + pub fn currency_pair_price(&self) -> &QuotePrice { + &self.currency_pair_price + } + + #[must_use] + pub fn id(&self) -> CurrencyPairId { + self.id + } + + #[must_use] + pub fn nonce(&self) -> CurrencyPairNonce { + self.nonce + } + + /// Converts from a raw protobuf `raw::CurrencyPairGenesis` to a validated + /// domain type [`CurrencyPairGenesis`]. + /// + /// # Errors + /// + /// - if the `currency_pair` field is missing + /// - if the `currency_pair` field is invalid + /// - if the `currency_pair_price` field is missing + /// - if the `currency_pair_price` field is invalid + pub fn try_from_raw( + raw: raw::CurrencyPairGenesis, + ) -> Result { + let currency_pair = raw + .currency_pair + .ok_or_else(|| CurrencyPairGenesisError::field_not_set("currency_pair"))? + .try_into() + .map_err(CurrencyPairGenesisError::currency_pair)?; + let currency_pair_price = { + let wire = raw.currency_pair_price.ok_or_else(|| { + CurrencyPairGenesisError::field_not_set("currency_pair_price") + })?; + QuotePrice::try_from_raw(wire) + .map_err(CurrencyPairGenesisError::currency_pair_price)? + }; + + let id = CurrencyPairId::new(raw.id); + let nonce = CurrencyPairNonce::new(raw.nonce); + Ok(Self { + currency_pair, + currency_pair_price, + id, + nonce, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::CurrencyPairGenesis { + raw::CurrencyPairGenesis { + currency_pair: Some(self.currency_pair.into_raw()), + currency_pair_price: Some(self.currency_pair_price.into_raw()), + id: self.id.get(), + nonce: self.nonce.get(), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct CurrencyPairGenesisError(#[from] CurrencyPairGenesisErrorKind); + + impl CurrencyPairGenesisError { + #[must_use] + fn field_not_set(name: &'static str) -> Self { + CurrencyPairGenesisErrorKind::FieldNotSet { + name, + } + .into() + } + + fn currency_pair(source: CurrencyPairError) -> Self { + CurrencyPairGenesisErrorKind::CurrencyPair { + source, + } + .into() + } + + #[must_use] + fn currency_pair_price(err: QuotePriceError) -> Self { + Self(CurrencyPairGenesisErrorKind::CurrencyPairPrice(err)) + } + } + + #[derive(Debug, thiserror::Error)] + enum CurrencyPairGenesisErrorKind { + #[error("required field not set: .{name}")] + FieldNotSet { name: &'static str }, + #[error("field `.currency_pair` was invalid")] + CurrencyPair { source: CurrencyPairError }, + #[error("field `.currency_pair_price` was invalid")] + CurrencyPairPrice(#[source] QuotePriceError), + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::GenesisState", into = "raw::GenesisState") + )] + #[derive(Debug, Clone)] + pub struct GenesisState { + pub currency_pair_genesis: Vec, + pub next_id: CurrencyPairId, + } + + impl TryFrom for GenesisState { + type Error = GenesisStateError; + + fn try_from(raw: raw::GenesisState) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::GenesisState { + fn from(genesis_state: GenesisState) -> Self { + genesis_state.into_raw() + } + } + + impl Protobuf for GenesisState { + type Error = GenesisStateError; + type Raw = raw::GenesisState; + + fn try_from_raw_ref(raw: &raw::GenesisState) -> Result { + let currency_pair_genesis = raw + .currency_pair_genesis + .clone() + .into_iter() + .map(CurrencyPairGenesis::try_from_raw) + .collect::, _>>() + .map_err(GenesisStateError::currency_pair_genesis_parse_error)?; + let next_id = CurrencyPairId::new(raw.next_id); + Ok(Self { + currency_pair_genesis, + next_id, + }) + } + + /// Converts from a raw protobuf `GenesisState` to a native `GenesisState`. + /// + /// # Errors + /// + /// - if any of the `currency_pair_genesis` are invalid + fn try_from_raw(raw: raw::GenesisState) -> Result { + let currency_pair_genesis = raw + .currency_pair_genesis + .into_iter() + .map(CurrencyPairGenesis::try_from_raw) + .collect::, _>>() + .map_err(GenesisStateError::currency_pair_genesis_parse_error)?; + let next_id = CurrencyPairId::new(raw.next_id); + Ok(Self { + currency_pair_genesis, + next_id, + }) + } + + fn to_raw(&self) -> raw::GenesisState { + raw::GenesisState { + currency_pair_genesis: self + .currency_pair_genesis + .clone() + .into_iter() + .map(CurrencyPairGenesis::into_raw) + .collect(), + next_id: self.next_id.get(), + } + } + + #[must_use] + fn into_raw(self) -> raw::GenesisState { + raw::GenesisState { + currency_pair_genesis: self + .currency_pair_genesis + .into_iter() + .map(CurrencyPairGenesis::into_raw) + .collect(), + next_id: self.next_id.get(), + } + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct GenesisStateError(GenesisStateErrorKind); + + impl GenesisStateError { + #[must_use] + fn currency_pair_genesis_parse_error(err: CurrencyPairGenesisError) -> Self { + Self(GenesisStateErrorKind::CurrencyPairGenesisParseError(err)) + } + } + + #[derive(Debug, thiserror::Error)] + enum GenesisStateErrorKind { + #[error("failed to parse genesis currency pair")] + CurrencyPairGenesisParseError(#[source] CurrencyPairGenesisError), + } +} diff --git a/crates/astria-core/src/connect/service.rs b/crates/astria-core/src/connect/service.rs new file mode 100644 index 0000000000..3af6c5848f --- /dev/null +++ b/crates/astria-core/src/connect/service.rs @@ -0,0 +1,88 @@ +pub mod v2 { + use indexmap::IndexMap; + + use crate::{ + connect::types::v2::{ + CurrencyPair, + CurrencyPairParseError, + ParsePriceError, + Price, + }, + generated::connect::service::v2 as raw, + }; + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct QueryPriceResponseError(#[from] QueryPriceResponseErrorKind); + + #[derive(Debug, thiserror::Error)] + #[error( + "failed validating wire type {}", + raw::QueryPriceResponseError::full_name() + )] + enum QueryPriceResponseErrorKind { + #[error("failed to parse key `{input}` in `.prices` field as currency pair")] + ParseCurrencyPair { + input: String, + source: CurrencyPairParseError, + }, + #[error("failed to parse value `{input}` in `.prices` field at key `{key}` as price")] + ParsePrice { + input: String, + key: String, + source: ParsePriceError, + }, + } + + pub struct QueryPricesResponse { + pub prices: IndexMap, + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, + pub version: String, + } + + impl QueryPricesResponse { + /// Converts the on-wire [`raw::QueryPricesReponse`] to a validated domain type + /// [`QueryPricesResponse`]. + /// + /// # Errors + /// Returns an error if: + /// + A key in the `.prices` map could not be parsed as a [`CurrencyPair`]. + /// + A value in the `.prices` map could not be parsed as [`Price`]. + pub fn try_from_raw( + wire: raw::QueryPricesResponse, + ) -> Result { + let raw::QueryPricesResponse { + prices, + timestamp, + version, + } = wire; + let prices = prices + .into_iter() + .map(|(key, value)| { + let currency_pair = match key.parse() { + Err(source) => { + return Err(QueryPriceResponseErrorKind::ParseCurrencyPair { + input: key, + source, + }); + } + Ok(parsed) => parsed, + }; + let price = value.parse().map_err(move |source| { + QueryPriceResponseErrorKind::ParsePrice { + input: value, + key, + source, + } + })?; + Ok((currency_pair, price)) + }) + .collect::>()?; + Ok(Self { + prices, + timestamp, + version, + }) + } + } +} diff --git a/crates/astria-core/src/connect/types.rs b/crates/astria-core/src/connect/types.rs new file mode 100644 index 0000000000..306495875a --- /dev/null +++ b/crates/astria-core/src/connect/types.rs @@ -0,0 +1,378 @@ +pub mod v2 { + use std::{ + fmt::Display, + num::ParseIntError, + str::FromStr, + }; + + use bytes::Bytes; + + use crate::generated::connect::types::v2 as raw; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + pub struct Price(u128); + + impl Price { + #[must_use] + pub fn new(value: u128) -> Self { + Self(value) + } + + #[must_use] + pub fn get(self) -> u128 { + self.0 + } + } + + impl Price { + pub fn checked_add(self, rhs: Self) -> Option { + self.get().checked_add(rhs.get()).map(Self) + } + + pub fn checked_div(self, rhs: u128) -> Option { + self.get().checked_div(rhs).map(Self) + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct ParsePriceError(#[from] ParseIntError); + + impl FromStr for Price { + type Err = ParsePriceError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self::new).map_err(Into::into) + } + } + + impl Display for Price { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + #[derive(Debug, thiserror::Error)] + #[error("failed decoding `{}` as u128 integer", crate::display::base64(.input))] + pub struct DecodePriceError { + input: Bytes, + } + + impl TryFrom for Price { + type Error = DecodePriceError; + + fn try_from(input: Bytes) -> Result { + // throw away the error because it does not contain extra information. + let be_bytes = <[u8; 16]>::try_from(&*input).map_err(|_| Self::Error { + input, + })?; + Ok(Price::new(u128::from_be_bytes(be_bytes))) + } + } + + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Base(String); + + impl Display for Base { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + #[derive(Debug, thiserror::Error)] + #[error( + "failed to parse input `{input}` as base part of currency pair; only ascii alpha \ + characters are permitted" + )] + pub struct ParseBaseError { + input: String, + } + + impl FromStr for Base { + type Err = ParseBaseError; + + fn from_str(s: &str) -> Result { + static REGEX: std::sync::OnceLock = std::sync::OnceLock::new(); + fn get_regex() -> &'static regex::Regex { + REGEX.get_or_init(|| regex::Regex::new(r"^[a-zA-Z]+$").expect("valid regex")) + } + // allocating here because the string will always be allocated on both branches. + // TODO: check if this string can be represented by a stack-optimized alternative + // like ecow, compact_str, or similar. + let input = s.to_string(); + if get_regex().find(s).is_none() { + return Err(Self::Err { + input, + }); + } + Ok(Self(input)) + } + } + + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Quote(String); + + impl Display for Quote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + #[derive(Debug, thiserror::Error)] + #[error( + "failed to parse input `{input}` as quote part of currency pair; only ascii alpha \ + characters are permitted" + )] + pub struct ParseQuoteError { + input: String, + } + + impl FromStr for Quote { + type Err = ParseQuoteError; + + fn from_str(s: &str) -> Result { + static REGEX: std::sync::OnceLock = std::sync::OnceLock::new(); + fn get_regex() -> &'static regex::Regex { + REGEX.get_or_init(|| regex::Regex::new(r"^[a-zA-Z]+$").expect("valid regex")) + } + // allocating here because the string will always be allocated on both branches. + // TODO: check if this string can be represented by a stack-optimized alternative + // like ecow, compact_str, or similar. + let input = s.to_string(); + if get_regex().find(s).is_none() { + return Err(Self::Err { + input, + }); + } + Ok(Self(input)) + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct CurrencyPairError(#[from] CurrencyPairErrorKind); + + #[derive(Debug, thiserror::Error)] + #[error("failed validating wire type `{}`", CurrencyPair::full_name())] + enum CurrencyPairErrorKind { + #[error("invalid field `.base`")] + ParseBase { source: ParseBaseError }, + #[error("invalid field `.quote`")] + ParseQuote { source: ParseQuoteError }, + } + + #[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::CurrencyPair", into = "raw::CurrencyPair") + )] + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct CurrencyPair { + base: Base, + quote: Quote, + } + + impl CurrencyPair { + #[must_use] + pub fn from_parts(base: Base, quote: Quote) -> Self { + Self { + base, + quote, + } + } + + /// Returns the `(base, quote)` pair that makes up this [`CurrencyPair`]. + #[must_use] + pub fn into_parts(self) -> (String, String) { + (self.base.0, self.quote.0) + } + + #[must_use] + pub fn base(&self) -> &str { + &self.base.0 + } + + #[must_use] + pub fn quote(&self) -> &str { + &self.quote.0 + } + + /// Converts a on-wire [`raw::CurrencyPair`] to a validated domain type [`CurrencyPair`]. + /// + /// # Errors + + /// Returns an error if: + /// + The `.base` field could not be parsed as a [`Base`]. + /// + The `.quote` field could not be parsed as [`Quote`]. + // allow reason: symmetry with all other `try_from_raw` methods that take ownership + #[expect(clippy::needless_pass_by_value, reason = "symmetry with other types")] + pub fn try_from_raw(raw: raw::CurrencyPair) -> Result { + let base = raw + .base + .parse() + .map_err(|source| CurrencyPairErrorKind::ParseBase { + source, + })?; + let quote = raw + .quote + .parse() + .map_err(|source| CurrencyPairErrorKind::ParseQuote { + source, + })?; + Ok(Self { + base, + quote, + }) + } + + #[must_use] + pub fn into_raw(self) -> raw::CurrencyPair { + raw::CurrencyPair { + base: self.base.0, + quote: self.quote.0, + } + } + } + + impl TryFrom for CurrencyPair { + type Error = CurrencyPairError; + + fn try_from(raw: raw::CurrencyPair) -> Result { + Self::try_from_raw(raw) + } + } + + impl From for raw::CurrencyPair { + fn from(currency_pair: CurrencyPair) -> Self { + currency_pair.into_raw() + } + } + + impl std::fmt::Display for CurrencyPair { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}/{}", self.base, self.quote) + } + } + + impl std::str::FromStr for CurrencyPair { + type Err = CurrencyPairParseError; + + fn from_str(s: &str) -> Result { + static REGEX: std::sync::OnceLock = std::sync::OnceLock::new(); + fn get_regex() -> &'static regex::Regex { + REGEX.get_or_init(|| { + regex::Regex::new(r"^([a-zA-Z]+)/([a-zA-Z]+)$").expect("valid regex") + }) + } + + let caps = get_regex() + .captures(s) + .ok_or_else(|| CurrencyPairParseError::invalid_currency_pair_string(s))?; + let base = caps + .get(1) + .expect("must have base string, as regex captured it") + .as_str(); + let quote = caps + .get(2) + .expect("must have quote string, as regex captured it") + .as_str(); + + Ok(Self { + base: Base(base.to_string()), + quote: Quote(quote.to_string()), + }) + } + } + + #[derive(Debug, thiserror::Error)] + #[error(transparent)] + pub struct CurrencyPairParseError(CurrencyPairParseErrorKind); + + #[derive(Debug, thiserror::Error)] + pub enum CurrencyPairParseErrorKind { + #[error("invalid currency pair string: {0}")] + InvalidCurrencyPairString(String), + } + + impl CurrencyPairParseError { + #[must_use] + fn invalid_currency_pair_string(s: &str) -> Self { + Self(CurrencyPairParseErrorKind::InvalidCurrencyPairString( + s.to_string(), + )) + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CurrencyPairId(u64); + + impl std::fmt::Display for CurrencyPairId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl CurrencyPairId { + #[must_use] + pub fn new(value: u64) -> Self { + Self(value) + } + + #[must_use] + pub fn get(self) -> u64 { + self.0 + } + + #[must_use] + pub fn increment(self) -> Option { + let new_id = self.get().checked_add(1)?; + Some(Self::new(new_id)) + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CurrencyPairNonce(u64); + + impl std::fmt::Display for CurrencyPairNonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl CurrencyPairNonce { + #[must_use] + pub fn new(value: u64) -> Self { + Self(value) + } + + #[must_use] + pub fn get(self) -> u64 { + self.0 + } + + #[must_use] + pub fn increment(self) -> Option { + let new_nonce = self.get().checked_add(1)?; + Some(Self::new(new_nonce)) + } + } +} + +#[cfg(test)] +mod test { + use super::v2::CurrencyPair; + + #[test] + fn currency_pair_parse() { + let currency_pair = "ETH/USD".parse::().unwrap(); + assert_eq!(currency_pair.base(), "ETH"); + assert_eq!(currency_pair.quote(), "USD"); + assert_eq!(currency_pair.to_string(), "ETH/USD"); + } + + #[test] + fn invalid_curreny_pair_is_rejected() { + let currency_pair = "ETHUSD".parse::(); + assert!(currency_pair.is_err()); + } +} diff --git a/crates/astria-core/src/display.rs b/crates/astria-core/src/display.rs new file mode 100644 index 0000000000..112a041393 --- /dev/null +++ b/crates/astria-core/src/display.rs @@ -0,0 +1,57 @@ +use std::fmt::{ + Display, + Formatter, + Result, +}; + +/// Format `bytes` using standard base64 formatting. +/// +/// See the [`base64::engine::general_purpose::STANDARD`] for the formatting definition. +/// +/// # Example +/// ``` +/// use astria_core::display; +/// let signature = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; +/// println!("received signature: {}", display::base64(&signature)); +/// ``` +pub fn base64 + ?Sized>(bytes: &T) -> Base64<'_> { + Base64(bytes.as_ref()) +} + +pub struct Base64<'a>(&'a [u8]); + +impl<'a> Display for Base64<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + use base64::{ + display::Base64Display, + engine::general_purpose::STANDARD, + }; + Base64Display::new(self.0, &STANDARD).fmt(f) + } +} + +/// A newtype wrapper of a byte slice that implements [`std::fmt::Display`]. +/// +/// To be used in tracing contexts. See the [`self::hex`] utility. +pub struct Hex<'a>(&'a [u8]); + +/// Format `bytes` as lower-cased hex. +/// +/// # Example +/// ``` +/// use astria_core::display; +/// let signature = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; +/// println!("received signature: {}", display::hex(&signature)); +/// ``` +pub fn hex + ?Sized>(bytes: &T) -> Hex<'_> { + Hex(bytes.as_ref()) +} + +impl<'a> Display for Hex<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + for byte in self.0 { + f.write_fmt(format_args!("{byte:02x}"))?; + } + Ok(()) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs b/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs index 4978ffdd2e..991867b813 100644 --- a/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs @@ -27,6 +27,8 @@ pub struct GenesisAppState { pub allowed_fee_assets: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, optional, tag = "10")] pub fees: ::core::option::Option, + #[prost(message, optional, tag = "11")] + pub connect: ::core::option::Option, } impl ::prost::Name for GenesisAppState { const NAME: &'static str = "GenesisAppState"; @@ -153,3 +155,22 @@ impl ::prost::Name for GenesisFees { ::prost::alloc::format!("astria.protocol.genesis.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ConnectGenesis { + #[prost(message, optional, tag = "1")] + pub market_map: ::core::option::Option< + super::super::super::super::connect::marketmap::v2::GenesisState, + >, + #[prost(message, optional, tag = "2")] + pub oracle: ::core::option::Option< + super::super::super::super::connect::oracle::v2::GenesisState, + >, +} +impl ::prost::Name for ConnectGenesis { + const NAME: &'static str = "ConnectGenesis"; + const PACKAGE: &'static str = "astria.protocol.genesis.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.genesis.v1.{}", Self::NAME) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.genesis.v1.serde.rs b/crates/astria-core/src/generated/astria.protocol.genesis.v1.serde.rs index d3b4b59d68..9ca59c33ca 100644 --- a/crates/astria-core/src/generated/astria.protocol.genesis.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.genesis.v1.serde.rs @@ -215,6 +215,115 @@ impl<'de> serde::Deserialize<'de> for AddressPrefixes { deserializer.deserialize_struct("astria.protocol.genesis.v1.AddressPrefixes", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ConnectGenesis { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.market_map.is_some() { + len += 1; + } + if self.oracle.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.genesis.v1.ConnectGenesis", len)?; + if let Some(v) = self.market_map.as_ref() { + struct_ser.serialize_field("marketMap", v)?; + } + if let Some(v) = self.oracle.as_ref() { + struct_ser.serialize_field("oracle", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ConnectGenesis { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market_map", + "marketMap", + "oracle", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MarketMap, + Oracle, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "marketMap" | "market_map" => Ok(GeneratedField::MarketMap), + "oracle" => Ok(GeneratedField::Oracle), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ConnectGenesis; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.genesis.v1.ConnectGenesis") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market_map__ = None; + let mut oracle__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MarketMap => { + if market_map__.is_some() { + return Err(serde::de::Error::duplicate_field("marketMap")); + } + market_map__ = map_.next_value()?; + } + GeneratedField::Oracle => { + if oracle__.is_some() { + return Err(serde::de::Error::duplicate_field("oracle")); + } + oracle__ = map_.next_value()?; + } + } + } + Ok(ConnectGenesis { + market_map: market_map__, + oracle: oracle__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.genesis.v1.ConnectGenesis", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GenesisAppState { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -253,6 +362,9 @@ impl serde::Serialize for GenesisAppState { if self.fees.is_some() { len += 1; } + if self.connect.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("astria.protocol.genesis.v1.GenesisAppState", len)?; if !self.chain_id.is_empty() { struct_ser.serialize_field("chainId", &self.chain_id)?; @@ -284,6 +396,9 @@ impl serde::Serialize for GenesisAppState { if let Some(v) = self.fees.as_ref() { struct_ser.serialize_field("fees", v)?; } + if let Some(v) = self.connect.as_ref() { + struct_ser.serialize_field("connect", v)?; + } struct_ser.end() } } @@ -312,6 +427,7 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { "allowed_fee_assets", "allowedFeeAssets", "fees", + "connect", ]; #[allow(clippy::enum_variant_names)] @@ -326,6 +442,7 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { IbcParameters, AllowedFeeAssets, Fees, + Connect, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -357,6 +474,7 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { "ibcParameters" | "ibc_parameters" => Ok(GeneratedField::IbcParameters), "allowedFeeAssets" | "allowed_fee_assets" => Ok(GeneratedField::AllowedFeeAssets), "fees" => Ok(GeneratedField::Fees), + "connect" => Ok(GeneratedField::Connect), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -386,6 +504,7 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { let mut ibc_parameters__ = None; let mut allowed_fee_assets__ = None; let mut fees__ = None; + let mut connect__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::ChainId => { @@ -448,6 +567,12 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { } fees__ = map_.next_value()?; } + GeneratedField::Connect => { + if connect__.is_some() { + return Err(serde::de::Error::duplicate_field("connect")); + } + connect__ = map_.next_value()?; + } } } Ok(GenesisAppState { @@ -461,6 +586,7 @@ impl<'de> serde::Deserialize<'de> for GenesisAppState { ibc_parameters: ibc_parameters__, allowed_fee_assets: allowed_fee_assets__.unwrap_or_default(), fees: fees__, + connect: connect__, }) } } diff --git a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs index 6bd2ba2fdc..0b38628394 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs @@ -36,7 +36,7 @@ pub mod action { SudoAddressChange(super::SudoAddressChange), #[prost(message, tag = "51")] ValidatorUpdate( - crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate, + super::super::super::super::super::astria_vendored::tendermint::abci::ValidatorUpdate, ), #[prost(message, tag = "52")] IbcRelayerChange(super::IbcRelayerChange), diff --git a/crates/astria-core/src/generated/astria.protocol.transaction.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.transaction.v1alpha1.rs index 75c1bf44b4..a310970f2b 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1alpha1.rs @@ -36,7 +36,7 @@ pub mod action { SudoAddressChange(super::SudoAddressChange), #[prost(message, tag = "51")] ValidatorUpdate( - crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate, + super::super::super::super::super::astria_vendored::tendermint::abci::ValidatorUpdate, ), #[prost(message, tag = "52")] IbcRelayerChange(super::IbcRelayerChange), diff --git a/crates/astria-core/src/generated/connect.abci.v2.rs b/crates/astria-core/src/generated/connect.abci.v2.rs new file mode 100644 index 0000000000..6940f8594b --- /dev/null +++ b/crates/astria-core/src/generated/connect.abci.v2.rs @@ -0,0 +1,17 @@ +/// OracleVoteExtension defines the vote extension structure for oracle prices. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct OracleVoteExtension { + /// Prices defines a map of id(CurrencyPair) -> price.Bytes() . i.e. 1 -> + /// 0x123.. (bytes). Notice the `id` function is determined by the + /// `CurrencyPairIDStrategy` used in the VoteExtensionHandler. + #[prost(btree_map = "uint64, bytes", tag = "1")] + pub prices: ::prost::alloc::collections::BTreeMap, +} +impl ::prost::Name for OracleVoteExtension { + const NAME: &'static str = "OracleVoteExtension"; + const PACKAGE: &'static str = "connect.abci.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.abci.v2.{}", Self::NAME) + } +} diff --git a/crates/astria-core/src/generated/connect.abci.v2.serde.rs b/crates/astria-core/src/generated/connect.abci.v2.serde.rs new file mode 100644 index 0000000000..b909661c94 --- /dev/null +++ b/crates/astria-core/src/generated/connect.abci.v2.serde.rs @@ -0,0 +1,96 @@ +impl serde::Serialize for OracleVoteExtension { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.prices.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.abci.v2.OracleVoteExtension", len)?; + if !self.prices.is_empty() { + let v: std::collections::HashMap<_, _> = self.prices.iter() + .map(|(k, v)| (k, pbjson::private::base64::encode(v))).collect(); + struct_ser.serialize_field("prices", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for OracleVoteExtension { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "prices", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Prices, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "prices" => Ok(GeneratedField::Prices), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = OracleVoteExtension; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.abci.v2.OracleVoteExtension") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut prices__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Prices => { + if prices__.is_some() { + return Err(serde::de::Error::duplicate_field("prices")); + } + prices__ = Some( + map_.next_value::, ::pbjson::private::BytesDeserialize<_>>>()? + .into_iter().map(|(k,v)| (k.0, v.0)).collect() + ); + } + } + } + Ok(OracleVoteExtension { + prices: prices__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.abci.v2.OracleVoteExtension", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/connect.marketmap.v2.rs b/crates/astria-core/src/generated/connect.marketmap.v2.rs new file mode 100644 index 0000000000..148e1e3db5 --- /dev/null +++ b/crates/astria-core/src/generated/connect.marketmap.v2.rs @@ -0,0 +1,793 @@ +/// Market encapsulates a Ticker and its provider-specific configuration. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Market { + /// Ticker represents a price feed for a given asset pair i.e. BTC/USD. The + /// price feed is scaled to a number of decimal places and has a minimum number + /// of providers required to consider the ticker valid. + #[prost(message, optional, tag = "1")] + pub ticker: ::core::option::Option, + /// ProviderConfigs is the list of provider-specific configs for this Market. + #[prost(message, repeated, tag = "2")] + pub provider_configs: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for Market { + const NAME: &'static str = "Market"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// Ticker represents a price feed for a given asset pair i.e. BTC/USD. The price +/// feed is scaled to a number of decimal places and has a minimum number of +/// providers required to consider the ticker valid. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Ticker { + /// CurrencyPair is the currency pair for this ticker. + #[prost(message, optional, tag = "1")] + pub currency_pair: ::core::option::Option, + /// Decimals is the number of decimal places for the ticker. The number of + /// decimal places is used to convert the price to a human-readable format. + #[prost(uint64, tag = "2")] + pub decimals: u64, + /// MinProviderCount is the minimum number of providers required to consider + /// the ticker valid. + #[prost(uint64, tag = "3")] + pub min_provider_count: u64, + /// Enabled is the flag that denotes if the Ticker is enabled for price + /// fetching by an oracle. + #[prost(bool, tag = "14")] + pub enabled: bool, + /// MetadataJSON is a string of JSON that encodes any extra configuration + /// for the given ticker. + #[prost(string, tag = "15")] + pub metadata_json: ::prost::alloc::string::String, +} +impl ::prost::Name for Ticker { + const NAME: &'static str = "Ticker"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProviderConfig { + /// Name corresponds to the name of the provider for which the configuration is + /// being set. + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// OffChainTicker is the off-chain representation of the ticker i.e. BTC/USD. + /// The off-chain ticker is unique to a given provider and is used to fetch the + /// price of the ticker from the provider. + #[prost(string, tag = "2")] + pub off_chain_ticker: ::prost::alloc::string::String, + /// NormalizeByPair is the currency pair for this ticker to be normalized by. + /// For example, if the desired Ticker is BTC/USD, this market could be reached + /// using: OffChainTicker = BTC/USDT NormalizeByPair = USDT/USD This field is + /// optional and nullable. + #[prost(message, optional, tag = "3")] + pub normalize_by_pair: ::core::option::Option, + /// Invert is a boolean indicating if the BASE and QUOTE of the market should + /// be inverted. i.e. BASE -> QUOTE, QUOTE -> BASE + #[prost(bool, tag = "4")] + pub invert: bool, + /// MetadataJSON is a string of JSON that encodes any extra configuration + /// for the given provider config. + #[prost(string, tag = "15")] + pub metadata_json: ::prost::alloc::string::String, +} +impl ::prost::Name for ProviderConfig { + const NAME: &'static str = "ProviderConfig"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// MarketMap maps ticker strings to their Markets. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketMap { + /// Markets is the full list of tickers and their associated configurations + /// to be stored on-chain. + #[prost(btree_map = "string, message", tag = "1")] + pub markets: ::prost::alloc::collections::BTreeMap< + ::prost::alloc::string::String, + Market, + >, +} +impl ::prost::Name for MarketMap { + const NAME: &'static str = "MarketMap"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// Params defines the parameters for the x/marketmap module. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Params { + /// MarketAuthorities is the list of authority accounts that are able to + /// control updating the marketmap. + #[prost(string, repeated, tag = "1")] + pub market_authorities: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Admin is an address that can remove addresses from the MarketAuthorities + /// list. Only governance can add to the MarketAuthorities or change the Admin. + #[prost(string, tag = "2")] + pub admin: ::prost::alloc::string::String, +} +impl ::prost::Name for Params { + const NAME: &'static str = "Params"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// GenesisState defines the x/marketmap module's genesis state. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenesisState { + /// MarketMap defines the global set of market configurations for all providers + /// and markets. + #[prost(message, optional, tag = "1")] + pub market_map: ::core::option::Option, + /// LastUpdated is the last block height that the market map was updated. + /// This field can be used as an optimization for clients checking if there + /// is a new update to the map. + #[prost(uint64, tag = "2")] + pub last_updated: u64, + /// Params are the parameters for the x/marketmap module. + #[prost(message, optional, tag = "3")] + pub params: ::core::option::Option, +} +impl ::prost::Name for GenesisState { + const NAME: &'static str = "GenesisState"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// MarketMapRequest is the query request for the MarketMap query. +/// It takes no arguments. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketMapRequest {} +impl ::prost::Name for MarketMapRequest { + const NAME: &'static str = "MarketMapRequest"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// MarketMapResponse is the query response for the MarketMap query. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketMapResponse { + /// MarketMap defines the global set of market configurations for all providers + /// and markets. + #[prost(message, optional, tag = "1")] + pub market_map: ::core::option::Option, + /// LastUpdated is the last block height that the market map was updated. + /// This field can be used as an optimization for clients checking if there + /// is a new update to the map. + #[prost(uint64, tag = "2")] + pub last_updated: u64, + /// ChainId is the chain identifier for the market map. + #[prost(string, tag = "3")] + pub chain_id: ::prost::alloc::string::String, +} +impl ::prost::Name for MarketMapResponse { + const NAME: &'static str = "MarketMapResponse"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// MarketRequest is the query request for the Market query. +/// It takes the currency pair of the market as an argument. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketRequest { + /// CurrencyPair is the currency pair associated with the market being + /// requested. + #[prost(message, optional, tag = "1")] + pub currency_pair: ::core::option::Option, +} +impl ::prost::Name for MarketRequest { + const NAME: &'static str = "MarketRequest"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// MarketResponse is the query response for the Market query. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketResponse { + /// Market is the configuration of a single market to be price-fetched for. + #[prost(message, optional, tag = "1")] + pub market: ::core::option::Option, +} +impl ::prost::Name for MarketResponse { + const NAME: &'static str = "MarketResponse"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// ParamsRequest is the request type for the Query/Params RPC method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ParamsRequest {} +impl ::prost::Name for ParamsRequest { + const NAME: &'static str = "ParamsRequest"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// ParamsResponse is the response type for the Query/Params RPC method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ParamsResponse { + #[prost(message, optional, tag = "1")] + pub params: ::core::option::Option, +} +impl ::prost::Name for ParamsResponse { + const NAME: &'static str = "ParamsResponse"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// LastUpdatedRequest is the request type for the Query/LastUpdated RPC +/// method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LastUpdatedRequest {} +impl ::prost::Name for LastUpdatedRequest { + const NAME: &'static str = "LastUpdatedRequest"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// LastUpdatedResponse is the response type for the Query/LastUpdated RPC +/// method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LastUpdatedResponse { + #[prost(uint64, tag = "1")] + pub last_updated: u64, +} +impl ::prost::Name for LastUpdatedResponse { + const NAME: &'static str = "LastUpdatedResponse"; + const PACKAGE: &'static str = "connect.marketmap.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.marketmap.v2.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod query_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Query is the query service for the x/marketmap module. + #[derive(Debug, Clone)] + pub struct QueryClient { + inner: tonic::client::Grpc, + } + impl QueryClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl QueryClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> QueryClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + QueryClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// MarketMap returns the full market map stored in the x/marketmap + /// module. + pub async fn market_map( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.marketmap.v2.Query/MarketMap", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.marketmap.v2.Query", "MarketMap")); + self.inner.unary(req, path, codec).await + } + /// Market returns a market stored in the x/marketmap + /// module. + pub async fn market( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.marketmap.v2.Query/Market", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.marketmap.v2.Query", "Market")); + self.inner.unary(req, path, codec).await + } + /// LastUpdated returns the last height the market map was updated at. + pub async fn last_updated( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.marketmap.v2.Query/LastUpdated", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.marketmap.v2.Query", "LastUpdated")); + self.inner.unary(req, path, codec).await + } + /// Params returns the current x/marketmap module parameters. + pub async fn params( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.marketmap.v2.Query/Params", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.marketmap.v2.Query", "Params")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod query_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with QueryServer. + #[async_trait] + pub trait Query: Send + Sync + 'static { + /// MarketMap returns the full market map stored in the x/marketmap + /// module. + async fn market_map( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Market returns a market stored in the x/marketmap + /// module. + async fn market( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// LastUpdated returns the last height the market map was updated at. + async fn last_updated( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Params returns the current x/marketmap module parameters. + async fn params( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + /// Query is the query service for the x/marketmap module. + #[derive(Debug)] + pub struct QueryServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl QueryServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for QueryServer + where + T: Query, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/connect.marketmap.v2.Query/MarketMap" => { + #[allow(non_camel_case_types)] + struct MarketMapSvc(pub Arc); + impl tonic::server::UnaryService + for MarketMapSvc { + type Response = super::MarketMapResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::market_map(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = MarketMapSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.marketmap.v2.Query/Market" => { + #[allow(non_camel_case_types)] + struct MarketSvc(pub Arc); + impl tonic::server::UnaryService + for MarketSvc { + type Response = super::MarketResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::market(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = MarketSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.marketmap.v2.Query/LastUpdated" => { + #[allow(non_camel_case_types)] + struct LastUpdatedSvc(pub Arc); + impl tonic::server::UnaryService + for LastUpdatedSvc { + type Response = super::LastUpdatedResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::last_updated(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = LastUpdatedSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.marketmap.v2.Query/Params" => { + #[allow(non_camel_case_types)] + struct ParamsSvc(pub Arc); + impl tonic::server::UnaryService + for ParamsSvc { + type Response = super::ParamsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::params(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = ParamsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for QueryServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for QueryServer { + const NAME: &'static str = "connect.marketmap.v2.Query"; + } +} diff --git a/crates/astria-core/src/generated/connect.marketmap.v2.serde.rs b/crates/astria-core/src/generated/connect.marketmap.v2.serde.rs new file mode 100644 index 0000000000..f73db9da9b --- /dev/null +++ b/crates/astria-core/src/generated/connect.marketmap.v2.serde.rs @@ -0,0 +1,1484 @@ +impl serde::Serialize for GenesisState { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.market_map.is_some() { + len += 1; + } + if self.last_updated != 0 { + len += 1; + } + if self.params.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.GenesisState", len)?; + if let Some(v) = self.market_map.as_ref() { + struct_ser.serialize_field("marketMap", v)?; + } + if self.last_updated != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("lastUpdated", ToString::to_string(&self.last_updated).as_str())?; + } + if let Some(v) = self.params.as_ref() { + struct_ser.serialize_field("params", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GenesisState { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market_map", + "marketMap", + "last_updated", + "lastUpdated", + "params", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MarketMap, + LastUpdated, + Params, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "marketMap" | "market_map" => Ok(GeneratedField::MarketMap), + "lastUpdated" | "last_updated" => Ok(GeneratedField::LastUpdated), + "params" => Ok(GeneratedField::Params), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GenesisState; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.GenesisState") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market_map__ = None; + let mut last_updated__ = None; + let mut params__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MarketMap => { + if market_map__.is_some() { + return Err(serde::de::Error::duplicate_field("marketMap")); + } + market_map__ = map_.next_value()?; + } + GeneratedField::LastUpdated => { + if last_updated__.is_some() { + return Err(serde::de::Error::duplicate_field("lastUpdated")); + } + last_updated__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Params => { + if params__.is_some() { + return Err(serde::de::Error::duplicate_field("params")); + } + params__ = map_.next_value()?; + } + } + } + Ok(GenesisState { + market_map: market_map__, + last_updated: last_updated__.unwrap_or_default(), + params: params__, + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.GenesisState", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for LastUpdatedRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.marketmap.v2.LastUpdatedRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LastUpdatedRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LastUpdatedRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.LastUpdatedRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(LastUpdatedRequest { + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.LastUpdatedRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for LastUpdatedResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.last_updated != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.LastUpdatedResponse", len)?; + if self.last_updated != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("lastUpdated", ToString::to_string(&self.last_updated).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for LastUpdatedResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "last_updated", + "lastUpdated", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + LastUpdated, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "lastUpdated" | "last_updated" => Ok(GeneratedField::LastUpdated), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = LastUpdatedResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.LastUpdatedResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut last_updated__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::LastUpdated => { + if last_updated__.is_some() { + return Err(serde::de::Error::duplicate_field("lastUpdated")); + } + last_updated__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(LastUpdatedResponse { + last_updated: last_updated__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.LastUpdatedResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Market { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.ticker.is_some() { + len += 1; + } + if !self.provider_configs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.Market", len)?; + if let Some(v) = self.ticker.as_ref() { + struct_ser.serialize_field("ticker", v)?; + } + if !self.provider_configs.is_empty() { + struct_ser.serialize_field("providerConfigs", &self.provider_configs)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Market { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "ticker", + "provider_configs", + "providerConfigs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Ticker, + ProviderConfigs, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "ticker" => Ok(GeneratedField::Ticker), + "providerConfigs" | "provider_configs" => Ok(GeneratedField::ProviderConfigs), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Market; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.Market") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut ticker__ = None; + let mut provider_configs__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Ticker => { + if ticker__.is_some() { + return Err(serde::de::Error::duplicate_field("ticker")); + } + ticker__ = map_.next_value()?; + } + GeneratedField::ProviderConfigs => { + if provider_configs__.is_some() { + return Err(serde::de::Error::duplicate_field("providerConfigs")); + } + provider_configs__ = Some(map_.next_value()?); + } + } + } + Ok(Market { + ticker: ticker__, + provider_configs: provider_configs__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.Market", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MarketMap { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.markets.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.MarketMap", len)?; + if !self.markets.is_empty() { + struct_ser.serialize_field("markets", &self.markets)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketMap { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "markets", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Markets, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "markets" => Ok(GeneratedField::Markets), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MarketMap; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.MarketMap") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut markets__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Markets => { + if markets__.is_some() { + return Err(serde::de::Error::duplicate_field("markets")); + } + markets__ = Some( + map_.next_value::>()? + ); + } + } + } + Ok(MarketMap { + markets: markets__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.MarketMap", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MarketMapRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.marketmap.v2.MarketMapRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketMapRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MarketMapRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.MarketMapRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(MarketMapRequest { + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.MarketMapRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MarketMapResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.market_map.is_some() { + len += 1; + } + if self.last_updated != 0 { + len += 1; + } + if !self.chain_id.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.MarketMapResponse", len)?; + if let Some(v) = self.market_map.as_ref() { + struct_ser.serialize_field("marketMap", v)?; + } + if self.last_updated != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("lastUpdated", ToString::to_string(&self.last_updated).as_str())?; + } + if !self.chain_id.is_empty() { + struct_ser.serialize_field("chainId", &self.chain_id)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketMapResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market_map", + "marketMap", + "last_updated", + "lastUpdated", + "chain_id", + "chainId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MarketMap, + LastUpdated, + ChainId, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "marketMap" | "market_map" => Ok(GeneratedField::MarketMap), + "lastUpdated" | "last_updated" => Ok(GeneratedField::LastUpdated), + "chainId" | "chain_id" => Ok(GeneratedField::ChainId), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MarketMapResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.MarketMapResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market_map__ = None; + let mut last_updated__ = None; + let mut chain_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MarketMap => { + if market_map__.is_some() { + return Err(serde::de::Error::duplicate_field("marketMap")); + } + market_map__ = map_.next_value()?; + } + GeneratedField::LastUpdated => { + if last_updated__.is_some() { + return Err(serde::de::Error::duplicate_field("lastUpdated")); + } + last_updated__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::ChainId => { + if chain_id__.is_some() { + return Err(serde::de::Error::duplicate_field("chainId")); + } + chain_id__ = Some(map_.next_value()?); + } + } + } + Ok(MarketMapResponse { + market_map: market_map__, + last_updated: last_updated__.unwrap_or_default(), + chain_id: chain_id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.MarketMapResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MarketRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.currency_pair.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.MarketRequest", len)?; + if let Some(v) = self.currency_pair.as_ref() { + struct_ser.serialize_field("currencyPair", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair", + "currencyPair", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPair, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPair" | "currency_pair" => Ok(GeneratedField::CurrencyPair), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MarketRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.MarketRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPair => { + if currency_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPair")); + } + currency_pair__ = map_.next_value()?; + } + } + } + Ok(MarketRequest { + currency_pair: currency_pair__, + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.MarketRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MarketResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.market.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.MarketResponse", len)?; + if let Some(v) = self.market.as_ref() { + struct_ser.serialize_field("market", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Market, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "market" => Ok(GeneratedField::Market), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MarketResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.MarketResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Market => { + if market__.is_some() { + return Err(serde::de::Error::duplicate_field("market")); + } + market__ = map_.next_value()?; + } + } + } + Ok(MarketResponse { + market: market__, + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.MarketResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Params { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.market_authorities.is_empty() { + len += 1; + } + if !self.admin.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.Params", len)?; + if !self.market_authorities.is_empty() { + struct_ser.serialize_field("marketAuthorities", &self.market_authorities)?; + } + if !self.admin.is_empty() { + struct_ser.serialize_field("admin", &self.admin)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Params { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market_authorities", + "marketAuthorities", + "admin", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MarketAuthorities, + Admin, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "marketAuthorities" | "market_authorities" => Ok(GeneratedField::MarketAuthorities), + "admin" => Ok(GeneratedField::Admin), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Params; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.Params") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market_authorities__ = None; + let mut admin__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MarketAuthorities => { + if market_authorities__.is_some() { + return Err(serde::de::Error::duplicate_field("marketAuthorities")); + } + market_authorities__ = Some(map_.next_value()?); + } + GeneratedField::Admin => { + if admin__.is_some() { + return Err(serde::de::Error::duplicate_field("admin")); + } + admin__ = Some(map_.next_value()?); + } + } + } + Ok(Params { + market_authorities: market_authorities__.unwrap_or_default(), + admin: admin__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.Params", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ParamsRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.marketmap.v2.ParamsRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ParamsRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ParamsRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.ParamsRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(ParamsRequest { + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.ParamsRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ParamsResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.params.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.ParamsResponse", len)?; + if let Some(v) = self.params.as_ref() { + struct_ser.serialize_field("params", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ParamsResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "params", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Params, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "params" => Ok(GeneratedField::Params), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ParamsResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.ParamsResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut params__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Params => { + if params__.is_some() { + return Err(serde::de::Error::duplicate_field("params")); + } + params__ = map_.next_value()?; + } + } + } + Ok(ParamsResponse { + params: params__, + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.ParamsResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for ProviderConfig { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.name.is_empty() { + len += 1; + } + if !self.off_chain_ticker.is_empty() { + len += 1; + } + if self.normalize_by_pair.is_some() { + len += 1; + } + if self.invert { + len += 1; + } + if !self.metadata_json.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.ProviderConfig", len)?; + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if !self.off_chain_ticker.is_empty() { + struct_ser.serialize_field("offChainTicker", &self.off_chain_ticker)?; + } + if let Some(v) = self.normalize_by_pair.as_ref() { + struct_ser.serialize_field("normalizeByPair", v)?; + } + if self.invert { + struct_ser.serialize_field("invert", &self.invert)?; + } + if !self.metadata_json.is_empty() { + struct_ser.serialize_field("metadataJSON", &self.metadata_json)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ProviderConfig { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "name", + "off_chain_ticker", + "offChainTicker", + "normalize_by_pair", + "normalizeByPair", + "invert", + "metadata_JSON", + "metadataJSON", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Name, + OffChainTicker, + NormalizeByPair, + Invert, + MetadataJson, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "name" => Ok(GeneratedField::Name), + "offChainTicker" | "off_chain_ticker" => Ok(GeneratedField::OffChainTicker), + "normalizeByPair" | "normalize_by_pair" => Ok(GeneratedField::NormalizeByPair), + "invert" => Ok(GeneratedField::Invert), + "metadataJSON" | "metadata_JSON" => Ok(GeneratedField::MetadataJson), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ProviderConfig; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.ProviderConfig") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut name__ = None; + let mut off_chain_ticker__ = None; + let mut normalize_by_pair__ = None; + let mut invert__ = None; + let mut metadata_json__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + GeneratedField::OffChainTicker => { + if off_chain_ticker__.is_some() { + return Err(serde::de::Error::duplicate_field("offChainTicker")); + } + off_chain_ticker__ = Some(map_.next_value()?); + } + GeneratedField::NormalizeByPair => { + if normalize_by_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("normalizeByPair")); + } + normalize_by_pair__ = map_.next_value()?; + } + GeneratedField::Invert => { + if invert__.is_some() { + return Err(serde::de::Error::duplicate_field("invert")); + } + invert__ = Some(map_.next_value()?); + } + GeneratedField::MetadataJson => { + if metadata_json__.is_some() { + return Err(serde::de::Error::duplicate_field("metadataJSON")); + } + metadata_json__ = Some(map_.next_value()?); + } + } + } + Ok(ProviderConfig { + name: name__.unwrap_or_default(), + off_chain_ticker: off_chain_ticker__.unwrap_or_default(), + normalize_by_pair: normalize_by_pair__, + invert: invert__.unwrap_or_default(), + metadata_json: metadata_json__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.ProviderConfig", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Ticker { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.currency_pair.is_some() { + len += 1; + } + if self.decimals != 0 { + len += 1; + } + if self.min_provider_count != 0 { + len += 1; + } + if self.enabled { + len += 1; + } + if !self.metadata_json.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.marketmap.v2.Ticker", len)?; + if let Some(v) = self.currency_pair.as_ref() { + struct_ser.serialize_field("currencyPair", v)?; + } + if self.decimals != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("decimals", ToString::to_string(&self.decimals).as_str())?; + } + if self.min_provider_count != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("minProviderCount", ToString::to_string(&self.min_provider_count).as_str())?; + } + if self.enabled { + struct_ser.serialize_field("enabled", &self.enabled)?; + } + if !self.metadata_json.is_empty() { + struct_ser.serialize_field("metadataJSON", &self.metadata_json)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Ticker { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair", + "currencyPair", + "decimals", + "min_provider_count", + "minProviderCount", + "enabled", + "metadata_JSON", + "metadataJSON", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPair, + Decimals, + MinProviderCount, + Enabled, + MetadataJson, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPair" | "currency_pair" => Ok(GeneratedField::CurrencyPair), + "decimals" => Ok(GeneratedField::Decimals), + "minProviderCount" | "min_provider_count" => Ok(GeneratedField::MinProviderCount), + "enabled" => Ok(GeneratedField::Enabled), + "metadataJSON" | "metadata_JSON" => Ok(GeneratedField::MetadataJson), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Ticker; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.marketmap.v2.Ticker") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair__ = None; + let mut decimals__ = None; + let mut min_provider_count__ = None; + let mut enabled__ = None; + let mut metadata_json__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPair => { + if currency_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPair")); + } + currency_pair__ = map_.next_value()?; + } + GeneratedField::Decimals => { + if decimals__.is_some() { + return Err(serde::de::Error::duplicate_field("decimals")); + } + decimals__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::MinProviderCount => { + if min_provider_count__.is_some() { + return Err(serde::de::Error::duplicate_field("minProviderCount")); + } + min_provider_count__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Enabled => { + if enabled__.is_some() { + return Err(serde::de::Error::duplicate_field("enabled")); + } + enabled__ = Some(map_.next_value()?); + } + GeneratedField::MetadataJson => { + if metadata_json__.is_some() { + return Err(serde::de::Error::duplicate_field("metadataJSON")); + } + metadata_json__ = Some(map_.next_value()?); + } + } + } + Ok(Ticker { + currency_pair: currency_pair__, + decimals: decimals__.unwrap_or_default(), + min_provider_count: min_provider_count__.unwrap_or_default(), + enabled: enabled__.unwrap_or_default(), + metadata_json: metadata_json__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.marketmap.v2.Ticker", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/connect.oracle.v2.rs b/crates/astria-core/src/generated/connect.oracle.v2.rs new file mode 100644 index 0000000000..0815919721 --- /dev/null +++ b/crates/astria-core/src/generated/connect.oracle.v2.rs @@ -0,0 +1,766 @@ +/// QuotePrice is the representation of the aggregated prices for a CurrencyPair, +/// where price represents the price of Base in terms of Quote +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QuotePrice { + #[prost(string, tag = "1")] + pub price: ::prost::alloc::string::String, + /// BlockTimestamp tracks the block height associated with this price update. + /// We include block timestamp alongside the price to ensure that smart + /// contracts and applications are not utilizing stale oracle prices + #[prost(message, optional, tag = "2")] + pub block_timestamp: ::core::option::Option<::pbjson_types::Timestamp>, + /// BlockHeight is height of block mentioned above + #[prost(uint64, tag = "3")] + pub block_height: u64, +} +impl ::prost::Name for QuotePrice { + const NAME: &'static str = "QuotePrice"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// CurrencyPairState represents the stateful information tracked by the x/oracle +/// module per-currency-pair. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CurrencyPairState { + /// QuotePrice is the latest price for a currency-pair, notice this value can + /// be null in the case that no price exists for the currency-pair + #[prost(message, optional, tag = "1")] + pub price: ::core::option::Option, + /// Nonce is the number of updates this currency-pair has received + #[prost(uint64, tag = "2")] + pub nonce: u64, + /// ID is the ID of the CurrencyPair + #[prost(uint64, tag = "3")] + pub id: u64, +} +impl ::prost::Name for CurrencyPairState { + const NAME: &'static str = "CurrencyPairState"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// CurrencyPairGenesis is the information necessary for initialization of a +/// CurrencyPair. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CurrencyPairGenesis { + /// The CurrencyPair to be added to module state + #[prost(message, optional, tag = "1")] + pub currency_pair: ::core::option::Option, + /// A genesis price if one exists (note this will be empty, unless it results + /// from forking the state of this module) + #[prost(message, optional, tag = "2")] + pub currency_pair_price: ::core::option::Option, + /// nonce is the nonce (number of updates) for the CP (same case as above, + /// likely 0 unless it results from fork of module) + #[prost(uint64, tag = "3")] + pub nonce: u64, + /// id is the ID of the CurrencyPair + #[prost(uint64, tag = "4")] + pub id: u64, +} +impl ::prost::Name for CurrencyPairGenesis { + const NAME: &'static str = "CurrencyPairGenesis"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GenesisState is the genesis-state for the x/oracle module, it takes a set of +/// predefined CurrencyPairGeneses +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GenesisState { + /// CurrencyPairGenesis is the set of CurrencyPairGeneses for the module. I.e + /// the starting set of CurrencyPairs for the module + information regarding + /// their latest update. + #[prost(message, repeated, tag = "1")] + pub currency_pair_genesis: ::prost::alloc::vec::Vec, + /// NextID is the next ID to be used for a CurrencyPair + #[prost(uint64, tag = "2")] + pub next_id: u64, +} +impl ::prost::Name for GenesisState { + const NAME: &'static str = "GenesisState"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAllCurrencyPairsRequest {} +impl ::prost::Name for GetAllCurrencyPairsRequest { + const NAME: &'static str = "GetAllCurrencyPairsRequest"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetAllCurrencyPairsResponse returns all CurrencyPairs that the module is +/// currently tracking. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAllCurrencyPairsResponse { + #[prost(message, repeated, tag = "1")] + pub currency_pairs: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for GetAllCurrencyPairsResponse { + const NAME: &'static str = "GetAllCurrencyPairsResponse"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetPriceRequest takes an identifier for the +/// CurrencyPair in the format base/quote. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPriceRequest { + /// CurrencyPair represents the pair that the user wishes to query. + #[prost(string, tag = "1")] + pub currency_pair: ::prost::alloc::string::String, +} +impl ::prost::Name for GetPriceRequest { + const NAME: &'static str = "GetPriceRequest"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetPriceResponse is the response from the GetPrice grpc method exposed from +/// the x/oracle query service. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPriceResponse { + /// QuotePrice represents the quote-price for the CurrencyPair given in + /// GetPriceRequest (possibly nil if no update has been made) + #[prost(message, optional, tag = "1")] + pub price: ::core::option::Option, + /// nonce represents the nonce for the CurrencyPair if it exists in state + #[prost(uint64, tag = "2")] + pub nonce: u64, + /// decimals represents the number of decimals that the quote-price is + /// represented in. It is used to scale the QuotePrice to its proper value. + #[prost(uint64, tag = "3")] + pub decimals: u64, + /// ID represents the identifier for the CurrencyPair. + #[prost(uint64, tag = "4")] + pub id: u64, +} +impl ::prost::Name for GetPriceResponse { + const NAME: &'static str = "GetPriceResponse"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetPricesRequest takes an identifier for the CurrencyPair +/// in the format base/quote. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPricesRequest { + #[prost(string, repeated, tag = "1")] + pub currency_pair_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +impl ::prost::Name for GetPricesRequest { + const NAME: &'static str = "GetPricesRequest"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetPricesResponse is the response from the GetPrices grpc method exposed from +/// the x/oracle query service. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPricesResponse { + #[prost(message, repeated, tag = "1")] + pub prices: ::prost::alloc::vec::Vec, +} +impl ::prost::Name for GetPricesResponse { + const NAME: &'static str = "GetPricesResponse"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetCurrencyPairMappingRequest is the GetCurrencyPairMapping request type. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetCurrencyPairMappingRequest {} +impl ::prost::Name for GetCurrencyPairMappingRequest { + const NAME: &'static str = "GetCurrencyPairMappingRequest"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// GetCurrencyPairMappingResponse is the GetCurrencyPairMapping response type. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetCurrencyPairMappingResponse { + /// currency_pair_mapping is a mapping of the id representing the currency pair + /// to the currency pair itself. + #[prost(btree_map = "uint64, message", tag = "1")] + pub currency_pair_mapping: ::prost::alloc::collections::BTreeMap< + u64, + super::super::types::v2::CurrencyPair, + >, +} +impl ::prost::Name for GetCurrencyPairMappingResponse { + const NAME: &'static str = "GetCurrencyPairMappingResponse"; + const PACKAGE: &'static str = "connect.oracle.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.oracle.v2.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod query_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Query is the query service for the x/oracle module. + #[derive(Debug, Clone)] + pub struct QueryClient { + inner: tonic::client::Grpc, + } + impl QueryClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl QueryClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> QueryClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + QueryClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Get all the currency pairs the x/oracle module is tracking price-data for. + pub async fn get_all_currency_pairs( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.oracle.v2.Query/GetAllCurrencyPairs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("connect.oracle.v2.Query", "GetAllCurrencyPairs"), + ); + self.inner.unary(req, path, codec).await + } + /// Given a CurrencyPair (or its identifier) return the latest QuotePrice for + /// that CurrencyPair. + pub async fn get_price( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.oracle.v2.Query/GetPrice", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.oracle.v2.Query", "GetPrice")); + self.inner.unary(req, path, codec).await + } + pub async fn get_prices( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.oracle.v2.Query/GetPrices", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.oracle.v2.Query", "GetPrices")); + self.inner.unary(req, path, codec).await + } + /// Get the mapping of currency pair ID -> currency pair. This is useful for + /// indexers that have access to the ID of a currency pair, but no way to get + /// the underlying currency pair from it. + pub async fn get_currency_pair_mapping( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.oracle.v2.Query/GetCurrencyPairMapping", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("connect.oracle.v2.Query", "GetCurrencyPairMapping"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod query_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with QueryServer. + #[async_trait] + pub trait Query: Send + Sync + 'static { + /// Get all the currency pairs the x/oracle module is tracking price-data for. + async fn get_all_currency_pairs( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Given a CurrencyPair (or its identifier) return the latest QuotePrice for + /// that CurrencyPair. + async fn get_price( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + async fn get_prices( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Get the mapping of currency pair ID -> currency pair. This is useful for + /// indexers that have access to the ID of a currency pair, but no way to get + /// the underlying currency pair from it. + async fn get_currency_pair_mapping( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Query is the query service for the x/oracle module. + #[derive(Debug)] + pub struct QueryServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl QueryServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for QueryServer + where + T: Query, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/connect.oracle.v2.Query/GetAllCurrencyPairs" => { + #[allow(non_camel_case_types)] + struct GetAllCurrencyPairsSvc(pub Arc); + impl< + T: Query, + > tonic::server::UnaryService + for GetAllCurrencyPairsSvc { + type Response = super::GetAllCurrencyPairsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_all_currency_pairs(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetAllCurrencyPairsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.oracle.v2.Query/GetPrice" => { + #[allow(non_camel_case_types)] + struct GetPriceSvc(pub Arc); + impl tonic::server::UnaryService + for GetPriceSvc { + type Response = super::GetPriceResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_price(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetPriceSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.oracle.v2.Query/GetPrices" => { + #[allow(non_camel_case_types)] + struct GetPricesSvc(pub Arc); + impl tonic::server::UnaryService + for GetPricesSvc { + type Response = super::GetPricesResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_prices(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetPricesSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.oracle.v2.Query/GetCurrencyPairMapping" => { + #[allow(non_camel_case_types)] + struct GetCurrencyPairMappingSvc(pub Arc); + impl< + T: Query, + > tonic::server::UnaryService + for GetCurrencyPairMappingSvc { + type Response = super::GetCurrencyPairMappingResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_currency_pair_mapping(inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetCurrencyPairMappingSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for QueryServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for QueryServer { + const NAME: &'static str = "connect.oracle.v2.Query"; + } +} diff --git a/crates/astria-core/src/generated/connect.oracle.v2.serde.rs b/crates/astria-core/src/generated/connect.oracle.v2.serde.rs new file mode 100644 index 0000000000..5bb4c9fa6b --- /dev/null +++ b/crates/astria-core/src/generated/connect.oracle.v2.serde.rs @@ -0,0 +1,1279 @@ +impl serde::Serialize for CurrencyPairGenesis { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.currency_pair.is_some() { + len += 1; + } + if self.currency_pair_price.is_some() { + len += 1; + } + if self.nonce != 0 { + len += 1; + } + if self.id != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.CurrencyPairGenesis", len)?; + if let Some(v) = self.currency_pair.as_ref() { + struct_ser.serialize_field("currencyPair", v)?; + } + if let Some(v) = self.currency_pair_price.as_ref() { + struct_ser.serialize_field("currencyPairPrice", v)?; + } + if self.nonce != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nonce", ToString::to_string(&self.nonce).as_str())?; + } + if self.id != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("id", ToString::to_string(&self.id).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CurrencyPairGenesis { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair", + "currencyPair", + "currency_pair_price", + "currencyPairPrice", + "nonce", + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPair, + CurrencyPairPrice, + Nonce, + Id, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPair" | "currency_pair" => Ok(GeneratedField::CurrencyPair), + "currencyPairPrice" | "currency_pair_price" => Ok(GeneratedField::CurrencyPairPrice), + "nonce" => Ok(GeneratedField::Nonce), + "id" => Ok(GeneratedField::Id), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CurrencyPairGenesis; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.CurrencyPairGenesis") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair__ = None; + let mut currency_pair_price__ = None; + let mut nonce__ = None; + let mut id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPair => { + if currency_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPair")); + } + currency_pair__ = map_.next_value()?; + } + GeneratedField::CurrencyPairPrice => { + if currency_pair_price__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPairPrice")); + } + currency_pair_price__ = map_.next_value()?; + } + GeneratedField::Nonce => { + if nonce__.is_some() { + return Err(serde::de::Error::duplicate_field("nonce")); + } + nonce__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(CurrencyPairGenesis { + currency_pair: currency_pair__, + currency_pair_price: currency_pair_price__, + nonce: nonce__.unwrap_or_default(), + id: id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.CurrencyPairGenesis", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for CurrencyPairState { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.price.is_some() { + len += 1; + } + if self.nonce != 0 { + len += 1; + } + if self.id != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.CurrencyPairState", len)?; + if let Some(v) = self.price.as_ref() { + struct_ser.serialize_field("price", v)?; + } + if self.nonce != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nonce", ToString::to_string(&self.nonce).as_str())?; + } + if self.id != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("id", ToString::to_string(&self.id).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CurrencyPairState { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "price", + "nonce", + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Price, + Nonce, + Id, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "price" => Ok(GeneratedField::Price), + "nonce" => Ok(GeneratedField::Nonce), + "id" => Ok(GeneratedField::Id), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CurrencyPairState; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.CurrencyPairState") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut price__ = None; + let mut nonce__ = None; + let mut id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Price => { + if price__.is_some() { + return Err(serde::de::Error::duplicate_field("price")); + } + price__ = map_.next_value()?; + } + GeneratedField::Nonce => { + if nonce__.is_some() { + return Err(serde::de::Error::duplicate_field("nonce")); + } + nonce__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(CurrencyPairState { + price: price__, + nonce: nonce__.unwrap_or_default(), + id: id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.CurrencyPairState", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GenesisState { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.currency_pair_genesis.is_empty() { + len += 1; + } + if self.next_id != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GenesisState", len)?; + if !self.currency_pair_genesis.is_empty() { + struct_ser.serialize_field("currencyPairGenesis", &self.currency_pair_genesis)?; + } + if self.next_id != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nextId", ToString::to_string(&self.next_id).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GenesisState { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair_genesis", + "currencyPairGenesis", + "next_id", + "nextId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPairGenesis, + NextId, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPairGenesis" | "currency_pair_genesis" => Ok(GeneratedField::CurrencyPairGenesis), + "nextId" | "next_id" => Ok(GeneratedField::NextId), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GenesisState; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GenesisState") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair_genesis__ = None; + let mut next_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPairGenesis => { + if currency_pair_genesis__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPairGenesis")); + } + currency_pair_genesis__ = Some(map_.next_value()?); + } + GeneratedField::NextId => { + if next_id__.is_some() { + return Err(serde::de::Error::duplicate_field("nextId")); + } + next_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(GenesisState { + currency_pair_genesis: currency_pair_genesis__.unwrap_or_default(), + next_id: next_id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GenesisState", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetAllCurrencyPairsRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.oracle.v2.GetAllCurrencyPairsRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetAllCurrencyPairsRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetAllCurrencyPairsRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetAllCurrencyPairsRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(GetAllCurrencyPairsRequest { + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetAllCurrencyPairsRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetAllCurrencyPairsResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.currency_pairs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetAllCurrencyPairsResponse", len)?; + if !self.currency_pairs.is_empty() { + struct_ser.serialize_field("currencyPairs", &self.currency_pairs)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetAllCurrencyPairsResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pairs", + "currencyPairs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPairs, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPairs" | "currency_pairs" => Ok(GeneratedField::CurrencyPairs), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetAllCurrencyPairsResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetAllCurrencyPairsResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pairs__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPairs => { + if currency_pairs__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPairs")); + } + currency_pairs__ = Some(map_.next_value()?); + } + } + } + Ok(GetAllCurrencyPairsResponse { + currency_pairs: currency_pairs__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetAllCurrencyPairsResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetCurrencyPairMappingRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.oracle.v2.GetCurrencyPairMappingRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetCurrencyPairMappingRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetCurrencyPairMappingRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetCurrencyPairMappingRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(GetCurrencyPairMappingRequest { + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetCurrencyPairMappingRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetCurrencyPairMappingResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.currency_pair_mapping.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetCurrencyPairMappingResponse", len)?; + if !self.currency_pair_mapping.is_empty() { + struct_ser.serialize_field("currencyPairMapping", &self.currency_pair_mapping)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetCurrencyPairMappingResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair_mapping", + "currencyPairMapping", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPairMapping, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPairMapping" | "currency_pair_mapping" => Ok(GeneratedField::CurrencyPairMapping), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetCurrencyPairMappingResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetCurrencyPairMappingResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair_mapping__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPairMapping => { + if currency_pair_mapping__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPairMapping")); + } + currency_pair_mapping__ = Some( + map_.next_value::, _>>()? + .into_iter().map(|(k,v)| (k.0, v)).collect() + ); + } + } + } + Ok(GetCurrencyPairMappingResponse { + currency_pair_mapping: currency_pair_mapping__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetCurrencyPairMappingResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetPriceRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.currency_pair.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetPriceRequest", len)?; + if !self.currency_pair.is_empty() { + struct_ser.serialize_field("currencyPair", &self.currency_pair)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetPriceRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair", + "currencyPair", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPair, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPair" | "currency_pair" => Ok(GeneratedField::CurrencyPair), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetPriceRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetPriceRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPair => { + if currency_pair__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPair")); + } + currency_pair__ = Some(map_.next_value()?); + } + } + } + Ok(GetPriceRequest { + currency_pair: currency_pair__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetPriceRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetPriceResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.price.is_some() { + len += 1; + } + if self.nonce != 0 { + len += 1; + } + if self.decimals != 0 { + len += 1; + } + if self.id != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetPriceResponse", len)?; + if let Some(v) = self.price.as_ref() { + struct_ser.serialize_field("price", v)?; + } + if self.nonce != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("nonce", ToString::to_string(&self.nonce).as_str())?; + } + if self.decimals != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("decimals", ToString::to_string(&self.decimals).as_str())?; + } + if self.id != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("id", ToString::to_string(&self.id).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetPriceResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "price", + "nonce", + "decimals", + "id", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Price, + Nonce, + Decimals, + Id, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "price" => Ok(GeneratedField::Price), + "nonce" => Ok(GeneratedField::Nonce), + "decimals" => Ok(GeneratedField::Decimals), + "id" => Ok(GeneratedField::Id), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetPriceResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetPriceResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut price__ = None; + let mut nonce__ = None; + let mut decimals__ = None; + let mut id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Price => { + if price__.is_some() { + return Err(serde::de::Error::duplicate_field("price")); + } + price__ = map_.next_value()?; + } + GeneratedField::Nonce => { + if nonce__.is_some() { + return Err(serde::de::Error::duplicate_field("nonce")); + } + nonce__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Decimals => { + if decimals__.is_some() { + return Err(serde::de::Error::duplicate_field("decimals")); + } + decimals__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Id => { + if id__.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(GetPriceResponse { + price: price__, + nonce: nonce__.unwrap_or_default(), + decimals: decimals__.unwrap_or_default(), + id: id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetPriceResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetPricesRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.currency_pair_ids.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetPricesRequest", len)?; + if !self.currency_pair_ids.is_empty() { + struct_ser.serialize_field("currencyPairIds", &self.currency_pair_ids)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetPricesRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "currency_pair_ids", + "currencyPairIds", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + CurrencyPairIds, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "currencyPairIds" | "currency_pair_ids" => Ok(GeneratedField::CurrencyPairIds), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetPricesRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetPricesRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut currency_pair_ids__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::CurrencyPairIds => { + if currency_pair_ids__.is_some() { + return Err(serde::de::Error::duplicate_field("currencyPairIds")); + } + currency_pair_ids__ = Some(map_.next_value()?); + } + } + } + Ok(GetPricesRequest { + currency_pair_ids: currency_pair_ids__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetPricesRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetPricesResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.prices.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.GetPricesResponse", len)?; + if !self.prices.is_empty() { + struct_ser.serialize_field("prices", &self.prices)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetPricesResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "prices", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Prices, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "prices" => Ok(GeneratedField::Prices), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetPricesResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.GetPricesResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut prices__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Prices => { + if prices__.is_some() { + return Err(serde::de::Error::duplicate_field("prices")); + } + prices__ = Some(map_.next_value()?); + } + } + } + Ok(GetPricesResponse { + prices: prices__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.GetPricesResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QuotePrice { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.price.is_empty() { + len += 1; + } + if self.block_timestamp.is_some() { + len += 1; + } + if self.block_height != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.oracle.v2.QuotePrice", len)?; + if !self.price.is_empty() { + struct_ser.serialize_field("price", &self.price)?; + } + if let Some(v) = self.block_timestamp.as_ref() { + struct_ser.serialize_field("blockTimestamp", v)?; + } + if self.block_height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("blockHeight", ToString::to_string(&self.block_height).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QuotePrice { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "price", + "block_timestamp", + "blockTimestamp", + "block_height", + "blockHeight", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Price, + BlockTimestamp, + BlockHeight, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "price" => Ok(GeneratedField::Price), + "blockTimestamp" | "block_timestamp" => Ok(GeneratedField::BlockTimestamp), + "blockHeight" | "block_height" => Ok(GeneratedField::BlockHeight), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QuotePrice; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.oracle.v2.QuotePrice") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut price__ = None; + let mut block_timestamp__ = None; + let mut block_height__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Price => { + if price__.is_some() { + return Err(serde::de::Error::duplicate_field("price")); + } + price__ = Some(map_.next_value()?); + } + GeneratedField::BlockTimestamp => { + if block_timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("blockTimestamp")); + } + block_timestamp__ = map_.next_value()?; + } + GeneratedField::BlockHeight => { + if block_height__.is_some() { + return Err(serde::de::Error::duplicate_field("blockHeight")); + } + block_height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(QuotePrice { + price: price__.unwrap_or_default(), + block_timestamp: block_timestamp__, + block_height: block_height__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.oracle.v2.QuotePrice", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/connect.service.v2.rs b/crates/astria-core/src/generated/connect.service.v2.rs new file mode 100644 index 0000000000..fadf917481 --- /dev/null +++ b/crates/astria-core/src/generated/connect.service.v2.rs @@ -0,0 +1,550 @@ +/// QueryPricesRequest defines the request type for the the Prices method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryPricesRequest {} +impl ::prost::Name for QueryPricesRequest { + const NAME: &'static str = "QueryPricesRequest"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// QueryPricesResponse defines the response type for the Prices method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryPricesResponse { + /// Prices defines the list of prices. + #[prost(btree_map = "string, string", tag = "1")] + pub prices: ::prost::alloc::collections::BTreeMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, + /// Timestamp defines the timestamp of the prices. + #[prost(message, optional, tag = "2")] + pub timestamp: ::core::option::Option<::pbjson_types::Timestamp>, + /// Version defines the version of the oracle service that provided the prices. + #[prost(string, tag = "3")] + pub version: ::prost::alloc::string::String, +} +impl ::prost::Name for QueryPricesResponse { + const NAME: &'static str = "QueryPricesResponse"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// QueryMarketMapRequest defines the request type for the MarketMap method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryMarketMapRequest {} +impl ::prost::Name for QueryMarketMapRequest { + const NAME: &'static str = "QueryMarketMapRequest"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// QueryMarketMapResponse defines the response type for the MarketMap method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryMarketMapResponse { + /// MarketMap defines the current market map configuration. + #[prost(message, optional, tag = "1")] + pub market_map: ::core::option::Option, +} +impl ::prost::Name for QueryMarketMapResponse { + const NAME: &'static str = "QueryMarketMapResponse"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// QueryVersionRequest defines the request type for the Version method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryVersionRequest {} +impl ::prost::Name for QueryVersionRequest { + const NAME: &'static str = "QueryVersionRequest"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// QueryVersionResponse defines the response type for the Version method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct QueryVersionResponse { + /// Version defines the current version of the oracle service. + #[prost(string, tag = "1")] + pub version: ::prost::alloc::string::String, +} +impl ::prost::Name for QueryVersionResponse { + const NAME: &'static str = "QueryVersionResponse"; + const PACKAGE: &'static str = "connect.service.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.service.v2.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod oracle_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Oracle defines the gRPC oracle service. + #[derive(Debug, Clone)] + pub struct OracleClient { + inner: tonic::client::Grpc, + } + impl OracleClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl OracleClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> OracleClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + OracleClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Prices defines a method for fetching the latest prices. + pub async fn prices( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.service.v2.Oracle/Prices", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.service.v2.Oracle", "Prices")); + self.inner.unary(req, path, codec).await + } + /// MarketMap defines a method for fetching the latest market map + /// configuration. + pub async fn market_map( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.service.v2.Oracle/MarketMap", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.service.v2.Oracle", "MarketMap")); + self.inner.unary(req, path, codec).await + } + /// Version defines a method for fetching the current version of the oracle + /// service. + pub async fn version( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/connect.service.v2.Oracle/Version", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("connect.service.v2.Oracle", "Version")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod oracle_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with OracleServer. + #[async_trait] + pub trait Oracle: Send + Sync + 'static { + /// Prices defines a method for fetching the latest prices. + async fn prices( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// MarketMap defines a method for fetching the latest market map + /// configuration. + async fn market_map( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Version defines a method for fetching the current version of the oracle + /// service. + async fn version( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Oracle defines the gRPC oracle service. + #[derive(Debug)] + pub struct OracleServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl OracleServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for OracleServer + where + T: Oracle, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/connect.service.v2.Oracle/Prices" => { + #[allow(non_camel_case_types)] + struct PricesSvc(pub Arc); + impl< + T: Oracle, + > tonic::server::UnaryService + for PricesSvc { + type Response = super::QueryPricesResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::prices(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = PricesSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.service.v2.Oracle/MarketMap" => { + #[allow(non_camel_case_types)] + struct MarketMapSvc(pub Arc); + impl< + T: Oracle, + > tonic::server::UnaryService + for MarketMapSvc { + type Response = super::QueryMarketMapResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::market_map(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = MarketMapSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/connect.service.v2.Oracle/Version" => { + #[allow(non_camel_case_types)] + struct VersionSvc(pub Arc); + impl< + T: Oracle, + > tonic::server::UnaryService + for VersionSvc { + type Response = super::QueryVersionResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::version(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = VersionSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for OracleServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for OracleServer { + const NAME: &'static str = "connect.service.v2.Oracle"; + } +} diff --git a/crates/astria-core/src/generated/connect.service.v2.serde.rs b/crates/astria-core/src/generated/connect.service.v2.serde.rs new file mode 100644 index 0000000000..a67adb978d --- /dev/null +++ b/crates/astria-core/src/generated/connect.service.v2.serde.rs @@ -0,0 +1,523 @@ +impl serde::Serialize for QueryMarketMapRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.service.v2.QueryMarketMapRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryMarketMapRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryMarketMapRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryMarketMapRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(QueryMarketMapRequest { + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryMarketMapRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QueryMarketMapResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.market_map.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.service.v2.QueryMarketMapResponse", len)?; + if let Some(v) = self.market_map.as_ref() { + struct_ser.serialize_field("marketMap", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryMarketMapResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "market_map", + "marketMap", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + MarketMap, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "marketMap" | "market_map" => Ok(GeneratedField::MarketMap), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryMarketMapResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryMarketMapResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut market_map__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::MarketMap => { + if market_map__.is_some() { + return Err(serde::de::Error::duplicate_field("marketMap")); + } + market_map__ = map_.next_value()?; + } + } + } + Ok(QueryMarketMapResponse { + market_map: market_map__, + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryMarketMapResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QueryPricesRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.service.v2.QueryPricesRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryPricesRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryPricesRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryPricesRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(QueryPricesRequest { + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryPricesRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QueryPricesResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.prices.is_empty() { + len += 1; + } + if self.timestamp.is_some() { + len += 1; + } + if !self.version.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.service.v2.QueryPricesResponse", len)?; + if !self.prices.is_empty() { + struct_ser.serialize_field("prices", &self.prices)?; + } + if let Some(v) = self.timestamp.as_ref() { + struct_ser.serialize_field("timestamp", v)?; + } + if !self.version.is_empty() { + struct_ser.serialize_field("version", &self.version)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryPricesResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "prices", + "timestamp", + "version", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Prices, + Timestamp, + Version, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "prices" => Ok(GeneratedField::Prices), + "timestamp" => Ok(GeneratedField::Timestamp), + "version" => Ok(GeneratedField::Version), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryPricesResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryPricesResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut prices__ = None; + let mut timestamp__ = None; + let mut version__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Prices => { + if prices__.is_some() { + return Err(serde::de::Error::duplicate_field("prices")); + } + prices__ = Some( + map_.next_value::>()? + ); + } + GeneratedField::Timestamp => { + if timestamp__.is_some() { + return Err(serde::de::Error::duplicate_field("timestamp")); + } + timestamp__ = map_.next_value()?; + } + GeneratedField::Version => { + if version__.is_some() { + return Err(serde::de::Error::duplicate_field("version")); + } + version__ = Some(map_.next_value()?); + } + } + } + Ok(QueryPricesResponse { + prices: prices__.unwrap_or_default(), + timestamp: timestamp__, + version: version__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryPricesResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QueryVersionRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("connect.service.v2.QueryVersionRequest", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryVersionRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryVersionRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryVersionRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(QueryVersionRequest { + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryVersionRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for QueryVersionResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.version.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.service.v2.QueryVersionResponse", len)?; + if !self.version.is_empty() { + struct_ser.serialize_field("version", &self.version)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for QueryVersionResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "version", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Version, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "version" => Ok(GeneratedField::Version), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = QueryVersionResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.service.v2.QueryVersionResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut version__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Version => { + if version__.is_some() { + return Err(serde::de::Error::duplicate_field("version")); + } + version__ = Some(map_.next_value()?); + } + } + } + Ok(QueryVersionResponse { + version: version__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.service.v2.QueryVersionResponse", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/connect.types.v2.rs b/crates/astria-core/src/generated/connect.types.v2.rs new file mode 100644 index 0000000000..4ee9cb5851 --- /dev/null +++ b/crates/astria-core/src/generated/connect.types.v2.rs @@ -0,0 +1,17 @@ +/// CurrencyPair is the standard representation of a pair of assets, where one +/// (Base) is priced in terms of the other (Quote) +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CurrencyPair { + #[prost(string, tag = "1")] + pub base: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub quote: ::prost::alloc::string::String, +} +impl ::prost::Name for CurrencyPair { + const NAME: &'static str = "CurrencyPair"; + const PACKAGE: &'static str = "connect.types.v2"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("connect.types.v2.{}", Self::NAME) + } +} diff --git a/crates/astria-core/src/generated/connect.types.v2.serde.rs b/crates/astria-core/src/generated/connect.types.v2.serde.rs new file mode 100644 index 0000000000..695fdcca9e --- /dev/null +++ b/crates/astria-core/src/generated/connect.types.v2.serde.rs @@ -0,0 +1,108 @@ +impl serde::Serialize for CurrencyPair { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.base.is_empty() { + len += 1; + } + if !self.quote.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("connect.types.v2.CurrencyPair", len)?; + if !self.base.is_empty() { + struct_ser.serialize_field("Base", &self.base)?; + } + if !self.quote.is_empty() { + struct_ser.serialize_field("Quote", &self.quote)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for CurrencyPair { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "Base", + "Quote", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Quote, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "Base" => Ok(GeneratedField::Base), + "Quote" => Ok(GeneratedField::Quote), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = CurrencyPair; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct connect.types.v2.CurrencyPair") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut quote__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("Base")); + } + base__ = Some(map_.next_value()?); + } + GeneratedField::Quote => { + if quote__.is_some() { + return Err(serde::de::Error::duplicate_field("Quote")); + } + quote__ = Some(map_.next_value()?); + } + } + } + Ok(CurrencyPair { + base: base__.unwrap_or_default(), + quote: quote__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("connect.types.v2.CurrencyPair", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index a0a39c4d2a..82f6420c7b 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -11,6 +11,8 @@ //! [`buf`]: https://buf.build //! [`tools/protobuf-compiler`]: ../../../../tools/protobuf-compiler +pub use astria::*; + #[path = ""] pub mod astria_vendored { #[path = ""] @@ -38,151 +40,207 @@ pub mod astria_vendored { } #[path = ""] -pub mod bundle { - pub mod v1alpha1 { - include!("astria.bundle.v1alpha1.rs"); - - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("astria.bundle.v1alpha1.serde.rs"); - } - } -} - -#[path = ""] -pub mod execution { - pub mod v1 { - include!("astria.execution.v1.rs"); - - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("astria.execution.v1.serde.rs"); - } - } -} - -#[path = ""] -pub mod primitive { - pub mod v1 { - include!("astria.primitive.v1.rs"); +pub mod astria { + #[path = ""] + pub mod bundle { + pub mod v1alpha1 { + include!("astria.bundle.v1alpha1.rs"); - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("astria.primitive.v1.serde.rs"); + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("astria.bundle.v1alpha1.serde.rs"); + } } } -} -#[path = ""] -pub mod protocol { - #[path = ""] - pub mod accounts { - #[path = "astria.protocol.accounts.v1.rs"] - pub mod v1; - } #[path = ""] - pub mod asset { - #[path = "astria.protocol.asset.v1.rs"] - pub mod v1; - } - #[path = ""] - pub mod bridge { - #[path = "astria.protocol.bridge.v1.rs"] - pub mod v1; - } - #[path = ""] - pub mod fees { - #[path = "astria.protocol.fees.v1.rs"] + pub mod execution { pub mod v1 { - include!("astria.protocol.fees.v1.rs"); - + include!("astria.execution.v1.rs"); + #[cfg(feature = "serde")] - mod _serde_impls { + mod _serde_impl { use super::*; - include!("astria.protocol.fees.v1.serde.rs"); + include!("astria.execution.v1.serde.rs"); } } } + #[path = ""] - pub mod genesis { + pub mod primitive { pub mod v1 { - include!("astria.protocol.genesis.v1.rs"); + include!("astria.primitive.v1.rs"); #[cfg(feature = "serde")] - mod _serde_impls { + mod _serde_impl { use super::*; - include!("astria.protocol.genesis.v1.serde.rs"); + include!("astria.primitive.v1.serde.rs"); } } } + #[path = ""] - pub mod memos { - pub mod v1 { - include!("astria.protocol.memos.v1.rs"); + pub mod protocol { + #[path = ""] + pub mod accounts { + #[path = "astria.protocol.accounts.v1.rs"] + pub mod v1; + } + #[path = ""] + pub mod asset { + #[path = "astria.protocol.asset.v1.rs"] + pub mod v1; + } + #[path = ""] + pub mod bridge { + #[path = "astria.protocol.bridge.v1.rs"] + pub mod v1; + } + #[path = ""] + pub mod fees { + #[path = "astria.protocol.fees.v1.rs"] + pub mod v1 { + include!("astria.protocol.fees.v1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impls { + use super::*; + include!("astria.protocol.fees.v1.serde.rs"); + } + } + } + #[path = ""] + pub mod genesis { + pub mod v1 { + include!("astria.protocol.genesis.v1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impls { + use super::*; + include!("astria.protocol.genesis.v1.serde.rs"); + } + } + } + #[path = ""] + pub mod memos { + pub mod v1 { + include!("astria.protocol.memos.v1.rs"); + + #[cfg(feature = "serde")] + mod _serde_impls { + use super::*; + include!("astria.protocol.memos.v1.serde.rs"); + } + } + } + #[path = ""] + pub mod transaction { + pub mod v1 { + include!("astria.protocol.transaction.v1.rs"); - #[cfg(feature = "serde")] - mod _serde_impls { - use super::*; - include!("astria.protocol.memos.v1.serde.rs"); + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("astria.protocol.transaction.v1.serde.rs"); + } } } } + #[path = ""] - pub mod transaction { + pub mod sequencerblock { pub mod v1 { - include!("astria.protocol.transaction.v1.rs"); + include!("astria.sequencerblock.v1.rs"); #[cfg(feature = "serde")] mod _serde_impl { use super::*; - include!("astria.protocol.transaction.v1.serde.rs"); + include!("astria.sequencerblock.v1.serde.rs"); } } } + + #[path = ""] + pub mod composer { + #[path = "astria.composer.v1.rs"] + pub mod v1; + } } #[path = ""] -pub mod sequencerblock { - pub mod v1alpha1 { - include!("astria.sequencerblock.v1alpha1.rs"); +pub mod celestia { + #[path = "celestia.blob.v1.rs"] + pub mod v1 { + include!("celestia.blob.v1.rs"); #[cfg(feature = "serde")] mod _serde_impl { use super::*; - include!("astria.sequencerblock.v1alpha1.serde.rs"); + include!("celestia.blob.v1.serde.rs"); } } +} - pub mod v1 { - include!("astria.sequencerblock.v1.rs"); +#[path = ""] +pub mod connect { + pub mod abci { + pub mod v2 { + include!("connect.abci.v2.rs"); - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("astria.sequencerblock.v1.serde.rs"); + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("connect.abci.v2.serde.rs"); + } } } -} -#[path = ""] -pub mod composer { - #[path = "astria.composer.v1.rs"] - pub mod v1; -} + pub mod marketmap { + pub mod v2 { + include!("connect.marketmap.v2.rs"); -#[path = ""] -pub mod celestia { - #[path = "celestia.blob.v1.rs"] - pub mod v1 { - include!("celestia.blob.v1.rs"); + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("connect.marketmap.v2.serde.rs"); + } + } + } - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("celestia.blob.v1.serde.rs"); + pub mod oracle { + pub mod v2 { + include!("connect.oracle.v2.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("connect.oracle.v2.serde.rs"); + } + } + } + + pub mod service { + pub mod v2 { + include!("connect.service.v2.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("connect.service.v2.serde.rs"); + } + } + } + + pub mod types { + pub mod v2 { + include!("connect.types.v2.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("connect.types.v2.serde.rs"); + } } } } diff --git a/crates/astria-core/src/lib.rs b/crates/astria-core/src/lib.rs index 5c97446d17..51e2f8477b 100644 --- a/crates/astria-core/src/lib.rs +++ b/crates/astria-core/src/lib.rs @@ -1,3 +1,4 @@ +pub use pbjson_types::Timestamp; use prost::Name; #[cfg(not(target_pointer_width = "64"))] @@ -13,7 +14,9 @@ compile_error!( )] pub mod generated; +pub mod connect; pub mod crypto; +pub mod display; pub mod execution; pub mod primitive; pub mod protocol; diff --git a/crates/astria-core/src/primitive/mod.rs b/crates/astria-core/src/primitive/mod.rs index a3a6d96c3f..f0044fc672 100644 --- a/crates/astria-core/src/primitive/mod.rs +++ b/crates/astria-core/src/primitive/mod.rs @@ -1 +1,2 @@ +pub use pbjson_types::Timestamp; pub mod v1; diff --git a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state_is_unchanged.snap b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state_is_unchanged.snap index 1949b88854..c782292789 100644 --- a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state_is_unchanged.snap +++ b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state_is_unchanged.snap @@ -132,5 +132,57 @@ expression: genesis_state() "base": {}, "multiplier": {} } + }, + "connect": { + "marketMap": { + "marketMap": { + "markets": { + "ETH/USD": { + "ticker": { + "currencyPair": { + "Base": "ETH", + "Quote": "USD" + }, + "decimals": "8", + "minProviderCount": "3", + "enabled": true + }, + "providerConfigs": [ + { + "name": "coingecko_api", + "offChainTicker": "ethereum/usd", + "normalizeByPair": { + "Base": "USDT", + "Quote": "USD" + } + } + ] + } + } + }, + "params": { + "marketAuthorities": [ + "astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm", + "astria1xnlvg0rle2u6auane79t4p27g8hxnj36ja960z" + ], + "admin": "astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm" + } + }, + "oracle": { + "currencyPairGenesis": [ + { + "currencyPair": { + "Base": "ETH", + "Quote": "USD" + }, + "currencyPairPrice": { + "price": "3138872234", + "blockTimestamp": "2024-07-04T19:46:35+00:00" + }, + "id": "1" + } + ], + "nextId": "1" + } } } diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index 69bb2269bd..7d1fd017b7 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -3,6 +3,10 @@ use std::convert::Infallible; pub use penumbra_ibc::params::IBCParameters; use crate::{ + connect::{ + market_map, + oracle, + }, generated::protocol::genesis::v1 as raw, primitive::v1::{ asset::{ @@ -36,6 +40,122 @@ use crate::{ Protobuf, }; +#[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(try_from = "raw::ConnectGenesis", into = "raw::ConnectGenesis") +)] +pub struct ConnectGenesis { + market_map: market_map::v2::GenesisState, + oracle: oracle::v2::GenesisState, +} + +impl ConnectGenesis { + #[must_use] + pub fn market_map(&self) -> &market_map::v2::GenesisState { + &self.market_map + } + + #[must_use] + pub fn oracle(&self) -> &oracle::v2::GenesisState { + &self.oracle + } +} + +impl Protobuf for ConnectGenesis { + type Error = ConnectGenesisError; + type Raw = raw::ConnectGenesis; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let Self::Raw { + market_map, + oracle, + } = raw; + let market_map = market_map + .as_ref() + .ok_or_else(|| Self::Error::field_not_set("market_map")) + .and_then(|market_map| { + market_map::v2::GenesisState::try_from_raw_ref(market_map) + .map_err(Self::Error::market_map) + })?; + let oracle = oracle + .as_ref() + .ok_or_else(|| Self::Error::field_not_set("oracle")) + .and_then(|oracle| { + oracle::v2::GenesisState::try_from_raw_ref(oracle).map_err(Self::Error::oracle) + })?; + Ok(Self { + market_map, + oracle, + }) + } + + fn to_raw(&self) -> Self::Raw { + let Self { + market_map, + oracle, + } = self; + Self::Raw { + market_map: Some(market_map.to_raw()), + oracle: Some(oracle.to_raw()), + } + } +} + +impl TryFrom for ConnectGenesis { + type Error = ::Error; + + fn try_from(value: raw::ConnectGenesis) -> Result { + Self::try_from_raw(value) + } +} + +impl From for raw::ConnectGenesis { + fn from(value: ConnectGenesis) -> Self { + value.into_raw() + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ConnectGenesisError(ConnectGenesisErrorKind); + +impl ConnectGenesisError { + fn field_not_set(name: &'static str) -> Self { + Self(ConnectGenesisErrorKind::FieldNotSet { + name, + }) + } + + fn market_map(source: market_map::v2::GenesisStateError) -> Self { + Self(ConnectGenesisErrorKind::MarketMap { + source, + }) + } + + fn oracle(source: oracle::v2::GenesisStateError) -> Self { + Self(ConnectGenesisErrorKind::Oracle { + source, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed ensuring invariants of {}", ConnectGenesis::full_name())] +enum ConnectGenesisErrorKind { + #[error("field was not set: `{name}`")] + FieldNotSet { name: &'static str }, + #[error("`market_map` field was invalid")] + MarketMap { + source: market_map::v2::GenesisStateError, + }, + #[error("`oracle` field was invalid")] + Oracle { + source: oracle::v2::GenesisStateError, + }, +} + /// The genesis state of Astria's Sequencer. /// /// Verified to only contain valid fields (right now, addresses that have the same base prefix @@ -57,6 +177,7 @@ pub struct GenesisAppState { ibc_parameters: IBCParameters, allowed_fee_assets: Vec, fees: GenesisFees, + connect: Option, } impl GenesisAppState { @@ -110,6 +231,11 @@ impl GenesisAppState { &self.fees } + #[must_use] + pub fn connect(&self) -> &Option { + &self.connect + } + fn ensure_address_has_base_prefix( &self, address: &Address, @@ -140,6 +266,26 @@ impl GenesisAppState { for (i, address) in self.ibc_relayer_addresses.iter().enumerate() { self.ensure_address_has_base_prefix(address, &format!(".ibc_relayer_addresses[{i}]"))?; } + + if let Some(connect) = &self.connect { + for (i, address) in connect + .market_map + .params + .market_authorities + .iter() + .enumerate() + { + self.ensure_address_has_base_prefix( + address, + &format!(".market_map.params.market_authorities[{i}]"), + )?; + } + self.ensure_address_has_base_prefix( + &connect.market_map.params.admin, + ".market_map.params.admin", + )?; + } + Ok(()) } } @@ -166,6 +312,7 @@ impl Protobuf for GenesisAppState { ibc_parameters, allowed_fee_assets, fees, + connect, } = raw; let address_prefixes = address_prefixes .as_ref() @@ -225,6 +372,12 @@ impl Protobuf for GenesisAppState { .ok_or_else(|| Self::Error::field_not_set("fees")) .and_then(|fees| GenesisFees::try_from_raw_ref(fees).map_err(Self::Error::fees))?; + let connect = if let Some(connect) = connect { + Some(ConnectGenesis::try_from_raw_ref(connect).map_err(Self::Error::connect)?) + } else { + None + }; + let this = Self { address_prefixes, accounts, @@ -236,6 +389,7 @@ impl Protobuf for GenesisAppState { ibc_parameters, allowed_fee_assets, fees, + connect, }; this.ensure_all_addresses_have_base_prefix() .map_err(Self::Error::address_does_not_match_base)?; @@ -254,6 +408,7 @@ impl Protobuf for GenesisAppState { ibc_parameters, allowed_fee_assets, fees, + connect, } = self; Self::Raw { address_prefixes: Some(address_prefixes.to_raw()), @@ -268,6 +423,7 @@ impl Protobuf for GenesisAppState { ibc_parameters: Some(ibc_parameters.to_raw()), allowed_fee_assets: allowed_fee_assets.iter().map(ToString::to_string).collect(), fees: Some(fees.to_raw()), + connect: connect.as_ref().map(ConnectGenesis::to_raw), } } } @@ -350,6 +506,12 @@ impl GenesisAppStateError { source, }) } + + fn connect(source: ConnectGenesisError) -> Self { + Self(GenesisAppStateErrorKind::Connect { + source, + }) + } } #[derive(Debug, thiserror::Error)] @@ -377,6 +539,8 @@ enum GenesisAppStateErrorKind { FieldNotSet { name: &'static str }, #[error("`native_asset_base_denomination` field was invalid")] NativeAssetBaseDenomination { source: ParseTracePrefixedError }, + #[error("`connect` field was invalid")] + Connect { source: ConnectGenesisError }, } #[derive(Debug, thiserror::Error)] @@ -789,8 +953,19 @@ enum FeesErrorKind { #[cfg(test)] mod tests { + use indexmap::indexmap; + use super::*; - use crate::primitive::v1::Address; + use crate::{ + connect::{ + market_map::v2::{ + MarketMap, + Params, + }, + types::v2::CurrencyPairId, + }, + primitive::v1::Address, + }; const ASTRIA_ADDRESS_PREFIX: &str = "astria"; @@ -826,8 +1001,61 @@ mod tests { .unwrap() } + fn genesis_state_markets() -> MarketMap { + use crate::connect::{ + market_map::v2::{ + Market, + MarketMap, + ProviderConfig, + Ticker, + }, + types::v2::CurrencyPair, + }; + + let markets = indexmap! { + "ETH/USD".to_string() => Market { + ticker: Ticker { + currency_pair: CurrencyPair::from_parts( + "ETH".parse().unwrap(), + "USD".parse().unwrap(), + ), + decimals: 8, + min_provider_count: 3, + enabled: true, + metadata_json: String::new(), + }, + provider_configs: vec![ProviderConfig { + name: "coingecko_api".to_string(), + off_chain_ticker: "ethereum/usd".to_string(), + normalize_by_pair: CurrencyPair::from_parts( + "USDT".parse().unwrap(), + "USD".parse().unwrap(), + ), + invert: false, + metadata_json: String::new(), + }], + }, + }; + + MarketMap { + markets, + } + } + #[expect(clippy::too_many_lines, reason = "for testing purposes")] fn proto_genesis_state() -> raw::GenesisAppState { + use crate::connect::{ + oracle::v2::{ + CurrencyPairGenesis, + QuotePrice, + }, + types::v2::{ + CurrencyPair, + CurrencyPairNonce, + Price, + }, + }; + raw::GenesisAppState { accounts: vec![ raw::Account { @@ -958,6 +1186,38 @@ mod tests { .to_raw(), ), }), + connect: Some( + ConnectGenesis { + market_map: market_map::v2::GenesisState { + market_map: genesis_state_markets(), + last_updated: 0, + params: Params { + market_authorities: vec![alice(), bob()], + admin: alice(), + }, + }, + oracle: oracle::v2::GenesisState { + currency_pair_genesis: vec![CurrencyPairGenesis { + id: CurrencyPairId::new(1), + nonce: CurrencyPairNonce::new(0), + currency_pair_price: QuotePrice { + price: Price::new(3_138_872_234_u128), + block_height: 0, + block_timestamp: pbjson_types::Timestamp { + seconds: 1_720_122_395, + nanos: 0, + }, + }, + currency_pair: CurrencyPair::from_parts( + "ETH".parse().unwrap(), + "USD".parse().unwrap(), + ), + }], + next_id: CurrencyPairId::new(1), + }, + } + .into_raw(), + ), } } @@ -1014,6 +1274,26 @@ mod tests { }, ".ibc_relayer_addresses[1]", ); + assert_bad_prefix( + raw::GenesisAppState { + connect: { + let mut connect = proto_genesis_state().connect; + connect + .as_mut() + .unwrap() + .market_map + .as_mut() + .unwrap() + .params + .as_mut() + .unwrap() + .market_authorities[0] = mallory().to_string(); + connect + }, + ..proto_genesis_state() + }, + ".market_map.params.market_authorities[0]", + ); assert_bad_prefix( raw::GenesisAppState { accounts: vec![ diff --git a/crates/astria-core/src/protocol/test_utils.rs b/crates/astria-core/src/protocol/test_utils.rs index 510c78e57e..99284603d3 100644 --- a/crates/astria-core/src/protocol/test_utils.rs +++ b/crates/astria-core/src/protocol/test_utils.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use bytes::Bytes; use prost::Message as _; +use tendermint::abci::types::ExtendedCommitInfo; use super::{ group_rollup_data_submissions_in_signed_transaction_transactions_by_rollup_id, @@ -141,6 +142,13 @@ impl ConfigureSequencerBlock { rollup_transactions.sort_unstable_keys(); let rollup_transactions_tree = derive_merkle_tree_from_rollup_txs(&rollup_transactions); + let extended_commit_info: tendermint_proto::abci::ExtendedCommitInfo = ExtendedCommitInfo { + round: 0u16.into(), + votes: vec![], + } + .into(); + let extended_commit_info_bytes = extended_commit_info.encode_to_vec(); + let rollup_ids_root = merkle::Tree::from_leaves( rollup_transactions .keys() @@ -148,6 +156,7 @@ impl ConfigureSequencerBlock { ) .root(); let mut data = vec![ + extended_commit_info_bytes, rollup_transactions_tree.root().to_vec(), rollup_ids_root.to_vec(), ]; diff --git a/crates/astria-core/src/sequencerblock/v1/block.rs b/crates/astria-core/src/sequencerblock/v1/block.rs index 48f8907fac..ce77c66faf 100644 --- a/crates/astria-core/src/sequencerblock/v1/block.rs +++ b/crates/astria-core/src/sequencerblock/v1/block.rs @@ -221,6 +221,10 @@ impl SequencerBlockError { Self(SequencerBlockErrorKind::IdProofInvalid(source)) } + fn no_extended_commit_info() -> Self { + Self(SequencerBlockErrorKind::NoExtendedCommitInfo) + } + fn no_rollup_transactions_root() -> Self { Self(SequencerBlockErrorKind::NoRollupTransactionsRoot) } @@ -287,6 +291,10 @@ enum SequencerBlockErrorKind { TransactionProofInvalid(#[source] merkle::audit::InvalidProof), #[error("failed constructing a rollup ID proof from the raw protobuf rollup ID proof")] IdProofInvalid(#[source] merkle::audit::InvalidProof), + #[error( + "the cometbft block.data field was too short and did not contain the extended commit info" + )] + NoExtendedCommitInfo, #[error( "the cometbft block.data field was too short and did not contain the rollup transaction \ root" @@ -779,6 +787,12 @@ impl SequencerBlock { let data_hash = tree.root(); let mut data_list = data.into_iter(); + + // TODO: this needs to go into the block header + let _extended_commit_info = data_list + .next() + .ok_or(SequencerBlockError::no_extended_commit_info())?; + let (rollup_transactions_root, rollup_ids_root) = rollup_transactions_and_ids_root_from_data(&mut data_list)?; @@ -850,14 +864,15 @@ impl SequencerBlock { } rollup_transactions.sort_unstable_keys(); - // action tree root is always the first tx in a block - let rollup_transactions_proof = tree.construct_proof(0).expect( - "the tree has at least one leaf; if this line is reached and `construct_proof` \ + // action tree root is always the second tx in a block + let rollup_transactions_proof = tree.construct_proof(1).expect( + "the tree has at least two leaves; if this line is reached and `construct_proof` \ returns None it means that the short circuiting checks above it have been removed", ); - let rollup_ids_proof = tree.construct_proof(1).expect( - "the tree has at least two leaves; if this line is reached and `construct_proof` \ + // rollup id tree root is always the third tx in a block + let rollup_ids_proof = tree.construct_proof(2).expect( + "the tree has at least three leaves; if this line is reached and `construct_proof` \ returns None it means that the short circuiting checks above it have been removed", ); diff --git a/crates/astria-sequencer-utils/Cargo.toml b/crates/astria-sequencer-utils/Cargo.toml index 9f0bed5d91..39edd6340e 100644 --- a/crates/astria-sequencer-utils/Cargo.toml +++ b/crates/astria-sequencer-utils/Cargo.toml @@ -21,6 +21,7 @@ ethers-core = "2.0.14" hex = { workspace = true } indenter = "0.3.3" itertools = { workspace = true } +pbjson-types = { workspace = true } prost = { workspace = true } rlp = "0.5.2" serde = { workspace = true } @@ -29,6 +30,7 @@ serde_json = { workspace = true } astria-core = { path = "../astria-core", features = ["brotli", "serde"] } astria-eyre = { path = "../astria-eyre" } astria-merkle = { path = "../astria-merkle" } +maplit = "1.0.2" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/crates/astria-sequencer-utils/src/genesis_example.rs b/crates/astria-sequencer-utils/src/genesis_example.rs index a464fd26c2..0769de29e7 100644 --- a/crates/astria-sequencer-utils/src/genesis_example.rs +++ b/crates/astria-sequencer-utils/src/genesis_example.rs @@ -5,10 +5,25 @@ use std::{ }; use astria_core::{ - generated::protocol::genesis::v1::{ - AddressPrefixes, - GenesisFees, - IbcParameters, + generated::{ + connect::{ + marketmap, + marketmap::v2::{ + Market, + MarketMap, + }, + oracle, + oracle::v2::{ + CurrencyPairGenesis, + QuotePrice, + }, + types::v2::CurrencyPair, + }, + protocol::genesis::v1::{ + AddressPrefixes, + GenesisFees, + IbcParameters, + }, }, primitive::v1::Address, protocol::{ @@ -66,6 +81,66 @@ fn charlie() -> Address { .unwrap() } +fn genesis_state_markets() -> MarketMap { + use astria_core::generated::connect::marketmap::v2::{ + ProviderConfig, + Ticker, + }; + use maplit::{ + btreemap, + convert_args, + }; + let markets = convert_args!(btreemap!( + "BTC/USD" => Market { + ticker: Some(Ticker { + currency_pair: Some(CurrencyPair { + base: "BTC".to_string(), + quote: "USD".to_string(), + }), + decimals: 8, + min_provider_count: 3, + enabled: true, + metadata_json: String::new(), + }), + provider_configs: vec![ProviderConfig { + name: "coingecko_api".to_string(), + off_chain_ticker: "bitcoin/usd".to_string(), + normalize_by_pair: Some(CurrencyPair { + base: "USDT".to_string(), + quote: "USD".to_string(), + }), + invert: false, + metadata_json: String::new(), + }], + }, + "ETH/USD" => Market { + ticker: Some(Ticker { + currency_pair: Some(CurrencyPair { + base: "ETH".to_string(), + quote: "USD".to_string(), + }), + decimals: 8, + min_provider_count: 3, + enabled: true, + metadata_json: String::new(), + }), + provider_configs: vec![ProviderConfig { + name: "coingecko_api".to_string(), + off_chain_ticker: "ethereum/usd".to_string(), + normalize_by_pair: Some(CurrencyPair { + base: "USDT".to_string(), + quote: "USD".to_string(), + }), + invert: false, + metadata_json: String::new(), + }], + }, + )); + MarketMap { + markets, + } +} + fn accounts() -> Vec { vec![ Account { @@ -106,6 +181,57 @@ fn proto_genesis_state() -> astria_core::generated::protocol::genesis::v1::Genes outbound_ics20_transfers_enabled: true, }), allowed_fee_assets: vec!["nria".parse().unwrap()], + connect: Some( + astria_core::generated::protocol::genesis::v1::ConnectGenesis { + market_map: Some( + astria_core::generated::connect::marketmap::v2::GenesisState { + market_map: Some(genesis_state_markets()), + last_updated: 0, + params: Some(marketmap::v2::Params { + market_authorities: vec![alice().to_string(), bob().to_string()], + admin: alice().to_string(), + }), + }, + ), + oracle: Some(oracle::v2::GenesisState { + currency_pair_genesis: vec![ + CurrencyPairGenesis { + id: 0, + nonce: 0, + currency_pair_price: Some(QuotePrice { + price: 5_834_065_777_u128.to_string(), + block_height: 0, + block_timestamp: Some(pbjson_types::Timestamp { + seconds: 1_720_122_395, + nanos: 0, + }), + }), + currency_pair: Some(CurrencyPair { + base: "BTC".to_string(), + quote: "USD".to_string(), + }), + }, + CurrencyPairGenesis { + id: 1, + nonce: 0, + currency_pair_price: Some(QuotePrice { + price: 3_138_872_234_u128.to_string(), + block_height: 0, + block_timestamp: Some(pbjson_types::Timestamp { + seconds: 1_720_122_395, + nanos: 0, + }), + }), + currency_pair: Some(CurrencyPair { + base: "ETH".to_string(), + quote: "USD".to_string(), + }), + }, + ], + next_id: 2, + }), + }, + ), fees: Some(GenesisFees { transfer: Some( TransferFeeComponents { diff --git a/crates/astria-sequencer-utils/src/genesis_parser.rs b/crates/astria-sequencer-utils/src/genesis_parser.rs index a61d67b681..a26770f490 100644 --- a/crates/astria-sequencer-utils/src/genesis_parser.rs +++ b/crates/astria-sequencer-utils/src/genesis_parser.rs @@ -95,13 +95,6 @@ mod tests { let mut a = json!({ "genesis_time": "2023-06-21T15:58:36.741257Z", "initial_height": "0", - "consensus_params": { - "validator": { - "pub_key_types": [ - "ed25519" - ] - } - } }); let b = json!({ @@ -124,13 +117,6 @@ mod tests { let output = json!({ "genesis_time": "2023-06-21T15:58:36.741257Z", "initial_height": "0", - "consensus_params": { - "validator": { - "pub_key_types": [ - "ed25519" - ] - } - }, "app_state": { "accounts": [ { diff --git a/crates/astria-sequencer/Cargo.toml b/crates/astria-sequencer/Cargo.toml index d56d38dd4c..68e7a291b8 100644 --- a/crates/astria-sequencer/Cargo.toml +++ b/crates/astria-sequencer/Cargo.toml @@ -14,6 +14,7 @@ benchmark = ["divan"] [dependencies] astria-core = { path = "../astria-core", features = [ "server", + "client", "serde", "unchecked-constructors", ] } @@ -37,6 +38,8 @@ cnidarium = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.8 "metrics", ] } ibc-proto = { version = "0.41.0", features = ["server"] } +# `is_sorted_by` is available in rust 1.81.0, but we haven't updated our msrv yet +is_sorted = "0.1.1" matchit = "0.7.2" tower = "0.4" tower-abci = "0.12.0" @@ -50,6 +53,7 @@ divan = { workspace = true, optional = true } futures = { workspace = true } hex = { workspace = true, features = ["serde"] } ibc-types = { workspace = true, features = ["with_serde"] } +indexmap = { workspace = true } penumbra-ibc = { workspace = true, features = ["component", "rpc"] } penumbra-proto = { workspace = true } penumbra-tower-trace = { workspace = true } diff --git a/crates/astria-sequencer/justfile b/crates/astria-sequencer/justfile index 8391be8864..bd2c96ce5d 100644 --- a/crates/astria-sequencer/justfile +++ b/crates/astria-sequencer/justfile @@ -12,14 +12,24 @@ run: cargo run run-cometbft: + #!/usr/bin/env bash + set -e + app_state_genesis="$(mktemp)" + genesis="$(mktemp)" + cometbft init + + # uncomment this line if you want to inspect `app_state_genesis` + trap "rm -f ${app_state_genesis@Q}" EXIT cargo run -p astria-sequencer-utils -- \ - generate-genesis-state -o app-genesis-state.json + generate-genesis-state -o "${app_state_genesis}" --force cargo run -p astria-sequencer-utils -- \ copy-genesis-state \ - --genesis-app-state-file=app-genesis-state.json \ - --destination-genesis-file=$HOME/.cometbft/config/genesis.json \ + --genesis-app-state-file="${app_state_genesis}" \ + --destination-genesis-file="$HOME/.cometbft/config/genesis.json" \ --chain-id=astria + jq ".consensus_params.abci.vote_extensions_enable_height = \"1\"" $HOME/.cometbft/config/genesis.json > "$genesis" && mv "$genesis" $HOME/.cometbft/config/genesis.json + sed -i'.bak' 's/timeout_commit = "1s"/timeout_commit = "2s"/g' ~/.cometbft/config/config.toml cometbft node diff --git a/crates/astria-sequencer/local.env.example b/crates/astria-sequencer/local.env.example index 7a794e616a..30830e98a2 100644 --- a/crates/astria-sequencer/local.env.example +++ b/crates/astria-sequencer/local.env.example @@ -33,6 +33,16 @@ ASTRIA_SEQUENCER_METRICS_HTTP_LISTENER_ADDR="127.0.0.1:9000" # `ASTRIA_SEQUENCER_FORCE_STDOUT` is set to `true`. ASTRIA_SEQUENCER_PRETTY_PRINT=false +# If the oracle is disabled. If false, the oracle_grpc_addr must be set. +# Should be false for validator nodes and true for non-validator nodes. +ASTRIA_SEQUENCER_NO_ORACLE=true + +# The gRPC endpoint for the oracle sidecar. +ASTRIA_SEQUENCER_ORACLE_GRPC_ADDR="http://127.0.0.1:8081" + +# The timeout for the responses from the oracle sidecar in milliseconds. +ASTRIA_SEQUENCER_ORACLE_CLIENT_TIMEOUT_MILLISECONDS=1000 + # If set to any non-empty value removes ANSI escape characters from the pretty # printed output. Note that this does nothing unless `ASTRIA_SEQUENCER_PRETTY_PRINT` # is set to `true`. @@ -55,3 +65,4 @@ OTEL_EXPORTER_OTLP_TRACES_COMPRESSION="gzip" OTEL_EXPORTER_OTLP_HEADERS="key1=value1,key2=value2" # The HTTP headers that will be set when sending gRPC requests. This takes precedence over `OTEL_EXPORTER_OTLP_HEADERS` if set. OTEL_EXPORTER_OTLP_TRACE_HEADERS="key1=value1,key2=value2" + diff --git a/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs b/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs index 6c46e5c6e9..e947408011 100644 --- a/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs +++ b/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs @@ -1,6 +1,11 @@ use std::collections::HashMap; use astria_core::{ + connect::market_map::v2::{ + MarketMap, + Params, + }, + generated::protocol::genesis::v1::ConnectGenesis, primitive::v1::asset::{ Denom, IbcPrefixed, @@ -153,6 +158,25 @@ pub(crate) fn proto_genesis_state() -> astria_core::generated::protocol::genesis }), allowed_fee_assets: vec![nria().to_string()], fees: Some(default_fees().to_raw()), + connect: Some(ConnectGenesis { + market_map: Some( + astria_core::connect::market_map::v2::GenesisState { + market_map: MarketMap { + markets: indexmap::IndexMap::new(), + }, + last_updated: 0, + params: Params { + market_authorities: vec![], + admin: astria_address_from_hex_string(ALICE_ADDRESS), + }, + } + .into_raw(), + ), + oracle: Some(astria_core::generated::connect::oracle::v2::GenesisState { + currency_pair_genesis: vec![], + next_id: 0, + }), + }), } } @@ -170,7 +194,10 @@ pub(crate) async fn initialize_app_with_storage( let snapshot = storage.latest_snapshot(); let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool, metrics).await.unwrap(); + let ve_handler = crate::app::vote_extension::Handler::new(None); + let mut app = App::new(snapshot, mempool, ve_handler, metrics) + .await + .unwrap(); let genesis_state = genesis_state.unwrap_or_else(self::genesis_state); @@ -179,6 +206,7 @@ pub(crate) async fn initialize_app_with_storage( genesis_state, genesis_validators, "test".to_string(), + 1, ) .await .unwrap(); diff --git a/crates/astria-sequencer/src/app/mod.rs b/crates/astria-sequencer/src/app/mod.rs index 99d13a115c..d659dd93bf 100644 --- a/crates/astria-sequencer/src/app/mod.rs +++ b/crates/astria-sequencer/src/app/mod.rs @@ -16,6 +16,8 @@ mod tests_breaking_changes; #[cfg(test)] mod tests_execute_transaction; +pub(crate) mod vote_extension; + use std::{ collections::VecDeque, sync::Arc, @@ -44,6 +46,7 @@ use astria_eyre::{ bail, ensure, eyre, + ContextCompat as _, OptionExt as _, Result, WrapErr as _, @@ -76,10 +79,12 @@ use tendermint::{ AppHash, Hash, }; +use tendermint_proto::abci::ExtendedCommitInfo; use tracing::{ debug, info, instrument, + warn, }; pub(crate) use self::{ @@ -95,6 +100,7 @@ use crate::{ StateWriteExt as _, }, address::StateWriteExt as _, + app::vote_extension::ProposalHandler, assets::StateWriteExt as _, authority::{ component::{ @@ -109,6 +115,10 @@ use crate::{ StateWriteExt as _, }, component::Component as _, + connect::{ + marketmap::component::MarketMapComponent, + oracle::component::OracleComponent, + }, fees::{ component::FeesComponent, StateReadExt as _, @@ -138,6 +148,27 @@ const EXECUTION_RESULTS_KEY: &str = "execution_results"; // cleared at the end of the block. const POST_TRANSACTION_EXECUTION_RESULT_KEY: &str = "post_transaction_execution_result"; +// the number of non-external transactions expected at the start of the block +// before vote extensions are enabled. +// +// consists of: +// 1. rollup data root +// 2. rollup IDs root +const INJECTED_TRANSACTIONS_COUNT_BEFORE_VOTE_EXTENSIONS_ENABLED: usize = 2; + +// the number of non-external transactions expected at the start of the block +// after vote extensions are enabled. +// +// consists of: +// 1. encoded `ExtendedCommitInfo` for the previous block +// 2. rollup data root +// 3. rollup IDs root +const INJECTED_TRANSACTIONS_COUNT_AFTER_VOTE_EXTENSIONS_ENABLED: usize = 3; + +// the height to set the `vote_extensions_enable_height` to in state if vote extensions are +// disabled. +const VOTE_EXTENSIONS_DISABLED_HEIGHT: u64 = u64::MAX; + /// The inter-block state being written to by the application. type InterBlockState = Arc>; @@ -232,6 +263,9 @@ pub(crate) struct App { )] app_hash: AppHash, + // used to create and verify vote extensions, if this is a validator node. + vote_extension_handler: vote_extension::Handler, + metrics: &'static Metrics, } @@ -239,6 +273,7 @@ impl App { pub(crate) async fn new( snapshot: Snapshot, mempool: Mempool, + vote_extension_handler: vote_extension::Handler, metrics: &'static Metrics, ) -> Result { debug!("initializing App instance"); @@ -265,6 +300,7 @@ impl App { recost_mempool: false, write_batch: None, app_hash, + vote_extension_handler, metrics, }) } @@ -276,6 +312,7 @@ impl App { genesis_state: GenesisAppState, genesis_validators: Vec, chain_id: String, + vote_extensions_enable_height: u64, ) -> Result { let mut state_tx = self .state @@ -305,6 +342,17 @@ impl App { .put_block_height(0) .wrap_err("failed to write block height to state")?; + // if `vote_extensions_enable_height` is 0, vote extensions are disabled. + // we set it to `u64::MAX` in state so checking if our current height is past + // the vote extensions enabled height will always be false. + let vote_extensions_enable_height = match vote_extensions_enable_height { + 0 => VOTE_EXTENSIONS_DISABLED_HEIGHT, + _ => vote_extensions_enable_height, + }; + state_tx + .put_vote_extensions_enable_height(vote_extensions_enable_height) + .wrap_err("failed to write vote extensions enabled height to state")?; + // call init_chain on all components FeesComponent::init_chain(&mut state_tx, &genesis_state) .await @@ -325,6 +373,15 @@ impl App { .await .wrap_err("init_chain failed on IbcComponent")?; + if vote_extensions_enable_height != VOTE_EXTENSIONS_DISABLED_HEIGHT { + MarketMapComponent::init_chain(&mut state_tx, &genesis_state) + .await + .wrap_err("init_chain failed on MarketMapComponent")?; + OracleComponent::init_chain(&mut state_tx, &genesis_state) + .await + .wrap_err("init_chain failed on OracleComponent")?; + } + state_tx.apply(); let app_hash = self @@ -365,11 +422,80 @@ impl App { self.executed_proposal_fingerprint = Some(prepare_proposal.clone().into()); self.update_state_for_new_round(&storage); - let mut block_size_constraints = BlockSizeConstraints::new( - usize::try_from(prepare_proposal.max_tx_bytes) - .wrap_err("failed to convert max_tx_bytes to usize")?, - ) - .wrap_err("failed to create block size constraints")?; + let vote_extensions_enable_height = self + .state + .get_vote_extensions_enable_height() + .await + .wrap_err("failed to get vote extensions enabled height")?; + + let (max_tx_bytes, encoded_extended_commit_info) = if vote_extensions_enable_height + <= prepare_proposal.height.value() + { + // create the extended commit info from the local last commit + let Some(last_commit) = prepare_proposal.local_last_commit else { + bail!("local last commit is empty; this should not occur") + }; + + // if this fails, we shouldn't return an error, but instead leave + // the vote extensions empty in this block for liveness. + // it's not a critical error if the oracle values are not updated for a block. + // + // note that at the height where vote extensions are enabled, the `extended_commit_info` + // will always be empty, as there were no vote extensions for the previous block. + let round = last_commit.round; + let extended_commit_info = match ProposalHandler::prepare_proposal( + &self.state, + prepare_proposal.height.into(), + last_commit, + ) + .await + { + Ok(info) => info.into_inner(), + Err(e) => { + warn!( + error = AsRef::::as_ref(&e), + "failed to generate extended commit info" + ); + tendermint::abci::types::ExtendedCommitInfo { + round, + votes: Vec::new(), + } + } + }; + + let mut encoded_extended_commit_info = + ExtendedCommitInfo::from(extended_commit_info).encode_to_vec(); + let max_tx_bytes = usize::try_from(prepare_proposal.max_tx_bytes) + .wrap_err("failed to convert max_tx_bytes to usize")?; + + // adjust max block size to account for extended commit info + ( + max_tx_bytes + .checked_sub(encoded_extended_commit_info.len()) + .unwrap_or_else(|| { + // zero the commit info if it's too large to fit in the block + // for liveness. + warn!( + encoded_extended_commit_info_len = encoded_extended_commit_info.len(), + max_tx_bytes, + "extended commit info is too large to fit in block; not including in \ + block" + ); + encoded_extended_commit_info.clear(); + max_tx_bytes + }), + Some(encoded_extended_commit_info), + ) + } else { + ( + usize::try_from(prepare_proposal.max_tx_bytes) + .wrap_err("failed to convert max_tx_bytes to usize")?, + None, + ) + }; + + let mut block_size_constraints = BlockSizeConstraints::new(max_tx_bytes) + .wrap_err("failed to create block size constraints")?; let block_data = BlockData { misbehavior: prepare_proposal.misbehavior, @@ -397,7 +523,16 @@ impl App { // generate commitment to sequence::Actions and deposits and commitment to the rollup IDs // included in the block let res = generate_rollup_datas_commitment(&signed_txs_included, deposits); - let txs = res.into_transactions(included_tx_bytes); + + let txs = match encoded_extended_commit_info { + Some(encoded_extended_commit_info) => { + std::iter::once(encoded_extended_commit_info.into()) + .chain(res.into_iter().chain(included_tx_bytes)) + .collect() + } + None => res.into_iter().chain(included_tx_bytes).collect(), + }; + Ok(abci::response::PrepareProposal { txs, }) @@ -457,6 +592,40 @@ impl App { self.update_state_for_new_round(&storage); let mut txs = VecDeque::from(process_proposal.txs.clone()); + + let vote_extensions_enable_height = self + .state + .get_vote_extensions_enable_height() + .await + .wrap_err("failed to get vote extensions enabled height")?; + + if vote_extensions_enable_height <= process_proposal.height.value() { + // if vote extensions are enabled, the first transaction in the block should be the + // extended commit info + let extended_commit_info_bytes = txs + .pop_front() + .wrap_err("no extended commit info in proposal")?; + + // decode the extended commit info and validate it + let extended_commit_info = + ExtendedCommitInfo::decode(extended_commit_info_bytes.as_ref()) + .wrap_err("failed to decode extended commit info")?; + let extended_commit_info = extended_commit_info + .try_into() + .wrap_err("failed to convert extended commit info from proto to native")?; + let Some(last_commit) = process_proposal.proposed_last_commit else { + bail!("proposed last commit is empty; this should not occur") + }; + ProposalHandler::validate_proposal( + &self.state, + process_proposal.height.value(), + &last_commit, + &extended_commit_info, + ) + .await + .wrap_err("failed to validate extended commit info")?; + } + let received_rollup_datas_root: [u8; 32] = txs .pop_front() .ok_or_eyre("no transaction commitment in proposal")? @@ -875,6 +1044,23 @@ impl App { Ok(()) } + #[instrument(name = "App::extend_vote", skip_all)] + pub(crate) async fn extend_vote( + &mut self, + _extend_vote: abci::request::ExtendVote, + ) -> Result { + self.vote_extension_handler.extend_vote(&self.state).await + } + + pub(crate) async fn verify_vote_extension( + &mut self, + vote_extension: abci::request::VerifyVoteExtension, + ) -> Result { + self.vote_extension_handler + .verify_vote_extension(&self.state, vote_extension) + .await + } + /// updates the app state after transaction execution, and generates the resulting /// `SequencerBlock`. /// @@ -918,13 +1104,25 @@ impl App { .put_deposits(&block_hash, deposits_in_this_block.clone()) .wrap_err("failed to put deposits to state")?; + let vote_extensions_enable_height = self + .state + .get_vote_extensions_enable_height() + .await + .wrap_err("failed to get vote extensions enabled height")?; + let injected_txs_count = if vote_extensions_enable_height <= height.value() { + INJECTED_TRANSACTIONS_COUNT_AFTER_VOTE_EXTENSIONS_ENABLED + } else { + INJECTED_TRANSACTIONS_COUNT_BEFORE_VOTE_EXTENSIONS_ENABLED + }; + // cometbft expects a result for every tx in the block, so we need to return a // tx result for the commitments, even though they're not actually user txs. // // the tx_results passed to this function only contain results for every user - // transaction, not the commitment, so its length is len(txs) - 2. + // transaction, not the commitment, so its length is len(txs) - 3. let mut finalize_block_tx_results: Vec = Vec::with_capacity(txs.len()); - finalize_block_tx_results.extend(std::iter::repeat(ExecTxResult::default()).take(2)); + finalize_block_tx_results + .extend(std::iter::repeat(ExecTxResult::default()).take(injected_txs_count)); finalize_block_tx_results.extend(tx_results); let sequencer_block = SequencerBlock::try_from_block_info_and_data( @@ -941,6 +1139,13 @@ impl App { .put_sequencer_block(sequencer_block) .wrap_err("failed to write sequencer block to state")?; + handle_consensus_param_updates( + &mut state_tx, + &end_block.consensus_param_updates, + vote_extensions_enable_height, + ) + .wrap_err("failed to handle consensus param updates")?; + let result = PostTransactionExecutionResult { events: end_block.events, validator_updates: end_block.validator_updates, @@ -975,11 +1180,48 @@ impl App { self.update_state_for_new_round(&storage); } - ensure!( - finalize_block.txs.len() >= 2, - "block must contain at least two transactions: the rollup transactions commitment and - rollup IDs commitment" - ); + let vote_extensions_enable_height = self + .state + .get_vote_extensions_enable_height() + .await + .wrap_err("failed to get vote extensions enabled height")?; + let injected_transactions_count = + if vote_extensions_enable_height <= finalize_block.height.value() { + ensure!( + finalize_block.txs.len() + >= INJECTED_TRANSACTIONS_COUNT_AFTER_VOTE_EXTENSIONS_ENABLED, + "block must contain at least three transactions: the extended commit info, \ + the rollup transactions commitment and rollup IDs commitment" + ); + + let extended_commit_info_bytes = + finalize_block.txs.first().expect("asserted length above"); + let extended_commit_info = + ExtendedCommitInfo::decode(extended_commit_info_bytes.as_ref()) + .wrap_err("failed to decode extended commit info")? + .try_into() + .context("failed to validate decoded extended commit info")?; + let mut state_tx: StateDelta>> = + StateDelta::new(self.state.clone()); + crate::app::vote_extension::apply_prices_from_vote_extensions( + &mut state_tx, + extended_commit_info, + finalize_block.time.into(), + finalize_block.height.value(), + ) + .await + .wrap_err("failed to apply prices from vote extensions")?; + let _ = self.apply(state_tx); + INJECTED_TRANSACTIONS_COUNT_AFTER_VOTE_EXTENSIONS_ENABLED + } else { + ensure!( + finalize_block.txs.len() + >= INJECTED_TRANSACTIONS_COUNT_BEFORE_VOTE_EXTENSIONS_ENABLED, + "block must contain at least two transactions: the rollup transactions \ + commitment and rollup IDs commitment" + ); + INJECTED_TRANSACTIONS_COUNT_BEFORE_VOTE_EXTENSIONS_ENABLED + }; // When the hash is not empty, we have already executed and cached the results if self.executed_proposal_hash.is_empty() { @@ -1003,8 +1245,9 @@ impl App { .wrap_err("failed to execute block")?; let mut tx_results = Vec::with_capacity(finalize_block.txs.len()); - // skip the first two transactions, as they are the rollup data commitments - for tx in finalize_block.txs.iter().skip(2) { + // skip the first `injected_transactions_count` transactions, as they are injected + // transactions + for tx in finalize_block.txs.iter().skip(injected_transactions_count) { let signed_tx = signed_transaction_from_bytes(tx) .wrap_err("protocol error; only valid txs should be finalized")?; @@ -1142,6 +1385,12 @@ impl App { FeesComponent::begin_block(&mut arc_state_tx, begin_block) .await .wrap_err("begin_block failed on FeesComponent")?; + MarketMapComponent::begin_block(&mut arc_state_tx, begin_block) + .await + .wrap_err("begin_block failed on MarketMapComponent")?; + OracleComponent::begin_block(&mut arc_state_tx, begin_block) + .await + .wrap_err("begin_block failed on OracleComponent")?; let state_tx = Arc::try_unwrap(arc_state_tx) .expect("components should not retain copies of shared state"); @@ -1206,6 +1455,12 @@ impl App { IbcComponent::end_block(&mut arc_state_tx, &end_block) .await .wrap_err("end_block failed on IbcComponent")?; + MarketMapComponent::end_block(&mut arc_state_tx, &end_block) + .await + .wrap_err("end_block failed on MarketMapComponent")?; + OracleComponent::end_block(&mut arc_state_tx, &end_block) + .await + .wrap_err("end_block failed on OracleComponent")?; let mut state_tx = Arc::try_unwrap(arc_state_tx) .expect("components should not retain copies of shared state"); @@ -1284,6 +1539,44 @@ impl App { } } +fn handle_consensus_param_updates( + state_tx: &mut StateDelta>>, + consensus_param_updates: &Option, + current_vote_extensions_enable_height: u64, +) -> Result<()> { + if let Some(consensus_param_updates) = &consensus_param_updates { + if let Some(new_vote_extensions_enable_height) = + consensus_param_updates.abci.vote_extensions_enable_height + { + // if vote extensions are already enabled, they cannot be disabled, + // and the `vote_extensions_enable_height` cannot be changed. + if current_vote_extensions_enable_height != VOTE_EXTENSIONS_DISABLED_HEIGHT { + warn!( + "vote extensions enable height already set to {}; ignoring update", + current_vote_extensions_enable_height + ); + return Ok(()); + } + + // vote extensions are currently disabled, so updating the enabled height to + // 0 (which also means disabling them) is a no-op. + if new_vote_extensions_enable_height.value() == 0 { + warn!("ignoring update to set vote extensions enable height to 0"); + return Ok(()); + } + + // TODO: when we implement an action to activate vote extensions, + // we must ensure that the action *also* writes the necessary state + // as done in `MarketMapComponent::init_chain` and `OracleComponent::init_chain`. + state_tx + .put_vote_extensions_enable_height(new_vote_extensions_enable_height.value()) + .wrap_err("failed to put vote extensions enable height")?; + } + } + + Ok(()) +} + // updates the mempool to reflect current state // // NOTE: this function locks the mempool until all accounts have been cleaned. diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap index 2006530974..9cdee0c76c 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 195, - 205, - 225, - 173, - 118, - 201, - 149, - 122, - 173, - 117, - 237, - 146, - 148, - 114, - 152, - 59, - 68, - 60, + 7, + 73, + 159, 33, - 65, - 41, - 154, - 249, - 85, - 76, - 183, - 32, + 203, + 171, + 43, + 107, + 191, + 198, + 89, + 66, + 213, + 54, + 135, 108, - 175, - 88, - 197, - 63 + 97, + 126, + 73, + 192, + 102, + 156, + 128, + 81, + 249, + 111, + 203, + 53, + 36, + 53, + 188, + 61 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap index 218d82f1f6..685595551e 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 48, - 214, - 34, - 61, + 75, + 218, + 136, + 254, + 179, + 193, + 82, + 27, + 253, + 94, + 227, + 174, + 229, + 51, + 196, + 185, + 210, + 71, + 247, + 132, + 229, + 90, + 138, 4, 228, - 103, - 148, - 143, - 144, - 228, - 158, - 243, - 185, - 202, - 88, - 179, - 89, - 99, - 98, - 113, - 240, - 167, - 127, - 88, - 153, - 200, - 213, + 145, + 229, 136, - 197, - 103, - 12 + 191, + 219, + 150, + 190 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap index 15352db3e9..a90e229cdf 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 163, + 231, + 14, + 30, + 46, + 75, + 144, + 96, + 107, + 201, + 50, 247, - 139, - 47, - 78, - 129, + 214, + 18, + 146, + 108, + 66, + 67, + 117, + 152, + 244, + 68, + 26, 169, - 19, - 217, - 165, + 236, + 241, 120, - 82, - 190, - 249, - 77, - 186, - 153, - 51, - 213, - 253, - 37, - 38, - 99, - 100, - 91, - 245, - 28, - 150, - 61, - 214, - 212, - 12 + 252, + 87, + 16, + 209, + 145, + 236 ] diff --git a/crates/astria-sequencer/src/app/state_ext.rs b/crates/astria-sequencer/src/app/state_ext.rs index b9a2e3b78e..48d97848de 100644 --- a/crates/astria-sequencer/src/app/state_ext.rs +++ b/crates/astria-sequencer/src/app/state_ext.rs @@ -99,6 +99,21 @@ pub(crate) trait StateReadExt: StateRead { .and_then(|value| storage::StorageVersion::try_from(value).map(u64::from)) .context("invalid storage version bytes") } + + #[instrument(skip_all)] + async fn get_vote_extensions_enable_height(&self) -> Result { + let Some(bytes) = self + .nonverifiable_get_raw(keys::VOTE_EXTENSIONS_ENABLED_HEIGHT.as_bytes()) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed to read raw vote extensions enabled height from state")? + else { + bail!("vote extensions enabled height not found"); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| storage::BlockHeight::try_from(value).map(u64::from)) + .context("invalid vote extensions enabled height bytes") + } } impl StateReadExt for T {} @@ -150,6 +165,15 @@ pub(crate) trait StateWriteExt: StateWrite { self.nonverifiable_put_raw(keys::storage_version_by_height(height).into_bytes(), bytes); Ok(()) } + + #[instrument(skip_all)] + fn put_vote_extensions_enable_height(&mut self, height: u64) -> Result<()> { + let bytes = StoredValue::from(storage::BlockHeight::from(height)) + .serialize() + .context("failed to serialize vote extensions enabled height")?; + self.nonverifiable_put_raw(keys::VOTE_EXTENSIONS_ENABLED_HEIGHT.into(), bytes); + Ok(()) + } } impl StateWriteExt for T {} diff --git a/crates/astria-sequencer/src/app/storage/keys.rs b/crates/astria-sequencer/src/app/storage/keys.rs index 564fab2cdf..e0c57f17b7 100644 --- a/crates/astria-sequencer/src/app/storage/keys.rs +++ b/crates/astria-sequencer/src/app/storage/keys.rs @@ -2,6 +2,7 @@ pub(in crate::app) const CHAIN_ID: &str = "app/chain_id"; pub(in crate::app) const REVISION_NUMBER: &str = "app/revision_number"; pub(in crate::app) const BLOCK_HEIGHT: &str = "app/block_height"; pub(in crate::app) const BLOCK_TIMESTAMP: &str = "app/block_timestamp"; +pub(in crate::app) const VOTE_EXTENSIONS_ENABLED_HEIGHT: &str = "app/vote_extensions_enable_height"; pub(in crate::app) fn storage_version_by_height(height: u64) -> String { format!("app/storage_version/{height}") diff --git a/crates/astria-sequencer/src/app/test_utils.rs b/crates/astria-sequencer/src/app/test_utils.rs index f9f1ba8de8..7f723ad235 100644 --- a/crates/astria-sequencer/src/app/test_utils.rs +++ b/crates/astria-sequencer/src/app/test_utils.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + collections::HashMap, + sync::Arc, +}; use astria_core::{ crypto::SigningKey, @@ -19,6 +22,8 @@ use astria_core::{ TransactionBody, }, }, + sequencerblock::v1::block::Deposit, + Protobuf, }; use bytes::Bytes; @@ -176,3 +181,29 @@ impl MockTxBuilder { Arc::new(tx.sign(&self.signer)) } } + +pub(crate) fn transactions_with_extended_commit_info_and_commitments( + txs: &[Transaction], + deposits: Option>>, +) -> Vec { + use prost::Message as _; + use tendermint::abci::types::ExtendedCommitInfo; + + use crate::proposal::commitment::generate_rollup_datas_commitment; + + let extended_commit_info: tendermint_proto::abci::ExtendedCommitInfo = ExtendedCommitInfo { + round: 0u16.into(), + votes: vec![], + } + .into(); + let commitments = generate_rollup_datas_commitment(txs, deposits.unwrap_or_default()); + let txs_with_commit_info: Vec = + std::iter::once(extended_commit_info.encode_to_vec().into()) + .chain( + commitments + .into_iter() + .chain(txs.iter().map(|tx| tx.to_raw().encode_to_vec().into())), + ) + .collect(); + txs_with_commit_info +} diff --git a/crates/astria-sequencer/src/app/tests_app/mempool.rs b/crates/astria-sequencer/src/app/tests_app/mempool.rs index adc304d585..7e16e645d8 100644 --- a/crates/astria-sequencer/src/app/tests_app/mempool.rs +++ b/crates/astria-sequencer/src/app/tests_app/mempool.rs @@ -19,7 +19,6 @@ use benchmark_and_test_utils::{ ALICE_ADDRESS, CAROL_ADDRESS, }; -use prost::Message as _; use tendermint::{ abci::{ self, @@ -42,7 +41,6 @@ use crate::{ astria_address_from_hex_string, nria, }, - proposal::commitment::generate_rollup_datas_commitment, }; #[tokio::test] @@ -82,7 +80,10 @@ async fn trigger_cleaning() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -100,19 +101,22 @@ async fn trigger_cleaning() { assert!(!app.recost_mempool, "flag should start out false"); // trigger with process_proposal - let commitments = generate_rollup_datas_commitment(&[tx_trigger.clone()], HashMap::new()); + let txs = transactions_with_extended_commit_info_and_commitments(&vec![tx_trigger], None); let process_proposal = abci::request::ProcessProposal { hash: Hash::try_from([99u8; 32].to_vec()).unwrap(), height: 1u32.into(), time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs: commitments.into_transactions(vec![tx_trigger.to_raw().encode_to_vec().into()]), - proposed_last_commit: None, + txs: txs.clone(), + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: Round::default(), + }), misbehavior: vec![], }; - app.process_proposal(process_proposal.clone(), storage.clone()) + app.process_proposal(process_proposal, storage.clone()) .await .unwrap(); assert!(app.recost_mempool, "flag should have been set"); @@ -120,14 +124,14 @@ async fn trigger_cleaning() { // trigger with finalize block app.recost_mempool = false; assert!(!app.recost_mempool, "flag should start out false"); - let commitments = generate_rollup_datas_commitment(&[tx_trigger.clone()], HashMap::new()); + let finalize_block = abci::request::FinalizeBlock { hash: Hash::try_from([97u8; 32].to_vec()).unwrap(), height: 1u32.into(), time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs: commitments.into_transactions(vec![tx_trigger.to_raw().encode_to_vec().into()]), + txs, decided_last_commit: CommitInfo { votes: vec![], round: Round::default(), @@ -135,7 +139,7 @@ async fn trigger_cleaning() { misbehavior: vec![], }; - app.finalize_block(finalize_block.clone(), storage.clone()) + app.finalize_block(finalize_block, storage.clone()) .await .unwrap(); assert!(app.recost_mempool, "flag should have been set"); @@ -176,7 +180,10 @@ async fn do_not_trigger_cleaning() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -277,7 +284,10 @@ async fn maintenance_recosting_promotes() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -291,8 +301,8 @@ async fn maintenance_recosting_promotes() { assert_eq!( res.txs.len(), - 3, - "only one transaction should've been valid (besides 2 generated txs)" + 4, + "only one transaction should've been valid (besides 3 generated txs)" ); assert_eq!( app.mempool.len().await, @@ -310,7 +320,10 @@ async fn maintenance_recosting_promotes() { next_validators_hash: Hash::default(), proposer_address: [1u8; 20].to_vec().try_into().unwrap(), txs: res.txs.clone(), - proposed_last_commit: None, + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; app.process_proposal(process_proposal, storage.clone()) @@ -348,7 +361,10 @@ async fn maintenance_recosting_promotes() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: 2u8.into(), time: Time::now(), @@ -362,8 +378,8 @@ async fn maintenance_recosting_promotes() { assert_eq!( res.txs.len(), - 3, - "one transaction should've been valid (besides 2 generated txs)" + 4, + "only one transaction should've been valid (besides 3 generated txs)" ); // see transfer went through @@ -458,7 +474,10 @@ async fn maintenance_funds_added_promotes() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -472,8 +491,8 @@ async fn maintenance_funds_added_promotes() { assert_eq!( res.txs.len(), - 3, - "only one transactions should've been valid (besides 2 generated txs)" + 4, + "only one transactions should've been valid (besides 3 generated txs)" ); app.executed_proposal_hash = Hash::try_from([97u8; 32].to_vec()).unwrap(); @@ -484,7 +503,10 @@ async fn maintenance_funds_added_promotes() { next_validators_hash: Hash::default(), proposer_address: [1u8; 20].to_vec().try_into().unwrap(), txs: res.txs.clone(), - proposed_last_commit: None, + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; app.process_proposal(process_proposal, storage.clone()) @@ -523,7 +545,10 @@ async fn maintenance_funds_added_promotes() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: 2u8.into(), time: Time::now(), @@ -537,8 +562,8 @@ async fn maintenance_funds_added_promotes() { assert_eq!( res.txs.len(), - 3, - "only one transactions should've been valid (besides 2 generated txs)" + 4, + "only one transactions should've been valid (besides 3 generated txs)" ); // finalize with finalize block diff --git a/crates/astria-sequencer/src/app/tests_app/mod.rs b/crates/astria-sequencer/src/app/tests_app/mod.rs index 1ca46cec4d..3d466f4095 100644 --- a/crates/astria-sequencer/src/app/tests_app/mod.rs +++ b/crates/astria-sequencer/src/app/tests_app/mod.rs @@ -42,7 +42,10 @@ use tendermint::{ PrepareProposal, ProcessProposal, }, - types::CommitInfo, + types::{ + CommitInfo, + ExtendedCommitInfo, + }, }, account, block::{ @@ -74,7 +77,6 @@ use crate::{ }, bridge::StateWriteExt as _, fees::StateReadExt as _, - proposal::commitment::generate_rollup_datas_commitment, }; fn default_tendermint_header() -> Header { @@ -260,16 +262,13 @@ async fn app_transfer_block_fees_to_sudo() { let signed_tx = tx.sign(&alice); let proposer_address: tendermint::account::Id = [99u8; 20].to_vec().try_into().unwrap(); - - let commitments = generate_rollup_datas_commitment(&[signed_tx.clone()], HashMap::new()); - let finalize_block = abci::request::FinalizeBlock { hash: Hash::try_from([0u8; 32].to_vec()).unwrap(), height: 1u32.into(), time: Time::now(), next_validators_hash: Hash::default(), proposer_address, - txs: commitments.into_transactions(vec![signed_tx.to_raw().encode_to_vec().into()]), + txs: transactions_with_extended_commit_info_and_commitments(&vec![signed_tx], None), decided_last_commit: CommitInfo { votes: vec![], round: Round::default(), @@ -375,7 +374,6 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { source_action_index: starting_index_of_action, }; let deposits = HashMap::from_iter(vec![(rollup_id, vec![expected_deposit.clone()])]); - let commitments = generate_rollup_datas_commitment(&[signed_tx.clone()], deposits.clone()); let finalize_block = abci::request::FinalizeBlock { hash: Hash::try_from([0u8; 32].to_vec()).unwrap(), @@ -383,7 +381,7 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs: commitments.into_transactions(vec![signed_tx.to_raw().encode_to_vec().into()]), + txs: transactions_with_extended_commit_info_and_commitments(&[signed_tx], Some(deposits)), decided_last_commit: CommitInfo { votes: vec![], round: Round::default(), @@ -467,7 +465,6 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { source_action_index: starting_index_of_action, }; let deposits = HashMap::from_iter(vec![(rollup_id, vec![expected_deposit.clone()])]); - let commitments = generate_rollup_datas_commitment(&[signed_tx.clone()], deposits.clone()); let timestamp = Time::now(); let block_hash = Hash::try_from([99u8; 32].to_vec()).unwrap(); @@ -477,7 +474,10 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { time: timestamp, next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs: commitments.into_transactions(vec![signed_tx.to_raw().encode_to_vec().into()]), + txs: transactions_with_extended_commit_info_and_commitments( + &[signed_tx.clone()], + Some(deposits), + ), decided_last_commit: CommitInfo { votes: vec![], round: Round::default(), @@ -513,7 +513,10 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { proposer_address, txs: vec![], max_tx_bytes: 1_000_000, - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; let proposal_fingerprint = prepare_proposal.clone().into(); @@ -541,7 +544,10 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), txs: finalize_block.txs.clone(), - proposed_last_commit: None, + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; @@ -640,7 +646,10 @@ async fn app_prepare_proposal_cometbft_max_bytes_overflow_ok() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 200_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -659,9 +668,9 @@ async fn app_prepare_proposal_cometbft_max_bytes_overflow_ok() { // see only first tx made it in assert_eq!( result.txs.len(), - 3, - "total transaction length should be three, including the two commitments and the one tx \ - that fit" + 4, + "total transaction length should be four, including the extended commit info, two \ + commitments and the one tx that fit" ); assert_eq!( app.mempool.len().await, @@ -729,7 +738,10 @@ async fn app_prepare_proposal_sequencer_max_bytes_overflow_ok() { let prepare_args = abci::request::PrepareProposal { max_tx_bytes: 600_000, // make large enough to overflow sequencer bytes first txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -748,9 +760,9 @@ async fn app_prepare_proposal_sequencer_max_bytes_overflow_ok() { // see only first tx made it in assert_eq!( result.txs.len(), - 3, - "total transaction length should be three, including the two commitments and the one tx \ - that fit" + 4, + "total transaction length should be four, including the extended commit info, two \ + commitments and the one tx that fit" ); assert_eq!( app.mempool.len().await, @@ -796,12 +808,6 @@ async fn app_process_proposal_sequencer_max_bytes_overflow_fail() { .sign(&alice); let txs: Vec = vec![tx_pass, tx_overflow]; - let generated_commitment = generate_rollup_datas_commitment(&txs, HashMap::new()); - let txs = generated_commitment.into_transactions( - txs.into_iter() - .map(|tx| tx.to_raw().encode_to_vec().into()) - .collect(), - ); let process_proposal = ProcessProposal { hash: Hash::default(), @@ -809,8 +815,11 @@ async fn app_process_proposal_sequencer_max_bytes_overflow_fail() { time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs, - proposed_last_commit: None, + txs: transactions_with_extended_commit_info_and_commitments(&txs, None), + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; @@ -846,12 +855,6 @@ async fn app_process_proposal_transaction_fails_to_execute_fails() { .sign(&alice); let txs: Vec = vec![tx_fail]; - let generated_commitment = generate_rollup_datas_commitment(&txs, HashMap::new()); - let txs = generated_commitment.into_transactions( - txs.into_iter() - .map(|tx| tx.to_raw().encode_to_vec().into()) - .collect(), - ); let process_proposal = ProcessProposal { hash: Hash::default(), @@ -859,8 +862,11 @@ async fn app_process_proposal_transaction_fails_to_execute_fails() { time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs, - proposed_last_commit: None, + txs: transactions_with_extended_commit_info_and_commitments(&txs, None), + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; diff --git a/crates/astria-sequencer/src/app/tests_block_ordering.rs b/crates/astria-sequencer/src/app/tests_block_ordering.rs index c8c28893f4..07cd0f3487 100644 --- a/crates/astria-sequencer/src/app/tests_block_ordering.rs +++ b/crates/astria-sequencer/src/app/tests_block_ordering.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - ops::Deref, -}; +use std::ops::Deref; use astria_core::{ protocol::transaction::v1::{ @@ -13,9 +10,15 @@ use astria_core::{ use bytes::Bytes; use prost::Message; use tendermint::{ - abci::request::{ - PrepareProposal, - ProcessProposal, + abci::{ + request::{ + PrepareProposal, + ProcessProposal, + }, + types::{ + CommitInfo, + ExtendedCommitInfo, + }, }, block::Height, Hash, @@ -23,20 +26,18 @@ use tendermint::{ }; use super::test_utils::get_alice_signing_key; -use crate::{ - app::{ - benchmark_and_test_utils::{ - initialize_app_with_storage, - mock_balances, - mock_tx_cost, - }, - test_utils::{ - get_bob_signing_key, - get_judy_signing_key, - MockTxBuilder, - }, +use crate::app::{ + benchmark_and_test_utils::{ + initialize_app_with_storage, + mock_balances, + mock_tx_cost, + }, + test_utils::{ + get_bob_signing_key, + get_judy_signing_key, + transactions_with_extended_commit_info_and_commitments, + MockTxBuilder, }, - proposal::commitment::generate_rollup_datas_commitment, }; #[tokio::test] @@ -72,21 +73,17 @@ async fn app_process_proposal_ordering_ok() { .clone(), ]; - let generated_commitment = generate_rollup_datas_commitment(&txs, HashMap::new()); - let txs = generated_commitment.into_transactions( - txs.into_iter() - .map(|tx| tx.to_raw().encode_to_vec().into()) - .collect(), - ); - let process_proposal = ProcessProposal { hash: Hash::Sha256([1; 32]), height: 1u32.into(), time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs, - proposed_last_commit: None, + txs: transactions_with_extended_commit_info_and_commitments(&txs, None), + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; @@ -120,21 +117,17 @@ async fn app_process_proposal_ordering_fail() { .clone(), ]; - let generated_commitment = generate_rollup_datas_commitment(&txs, HashMap::new()); - let txs = generated_commitment.into_transactions( - txs.into_iter() - .map(|tx| tx.to_raw().encode_to_vec().into()) - .collect(), - ); - let process_proposal = ProcessProposal { hash: Hash::default(), height: 1u32.into(), time: Time::now(), next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs, - proposed_last_commit: None, + txs: transactions_with_extended_commit_info_and_commitments(&txs, None), + proposed_last_commit: Some(CommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], }; @@ -189,7 +182,10 @@ async fn app_prepare_proposal_account_block_misordering_ok() { let prepare_args = PrepareProposal { max_tx_bytes: 600_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: Height::default(), time: Time::now(), @@ -203,8 +199,8 @@ async fn app_prepare_proposal_account_block_misordering_ok() { .expect("incorrect account ordering shouldn't cause blocks to fail"); assert_eq!( - prepare_proposal_result.txs[2], - Into::::into(tx_0.to_raw().encode_to_vec()), + prepare_proposal_result.txs.last().unwrap(), + &Into::::into(tx_0.to_raw().encode_to_vec()), "expected to contain first transaction" ); @@ -222,7 +218,10 @@ async fn app_prepare_proposal_account_block_misordering_ok() { let prepare_args = PrepareProposal { max_tx_bytes: 600_000, txs: vec![], - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + votes: vec![], + round: 0u16.into(), + }), misbehavior: vec![], height: 1u32.into(), time: Time::now(), @@ -235,8 +234,8 @@ async fn app_prepare_proposal_account_block_misordering_ok() { .expect("incorrect account ordering shouldn't cause blocks to fail"); assert_eq!( - prepare_proposal_result.txs[2], - Into::::into(tx_1.to_raw().encode_to_vec()), + prepare_proposal_result.txs.last().unwrap(), + &Into::::into(tx_1.to_raw().encode_to_vec()), "expected to contain second transaction" ); diff --git a/crates/astria-sequencer/src/app/tests_breaking_changes.rs b/crates/astria-sequencer/src/app/tests_breaking_changes.rs index 031538a057..43f4c1ba09 100644 --- a/crates/astria-sequencer/src/app/tests_breaking_changes.rs +++ b/crates/astria-sequencer/src/app/tests_breaking_changes.rs @@ -37,10 +37,7 @@ use astria_core::{ Protobuf, }; use cnidarium::StateDelta; -use prost::{ - bytes::Bytes, - Message as _, -}; +use prost::bytes::Bytes; use tendermint::{ abci, abci::types::CommitInfo, @@ -62,6 +59,7 @@ use crate::{ get_alice_signing_key, get_bridge_signing_key, initialize_app, + transactions_with_extended_commit_info_and_commitments, }, }, authority::StateReadExt as _, @@ -72,7 +70,6 @@ use crate::{ ASTRIA_PREFIX, }, bridge::StateWriteExt as _, - proposal::commitment::generate_rollup_datas_commitment, }; #[tokio::test] @@ -136,7 +133,6 @@ async fn app_finalize_block_snapshot() { source_action_index: starting_index_of_action, }; let deposits = HashMap::from_iter(vec![(rollup_id, vec![expected_deposit.clone()])]); - let commitments = generate_rollup_datas_commitment(&[signed_tx.clone()], deposits.clone()); let timestamp = Time::unix_epoch(); let block_hash = Hash::try_from([99u8; 32].to_vec()).unwrap(); @@ -146,7 +142,10 @@ async fn app_finalize_block_snapshot() { time: timestamp, next_validators_hash: Hash::default(), proposer_address: [0u8; 20].to_vec().try_into().unwrap(), - txs: commitments.into_transactions(vec![signed_tx.to_raw().encode_to_vec().into()]), + txs: transactions_with_extended_commit_info_and_commitments( + &vec![signed_tx], + Some(deposits), + ), decided_last_commit: CommitInfo { votes: vec![], round: Round::default(), diff --git a/crates/astria-sequencer/src/app/vote_extension.rs b/crates/astria-sequencer/src/app/vote_extension.rs new file mode 100644 index 0000000000..6d8470036e --- /dev/null +++ b/crates/astria-sequencer/src/app/vote_extension.rs @@ -0,0 +1,683 @@ +use std::collections::HashMap; + +use astria_core::{ + connect::{ + abci::v2::OracleVoteExtension, + oracle::v2::QuotePrice, + service::v2::QueryPricesResponse, + types::v2::{ + CurrencyPair, + Price, + }, + }, + crypto::Signature, + generated::connect::{ + abci::v2::OracleVoteExtension as RawOracleVoteExtension, + service::v2::{ + oracle_client::OracleClient, + QueryPricesRequest, + }, + }, +}; +use astria_eyre::eyre::{ + bail, + ensure, + ContextCompat as _, + Result, + WrapErr as _, +}; +use indexmap::IndexMap; +use prost::Message as _; +use tendermint::{ + abci, + abci::types::{ + BlockSignatureInfo::Flag, + CommitInfo, + ExtendedCommitInfo, + }, +}; +use tendermint_proto::google::protobuf::Timestamp; +use tonic::transport::Channel; +use tracing::{ + debug, + info, + instrument, + warn, +}; + +use crate::{ + address::StateReadExt as _, + app::state_ext::StateReadExt, + authority::StateReadExt as _, + connect::oracle::{ + currency_pair_strategy::DefaultCurrencyPairStrategy, + state_ext::StateWriteExt, + }, +}; + +// https://github.com/skip-mev/connect/blob/793b2e874d6e720bd288e82e782502e41cf06f8c/abci/types/constants.go#L6 +const MAXIMUM_PRICE_BYTE_LEN: usize = 33; + +pub(crate) struct Handler { + // gRPC client for the connect oracle sidecar. + oracle_client: Option>, +} + +impl Handler { + pub(crate) fn new(oracle_client: Option>) -> Self { + Self { + oracle_client, + } + } + + pub(crate) async fn extend_vote( + &mut self, + state: &S, + ) -> Result { + let Some(oracle_client) = self.oracle_client.as_mut() else { + // we allow validators to *not* use the oracle sidecar currently, + // so this will get converted to an empty vote extension when bubbled up. + // + // however, if >1/3 of validators are not using the oracle, the prices will not update. + bail!("oracle client not set") + }; + + // if we fail to get prices within the timeout duration, we will return an empty vote + // extension to ensure liveness. + let rsp = match oracle_client.prices(QueryPricesRequest {}).await { + Ok(rsp) => rsp.into_inner(), + Err(e) => { + bail!("failed to get prices from oracle sidecar: {e:#}",); + } + }; + + let query_prices_response = + astria_core::connect::service::v2::QueryPricesResponse::try_from_raw(rsp) + .wrap_err("failed to validate prices server response")?; + let oracle_vote_extension = transform_oracle_service_prices(state, query_prices_response) + .await + .wrap_err("failed to transform oracle service prices")?; + + Ok(abci::response::ExtendVote { + vote_extension: oracle_vote_extension.into_raw().encode_to_vec().into(), + }) + } + + pub(crate) async fn verify_vote_extension( + &self, + state: &S, + vote: abci::request::VerifyVoteExtension, + ) -> Result { + if vote.vote_extension.is_empty() { + return Ok(abci::response::VerifyVoteExtension::Accept); + } + + let max_num_currency_pairs = + DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state, false) + .await + .wrap_err("failed to get max number of currency pairs")?; + + let response = match verify_vote_extension(vote.vote_extension, max_num_currency_pairs) { + Ok(()) => abci::response::VerifyVoteExtension::Accept, + Err(e) => { + tracing::warn!(error = %e, "failed to verify vote extension"); + abci::response::VerifyVoteExtension::Reject + } + }; + Ok(response) + } +} + +// see https://github.com/skip-mev/connect/blob/5b07f91d6c0110e617efda3f298f147a31da0f25/abci/ve/utils.go#L24 +fn verify_vote_extension( + oracle_vote_extension_bytes: bytes::Bytes, + max_num_currency_pairs: u64, +) -> Result<()> { + let oracle_vote_extension = RawOracleVoteExtension::decode(oracle_vote_extension_bytes) + .wrap_err("failed to decode oracle vote extension")?; + + ensure!( + u64::try_from(oracle_vote_extension.prices.len()).ok() <= Some(max_num_currency_pairs), + "number of oracle vote extension prices exceeds max expected number of currency pairs" + ); + + for prices in oracle_vote_extension.prices.values() { + ensure!( + prices.len() <= MAXIMUM_PRICE_BYTE_LEN, + "encoded price length exceeded {MAXIMUM_PRICE_BYTE_LEN}" + ); + } + + Ok(()) +} + +// see https://github.com/skip-mev/connect/blob/158cde8a4b774ac4eec5c6d1a2c16de6a8c6abb5/abci/ve/vote_extension.go#L290 +#[instrument(skip_all)] +async fn transform_oracle_service_prices( + state: &S, + rsp: QueryPricesResponse, +) -> Result { + use astria_core::connect::types::v2::CurrencyPairId; + use futures::StreamExt as _; + + let futures = futures::stream::FuturesUnordered::new(); + for (currency_pair, price) in rsp.prices { + futures.push(async move { + ( + DefaultCurrencyPairStrategy::id(state, ¤cy_pair).await, + currency_pair, + price, + ) + }); + } + + let result: Vec<(Result>, CurrencyPair, Price)> = + futures.collect().await; + let strategy_prices = result.into_iter().filter_map(|(get_id_result, currency_pair, price)| { + let id = match get_id_result { + Ok(Some(id)) => id, + Ok(None) => { + debug!(%currency_pair, "currency pair ID not found in state; skipping"); + return None; + } + Err(err) => { + // FIXME: this event can be removed once all instrumented functions + // can generate an error event. + warn!(%currency_pair, "failed to fetch ID for currency pair; cancelling transformation"); + return Some(Err(err).wrap_err("failed to fetch currency pair ID")); + } + }; + Some(Ok((id, price))) + }).collect::>>()?; + + Ok(OracleVoteExtension { + prices: strategy_prices, + }) +} + +pub(crate) struct ValidatedExtendedCommitInfo(ExtendedCommitInfo); + +impl ValidatedExtendedCommitInfo { + pub(crate) fn into_inner(self) -> ExtendedCommitInfo { + self.0 + } +} + +pub(crate) struct ProposalHandler; + +impl ProposalHandler { + // called during prepare_proposal; prunes and validates the local extended commit info + // received during the previous block's voting period. + // + // the returned extended commit info will be proposed this block. + pub(crate) async fn prepare_proposal( + state: &S, + height: u64, + mut extended_commit_info: ExtendedCommitInfo, + ) -> Result { + if height == 1 { + // we're proposing block 1, so nothing to validate + info!( + "skipping vote extension proposal for block 1, as there were no previous vote \ + extensions" + ); + return Ok(ValidatedExtendedCommitInfo(extended_commit_info)); + } + + let max_num_currency_pairs = + DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state, true) + .await + .wrap_err("failed to get max number of currency pairs")?; + + for vote in &mut extended_commit_info.votes { + if let Err(e) = + verify_vote_extension(vote.vote_extension.clone(), max_num_currency_pairs) + { + let address = state + .try_base_prefixed(vote.validator.address.as_slice()) + .await + .wrap_err("failed to construct validator address with base prefix")?; + debug!( + error = AsRef::::as_ref(&e), + validator = address.to_string(), + "failed to verify vote extension; pruning from proposal" + ); + vote.sig_info = Flag(tendermint::block::BlockIdFlag::Absent); + vote.extension_signature = None; + vote.vote_extension.clear(); + } + } + + validate_vote_extensions(state, height, &extended_commit_info) + .await + .wrap_err("failed to validate vote extensions in prepare_proposal")?; + + Ok(ValidatedExtendedCommitInfo(extended_commit_info)) + } + + // called during process_proposal; validates the proposed extended commit info. + pub(crate) async fn validate_proposal( + state: &S, + height: u64, + last_commit: &CommitInfo, + extended_commit_info: &ExtendedCommitInfo, + ) -> Result<()> { + if height == 1 { + // we're processing block 1, so nothing to validate (no last commit yet) + info!( + "skipping vote extension validation for block 1, as there were no previous vote \ + extensions" + ); + return Ok(()); + } + + // inside process_proposal, we must validate the vote extensions proposed against the last + // commit proposed + validate_extended_commit_against_last_commit(last_commit, extended_commit_info)?; + + validate_vote_extensions(state, height, extended_commit_info) + .await + .wrap_err("failed to validate vote extensions in validate_extended_commit_info")?; + + let max_num_currency_pairs = + DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state, true) + .await + .wrap_err("failed to get max number of currency pairs")?; + + for vote in &extended_commit_info.votes { + verify_vote_extension(vote.vote_extension.clone(), max_num_currency_pairs) + .wrap_err("failed to verify vote extension in validate_proposal")?; + } + + Ok(()) + } +} + +// see https://github.com/skip-mev/connect/blob/5b07f91d6c0110e617efda3f298f147a31da0f25/abci/ve/utils.go#L111 +async fn validate_vote_extensions( + state: &S, + height: u64, + extended_commit_info: &ExtendedCommitInfo, +) -> Result<()> { + use tendermint_proto::v0_38::types::CanonicalVoteExtension; + + let chain_id = state + .get_chain_id() + .await + .wrap_err("failed to get chain id")?; + + // total validator voting power + let mut total_voting_power: u64 = 0; + // the total voting power of all validators which submitted vote extensions + let mut submitted_voting_power: u64 = 0; + + let validator_set = state + .get_validator_set() + .await + .wrap_err("failed to get validator set")?; + + for vote in &extended_commit_info.votes { + let address = state + .try_base_prefixed(vote.validator.address.as_slice()) + .await + .wrap_err("failed to construct validator address with base prefix")?; + + total_voting_power = total_voting_power.saturating_add(vote.validator.power.value()); + + if vote.sig_info == Flag(tendermint::block::BlockIdFlag::Commit) { + ensure!( + vote.extension_signature.is_some(), + "vote extension signature is missing for validator {address}", + ); + } + + if vote.sig_info != Flag(tendermint::block::BlockIdFlag::Commit) { + ensure!( + vote.vote_extension.is_empty(), + "non-commit vote extension present for validator {address}" + ); + ensure!( + vote.extension_signature.is_none(), + "non-commit extension signature present for validator {address}", + ); + } + + if vote.sig_info != Flag(tendermint::block::BlockIdFlag::Commit) { + continue; + } + + submitted_voting_power = + submitted_voting_power.saturating_add(vote.validator.power.value()); + + let verification_key = &validator_set + .get(&vote.validator.address) + .wrap_err("validator not found")? + .verification_key; + + let vote_extension = CanonicalVoteExtension { + extension: vote.vote_extension.to_vec(), + height: i64::try_from(height.checked_sub(1).expect( + "can subtract 1 from height as this function is only called for block height >1", + )) + .expect("block height must fit in an i64"), + round: i64::from(extended_commit_info.round.value()), + chain_id: chain_id.to_string(), + }; + + let message = vote_extension.encode_length_delimited_to_vec(); + let signature = Signature::try_from( + vote.extension_signature + .as_ref() + .expect("extension signature is some, as it was checked above") + .as_bytes(), + ) + .wrap_err("failed to create signature")?; + verification_key + .verify(&signature, &message) + .wrap_err("failed to verify signature for vote extension")?; + } + + // this shouldn't happen, but good to check anyways + if total_voting_power == 0 { + bail!("total voting power is zero"); + } + + let required_voting_power = total_voting_power + .checked_mul(2) + .wrap_err("failed to multiply total voting power by 2")? + .checked_div(3) + .wrap_err("failed to divide total voting power by 3")? + .checked_add(1) + .wrap_err("failed to add 1 from total voting power")?; + ensure!( + submitted_voting_power >= required_voting_power, + "submitted voting power is less than required voting power", + ); + + debug!( + submitted_voting_power, + total_voting_power, "validated extended commit info" + ); + Ok(()) +} + +fn validate_extended_commit_against_last_commit( + last_commit: &CommitInfo, + extended_commit_info: &ExtendedCommitInfo, +) -> Result<()> { + ensure!( + last_commit.round == extended_commit_info.round, + "last commit round does not match extended commit round" + ); + + ensure!( + last_commit.votes.len() == extended_commit_info.votes.len(), + "last commit votes length does not match extended commit votes length" + ); + + ensure!( + is_sorted::IsSorted::is_sorted_by(&mut extended_commit_info.votes.iter(), |a, b| { + if a.validator.power == b.validator.power { + // addresses sorted in ascending order, if the powers are the same + a.validator.address.partial_cmp(&b.validator.address) + } else { + // powers sorted in descending order + a.validator + .power + .partial_cmp(&b.validator.power) + .map(std::cmp::Ordering::reverse) + } + }), + "extended commit votes are not sorted by voting power", + ); + + for (last_commit_vote, extended_commit_info_vote) in last_commit + .votes + .iter() + .zip(extended_commit_info.votes.iter()) + { + ensure!( + last_commit_vote.validator.address == extended_commit_info_vote.validator.address, + "last commit vote address does not match extended commit vote address" + ); + ensure!( + last_commit_vote.validator.power == extended_commit_info_vote.validator.power, + "last commit vote power does not match extended commit vote power" + ); + + // vote is absent; no need to check for the block id flag matching the last commit + if extended_commit_info_vote.sig_info == Flag(tendermint::block::BlockIdFlag::Absent) + && extended_commit_info_vote.vote_extension.is_empty() + && extended_commit_info_vote.extension_signature.is_none() + { + continue; + } + + ensure!( + extended_commit_info_vote.sig_info == last_commit_vote.sig_info, + "last commit vote sig info does not match extended commit vote sig info" + ); + } + + Ok(()) +} + +pub(crate) async fn apply_prices_from_vote_extensions( + state: &mut S, + extended_commit_info: ExtendedCommitInfo, + timestamp: Timestamp, + height: u64, +) -> Result<()> { + let votes = extended_commit_info + .votes + .iter() + .map(|vote| { + let raw = RawOracleVoteExtension::decode(vote.vote_extension.clone()) + .wrap_err("failed to decode oracle vote extension")?; + OracleVoteExtension::try_from_raw(raw) + .wrap_err("failed to validate oracle vote extension") + }) + .collect::>>() + .wrap_err("failed to extract oracle vote extension from extended commit info")?; + + let prices = aggregate_oracle_votes(state, votes) + .await + .wrap_err("failed to aggregate oracle votes")?; + + for (currency_pair, price) in prices { + let price = QuotePrice { + price, + block_timestamp: astria_core::Timestamp { + seconds: timestamp.seconds, + nanos: timestamp.nanos, + }, + block_height: height, + }; + + state + .put_price_for_currency_pair(currency_pair, price) + .await + .wrap_err("failed to put price")?; + } + + Ok(()) +} + +async fn aggregate_oracle_votes( + state: &S, + votes: Vec, +) -> Result> { + // validators are not weighted right now, so we just take the median price for each currency + // pair + // + // skip uses a stake-weighted median: https://github.com/skip-mev/connect/blob/19a916122110cfd0e98d93978107d7ada1586918/pkg/math/voteweighted/voteweighted.go#L59 + // we can implement this later, when we have stake weighting. + let mut currency_pair_to_price_list = HashMap::new(); + for vote in votes { + for (id, price) in vote.prices { + let Some(currency_pair) = DefaultCurrencyPairStrategy::from_id(state, id) + .await + .wrap_err("failed to get currency pair from id")? + else { + continue; + }; + currency_pair_to_price_list + .entry(currency_pair) + .and_modify(|prices: &mut Vec| prices.push(price)) + .or_insert(vec![price]); + } + } + + let mut prices = HashMap::new(); + for (currency_pair, mut price_list) in currency_pair_to_price_list { + price_list.sort_unstable(); + let midpoint = price_list + .len() + .checked_div(2) + .expect("has a result because RHS is not 0"); + let median_price = if price_list.len() % 2 == 0 { + 'median_from_even: { + let Some(left) = price_list.get(midpoint) else { + break 'median_from_even None; + }; + let Some(right_idx) = midpoint.checked_add(1) else { + break 'median_from_even None; + }; + let Some(right) = price_list.get(right_idx).copied() else { + break 'median_from_even None; + }; + left.checked_add(right).and_then(|sum| sum.checked_div(2)) + } + } else { + price_list.get(midpoint).copied() + } + .unwrap_or_else(|| Price::new(0)); + prices.insert(currency_pair, median_price); + } + + Ok(prices) +} + +#[cfg(test)] +mod test { + use astria_core::{ + crypto::SigningKey, + protocol::transaction::v1::action::ValidatorUpdate, + }; + use cnidarium::StateDelta; + use tendermint::abci::types::{ + ExtendedVoteInfo, + Validator, + }; + use tendermint_proto::types::CanonicalVoteExtension; + + use super::*; + use crate::{ + address::StateWriteExt as _, + app::StateWriteExt as _, + authority::{ + StateWriteExt as _, + ValidatorSet, + }, + }; + + #[test] + fn verify_vote_extension_empty_ok() { + verify_vote_extension(vec![].into(), 100).unwrap(); + } + + #[tokio::test] + async fn validate_vote_extensions_insufficient_voting_power() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(&snapshot); + state + .put_chain_id_and_revision_number("test-0".try_into().unwrap()) + .unwrap(); + let validator_set = ValidatorSet::new_from_updates(vec![ + ValidatorUpdate { + power: 1u16.into(), + verification_key: SigningKey::from([0; 32]).verification_key(), + }, + ValidatorUpdate { + power: 2u16.into(), + verification_key: SigningKey::from([1; 32]).verification_key(), + }, + ]); + state.put_validator_set(validator_set).unwrap(); + state.put_base_prefix("astria".to_string()).unwrap(); + + let extended_commit_info = ExtendedCommitInfo { + round: 1u16.into(), + votes: vec![ExtendedVoteInfo { + validator: Validator { + address: *SigningKey::from([0; 32]).verification_key().address_bytes(), + power: 1u16.into(), + }, + sig_info: Flag(tendermint::block::BlockIdFlag::Nil), + extension_signature: None, + vote_extension: vec![].into(), + }], + }; + assert!( + validate_vote_extensions(&state, 1, &extended_commit_info) + .await + .unwrap_err() + .to_string() + .contains("submitted voting power is less than required voting power") + ); + } + + #[tokio::test] + async fn validate_vote_extensions_ok() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(&snapshot); + + let chain_id: tendermint::chain::Id = "test-0".try_into().unwrap(); + state + .put_chain_id_and_revision_number(chain_id.clone()) + .unwrap(); + let validator_set = ValidatorSet::new_from_updates(vec![ + ValidatorUpdate { + power: 5u16.into(), + verification_key: SigningKey::from([0; 32]).verification_key(), + }, + ValidatorUpdate { + power: 2u16.into(), + verification_key: SigningKey::from([1; 32]).verification_key(), + }, + ]); + state.put_validator_set(validator_set).unwrap(); + state.put_base_prefix("astria".to_string()).unwrap(); + + let round = 1u16; + let vote_extension_height = 1u64; + let vote_extension_message = b"noot".to_vec(); + let vote_extension = CanonicalVoteExtension { + extension: vote_extension_message.clone(), + height: vote_extension_height.try_into().unwrap(), + round: i64::from(round), + chain_id: chain_id.to_string(), + }; + + let message = vote_extension.encode_length_delimited_to_vec(); + let signature = SigningKey::from([0; 32]).sign(&message); + + let extended_commit_info = ExtendedCommitInfo { + round: round.into(), + votes: vec![ExtendedVoteInfo { + validator: Validator { + address: *SigningKey::from([0; 32]).verification_key().address_bytes(), + power: 1u16.into(), + }, + sig_info: Flag(tendermint::block::BlockIdFlag::Commit), + extension_signature: Some(signature.to_bytes().to_vec().try_into().unwrap()), + vote_extension: vote_extension_message.into(), + }], + }; + validate_vote_extensions(&state, vote_extension_height + 1, &extended_commit_info) + .await + .unwrap(); + } +} diff --git a/crates/astria-sequencer/src/config.rs b/crates/astria-sequencer/src/config.rs index 00db5f637b..f5103c27bf 100644 --- a/crates/astria-sequencer/src/config.rs +++ b/crates/astria-sequencer/src/config.rs @@ -30,6 +30,13 @@ pub struct Config { pub metrics_http_listener_addr: String, /// Writes a human readable format to stdout instead of JSON formatted OTEL trace data. pub pretty_print: bool, + /// If the oracle is disabled. If false, the `oracle_grpc_addr` must be set. + /// Should be false for validator nodes and true for non-validator nodes. + pub no_oracle: bool, + /// The gRPC endpoint for the oracle sidecar. + pub oracle_grpc_addr: String, + /// The timeout for the responses from the oracle sidecar in milliseconds. + pub oracle_client_timeout_milliseconds: u64, /// The maximum number of transactions that can be parked in the mempool. pub mempool_parked_max_tx_count: usize, } diff --git a/crates/astria-sequencer/src/connect/marketmap/component.rs b/crates/astria-sequencer/src/connect/marketmap/component.rs new file mode 100644 index 0000000000..9f821e8aba --- /dev/null +++ b/crates/astria-sequencer/src/connect/marketmap/component.rs @@ -0,0 +1,57 @@ +use std::sync::Arc; + +use astria_core::protocol::genesis::v1::GenesisAppState; +use astria_eyre::eyre::{ + Result, + WrapErr as _, +}; +use cnidarium::StateWrite; +use tendermint::abci::request::{ + BeginBlock, + EndBlock, +}; +use tracing::instrument; + +use super::state_ext::StateWriteExt as _; +use crate::component::Component; + +#[derive(Default)] +pub(crate) struct MarketMapComponent; + +#[async_trait::async_trait] +impl Component for MarketMapComponent { + type AppState = GenesisAppState; + + #[instrument(name = "MarketMapComponent::init_chain", skip(state))] + async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> { + if let Some(connect) = app_state.connect() { + // TODO: put market map authorites and admin in state; + // only required for related actions however + + state + .put_market_map(connect.market_map().market_map.clone()) + .wrap_err("failed to put market map")?; + state + .put_params(connect.market_map().params.clone()) + .wrap_err("failed to put params")?; + } + + Ok(()) + } + + #[instrument(name = "MarketMapComponent::begin_block", skip(_state))] + async fn begin_block( + _state: &mut Arc, + _begin_block: &BeginBlock, + ) -> Result<()> { + Ok(()) + } + + #[instrument(name = "MarketMapComponent::end_block", skip(_state))] + async fn end_block( + _state: &mut Arc, + _end_block: &EndBlock, + ) -> Result<()> { + Ok(()) + } +} diff --git a/crates/astria-sequencer/src/connect/marketmap/mod.rs b/crates/astria-sequencer/src/connect/marketmap/mod.rs new file mode 100644 index 0000000000..7e016778de --- /dev/null +++ b/crates/astria-sequencer/src/connect/marketmap/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod component; +pub(crate) mod state_ext; diff --git a/crates/astria-sequencer/src/connect/marketmap/state_ext.rs b/crates/astria-sequencer/src/connect/marketmap/state_ext.rs new file mode 100644 index 0000000000..5c09935068 --- /dev/null +++ b/crates/astria-sequencer/src/connect/marketmap/state_ext.rs @@ -0,0 +1,110 @@ +use astria_core::connect::market_map::v2::{ + MarketMap, + Params, +}; +use astria_eyre::{ + anyhow_to_eyre, + eyre::{ + Result, + WrapErr as _, + }, +}; +use async_trait::async_trait; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; +use cnidarium::{ + StateRead, + StateWrite, +}; +use tracing::instrument; + +const MARKET_MAP_KEY: &str = "connectmarketmap"; +const PARAMS_KEY: &str = "connectparams"; +const MARKET_MAP_LAST_UPDATED_KEY: &str = "connectmarketmaplastupdated"; + +/// Newtype wrapper to read and write a u64 from rocksdb. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +struct Height(u64); + +#[async_trait] +pub(crate) trait StateReadExt: StateRead { + #[instrument(skip_all)] + async fn get_market_map(&self) -> Result> { + let bytes = self + .get_raw(MARKET_MAP_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed to get market map from state")?; + match bytes { + Some(bytes) => { + let market_map = + serde_json::from_slice(&bytes).wrap_err("failed to deserialize market map")?; + Ok(Some(market_map)) + } + None => Ok(None), + } + } + + #[instrument(skip_all)] + async fn get_market_map_last_updated_height(&self) -> Result { + let Some(bytes) = self + .get_raw(MARKET_MAP_LAST_UPDATED_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading market map last updated height from state")? + else { + return Ok(0); + }; + let Height(height) = Height::try_from_slice(&bytes).wrap_err("invalid height bytes")?; + Ok(height) + } + + #[instrument(skip_all)] + async fn get_params(&self) -> Result> { + let bytes = self + .get_raw(PARAMS_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed to get params from state")?; + match bytes { + Some(bytes) => { + let params = + serde_json::from_slice(&bytes).wrap_err("failed to deserialize params")?; + Ok(Some(params)) + } + None => Ok(None), + } + } +} + +impl StateReadExt for T {} + +#[async_trait] +pub(crate) trait StateWriteExt: StateWrite { + #[instrument(skip_all)] + fn put_market_map(&mut self, market_map: MarketMap) -> Result<()> { + let bytes = serde_json::to_vec(&market_map).wrap_err("failed to serialize market map")?; + self.put_raw(MARKET_MAP_KEY.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_market_map_last_updated_height(&mut self, height: u64) -> Result<()> { + self.put_raw( + MARKET_MAP_LAST_UPDATED_KEY.to_string(), + borsh::to_vec(&Height(height)).wrap_err("failed to serialize height")?, + ); + Ok(()) + } + + #[instrument(skip_all)] + fn put_params(&mut self, params: Params) -> Result<()> { + let bytes = serde_json::to_vec(¶ms).wrap_err("failed to serialize params")?; + self.put_raw(PARAMS_KEY.to_string(), bytes); + Ok(()) + } +} + +impl StateWriteExt for T {} diff --git a/crates/astria-sequencer/src/connect/mod.rs b/crates/astria-sequencer/src/connect/mod.rs new file mode 100644 index 0000000000..8313d335c1 --- /dev/null +++ b/crates/astria-sequencer/src/connect/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod marketmap; +pub(crate) mod oracle; diff --git a/crates/astria-sequencer/src/connect/oracle/component.rs b/crates/astria-sequencer/src/connect/oracle/component.rs new file mode 100644 index 0000000000..ddb3a18a6e --- /dev/null +++ b/crates/astria-sequencer/src/connect/oracle/component.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use astria_core::{ + connect::oracle::v2::CurrencyPairState, + protocol::genesis::v1::GenesisAppState, +}; +use astria_eyre::eyre::{ + Result, + WrapErr as _, +}; +use tendermint::abci::request::{ + BeginBlock, + EndBlock, +}; +use tracing::instrument; + +use super::state_ext::StateWriteExt; +use crate::component::Component; + +#[derive(Default)] +pub(crate) struct OracleComponent; + +#[async_trait::async_trait] +impl Component for OracleComponent { + type AppState = GenesisAppState; + + #[instrument(name = "OracleComponent::init_chain", skip(state))] + async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> { + if let Some(connect) = app_state.connect() { + for currency_pair in &connect.oracle().currency_pair_genesis { + let currency_pair_state = CurrencyPairState { + id: currency_pair.id(), + nonce: currency_pair.nonce(), + price: currency_pair.currency_pair_price().clone(), + }; + state + .put_currency_pair_state( + currency_pair.currency_pair().clone(), + currency_pair_state, + ) + .wrap_err("failed to write currency pair to state")?; + } + + state + .put_next_currency_pair_id(connect.oracle().next_id) + .wrap_err("failed to put next currency pair id")?; + state + .put_num_currency_pairs(connect.oracle().currency_pair_genesis.len() as u64) + .wrap_err("failed to put number of currency pairs")?; + state + .put_num_removed_currency_pairs(0) + .wrap_err("failed to put number of removed currency pairs")?; + } + Ok(()) + } + + #[instrument(name = "OracleComponent::begin_block", skip(_state))] + async fn begin_block( + _state: &mut Arc, + _begin_block: &BeginBlock, + ) -> Result<()> { + Ok(()) + } + + #[instrument(name = "OracleComponent::end_block", skip(_state))] + async fn end_block( + _state: &mut Arc, + _end_block: &EndBlock, + ) -> Result<()> { + Ok(()) + } +} diff --git a/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs b/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs new file mode 100644 index 0000000000..3e21daddc8 --- /dev/null +++ b/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs @@ -0,0 +1,49 @@ +use astria_core::connect::types::v2::{ + CurrencyPair, + CurrencyPairId, +}; +use astria_eyre::eyre::{ + Result, + WrapErr as _, +}; + +use crate::connect::oracle::state_ext::StateReadExt; + +/// see +pub(crate) struct DefaultCurrencyPairStrategy; + +impl DefaultCurrencyPairStrategy { + pub(crate) async fn id( + state: &S, + currency_pair: &CurrencyPair, + ) -> Result> { + state.get_currency_pair_id(currency_pair).await + } + + pub(crate) async fn from_id( + state: &S, + id: CurrencyPairId, + ) -> Result> { + state.get_currency_pair(id).await + } + + pub(crate) async fn get_max_num_currency_pairs( + state: &S, + is_proposal_phase: bool, + ) -> Result { + let current = state + .get_num_currency_pairs() + .await + .wrap_err("failed to get number of currency pairs")?; + + if is_proposal_phase { + let removed = state + .get_num_removed_currency_pairs() + .await + .wrap_err("failed to get number of removed currency pairs")?; + Ok(current.saturating_add(removed)) + } else { + Ok(current) + } + } +} diff --git a/crates/astria-sequencer/src/connect/oracle/mod.rs b/crates/astria-sequencer/src/connect/oracle/mod.rs new file mode 100644 index 0000000000..bdd1fd2402 --- /dev/null +++ b/crates/astria-sequencer/src/connect/oracle/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod component; +pub(crate) mod currency_pair_strategy; +pub(crate) mod state_ext; diff --git a/crates/astria-sequencer/src/connect/oracle/state_ext.rs b/crates/astria-sequencer/src/connect/oracle/state_ext.rs new file mode 100644 index 0000000000..6096c8189f --- /dev/null +++ b/crates/astria-sequencer/src/connect/oracle/state_ext.rs @@ -0,0 +1,550 @@ +use std::{ + pin::Pin, + task::{ + ready, + Context, + Poll, + }, +}; + +use astria_core::connect::{ + oracle::v2::{ + CurrencyPairState, + QuotePrice, + }, + types::v2::{ + CurrencyPair, + CurrencyPairId, + CurrencyPairNonce, + }, +}; +use astria_eyre::{ + anyhow_to_eyre, + eyre::{ + ContextCompat as _, + Result, + WrapErr as _, + }, +}; +use async_trait::async_trait; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; +use cnidarium::{ + StateRead, + StateWrite, +}; +use futures::Stream; +use pin_project_lite::pin_project; +use tracing::instrument; + +mod in_state { + //! Contains all borsh datatypes that are written to/read from state. + + use astria_eyre::eyre::{ + Result, + WrapErr as _, + }; + use borsh::{ + BorshDeserialize, + BorshSerialize, + }; + + #[derive(BorshSerialize, BorshDeserialize, Debug)] + pub(super) struct CurrencyPairId(pub(super) u64); + + impl From for super::CurrencyPairId { + fn from(value: CurrencyPairId) -> Self { + Self::new(value.0) + } + } + + impl From for CurrencyPairId { + fn from(value: super::CurrencyPairId) -> Self { + Self(value.get()) + } + } + + #[derive(BorshSerialize, BorshDeserialize, Debug)] + pub(super) struct CurrencyPairNonce(pub(super) u64); + + impl From for super::CurrencyPairNonce { + fn from(value: CurrencyPairNonce) -> Self { + Self::new(value.0) + } + } + + impl From for CurrencyPairNonce { + fn from(value: super::CurrencyPairNonce) -> Self { + Self(value.get()) + } + } + + #[derive(BorshSerialize, BorshDeserialize, Debug)] + pub(super) struct CurrencyPair { + base: String, + quote: String, + } + + impl TryFrom for super::CurrencyPair { + type Error = astria_eyre::eyre::Error; + + fn try_from(value: CurrencyPair) -> Result { + Ok(Self::from_parts( + value.base.parse().with_context(|| { + format!( + "failed to parse state-fetched `{}` as currency pair base", + value.base + ) + })?, + value.quote.parse().with_context(|| { + format!( + "failed to parse state-fetched `{}` as currency pair quote", + value.quote + ) + })?, + )) + } + } + + impl From for CurrencyPair { + fn from(value: super::CurrencyPair) -> Self { + let (base, quote) = value.into_parts(); + Self { + base, + quote, + } + } + } + + #[derive(Debug, BorshSerialize, BorshDeserialize)] + struct Timestamp { + seconds: i64, + nanos: i32, + } + + impl From for Timestamp { + fn from(value: astria_core::primitive::Timestamp) -> Self { + Self { + seconds: value.seconds, + nanos: value.nanos, + } + } + } + + impl From for astria_core::primitive::Timestamp { + fn from(value: Timestamp) -> Self { + Self { + seconds: value.seconds, + nanos: value.nanos, + } + } + } + + #[derive(Debug, BorshSerialize, BorshDeserialize)] + struct Price(u128); + + impl From for Price { + fn from(value: astria_core::connect::types::v2::Price) -> Self { + Self(value.get()) + } + } + + impl From for astria_core::connect::types::v2::Price { + fn from(value: Price) -> Self { + Self::new(value.0) + } + } + + #[derive(Debug, BorshSerialize, BorshDeserialize)] + pub(super) struct QuotePrice { + price: Price, + block_timestamp: Timestamp, + block_height: u64, + } + + impl From for QuotePrice { + fn from(value: super::QuotePrice) -> Self { + Self { + price: value.price.into(), + block_timestamp: value.block_timestamp.into(), + block_height: value.block_height, + } + } + } + + impl From for super::QuotePrice { + fn from(value: QuotePrice) -> Self { + Self { + price: value.price.into(), + block_timestamp: value.block_timestamp.into(), + block_height: value.block_height, + } + } + } + + #[derive(Debug, BorshSerialize, BorshDeserialize)] + pub(super) struct CurrencyPairState { + pub(super) price: QuotePrice, + pub(super) nonce: CurrencyPairNonce, + pub(super) id: CurrencyPairId, + } + + impl From for CurrencyPairState { + fn from(value: super::CurrencyPairState) -> Self { + Self { + price: value.price.into(), + nonce: value.nonce.into(), + id: value.id.into(), + } + } + } + + impl From for super::CurrencyPairState { + fn from(value: CurrencyPairState) -> Self { + Self { + price: value.price.into(), + nonce: value.nonce.into(), + id: value.id.into(), + } + } + } +} + +const CURRENCY_PAIR_TO_ID_PREFIX: &str = "oraclecpid"; +const ID_TO_CURRENCY_PAIR_PREFIX: &str = "oracleidcp"; +const CURRENCY_PAIR_STATE_PREFIX: &str = "oraclecpstate"; + +const NUM_CURRENCY_PAIRS_KEY: &str = "oraclenumcps"; +const NUM_REMOVED_CURRENCY_PAIRS_KEY: &str = "oraclenumremovedcps"; +const NEXT_CURRENCY_PAIR_ID_KEY: &str = "oraclenextcpid"; + +fn currency_pair_to_id_storage_key(currency_pair: &CurrencyPair) -> String { + format!("{CURRENCY_PAIR_TO_ID_PREFIX}/{currency_pair}",) +} + +fn id_to_currency_pair_storage_key(id: CurrencyPairId) -> String { + format!("{ID_TO_CURRENCY_PAIR_PREFIX}/{id}") +} + +fn currency_pair_state_storage_key(currency_pair: &CurrencyPair) -> String { + format!("{CURRENCY_PAIR_STATE_PREFIX}/{currency_pair}",) +} + +/// Newtype wrapper to read and write a u64 from rocksdb. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +struct Count(u64); + +pin_project! { + pub(crate) struct CurrencyPairsWithIdsStream { + #[pin] + underlying: St, + } +} + +pub(crate) struct CurrencyPairWithId { + pub(crate) id: u64, + pub(crate) currency_pair: CurrencyPair, +} + +impl Stream for CurrencyPairsWithIdsStream +where + St: Stream)>>, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + let (key, bytes) = match ready!(this.underlying.as_mut().poll_next(cx)) { + Some(Ok(item)) => item, + Some(Err(err)) => { + return Poll::Ready(Some( + Err(anyhow_to_eyre(err)).wrap_err("failed reading from state"), + )); + } + None => return Poll::Ready(None), + }; + let in_state::CurrencyPairId(id) = in_state::CurrencyPairId::try_from_slice(&bytes) + .with_context(|| { + "failed decoding bytes read from state as currency pair ID for key `{key}`" + })?; + let currency_pair = match extract_currency_pair_from_key(&key) { + Err(err) => { + return Poll::Ready(Some(Err(err).with_context(|| { + format!("failed to extract currency pair from key `{key}`") + }))); + } + Ok(parsed) => parsed, + }; + Poll::Ready(Some(Ok(CurrencyPairWithId { + id, + currency_pair, + }))) + } +} + +pin_project! { + pub(crate) struct CurrencyPairsStream { + #[pin] + underlying: St, + } +} + +impl Stream for CurrencyPairsStream +where + St: Stream>, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + let key = match ready!(this.underlying.as_mut().poll_next(cx)) { + Some(Ok(item)) => item, + Some(Err(err)) => { + return Poll::Ready(Some( + Err(anyhow_to_eyre(err)).wrap_err("failed reading from state"), + )); + } + None => return Poll::Ready(None), + }; + let currency_pair = match extract_currency_pair_from_key(&key) { + Err(err) => { + return Poll::Ready(Some(Err(err).with_context(|| { + format!("failed to extract currency pair from key `{key}`") + }))); + } + Ok(parsed) => parsed, + }; + Poll::Ready(Some(Ok(currency_pair))) + } +} + +fn extract_currency_pair_from_key(key: &str) -> Result { + key.strip_prefix(CURRENCY_PAIR_TO_ID_PREFIX) + .wrap_err("failed to strip prefix from currency pair state key")? + .parse::() + .wrap_err("failed to parse storage key suffix as currency pair") +} + +#[async_trait] +pub(crate) trait StateReadExt: StateRead { + #[instrument(skip_all)] + async fn get_currency_pair_id( + &self, + currency_pair: &CurrencyPair, + ) -> Result> { + let Some(bytes) = self + .get_raw(¤cy_pair_to_id_storage_key(currency_pair)) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading currency pair id from state")? + else { + return Ok(None); + }; + in_state::CurrencyPairId::try_from_slice(&bytes) + .wrap_err("invalid currency pair id bytes") + .map(|id| Some(id.into())) + } + + #[instrument(skip_all)] + async fn get_currency_pair(&self, id: CurrencyPairId) -> Result> { + let Some(bytes) = self + .get_raw(&id_to_currency_pair_storage_key(id)) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading currency pair from state")? + else { + return Ok(None); + }; + let currency_pair = borsh::from_slice::(&bytes) + .wrap_err("failed to deserialize bytes read from state as currency pair")? + .try_into() + .wrap_err("failed converting in-state currency pair into domain type currency pair")?; + Ok(Some(currency_pair)) + } + + #[instrument(skip_all)] + fn currency_pairs_with_ids(&self) -> CurrencyPairsWithIdsStream { + CurrencyPairsWithIdsStream { + underlying: self.prefix_raw(CURRENCY_PAIR_TO_ID_PREFIX), + } + } + + #[instrument(skip_all)] + fn currency_pairs(&self) -> CurrencyPairsStream { + CurrencyPairsStream { + underlying: self.prefix_keys(CURRENCY_PAIR_STATE_PREFIX), + } + } + + #[instrument(skip_all)] + async fn get_num_currency_pairs(&self) -> Result { + let Some(bytes) = self + .get_raw(NUM_CURRENCY_PAIRS_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading number of currency pairs from state")? + else { + return Ok(0); + }; + let Count(num_currency_pairs) = + Count::try_from_slice(&bytes).wrap_err("invalid number of currency pairs bytes")?; + Ok(num_currency_pairs) + } + + #[instrument(skip_all)] + async fn get_num_removed_currency_pairs(&self) -> Result { + let Some(bytes) = self + .get_raw(NUM_REMOVED_CURRENCY_PAIRS_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading number of removed currency pairs from state")? + else { + return Ok(0); + }; + let Count(num_removed_currency_pairs) = Count::try_from_slice(&bytes) + .wrap_err("invalid number of removed currency pairs bytes")?; + Ok(num_removed_currency_pairs) + } + + #[instrument(skip_all)] + async fn get_currency_pair_state( + &self, + currency_pair: &CurrencyPair, + ) -> Result> { + let bytes = self + .get_raw(¤cy_pair_state_storage_key(currency_pair)) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed to get currency pair state from state")?; + bytes + .map(|bytes| { + borsh::from_slice::(&bytes) + .wrap_err("failed to deserialize bytes read from state as currency pair state") + .map(Into::into) + }) + .transpose() + } + + #[instrument(skip_all)] + async fn get_next_currency_pair_id(&self) -> Result { + let Some(bytes) = self + .get_raw(NEXT_CURRENCY_PAIR_ID_KEY) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading next currency pair id from state")? + else { + return Ok(CurrencyPairId::new(0)); + }; + let next_currency_pair_id = in_state::CurrencyPairId::try_from_slice(&bytes) + .wrap_err("invalid next currency pair id bytes")? + .into(); + Ok(next_currency_pair_id) + } +} + +impl StateReadExt for T {} + +#[async_trait] +pub(crate) trait StateWriteExt: StateWrite { + #[instrument(skip_all)] + fn put_currency_pair_id( + &mut self, + currency_pair: &CurrencyPair, + id: CurrencyPairId, + ) -> Result<()> { + let bytes = borsh::to_vec(&in_state::CurrencyPairId::from(id)) + .wrap_err("failed to serialize currency pair id")?; + self.put_raw(currency_pair_to_id_storage_key(currency_pair), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_currency_pair(&mut self, id: CurrencyPairId, currency_pair: CurrencyPair) -> Result<()> { + let bytes = borsh::to_vec(&in_state::CurrencyPair::from(currency_pair)) + .wrap_err("failed to serialize currency pair")?; + self.put_raw(id_to_currency_pair_storage_key(id), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_num_currency_pairs(&mut self, num_currency_pairs: u64) -> Result<()> { + let bytes = borsh::to_vec(&Count(num_currency_pairs)) + .wrap_err("failed to serialize number of currency pairs")?; + self.put_raw(NUM_CURRENCY_PAIRS_KEY.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_num_removed_currency_pairs(&mut self, num_removed_currency_pairs: u64) -> Result<()> { + let bytes = borsh::to_vec(&Count(num_removed_currency_pairs)) + .wrap_err("failed to serialize number of removed currency pairs")?; + self.put_raw(NUM_REMOVED_CURRENCY_PAIRS_KEY.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + fn put_currency_pair_state( + &mut self, + currency_pair: CurrencyPair, + currency_pair_state: CurrencyPairState, + ) -> Result<()> { + let currency_pair_id = currency_pair_state.id; + let bytes = borsh::to_vec(&in_state::CurrencyPairState::from(currency_pair_state)) + .wrap_err("failed to serialize currency pair state")?; + self.put_raw(currency_pair_state_storage_key(¤cy_pair), bytes); + + self.put_currency_pair_id(¤cy_pair, currency_pair_id) + .wrap_err("failed to put currency pair id")?; + self.put_currency_pair(currency_pair_id, currency_pair) + .wrap_err("failed to put currency pair")?; + Ok(()) + } + + #[instrument(skip_all)] + fn put_next_currency_pair_id(&mut self, next_currency_pair_id: CurrencyPairId) -> Result<()> { + let bytes = borsh::to_vec(&in_state::CurrencyPairId::from(next_currency_pair_id)) + .wrap_err("failed to serialize next currency pair id")?; + self.put_raw(NEXT_CURRENCY_PAIR_ID_KEY.to_string(), bytes); + Ok(()) + } + + #[instrument(skip_all)] + async fn put_price_for_currency_pair( + &mut self, + currency_pair: CurrencyPair, + price: QuotePrice, + ) -> Result<()> { + let state = if let Some(mut state) = self + .get_currency_pair_state(¤cy_pair) + .await + .wrap_err("failed to get currency pair state")? + { + state.price = price; + state.nonce = state + .nonce + .increment() + .wrap_err("increment nonce overflowed")?; + state + } else { + let id = self + .get_next_currency_pair_id() + .await + .wrap_err("failed to read next currency pair ID")?; + let next_id = id.increment().wrap_err("increment ID overflowed")?; + self.put_next_currency_pair_id(next_id) + .wrap_err("failed to put next currency pair ID")?; + CurrencyPairState { + price, + nonce: CurrencyPairNonce::new(0), + id, + } + }; + self.put_currency_pair_state(currency_pair, state) + .wrap_err("failed to put currency pair state")?; + Ok(()) + } +} + +impl StateWriteExt for T {} diff --git a/crates/astria-sequencer/src/grpc/connect.rs b/crates/astria-sequencer/src/grpc/connect.rs new file mode 100644 index 0000000000..bb1b372f96 --- /dev/null +++ b/crates/astria-sequencer/src/grpc/connect.rs @@ -0,0 +1,301 @@ +use std::{ + str::FromStr, + sync::Arc, +}; + +use astria_core::{ + connect::types::v2::CurrencyPair, + generated::connect::{ + marketmap::v2::{ + query_server::Query as MarketMapQueryService, + LastUpdatedRequest, + LastUpdatedResponse, + MarketMapRequest, + MarketMapResponse, + MarketRequest, + MarketResponse, + ParamsRequest, + ParamsResponse, + }, + oracle::v2::{ + query_server::Query as OracleService, + GetAllCurrencyPairsRequest, + GetAllCurrencyPairsResponse, + GetCurrencyPairMappingRequest, + GetCurrencyPairMappingResponse, + GetPriceRequest, + GetPriceResponse, + GetPricesRequest, + GetPricesResponse, + }, + }, +}; +use cnidarium::Storage; +use futures::{ + TryFutureExt as _, + TryStreamExt as _, +}; +use tonic::{ + Request, + Response, + Status, +}; +use tracing::instrument; + +use crate::{ + app::StateReadExt as _, + connect::{ + marketmap::state_ext::StateReadExt as _, + oracle::state_ext::{ + CurrencyPairWithId, + StateReadExt as _, + }, + }, +}; + +pub(crate) struct SequencerServer { + storage: Storage, +} + +impl SequencerServer { + pub(crate) fn new(storage: Storage) -> Self { + Self { + storage, + } + } +} + +#[async_trait::async_trait] +impl MarketMapQueryService for SequencerServer { + #[instrument(skip_all)] + async fn market_map( + self: Arc, + _request: Request, + ) -> Result, Status> { + let snapshot = self.storage.latest_snapshot(); + let market_map = snapshot.get_market_map().await.map_err(|e| { + Status::internal(format!( + "failed to get block market map from storage: {e:#}" + )) + })?; + let last_updated = snapshot + .get_market_map_last_updated_height() + .await + .map_err(|e| { + Status::internal(format!( + "failed to get block market map last updated height from storage: {e:#}" + )) + })?; + let chain_id = snapshot + .get_chain_id() + .await + .map_err(|e| Status::internal(format!("failed to get chain id from storage: {e:#}")))?; + + Ok(Response::new(MarketMapResponse { + market_map: market_map.map(astria_core::connect::market_map::v2::MarketMap::into_raw), + last_updated, + chain_id: chain_id.to_string(), + })) + } + + #[instrument(skip_all)] + async fn market( + self: Arc, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("market endpoint is not implemented")) + } + + #[instrument(skip_all)] + async fn last_updated( + self: Arc, + _request: Request, + ) -> Result, Status> { + let snapshot = self.storage.latest_snapshot(); + let last_updated = snapshot + .get_market_map_last_updated_height() + .await + .map_err(|e| { + Status::internal(format!( + "failed to get block market map last updated height from storage: {e:#}" + )) + })?; + + Ok(Response::new(LastUpdatedResponse { + last_updated, + })) + } + + #[instrument(skip_all)] + async fn params( + self: Arc, + _request: Request, + ) -> Result, Status> { + let snapshot = self.storage.latest_snapshot(); + let params = snapshot.get_params().await.map_err(|e| { + Status::internal(format!("failed to get block params from storage: {e:#}")) + })?; + + Ok(Response::new(ParamsResponse { + params: params.map(astria_core::connect::market_map::v2::Params::into_raw), + })) + } +} + +#[async_trait::async_trait] +impl OracleService for SequencerServer { + #[instrument(skip_all)] + async fn get_all_currency_pairs( + self: Arc, + _request: Request, + ) -> Result, Status> { + let snapshot = self.storage.latest_snapshot(); + let currency_pairs = snapshot + .currency_pairs() + .map_ok(CurrencyPair::into_raw) + .try_collect() + .map_err(|err| { + Status::internal(format!( + "failed to get all currency pairs from storage: {err:#}" + )) + }) + .await?; + Ok(Response::new(GetAllCurrencyPairsResponse { + currency_pairs, + })) + } + + #[instrument(skip_all)] + async fn get_price( + self: Arc, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let Ok(currency_pair) = request.currency_pair.parse() else { + return Err(Status::invalid_argument("currency pair is invalid")); + }; + + let snapshot = self.storage.latest_snapshot(); + let Some(state) = snapshot + .get_currency_pair_state(¤cy_pair) + .await + .map_err(|e| { + Status::internal(format!( + "failed to get currency pair state from storage: {e:#}" + )) + })? + else { + return Err(Status::not_found("currency pair state not found")); + }; + + let Some(market_map) = snapshot.get_market_map().await.map_err(|e| { + Status::internal(format!( + "failed to get block market map from storage: {e:#}" + )) + })? + else { + return Err(Status::internal("market map not found")); + }; + + let Some(market) = market_map.markets.get(¤cy_pair.to_string()) else { + return Err(Status::not_found(format!( + "market not found for {currency_pair}" + ))); + }; + + Ok(Response::new(GetPriceResponse { + price: Some(state.price.into_raw()), + nonce: state.nonce.get(), + id: state.id.get(), + decimals: market.ticker.decimals, + })) + } + + #[instrument(skip_all)] + async fn get_prices( + self: Arc, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let currency_pairs = match request + .currency_pair_ids + .into_iter() + .map(|s| CurrencyPair::from_str(&s)) + .collect::, _>>() + { + Ok(currency_pairs) => currency_pairs, + Err(e) => { + return Err(Status::invalid_argument(format!( + "invalid currency pair id: {e:#}" + ))); + } + }; + + let snapshot = self.storage.latest_snapshot(); + let Some(market_map) = snapshot.get_market_map().await.map_err(|e| { + Status::internal(format!( + "failed to get block market map from storage: {e:#}" + )) + })? + else { + return Err(Status::internal("market map not found")); + }; + + let mut prices = Vec::new(); + for currency_pair in currency_pairs { + let Some(state) = snapshot + .get_currency_pair_state(¤cy_pair) + .await + .map_err(|e| { + Status::internal(format!("failed to get state from storage: {e:#}")) + })? + else { + return Err(Status::not_found(format!( + "currency pair state for {currency_pair} not found" + ))); + }; + + let Some(market) = market_map.markets.get(¤cy_pair.to_string()) else { + return Err(Status::not_found(format!( + "market not found for {currency_pair}" + ))); + }; + + prices.push(GetPriceResponse { + price: Some(state.price.into_raw()), + nonce: state.nonce.get(), + id: state.id.get(), + decimals: market.ticker.decimals, + }); + } + Ok(Response::new(GetPricesResponse { + prices, + })) + } + + #[instrument(skip_all)] + async fn get_currency_pair_mapping( + self: Arc, + _request: Request, + ) -> Result, Status> { + let snapshot = self.storage.latest_snapshot(); + let stream = snapshot.currency_pairs_with_ids(); + let currency_pair_mapping = stream + .map_ok( + |CurrencyPairWithId { + id, + currency_pair, + }| (id, currency_pair.into_raw()), + ) + .try_collect() + .map_err(|err| { + Status::internal(format!( + "failed to get currency pair mapping from storage: {err:#}" + )) + }) + .await?; + Ok(Response::new(GetCurrencyPairMappingResponse { + currency_pair_mapping, + })) + } +} diff --git a/crates/astria-sequencer/src/grpc/mod.rs b/crates/astria-sequencer/src/grpc/mod.rs index 2de987dd92..023e7386b5 100644 --- a/crates/astria-sequencer/src/grpc/mod.rs +++ b/crates/astria-sequencer/src/grpc/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod connect; pub(crate) mod sequencer; mod state_ext; pub(crate) mod storage; diff --git a/crates/astria-sequencer/src/grpc/sequencer.rs b/crates/astria-sequencer/src/grpc/sequencer.rs index 7d74f077f6..5a3b0f840c 100644 --- a/crates/astria-sequencer/src/grpc/sequencer.rs +++ b/crates/astria-sequencer/src/grpc/sequencer.rs @@ -251,7 +251,7 @@ mod tests { } #[tokio::test] - async fn test_get_sequencer_block() { + async fn get_sequencer_block_ok() { let block = make_test_sequencer_block(1); let storage = cnidarium::TempStorage::new().await.unwrap(); let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); diff --git a/crates/astria-sequencer/src/lib.rs b/crates/astria-sequencer/src/lib.rs index 8701ee6a72..b105316761 100644 --- a/crates/astria-sequencer/src/lib.rs +++ b/crates/astria-sequencer/src/lib.rs @@ -11,6 +11,7 @@ pub(crate) mod bridge; mod build_info; pub(crate) mod component; pub mod config; +pub(crate) mod connect; pub(crate) mod fees; pub(crate) mod grpc; pub(crate) mod ibc; diff --git a/crates/astria-sequencer/src/proposal/commitment.rs b/crates/astria-sequencer/src/proposal/commitment.rs index 52a5039738..83f3be7db1 100644 --- a/crates/astria-sequencer/src/proposal/commitment.rs +++ b/crates/astria-sequencer/src/proposal/commitment.rs @@ -23,15 +23,12 @@ impl GeneratedCommitments { /// The total size of the commitments in bytes. pub(crate) const TOTAL_SIZE: usize = 64; - /// Converts the commitments plus external transaction data into a vector of bytes - /// which can be used as the block's transactions. - #[must_use] - pub(crate) fn into_transactions(self, mut tx_data: Vec) -> Vec { - let mut txs = Vec::with_capacity(tx_data.len().saturating_add(2)); - txs.push(self.rollup_datas_root.to_vec().into()); - txs.push(self.rollup_ids_root.to_vec().into()); - txs.append(&mut tx_data); - txs + pub(crate) fn into_iter(self) -> impl Iterator { + [ + self.rollup_datas_root.to_vec().into(), + self.rollup_ids_root.to_vec().into(), + ] + .into_iter() } } diff --git a/crates/astria-sequencer/src/sequencer.rs b/crates/astria-sequencer/src/sequencer.rs index 8d0dca32c6..a9b9e0f76d 100644 --- a/crates/astria-sequencer/src/sequencer.rs +++ b/crates/astria-sequencer/src/sequencer.rs @@ -1,4 +1,14 @@ -use astria_core::generated::sequencerblock::v1::sequencer_service_server::SequencerServiceServer; +use astria_core::generated::{ + connect::{ + marketmap::v2::query_server::QueryServer as MarketMapQueryServer, + oracle::v2::query_server::QueryServer as OracleQueryServer, + service::v2::{ + oracle_client::OracleClient, + QueryPricesRequest, + }, + }, + sequencerblock::v1::sequencer_service_server::SequencerServiceServer, +}; use astria_eyre::{ anyhow_to_eyre, eyre::{ @@ -26,15 +36,23 @@ use tokio::{ }, task::JoinHandle, }; +use tonic::transport::{ + Endpoint, + Uri, +}; use tower_abci::v038::Server; use tracing::{ + debug, error, info, instrument, + warn, }; use crate::{ + address::StateReadExt as _, app::App, + assets::StateReadExt as _, config::Config, grpc::sequencer::SequencerServer, ibc::host_interface::AstriaHost, @@ -84,10 +102,57 @@ impl Sequencer { .wrap_err("failed to load storage backing chain state")?; let snapshot = storage.latest_snapshot(); + // the native asset should be configurable only at genesis. + // the genesis state must include the native asset's base + // denomination, and it is set in storage during init_chain. + // on subsequent startups, we load the native asset from storage. + if storage.latest_version() != u64::MAX { + let _ = snapshot + .get_native_asset() + .await + .context("failed to query state for native asset")?; + let _ = snapshot + .get_base_prefix() + .await + .context("failed to query state for base prefix")?; + } + + let oracle_client = if config.no_oracle { + None + } else { + let uri: Uri = config + .oracle_grpc_addr + .parse() + .context("failed parsing oracle grpc address as Uri")?; + let endpoint = Endpoint::from(uri.clone()).timeout(std::time::Duration::from_millis( + config.oracle_client_timeout_milliseconds, + )); + let mut oracle_client = OracleClient::new( + endpoint + .connect() + .await + .wrap_err("failed to connect to oracle sidecar")?, + ); + + // ensure the oracle sidecar is reachable + // TODO: allow this to retry in case the oracle sidecar is not ready yet + if let Err(e) = oracle_client.prices(QueryPricesRequest::default()).await { + warn!(uri = %uri, error = %e, "oracle sidecar is unreachable"); + } else { + debug!(uri = %uri, "oracle sidecar is reachable"); + }; + Some(oracle_client) + }; + let mempool = Mempool::new(metrics, config.mempool_parked_max_tx_count); - let app = App::new(snapshot, mempool.clone(), metrics) - .await - .wrap_err("failed to initialize app")?; + let app = App::new( + snapshot, + mempool.clone(), + crate::app::vote_extension::Handler::new(oracle_client), + metrics, + ) + .await + .wrap_err("failed to initialize app")?; let consensus_service = tower::ServiceBuilder::new() .layer(request_span::layer(|req: &ConsensusRequest| { @@ -172,6 +237,8 @@ fn start_grpc_server( let ibc = penumbra_ibc::component::rpc::IbcQuery::::new(storage.clone()); let sequencer_api = SequencerServer::new(storage.clone(), mempool); + let market_map_api = crate::grpc::connect::SequencerServer::new(storage.clone()); + let oracle_api = crate::grpc::connect::SequencerServer::new(storage.clone()); let cors_layer: CorsLayer = CorsLayer::permissive(); // TODO: setup HTTPS? @@ -195,7 +262,9 @@ fn start_grpc_server( .add_service(ClientQueryServer::new(ibc.clone())) .add_service(ChannelQueryServer::new(ibc.clone())) .add_service(ConnectionQueryServer::new(ibc.clone())) - .add_service(SequencerServiceServer::new(sequencer_api)); + .add_service(SequencerServiceServer::new(sequencer_api)) + .add_service(MarketMapQueryServer::new(market_map_api)) + .add_service(OracleQueryServer::new(oracle_api)); info!(grpc_addr = grpc_addr.to_string(), "starting grpc server"); tokio::task::spawn( diff --git a/crates/astria-sequencer/src/service/consensus.rs b/crates/astria-sequencer/src/service/consensus.rs index 057fa091eb..f8329f57b3 100644 --- a/crates/astria-sequencer/src/service/consensus.rs +++ b/crates/astria-sequencer/src/service/consensus.rs @@ -98,13 +98,26 @@ impl Consensus { }, ) } - ConsensusRequest::ExtendVote(_) => { - ConsensusResponse::ExtendVote(response::ExtendVote { - vote_extension: vec![].into(), + ConsensusRequest::ExtendVote(extend_vote) => { + ConsensusResponse::ExtendVote(match self.handle_extend_vote(extend_vote).await { + Ok(response) => response, + Err(e) => { + warn!( + error = AsRef::::as_ref(&e), + "failed to extend vote, returning empty vote extension" + ); + response::ExtendVote { + vote_extension: vec![].into(), + } + } }) } - ConsensusRequest::VerifyVoteExtension(_) => { - ConsensusResponse::VerifyVoteExtension(response::VerifyVoteExtension::Accept) + ConsensusRequest::VerifyVoteExtension(vote_extension) => { + ConsensusResponse::VerifyVoteExtension( + self.handle_verify_vote_extension(vote_extension) + .await + .wrap_err("failed to verify vote extension")?, + ) } ConsensusRequest::FinalizeBlock(finalize_block) => ConsensusResponse::FinalizeBlock( self.finalize_block(finalize_block) @@ -141,6 +154,14 @@ impl Consensus { "failed converting cometbft genesis validators to astria validators", )?, init_chain.chain_id, + // if `vote_extensions_enable_height` is zero, vote extensions are disabled. + // see https://docs.cometbft.com/v1.0/spec/core/data_structures#abciparams + init_chain + .consensus_params + .abci + .vote_extensions_enable_height + .unwrap_or_default() + .value(), ) .await .wrap_err("failed to call init_chain")?; @@ -176,6 +197,28 @@ impl Consensus { } #[instrument(skip_all)] + async fn handle_extend_vote( + &mut self, + extend_vote: request::ExtendVote, + ) -> Result { + let extend_vote = self.app.extend_vote(extend_vote).await?; + Ok(extend_vote) + } + + #[instrument(skip_all)] + async fn handle_verify_vote_extension( + &mut self, + vote_extension: request::VerifyVoteExtension, + ) -> Result { + self.app.verify_vote_extension(vote_extension).await + } + + #[instrument(skip_all, fields( + hash = %finalize_block.hash, + height = %finalize_block.height, + time = %finalize_block.time, + proposer = %finalize_block.proposer_address + ))] async fn finalize_block( &mut self, finalize_block: request::FinalizeBlock, @@ -220,6 +263,10 @@ mod tests { use rand::rngs::OsRng; use telemetry::Metrics as _; use tendermint::{ + abci::types::{ + CommitInfo, + ExtendedCommitInfo, + }, account::Id, Hash, Time, @@ -255,7 +302,10 @@ mod tests { request::PrepareProposal { txs: vec![], max_tx_bytes: 1024, - local_last_commit: None, + local_last_commit: Some(ExtendedCommitInfo { + round: 0u16.into(), + votes: vec![], + }), misbehavior: vec![], height: 1u32.into(), time: Time::now(), @@ -265,9 +315,21 @@ mod tests { } fn new_process_proposal_request(txs: Vec) -> request::ProcessProposal { + let extended_commit_info: tendermint_proto::abci::ExtendedCommitInfo = ExtendedCommitInfo { + round: 0u16.into(), + votes: vec![], + } + .into(); + let bytes = extended_commit_info.encode_to_vec(); + let mut txs_with_commit_info = vec![bytes.into()]; + txs_with_commit_info.extend(txs); + request::ProcessProposal { - txs, - proposed_last_commit: None, + txs: txs_with_commit_info, + proposed_last_commit: Some(CommitInfo { + round: 0u16.into(), + votes: vec![], + }), misbehavior: vec![], hash: Hash::try_from([0u8; 32].to_vec()).unwrap(), height: 1u32.into(), @@ -296,23 +358,32 @@ mod tests { .await .unwrap(); - let res = generate_rollup_datas_commitment(&vec![(*signed_tx).clone()], HashMap::new()); + let commitments = + generate_rollup_datas_commitment(&vec![(*signed_tx).clone()], HashMap::new()); let prepare_proposal = new_prepare_proposal_request(); let prepare_proposal_response = consensus_service .handle_prepare_proposal(prepare_proposal) .await .unwrap(); + // let mut expected_txs = vec![b"".to_vec().into()]; + let commitments_and_txs: Vec = commitments.into_iter().chain(txs).collect(); + // expected_txs.extend(commitments_and_txs.clone()); + + let expected_txs: Vec = std::iter::once(b"".to_vec().into()) + .chain(commitments_and_txs.clone()) + .collect(); + assert_eq!( prepare_proposal_response, response::PrepareProposal { - txs: res.into_transactions(txs) + txs: expected_txs, } ); let (mut consensus_service, _) = new_consensus_service(Some(signing_key.verification_key())).await; - let process_proposal = new_process_proposal_request(prepare_proposal_response.txs); + let process_proposal = new_process_proposal_request(commitments_and_txs); consensus_service .handle_process_proposal(process_proposal) .await @@ -328,8 +399,10 @@ mod tests { let signed_tx = tx.sign(&signing_key); let tx_bytes = signed_tx.clone().into_raw().encode_to_vec(); let txs = vec![tx_bytes.into()]; - let res = generate_rollup_datas_commitment(&vec![signed_tx], HashMap::new()); - let process_proposal = new_process_proposal_request(res.into_transactions(txs)); + let commitments = generate_rollup_datas_commitment(&vec![signed_tx], HashMap::new()); + let tx_data = commitments.into_iter().chain(txs.clone()).collect(); + let process_proposal = new_process_proposal_request(tx_data); + consensus_service .handle_process_proposal(process_proposal) .await @@ -355,15 +428,13 @@ mod tests { async fn process_proposal_fail_wrong_commitment_length() { let (mut consensus_service, _) = new_consensus_service(None).await; let process_proposal = new_process_proposal_request(vec![[0u8; 16].to_vec().into()]); - assert!( - consensus_service - .handle_process_proposal(process_proposal) - .await - .err() - .unwrap() - .to_string() - .contains("transaction commitment must be 32 bytes") - ); + let err = consensus_service + .handle_process_proposal(process_proposal) + .await + .err() + .unwrap() + .to_string(); + assert!(err.contains("transaction commitment must be 32 bytes")); } #[tokio::test] @@ -388,17 +459,20 @@ mod tests { async fn prepare_proposal_empty_block() { let (mut consensus_service, _) = new_consensus_service(None).await; let txs = vec![]; - let res = generate_rollup_datas_commitment(&txs.clone(), HashMap::new()); + let commitments = generate_rollup_datas_commitment(&txs, HashMap::new()); let prepare_proposal = new_prepare_proposal_request(); let prepare_proposal_response = consensus_service .handle_prepare_proposal(prepare_proposal) .await .unwrap(); + let expected_txs = std::iter::once(b"".to_vec().into()) + .chain(commitments.into_iter()) + .collect(); assert_eq!( prepare_proposal_response, response::PrepareProposal { - txs: res.into_transactions(vec![]), + txs: expected_txs, } ); } @@ -407,8 +481,8 @@ mod tests { async fn process_proposal_ok_empty_block() { let (mut consensus_service, _) = new_consensus_service(None).await; let txs = vec![]; - let res = generate_rollup_datas_commitment(&txs, HashMap::new()); - let process_proposal = new_process_proposal_request(res.into_transactions(vec![])); + let commitments = generate_rollup_datas_commitment(&txs, HashMap::new()); + let process_proposal = new_process_proposal_request(commitments.into_iter().collect()); consensus_service .handle_process_proposal(process_proposal) .await @@ -472,10 +546,23 @@ mod tests { let snapshot = storage.latest_snapshot(); let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool.clone(), metrics).await.unwrap(); - app.init_chain(storage.clone(), genesis_state, vec![], "test".to_string()) - .await - .unwrap(); + let mut app = App::new( + snapshot, + mempool.clone(), + crate::app::vote_extension::Handler::new(None), + metrics, + ) + .await + .unwrap(); + app.init_chain( + storage.clone(), + genesis_state, + vec![], + "test".to_string(), + 1, + ) + .await + .unwrap(); app.commit(storage.clone()).await; let (_tx, rx) = mpsc::channel(1); @@ -494,9 +581,10 @@ mod tests { let signed_tx = Arc::new(tx.sign(&signing_key)); let tx_bytes = signed_tx.to_raw().encode_to_vec(); let txs = vec![tx_bytes.clone().into()]; - let res = generate_rollup_datas_commitment(&vec![(*signed_tx).clone()], HashMap::new()); + let commitments = + generate_rollup_datas_commitment(&vec![(*signed_tx).clone()], HashMap::new()); - let block_data = res.into_transactions(txs.clone()); + let block_data: Vec = commitments.into_iter().chain(txs.clone()).collect(); let data_hash = merkle::Tree::from_leaves(block_data.iter().map(sha2::Sha256::digest)).root(); let mut header = default_header(); @@ -508,6 +596,7 @@ mod tests { .unwrap(); let process_proposal = new_process_proposal_request(block_data.clone()); + let txs = process_proposal.txs.clone(); consensus_service .handle_request(ConsensusRequest::ProcessProposal(process_proposal)) .await @@ -524,7 +613,7 @@ mod tests { votes: vec![], }, misbehavior: vec![], - txs: block_data, + txs, }; consensus_service .handle_request(ConsensusRequest::FinalizeBlock(finalize_block)) diff --git a/crates/astria-sequencer/src/service/mempool/tests.rs b/crates/astria-sequencer/src/service/mempool/tests.rs index 2baa7bda6b..b85f41df7c 100644 --- a/crates/astria-sequencer/src/service/mempool/tests.rs +++ b/crates/astria-sequencer/src/service/mempool/tests.rs @@ -31,12 +31,20 @@ async fn future_nonces_are_accepted() { let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mut mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool.clone(), metrics).await.unwrap(); - let genesis_state = genesis_state(); - - app.init_chain(storage.clone(), genesis_state, vec![], "test".to_string()) + let ve_handler = crate::app::vote_extension::Handler::new(None); + let mut app = App::new(snapshot, mempool.clone(), ve_handler, metrics) .await .unwrap(); + + app.init_chain( + storage.clone(), + genesis_state(), + vec![], + "test".to_string(), + 0, + ) + .await + .unwrap(); app.commit(storage.clone()).await; let the_future_nonce = 10; @@ -61,12 +69,20 @@ async fn rechecks_pass() { let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mut mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool.clone(), metrics).await.unwrap(); - let genesis_state = genesis_state(); - - app.init_chain(storage.clone(), genesis_state, vec![], "test".to_string()) + let ve_handler = crate::app::vote_extension::Handler::new(None); + let mut app = App::new(snapshot, mempool.clone(), ve_handler, metrics) .await .unwrap(); + + app.init_chain( + storage.clone(), + genesis_state(), + vec![], + "test".to_string(), + 0, + ) + .await + .unwrap(); app.commit(storage.clone()).await; let tx = MockTxBuilder::new().nonce(0).build(); @@ -99,12 +115,20 @@ async fn can_reinsert_after_recheck_fail() { let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mut mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool.clone(), metrics).await.unwrap(); - let genesis_state = genesis_state(); - - app.init_chain(storage.clone(), genesis_state, vec![], "test".to_string()) + let ve_handler = crate::app::vote_extension::Handler::new(None); + let mut app = App::new(snapshot, mempool.clone(), ve_handler, metrics) .await .unwrap(); + + app.init_chain( + storage.clone(), + genesis_state(), + vec![], + "test".to_string(), + 0, + ) + .await + .unwrap(); app.commit(storage.clone()).await; let tx = MockTxBuilder::new().nonce(0).build(); @@ -147,12 +171,20 @@ async fn recheck_adds_non_tracked_tx() { let metrics = Box::leak(Box::new(Metrics::noop_metrics(&()).unwrap())); let mut mempool = Mempool::new(metrics, 100); - let mut app = App::new(snapshot, mempool.clone(), metrics).await.unwrap(); - let genesis_state = genesis_state(); - - app.init_chain(storage.clone(), genesis_state, vec![], "test".to_string()) + let ve_handler = crate::app::vote_extension::Handler::new(None); + let mut app = App::new(snapshot, mempool.clone(), ve_handler, metrics) .await .unwrap(); + + app.init_chain( + storage.clone(), + genesis_state(), + vec![], + "test".to_string(), + 0, + ) + .await + .unwrap(); app.commit(storage.clone()).await; let tx = MockTxBuilder::new().nonce(0).build(); diff --git a/dev/values/validators/all-without-native.yml b/dev/values/validators/all-without-native.yml index 8452e71d2c..c1e1f7683d 100644 --- a/dev/values/validators/all-without-native.yml +++ b/dev/values/validators/all-without-native.yml @@ -7,6 +7,7 @@ genesis: addressPrefixes: base: "astria" authoritySudoAddress: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm + marketAdminAddress: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm ibc: enabled: true inboundEnabled: true diff --git a/dev/values/validators/all.yml b/dev/values/validators/all.yml index 0b482a4e62..d62c054191 100644 --- a/dev/values/validators/all.yml +++ b/dev/values/validators/all.yml @@ -7,6 +7,7 @@ genesis: addressPrefixes: base: "astria" authoritySudoAddress: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm + marketAdminAddress: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm nativeAssetBaseDenomination: nria allowedFeeAssets: - nria diff --git a/proto/protocolapis/astria/protocol/genesis/v1/types.proto b/proto/protocolapis/astria/protocol/genesis/v1/types.proto index b32adf48a0..2fb05eb0ea 100644 --- a/proto/protocolapis/astria/protocol/genesis/v1/types.proto +++ b/proto/protocolapis/astria/protocol/genesis/v1/types.proto @@ -4,6 +4,8 @@ package astria.protocol.genesis.v1; import "astria/primitive/v1/types.proto"; import "astria/protocol/fees/v1/types.proto"; +import "connect/marketmap/v2/genesis.proto"; +import "connect/oracle/v2/genesis.proto"; message GenesisAppState { string chain_id = 1; @@ -16,6 +18,7 @@ message GenesisAppState { IbcParameters ibc_parameters = 8; repeated string allowed_fee_assets = 9; GenesisFees fees = 10; + ConnectGenesis connect = 11; } message Account { @@ -57,3 +60,8 @@ message GenesisFees { astria.protocol.fees.v1.TransferFeeComponents transfer = 13; astria.protocol.fees.v1.ValidatorUpdateFeeComponents validator_update = 14; } + +message ConnectGenesis { + connect.marketmap.v2.GenesisState market_map = 1; + connect.oracle.v2.GenesisState oracle = 2; +} diff --git a/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto deleted file mode 100644 index 7ba82446cc..0000000000 --- a/proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto +++ /dev/null @@ -1,59 +0,0 @@ -syntax = "proto3"; - -package astria.protocol.genesis.v1alpha1; - -import "astria/primitive/v1/types.proto"; -import "astria/protocol/fees/v1alpha1/types.proto"; - -message GenesisAppState { - string chain_id = 1; - AddressPrefixes address_prefixes = 2; - repeated Account accounts = 3; - astria.primitive.v1.Address authority_sudo_address = 4; - astria.primitive.v1.Address ibc_sudo_address = 5; - repeated astria.primitive.v1.Address ibc_relayer_addresses = 6; - string native_asset_base_denomination = 7; - IbcParameters ibc_parameters = 8; - repeated string allowed_fee_assets = 9; - GenesisFees fees = 10; -} - -message Account { - astria.primitive.v1.Address address = 1; - astria.primitive.v1.Uint128 balance = 2; -} - -message AddressPrefixes { - // The base prefix used for most Astria Sequencer addresses. - string base = 1; - // The prefix used for sending ics20 transfers to IBC chains - // that enforce a bech32 format of the packet sender. - string ibc_compat = 2; -} - -// IBC configuration data. -message IbcParameters { - // Whether IBC (forming connections, processing IBC packets) is enabled. - bool ibc_enabled = 1; - // Whether inbound ICS-20 transfers are enabled - bool inbound_ics20_transfers_enabled = 2; - // Whether outbound ICS-20 transfers are enabled - bool outbound_ics20_transfers_enabled = 3; -} - -message GenesisFees { - astria.protocol.fees.v1alpha1.BridgeLockFeeComponents bridge_lock = 1; - astria.protocol.fees.v1alpha1.BridgeSudoChangeFeeComponents bridge_sudo_change = 2; - astria.protocol.fees.v1alpha1.BridgeUnlockFeeComponents bridge_unlock = 3; - astria.protocol.fees.v1alpha1.FeeAssetChangeFeeComponents fee_asset_change = 4; - astria.protocol.fees.v1alpha1.FeeChangeFeeComponents fee_change = 5; - astria.protocol.fees.v1alpha1.IbcRelayFeeComponents ibc_relay = 7; - astria.protocol.fees.v1alpha1.IbcRelayerChangeFeeComponents ibc_relayer_change = 6; - astria.protocol.fees.v1alpha1.IbcSudoChangeFeeComponents ibc_sudo_change = 8; - astria.protocol.fees.v1alpha1.Ics20WithdrawalFeeComponents ics20_withdrawal = 9; - astria.protocol.fees.v1alpha1.InitBridgeAccountFeeComponents init_bridge_account = 10; - astria.protocol.fees.v1alpha1.RollupDataSubmissionFeeComponents rollup_data_submission = 11; - astria.protocol.fees.v1alpha1.SudoAddressChangeFeeComponents sudo_address_change = 12; - astria.protocol.fees.v1alpha1.TransferFeeComponents transfer = 13; - astria.protocol.fees.v1alpha1.ValidatorUpdateFeeComponents validator_update = 14; -} diff --git a/proto/vendored/connect/abci/v2/vote_extensions.proto b/proto/vendored/connect/abci/v2/vote_extensions.proto new file mode 100644 index 0000000000..5fcdcf7f2b --- /dev/null +++ b/proto/vendored/connect/abci/v2/vote_extensions.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +package connect.abci.v2; + +option go_package = "github.com/skip-mev/connect/v2/abci/ve/types"; + +// OracleVoteExtension defines the vote extension structure for oracle prices. +message OracleVoteExtension { + // Prices defines a map of id(CurrencyPair) -> price.Bytes() . i.e. 1 -> + // 0x123.. (bytes). Notice the `id` function is determined by the + // `CurrencyPairIDStrategy` used in the VoteExtensionHandler. + map prices = 1; +} diff --git a/proto/vendored/connect/marketmap/v2/genesis.proto b/proto/vendored/connect/marketmap/v2/genesis.proto new file mode 100644 index 0000000000..3ce6bff2a0 --- /dev/null +++ b/proto/vendored/connect/marketmap/v2/genesis.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package connect.marketmap.v2; + +import "connect/marketmap/v2/market.proto"; +import "connect/marketmap/v2/params.proto"; + +option go_package = "github.com/skip-mev/connect/v2/x/marketmap/types"; + +// GenesisState defines the x/marketmap module's genesis state. +message GenesisState { + // MarketMap defines the global set of market configurations for all providers + // and markets. + MarketMap market_map = 1; + + // LastUpdated is the last block height that the market map was updated. + // This field can be used as an optimization for clients checking if there + // is a new update to the map. + uint64 last_updated = 2; + + // Params are the parameters for the x/marketmap module. + Params params = 3; +} diff --git a/proto/vendored/connect/marketmap/v2/market.proto b/proto/vendored/connect/marketmap/v2/market.proto new file mode 100644 index 0000000000..0ceda1e520 --- /dev/null +++ b/proto/vendored/connect/marketmap/v2/market.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; +package connect.marketmap.v2; + +import "connect/types/v2/currency_pair.proto"; + +option go_package = "github.com/skip-mev/connect/v2/x/marketmap/types"; + +// Market encapsulates a Ticker and its provider-specific configuration. +message Market { + // Ticker represents a price feed for a given asset pair i.e. BTC/USD. The + // price feed is scaled to a number of decimal places and has a minimum number + // of providers required to consider the ticker valid. + Ticker ticker = 1; + + // ProviderConfigs is the list of provider-specific configs for this Market. + repeated ProviderConfig provider_configs = 2; +} + +// Ticker represents a price feed for a given asset pair i.e. BTC/USD. The price +// feed is scaled to a number of decimal places and has a minimum number of +// providers required to consider the ticker valid. +message Ticker { + // CurrencyPair is the currency pair for this ticker. + connect.types.v2.CurrencyPair currency_pair = 1; + + // Decimals is the number of decimal places for the ticker. The number of + // decimal places is used to convert the price to a human-readable format. + uint64 decimals = 2; + + // MinProviderCount is the minimum number of providers required to consider + // the ticker valid. + uint64 min_provider_count = 3; + + // Enabled is the flag that denotes if the Ticker is enabled for price + // fetching by an oracle. + bool enabled = 14; + + // MetadataJSON is a string of JSON that encodes any extra configuration + // for the given ticker. + string metadata_JSON = 15; +} + +message ProviderConfig { + // Name corresponds to the name of the provider for which the configuration is + // being set. + string name = 1; + + // OffChainTicker is the off-chain representation of the ticker i.e. BTC/USD. + // The off-chain ticker is unique to a given provider and is used to fetch the + // price of the ticker from the provider. + string off_chain_ticker = 2; + + // NormalizeByPair is the currency pair for this ticker to be normalized by. + // For example, if the desired Ticker is BTC/USD, this market could be reached + // using: OffChainTicker = BTC/USDT NormalizeByPair = USDT/USD This field is + // optional and nullable. + connect.types.v2.CurrencyPair normalize_by_pair = 3; + + // Invert is a boolean indicating if the BASE and QUOTE of the market should + // be inverted. i.e. BASE -> QUOTE, QUOTE -> BASE + bool invert = 4; + + // MetadataJSON is a string of JSON that encodes any extra configuration + // for the given provider config. + string metadata_JSON = 15; +} + +// MarketMap maps ticker strings to their Markets. +message MarketMap { + // Markets is the full list of tickers and their associated configurations + // to be stored on-chain. + map markets = 1; +} diff --git a/proto/vendored/connect/marketmap/v2/params.proto b/proto/vendored/connect/marketmap/v2/params.proto new file mode 100644 index 0000000000..b880804f07 --- /dev/null +++ b/proto/vendored/connect/marketmap/v2/params.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package connect.marketmap.v2; + +option go_package = "github.com/skip-mev/connect/v2/x/marketmap/types"; + +// Params defines the parameters for the x/marketmap module. +message Params { + // MarketAuthorities is the list of authority accounts that are able to + // control updating the marketmap. + repeated string market_authorities = 1; + + // Admin is an address that can remove addresses from the MarketAuthorities + // list. Only governance can add to the MarketAuthorities or change the Admin. + string admin = 2; +} diff --git a/proto/vendored/connect/marketmap/v2/query.proto b/proto/vendored/connect/marketmap/v2/query.proto new file mode 100644 index 0000000000..765dbf29d0 --- /dev/null +++ b/proto/vendored/connect/marketmap/v2/query.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; +package connect.marketmap.v2; + +import "connect/marketmap/v2/market.proto"; +import "connect/marketmap/v2/params.proto"; +import "connect/types/v2/currency_pair.proto"; +import "google/api/annotations.proto"; + +option go_package = "github.com/skip-mev/connect/v2/x/marketmap/types"; + +// Query is the query service for the x/marketmap module. +service Query { + // MarketMap returns the full market map stored in the x/marketmap + // module. + rpc MarketMap(MarketMapRequest) returns (MarketMapResponse) { + option (google.api.http) = {get: "/connect/marketmap/v2/marketmap"}; + } + + // Market returns a market stored in the x/marketmap + // module. + rpc Market(MarketRequest) returns (MarketResponse) { + option (google.api.http) = {get: "/connect/marketmap/v2/market"}; + } + + // LastUpdated returns the last height the market map was updated at. + rpc LastUpdated(LastUpdatedRequest) returns (LastUpdatedResponse) { + option (google.api.http) = {get: "/connect/marketmap/v2/last_updated"}; + } + + // Params returns the current x/marketmap module parameters. + rpc Params(ParamsRequest) returns (ParamsResponse) { + option (google.api.http) = {get: "/connect/marketmap/v2/params"}; + } +} + +// MarketMapRequest is the query request for the MarketMap query. +// It takes no arguments. +message MarketMapRequest {} + +// MarketMapResponse is the query response for the MarketMap query. +message MarketMapResponse { + // MarketMap defines the global set of market configurations for all providers + // and markets. + MarketMap market_map = 1; + + // LastUpdated is the last block height that the market map was updated. + // This field can be used as an optimization for clients checking if there + // is a new update to the map. + uint64 last_updated = 2; + + // ChainId is the chain identifier for the market map. + string chain_id = 3; +} + +// MarketRequest is the query request for the Market query. +// It takes the currency pair of the market as an argument. +message MarketRequest { + // CurrencyPair is the currency pair associated with the market being + // requested. + connect.types.v2.CurrencyPair currency_pair = 1; +} + +// MarketResponse is the query response for the Market query. +message MarketResponse { + // Market is the configuration of a single market to be price-fetched for. + Market market = 1; +} + +// ParamsRequest is the request type for the Query/Params RPC method. +message ParamsRequest {} + +// ParamsResponse is the response type for the Query/Params RPC method. +message ParamsResponse { + Params params = 1; +} + +// LastUpdatedRequest is the request type for the Query/LastUpdated RPC +// method. +message LastUpdatedRequest {} + +// LastUpdatedResponse is the response type for the Query/LastUpdated RPC +// method. +message LastUpdatedResponse { + uint64 last_updated = 1; +} diff --git a/proto/vendored/connect/oracle/v2/genesis.proto b/proto/vendored/connect/oracle/v2/genesis.proto new file mode 100644 index 0000000000..fa371fa6e1 --- /dev/null +++ b/proto/vendored/connect/oracle/v2/genesis.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; +package connect.oracle.v2; + +import "connect/types/v2/currency_pair.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/skip-mev/connect/v2/x/oracle/types"; + +// QuotePrice is the representation of the aggregated prices for a CurrencyPair, +// where price represents the price of Base in terms of Quote +message QuotePrice { + string price = 1; + + // BlockTimestamp tracks the block height associated with this price update. + // We include block timestamp alongside the price to ensure that smart + // contracts and applications are not utilizing stale oracle prices + google.protobuf.Timestamp block_timestamp = 2; + + // BlockHeight is height of block mentioned above + uint64 block_height = 3; +} + +// CurrencyPairState represents the stateful information tracked by the x/oracle +// module per-currency-pair. +message CurrencyPairState { + // QuotePrice is the latest price for a currency-pair, notice this value can + // be null in the case that no price exists for the currency-pair + QuotePrice price = 1; + + // Nonce is the number of updates this currency-pair has received + uint64 nonce = 2; + + // ID is the ID of the CurrencyPair + uint64 id = 3; +} + +// CurrencyPairGenesis is the information necessary for initialization of a +// CurrencyPair. +message CurrencyPairGenesis { + // The CurrencyPair to be added to module state + connect.types.v2.CurrencyPair currency_pair = 1; + // A genesis price if one exists (note this will be empty, unless it results + // from forking the state of this module) + QuotePrice currency_pair_price = 2; + // nonce is the nonce (number of updates) for the CP (same case as above, + // likely 0 unless it results from fork of module) + uint64 nonce = 3; + // id is the ID of the CurrencyPair + uint64 id = 4; +} + +// GenesisState is the genesis-state for the x/oracle module, it takes a set of +// predefined CurrencyPairGeneses +message GenesisState { + // CurrencyPairGenesis is the set of CurrencyPairGeneses for the module. I.e + // the starting set of CurrencyPairs for the module + information regarding + // their latest update. + repeated CurrencyPairGenesis currency_pair_genesis = 1; + + // NextID is the next ID to be used for a CurrencyPair + uint64 next_id = 2; +} diff --git a/proto/vendored/connect/oracle/v2/query.proto b/proto/vendored/connect/oracle/v2/query.proto new file mode 100644 index 0000000000..fbdd3e0e55 --- /dev/null +++ b/proto/vendored/connect/oracle/v2/query.proto @@ -0,0 +1,88 @@ +syntax = "proto3"; +package connect.oracle.v2; + +import "connect/oracle/v2/genesis.proto"; +import "connect/types/v2/currency_pair.proto"; +import "google/api/annotations.proto"; + +option go_package = "github.com/skip-mev/connect/v2/x/oracle/types"; + +// Query is the query service for the x/oracle module. +service Query { + // Get all the currency pairs the x/oracle module is tracking price-data for. + rpc GetAllCurrencyPairs(GetAllCurrencyPairsRequest) returns (GetAllCurrencyPairsResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/get_all_tickers"}; + } + + // Given a CurrencyPair (or its identifier) return the latest QuotePrice for + // that CurrencyPair. + rpc GetPrice(GetPriceRequest) returns (GetPriceResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/get_price"}; + } + + rpc GetPrices(GetPricesRequest) returns (GetPricesResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/get_prices"}; + } + + // Get the mapping of currency pair ID -> currency pair. This is useful for + // indexers that have access to the ID of a currency pair, but no way to get + // the underlying currency pair from it. + rpc GetCurrencyPairMapping(GetCurrencyPairMappingRequest) returns (GetCurrencyPairMappingResponse) { + option (google.api.http) = { + get: "/connect/oracle/v2/get_currency_pair_mapping" + additional_bindings: [] + }; + } +} + +message GetAllCurrencyPairsRequest {} + +// GetAllCurrencyPairsResponse returns all CurrencyPairs that the module is +// currently tracking. +message GetAllCurrencyPairsResponse { + repeated connect.types.v2.CurrencyPair currency_pairs = 1; +} + +// GetPriceRequest takes an identifier for the +// CurrencyPair in the format base/quote. +message GetPriceRequest { + // CurrencyPair represents the pair that the user wishes to query. + string currency_pair = 1; +} + +// GetPriceResponse is the response from the GetPrice grpc method exposed from +// the x/oracle query service. +message GetPriceResponse { + // QuotePrice represents the quote-price for the CurrencyPair given in + // GetPriceRequest (possibly nil if no update has been made) + QuotePrice price = 1; + // nonce represents the nonce for the CurrencyPair if it exists in state + uint64 nonce = 2; + // decimals represents the number of decimals that the quote-price is + // represented in. It is used to scale the QuotePrice to its proper value. + uint64 decimals = 3; + // ID represents the identifier for the CurrencyPair. + uint64 id = 4; +} + +// GetPricesRequest takes an identifier for the CurrencyPair +// in the format base/quote. +message GetPricesRequest { + repeated string currency_pair_ids = 1; +} + +// GetPricesResponse is the response from the GetPrices grpc method exposed from +// the x/oracle query service. +message GetPricesResponse { + repeated GetPriceResponse prices = 1; +} + +// GetCurrencyPairMappingRequest is the GetCurrencyPairMapping request type. +message GetCurrencyPairMappingRequest {} + +// GetCurrencyPairMappingResponse is the GetCurrencyPairMapping response type. +message GetCurrencyPairMappingResponse { + // currency_pair_mapping is a mapping of the id representing the currency pair + // to the currency pair itself. + map currency_pair_mapping = 1; +} diff --git a/proto/vendored/connect/service/v2/oracle.proto b/proto/vendored/connect/service/v2/oracle.proto new file mode 100644 index 0000000000..1beb98f429 --- /dev/null +++ b/proto/vendored/connect/service/v2/oracle.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; +package connect.service.v2; + +import "connect/marketmap/v2/market.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/skip-mev/connect/v2/service/servers/oracle/types"; + +// Oracle defines the gRPC oracle service. +service Oracle { + // Prices defines a method for fetching the latest prices. + rpc Prices(QueryPricesRequest) returns (QueryPricesResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/prices"}; + } + + // MarketMap defines a method for fetching the latest market map + // configuration. + rpc MarketMap(QueryMarketMapRequest) returns (QueryMarketMapResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/marketmap"}; + } + + // Version defines a method for fetching the current version of the oracle + // service. + rpc Version(QueryVersionRequest) returns (QueryVersionResponse) { + option (google.api.http) = {get: "/connect/oracle/v2/version"}; + } +} + +// QueryPricesRequest defines the request type for the the Prices method. +message QueryPricesRequest {} + +// QueryPricesResponse defines the response type for the Prices method. +message QueryPricesResponse { + // Prices defines the list of prices. + map prices = 1; + + // Timestamp defines the timestamp of the prices. + google.protobuf.Timestamp timestamp = 2; + + // Version defines the version of the oracle service that provided the prices. + string version = 3; +} + +// QueryMarketMapRequest defines the request type for the MarketMap method. +message QueryMarketMapRequest {} + +// QueryMarketMapResponse defines the response type for the MarketMap method. +message QueryMarketMapResponse { + // MarketMap defines the current market map configuration. + connect.marketmap.v2.MarketMap market_map = 1; +} + +// QueryVersionRequest defines the request type for the Version method. +message QueryVersionRequest {} + +// QueryVersionResponse defines the response type for the Version method. +message QueryVersionResponse { + // Version defines the current version of the oracle service. + string version = 1; +} diff --git a/proto/vendored/connect/types/v2/currency_pair.proto b/proto/vendored/connect/types/v2/currency_pair.proto new file mode 100644 index 0000000000..67c2e86d12 --- /dev/null +++ b/proto/vendored/connect/types/v2/currency_pair.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package connect.types.v2; + +option go_package = "github.com/skip-mev/connect/v2/pkg/types"; + +// CurrencyPair is the standard representation of a pair of assets, where one +// (Base) is priced in terms of the other (Quote) +message CurrencyPair { + string Base = 1; + string Quote = 2; +} diff --git a/tools/protobuf-compiler/src/main.rs b/tools/protobuf-compiler/src/main.rs index 1034672e90..8d4716189a 100644 --- a/tools/protobuf-compiler/src/main.rs +++ b/tools/protobuf-compiler/src/main.rs @@ -63,19 +63,18 @@ fn main() { .build_client(true) .build_server(true) .emit_rerun_if_changed(false) + .btree_map([".connect"]) .bytes([ ".astria", + ".connect", ".celestia", + ".connect", ".cosmos", ".tendermint", ]) .client_mod_attribute(".", "#[cfg(feature=\"client\")]") .server_mod_attribute(".", "#[cfg(feature=\"server\")]") .extern_path(".astria_vendored.penumbra", "::penumbra-proto") - .extern_path( - ".astria_vendored.tendermint.abci.ValidatorUpdate", - "crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate", - ) .type_attribute(".astria.primitive.v1.Uint128", "#[derive(Copy)]") .type_attribute( ".astria.protocol.genesis.v1.IbcParameters", @@ -97,11 +96,13 @@ fn main() { pbjson_build::Builder::new() .register_descriptors(&descriptor_set) .unwrap() + .btree_map([".connect"]) .out_dir(&out_dir) .build(&[ ".astria", ".astria_vendored", ".celestia", + ".connect", ".cosmos", ".tendermint", ]) @@ -141,6 +142,7 @@ fn clean_non_astria_code(generated: &mut ContentMap) { !name.starts_with("astria.") && !name.starts_with("astria_vendored.") && !name.starts_with("celestia.") + && !name.starts_with("connect.") && !name.starts_with("cosmos.") && !name.starts_with("tendermint.") })