From 1450ca591d689419a382b8b06241af32e4d7bbf0 Mon Sep 17 00:00:00 2001 From: noot <36753753+noot@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:15:36 -0500 Subject: [PATCH] feat(sequencer): integrate connect oracle and vote extension logic (#1236) ## Summary integrate skip's [connect](https://github.com/skip-mev/connect) (formerly named `slinky`) oracle service into astria. at a high level, connect consists of an oracle sidecar program, which interacts with a validator node to provide price data, and various cosmos-sdk modules. since astria isn't cosmos, the relevant cosmos modules (x/marketmap and x/oracle) were essentially ported into the `connect` module of the sequencer app, which consists of two components, `market_map` and `oracle`. the sequencer app was updated to talk to the sidecar during the `extend_vote` phase of consensus to gather prices to put into a vote extension. the vote extension validation logic, proposal logic, and finalization logic were also ported from connect. ## Background we want oracle data to be available to rollups (and maybe on the sequencer itself too?) ## Changes * import relevant protos from connect and create native rust types for them * update the sequencer genesis state to contain `market_map` and `oracle` values * implement the `market_map` component for the sequencer * update the sequencer grpc service to support the market_map grpc service, which is required by the oracle sidecar to retrieve the market map from the chain * implement the `oracle` component for the sequencer and the query service for this component * implement `extend_vote` logic which gets the prices from the sidecar and turns them into a vote extension * implement `verify_vote_extension` logic which performs basic validation on a vote extension during the consensus phase * implement `prepare_proposal` logic which gathers the vote extensions from the previous block, prunes any invalid votes, and performs additional validation to create a valid set of VEs * implement `process_proposal` logic which validates the set of VEs proposed, checking signatures and that the voting power is >2/3 amongst other things * implement `finalize_block` logic which writes the updated prices to state based on the committed vote extensions. skip uses stake-weighted median to calculate the final price, but we don't have stake-weighting yet, so i just took the median. * TODO: implement the connect cosmos `Msg` types as sequencer actions (follow-up) * https://github.com/skip-mev/slinky/blob/158cde8a4b774ac4eec5c6d1a2c16de6a8c6abb5/proto/slinky/oracle/v1/tx.proto * https://github.com/skip-mev/slinky/blob/158cde8a4b774ac4eec5c6d1a2c16de6a8c6abb5/proto/slinky/marketmap/v1/tx.proto * TODO: update `SequencerBlockHeader` to contain the extended commit info + a proof for it (also follow-up) * TODO: implement the `DeltaCurrencyPairStrategy` - right now only the `DefaultCurrencyPairStrategy` is implemented. can also do in follow-up ## Testing TODO: run this on a multi-validator network also clone connect: https://github.com/skip-mev/connect/tree/main install go 1.22 build and run connect: ```sh make build go run scripts/genesis.go --use-coingecko=true --temp-file=markets.json ./build/connect --market-config-path markets.json --port 8081 ``` checkout `noot/slinky` branch of astria run sequencer app and `ASTRIA_SEQUENCER_NO_ORACLE=false` in `.env`: ```sh rm -rf /tmp/astria_db rm -rf ~/.cometbft just run just run-cometbft ``` should see a sequencer log like: ```sh astria_sequencer::sequencer: oracle sidecar is reachable ``` should see a connect log like: ```sh {"level":"info","ts":"2024-07-02T14:33:46.318-0400","caller":"marketmap/fetcher.go:147","msg":"successfully fetched market map data from module; checking if market map has changed","pid":727051,"process":"oracle","fetcher":"marketmap_api"} ``` then, when blocks are made, should see logs like the following for each block: ``` 2024-07-05T02:49:21.254163Z DEBUG handle_request:handle_process_proposal: astria_sequencer::service::consensus: proposal processed height=28 time=2024-07-05T02:49:19.143352683Z tx_count=3 proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD hash=7rmdhOsaW2a0NCZUwSE5yqt2AVR3cOPgGGb4Bb0kpRM= next_validators_hash=F6N7YDQZKfXQld95iV0AmQKNa8DiAxrDnTAcn323QSU= 2024-07-05T02:49:21.310218Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: got prices from oracle sidecar; transforming prices prices_count=118 2024-07-05T02:49:21.323262Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: transformed price for inclusion in vote extension currency_pair="BTC/USD" id=0 price=5683583007 2024-07-05T02:49:21.326070Z DEBUG handle_request:handle_extend_vote:App::extend_vote: astria_sequencer::app::vote_extension: transformed price for inclusion in vote extension currency_pair="ETH/USD" id=1 price=3055069469 2024-07-05T02:49:21.384266Z DEBUG handle_request:finalize_block:App::finalize_block: astria_sequencer::app::vote_extension: applied price from vote extension currency_pair="BTC/USD" price=5683583007 hash=EEB99D84EB1A5B66B4342654C12139CAAB7601547770E3E01866F805BD24A513 height=28 time=2024-07-05T02:49:19.143352683Z proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD 2024-07-05T02:49:21.384553Z DEBUG handle_request:finalize_block:App::finalize_block: astria_sequencer::app::vote_extension: applied price from vote extension currency_pair="ETH/USD" price=3055069469 hash=EEB99D84EB1A5B66B4342654C12139CAAB7601547770E3E01866F805BD24A513 height=28 time=2024-07-05T02:49:19.143352683Z proposer=7BE21CDEB6FDCC9299A51F44C6B390EA990E88CD ``` ## Breaking Changelist * the PR adds a new proposer-added transaction at the start of the block only if vote extensions are enabled. then, there will be 3 special "txs" expected when before there were only 2. however if vote extensions are disabled, this won't make a difference. * the genesis was updated with a new optional field `connect`, however as this field is optional, it is non-breaking with existing networks. * if the `connect` genesis field is set, the sequencer state will change, as the genesis state changes and new values are stored in state. however this does not affect syncing existing networks, as the genesis of the existing network can be used as-is. * additionally, vote extension participation is optional - if <=2/3 validators participate, blocks are still finalized, just no new oracle data will be published. --------- Co-authored-by: Richard Janis Goldschmidt --- Cargo.lock | 11 + charts/sequencer/Chart.yaml | 2 +- .../files/cometbft/config/genesis.json | 26 +- charts/sequencer/templates/configmaps.yaml | 3 + charts/sequencer/values.yaml | 5 + crates/astria-core/Cargo.toml | 1 + crates/astria-core/src/connect/abci.rs | 74 + crates/astria-core/src/connect/market_map.rs | 580 +++++++ crates/astria-core/src/connect/mod.rs | 5 + crates/astria-core/src/connect/oracle.rs | 422 +++++ crates/astria-core/src/connect/service.rs | 88 + crates/astria-core/src/connect/types.rs | 378 +++++ crates/astria-core/src/display.rs | 57 + .../generated/astria.protocol.genesis.v1.rs | 21 + .../astria.protocol.genesis.v1.serde.rs | 126 ++ .../astria.protocol.transaction.v1.rs | 2 +- .../astria.protocol.transaction.v1alpha1.rs | 2 +- .../src/generated/connect.abci.v2.rs | 17 + .../src/generated/connect.abci.v2.serde.rs | 96 ++ .../src/generated/connect.marketmap.v2.rs | 793 +++++++++ .../generated/connect.marketmap.v2.serde.rs | 1484 +++++++++++++++++ .../src/generated/connect.oracle.v2.rs | 766 +++++++++ .../src/generated/connect.oracle.v2.serde.rs | 1279 ++++++++++++++ .../src/generated/connect.service.v2.rs | 550 ++++++ .../src/generated/connect.service.v2.serde.rs | 523 ++++++ .../src/generated/connect.types.v2.rs | 17 + .../src/generated/connect.types.v2.serde.rs | 108 ++ crates/astria-core/src/generated/mod.rs | 250 +-- crates/astria-core/src/lib.rs | 3 + crates/astria-core/src/primitive/mod.rs | 1 + ...v1__tests__genesis_state_is_unchanged.snap | 52 + crates/astria-core/src/protocol/genesis/v1.rs | 282 +++- crates/astria-core/src/protocol/test_utils.rs | 9 + .../src/sequencerblock/v1/block.rs | 25 +- crates/astria-sequencer-utils/Cargo.toml | 2 + .../src/genesis_example.rs | 134 +- .../src/genesis_parser.rs | 14 - crates/astria-sequencer/Cargo.toml | 4 + crates/astria-sequencer/justfile | 16 +- crates/astria-sequencer/local.env.example | 11 + .../src/app/benchmark_and_test_utils.rs | 30 +- crates/astria-sequencer/src/app/mod.rs | 323 +++- ...ransaction_with_every_action_snapshot.snap | 60 +- ..._changes__app_finalize_block_snapshot.snap | 58 +- ...reaking_changes__app_genesis_snapshot.snap | 58 +- crates/astria-sequencer/src/app/state_ext.rs | 24 + .../astria-sequencer/src/app/storage/keys.rs | 1 + crates/astria-sequencer/src/app/test_utils.rs | 33 +- .../src/app/tests_app/mempool.rs | 75 +- .../astria-sequencer/src/app/tests_app/mod.rs | 78 +- .../src/app/tests_block_ordering.rs | 87 +- .../src/app/tests_breaking_changes.rs | 13 +- .../src/app/vote_extension.rs | 683 ++++++++ crates/astria-sequencer/src/config.rs | 7 + .../src/connect/marketmap/component.rs | 57 + .../src/connect/marketmap/mod.rs | 2 + .../src/connect/marketmap/state_ext.rs | 110 ++ crates/astria-sequencer/src/connect/mod.rs | 2 + .../src/connect/oracle/component.rs | 72 + .../connect/oracle/currency_pair_strategy.rs | 49 + .../src/connect/oracle/mod.rs | 3 + .../src/connect/oracle/state_ext.rs | 550 ++++++ crates/astria-sequencer/src/grpc/connect.rs | 301 ++++ crates/astria-sequencer/src/grpc/mod.rs | 1 + crates/astria-sequencer/src/grpc/sequencer.rs | 2 +- crates/astria-sequencer/src/lib.rs | 1 + .../src/proposal/commitment.rs | 15 +- crates/astria-sequencer/src/sequencer.rs | 79 +- .../astria-sequencer/src/service/consensus.rs | 155 +- .../src/service/mempool/tests.rs | 64 +- dev/values/validators/all-without-native.yml | 1 + dev/values/validators/all.yml | 1 + .../astria/protocol/genesis/v1/types.proto | 8 + .../protocol/genesis/v1alpha1/types.proto | 59 - .../connect/abci/v2/vote_extensions.proto | 12 + .../connect/marketmap/v2/genesis.proto | 22 + .../connect/marketmap/v2/market.proto | 73 + .../connect/marketmap/v2/params.proto | 15 + .../vendored/connect/marketmap/v2/query.proto | 85 + .../vendored/connect/oracle/v2/genesis.proto | 62 + proto/vendored/connect/oracle/v2/query.proto | 88 + .../vendored/connect/service/v2/oracle.proto | 61 + .../connect/types/v2/currency_pair.proto | 11 + tools/protobuf-compiler/src/main.rs | 10 +- 84 files changed, 11269 insertions(+), 471 deletions(-) create mode 100644 crates/astria-core/src/connect/abci.rs create mode 100644 crates/astria-core/src/connect/market_map.rs create mode 100644 crates/astria-core/src/connect/mod.rs create mode 100644 crates/astria-core/src/connect/oracle.rs create mode 100644 crates/astria-core/src/connect/service.rs create mode 100644 crates/astria-core/src/connect/types.rs create mode 100644 crates/astria-core/src/display.rs create mode 100644 crates/astria-core/src/generated/connect.abci.v2.rs create mode 100644 crates/astria-core/src/generated/connect.abci.v2.serde.rs create mode 100644 crates/astria-core/src/generated/connect.marketmap.v2.rs create mode 100644 crates/astria-core/src/generated/connect.marketmap.v2.serde.rs create mode 100644 crates/astria-core/src/generated/connect.oracle.v2.rs create mode 100644 crates/astria-core/src/generated/connect.oracle.v2.serde.rs create mode 100644 crates/astria-core/src/generated/connect.service.v2.rs create mode 100644 crates/astria-core/src/generated/connect.service.v2.serde.rs create mode 100644 crates/astria-core/src/generated/connect.types.v2.rs create mode 100644 crates/astria-core/src/generated/connect.types.v2.serde.rs create mode 100644 crates/astria-sequencer/src/app/vote_extension.rs create mode 100644 crates/astria-sequencer/src/connect/marketmap/component.rs create mode 100644 crates/astria-sequencer/src/connect/marketmap/mod.rs create mode 100644 crates/astria-sequencer/src/connect/marketmap/state_ext.rs create mode 100644 crates/astria-sequencer/src/connect/mod.rs create mode 100644 crates/astria-sequencer/src/connect/oracle/component.rs create mode 100644 crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs create mode 100644 crates/astria-sequencer/src/connect/oracle/mod.rs create mode 100644 crates/astria-sequencer/src/connect/oracle/state_ext.rs create mode 100644 crates/astria-sequencer/src/grpc/connect.rs delete mode 100644 proto/protocolapis/astria/protocol/genesis/v1alpha1/types.proto create mode 100644 proto/vendored/connect/abci/v2/vote_extensions.proto create mode 100644 proto/vendored/connect/marketmap/v2/genesis.proto create mode 100644 proto/vendored/connect/marketmap/v2/market.proto create mode 100644 proto/vendored/connect/marketmap/v2/params.proto create mode 100644 proto/vendored/connect/marketmap/v2/query.proto create mode 100644 proto/vendored/connect/oracle/v2/genesis.proto create mode 100644 proto/vendored/connect/oracle/v2/query.proto create mode 100644 proto/vendored/connect/service/v2/oracle.proto create mode 100644 proto/vendored/connect/types/v2/currency_pair.proto 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.") })