diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1.rs index b0bee8e3bd..d2ae636f78 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1.rs @@ -238,6 +238,51 @@ impl ::prost::Name for PriceFeedFeeComponents { ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ChangeMarketsFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for ChangeMarketsFeeComponents { + const NAME: &'static str = "ChangeMarketsFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateMarketMapParamsFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for UpdateMarketMapParamsFeeComponents { + const NAME: &'static str = "UpdateMarketMapParamsFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveMarketAuthoritiesFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for RemoveMarketAuthoritiesFeeComponents { + const NAME: &'static str = "RemoveMarketAuthoritiesFeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) + } +} /// Response to a transaction fee ABCI query. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs index 92475909cf..1f223bf19b 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs @@ -322,6 +322,114 @@ impl<'de> serde::Deserialize<'de> for BridgeUnlockFeeComponents { deserializer.deserialize_struct("astria.protocol.fees.v1.BridgeUnlockFeeComponents", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ChangeMarketsFeeComponents { + #[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_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1.ChangeMarketsFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ChangeMarketsFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ChangeMarketsFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.ChangeMarketsFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = 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__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(ChangeMarketsFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.ChangeMarketsFeeComponents", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for FeeAssetChangeFeeComponents { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1186,6 +1294,114 @@ impl<'de> serde::Deserialize<'de> for PriceFeedFeeComponents { deserializer.deserialize_struct("astria.protocol.fees.v1.PriceFeedFeeComponents", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for RemoveMarketAuthoritiesFeeComponents { + #[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_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1.RemoveMarketAuthoritiesFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for RemoveMarketAuthoritiesFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = RemoveMarketAuthoritiesFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.RemoveMarketAuthoritiesFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = 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__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(RemoveMarketAuthoritiesFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.RemoveMarketAuthoritiesFeeComponents", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for RollupDataSubmissionFeeComponents { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1729,6 +1945,114 @@ impl<'de> serde::Deserialize<'de> for TransferFeeComponents { deserializer.deserialize_struct("astria.protocol.fees.v1.TransferFeeComponents", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UpdateMarketMapParamsFeeComponents { + #[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_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1.UpdateMarketMapParamsFeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateMarketMapParamsFeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = UpdateMarketMapParamsFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.UpdateMarketMapParamsFeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = 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__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(UpdateMarketMapParamsFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.UpdateMarketMapParamsFeeComponents", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for ValidatorUpdateFeeComponents { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result 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 f972093090..2b2eae1a89 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs @@ -473,7 +473,7 @@ impl ::prost::Name for IbcSudoChange { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PriceFeed { - #[prost(oneof = "price_feed::Value", tags = "1")] + #[prost(oneof = "price_feed::Value", tags = "1, 2")] pub value: ::core::option::Option, } /// Nested message and enum types in `PriceFeed`. @@ -483,6 +483,8 @@ pub mod price_feed { pub enum Value { #[prost(message, tag = "1")] Oracle(super::CurrencyPairsChange), + #[prost(message, tag = "2")] + MarketMap(super::MarketMapChange), } } impl ::prost::Name for PriceFeed { @@ -532,6 +534,90 @@ impl ::prost::Name for CurrencyPairs { ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MarketMapChange { + #[prost(oneof = "market_map_change::Value", tags = "1, 2")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `MarketMapChange`. +pub mod market_map_change { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(message, tag = "1")] + Markets(super::ChangeMarkets), + #[prost(message, tag = "2")] + Params(super::UpdateMarketMapParams), + } +} +impl ::prost::Name for MarketMapChange { + const NAME: &'static str = "MarketMapChange"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} +/// Either creates, updates existing, or removes markets. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ChangeMarkets { + #[prost(oneof = "change_markets::Action", tags = "1, 2, 3")] + pub action: ::core::option::Option, +} +/// Nested message and enum types in `ChangeMarkets`. +pub mod change_markets { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Action { + #[prost(message, tag = "1")] + Create(super::Markets), + #[prost(message, tag = "2")] + Update(super::Markets), + #[prost(message, tag = "3")] + Remove(super::Markets), + } +} +impl ::prost::Name for ChangeMarkets { + const NAME: &'static str = "ChangeMarkets"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} +/// A list of markets for creation, updating, or removal. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Markets { + #[prost(message, repeated, tag = "1")] + pub markets: ::prost::alloc::vec::Vec< + super::super::super::super::connect::marketmap::v2::Market, + >, +} +impl ::prost::Name for Markets { + const NAME: &'static str = "Markets"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} +/// Updates the market map parameters. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpdateMarketMapParams { + /// Params defines the new parameters for the x/marketmap module. + #[prost(message, optional, tag = "1")] + pub params: ::core::option::Option< + super::super::super::super::connect::marketmap::v2::Params, + >, +} +impl ::prost::Name for UpdateMarketMapParams { + const NAME: &'static str = "UpdateMarketMapParams"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} /// `Transaction` is a transaction `TransactionBody` together with a public /// ket and a signature. #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs b/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs index 55f87c3c62..1b215bbc9f 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs @@ -796,6 +796,128 @@ impl<'de> serde::Deserialize<'de> for BridgeUnlock { deserializer.deserialize_struct("astria.protocol.transaction.v1.BridgeUnlock", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ChangeMarkets { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.action.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.transaction.v1.ChangeMarkets", len)?; + if let Some(v) = self.action.as_ref() { + match v { + change_markets::Action::Create(v) => { + struct_ser.serialize_field("create", v)?; + } + change_markets::Action::Update(v) => { + struct_ser.serialize_field("update", v)?; + } + change_markets::Action::Remove(v) => { + struct_ser.serialize_field("remove", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ChangeMarkets { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "create", + "update", + "remove", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Create, + Update, + Remove, + } + 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 { + "create" => Ok(GeneratedField::Create), + "update" => Ok(GeneratedField::Update), + "remove" => Ok(GeneratedField::Remove), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ChangeMarkets; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.ChangeMarkets") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut action__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Create => { + if action__.is_some() { + return Err(serde::de::Error::duplicate_field("create")); + } + action__ = map_.next_value::<::std::option::Option<_>>()?.map(change_markets::Action::Create) +; + } + GeneratedField::Update => { + if action__.is_some() { + return Err(serde::de::Error::duplicate_field("update")); + } + action__ = map_.next_value::<::std::option::Option<_>>()?.map(change_markets::Action::Update) +; + } + GeneratedField::Remove => { + if action__.is_some() { + return Err(serde::de::Error::duplicate_field("remove")); + } + action__ = map_.next_value::<::std::option::Option<_>>()?.map(change_markets::Action::Remove) +; + } + } + } + Ok(ChangeMarkets { + action: action__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.ChangeMarkets", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for CurrencyPairs { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -2147,6 +2269,206 @@ impl<'de> serde::Deserialize<'de> for InitBridgeAccount { deserializer.deserialize_struct("astria.protocol.transaction.v1.InitBridgeAccount", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for MarketMapChange { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.transaction.v1.MarketMapChange", len)?; + if let Some(v) = self.value.as_ref() { + match v { + market_map_change::Value::Markets(v) => { + struct_ser.serialize_field("markets", v)?; + } + market_map_change::Value::Params(v) => { + struct_ser.serialize_field("params", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MarketMapChange { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "markets", + "params", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Markets, + 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 { + "markets" => Ok(GeneratedField::Markets), + "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 = MarketMapChange; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.MarketMapChange") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Markets => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("markets")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(market_map_change::Value::Markets) +; + } + GeneratedField::Params => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("params")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(market_map_change::Value::Params) +; + } + } + } + Ok(MarketMapChange { + value: value__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.MarketMapChange", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Markets { + #[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("astria.protocol.transaction.v1.Markets", len)?; + if !self.markets.is_empty() { + struct_ser.serialize_field("markets", &self.markets)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Markets { + #[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 = Markets; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.Markets") + } + + 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(Markets { + markets: markets__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.Markets", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for PriceFeed { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -2164,6 +2486,9 @@ impl serde::Serialize for PriceFeed { price_feed::Value::Oracle(v) => { struct_ser.serialize_field("oracle", v)?; } + price_feed::Value::MarketMap(v) => { + struct_ser.serialize_field("marketMap", v)?; + } } } struct_ser.end() @@ -2177,11 +2502,14 @@ impl<'de> serde::Deserialize<'de> for PriceFeed { { const FIELDS: &[&str] = &[ "oracle", + "market_map", + "marketMap", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Oracle, + MarketMap, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -2204,6 +2532,7 @@ impl<'de> serde::Deserialize<'de> for PriceFeed { { match value { "oracle" => Ok(GeneratedField::Oracle), + "marketMap" | "market_map" => Ok(GeneratedField::MarketMap), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -2231,6 +2560,13 @@ impl<'de> serde::Deserialize<'de> for PriceFeed { return Err(serde::de::Error::duplicate_field("oracle")); } value__ = map_.next_value::<::std::option::Option<_>>()?.map(price_feed::Value::Oracle) +; + } + GeneratedField::MarketMap => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("marketMap")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(price_feed::Value::MarketMap) ; } } @@ -2959,3 +3295,94 @@ impl<'de> serde::Deserialize<'de> for Transfer { deserializer.deserialize_struct("astria.protocol.transaction.v1.Transfer", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for UpdateMarketMapParams { + #[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("astria.protocol.transaction.v1.UpdateMarketMapParams", len)?; + if let Some(v) = self.params.as_ref() { + struct_ser.serialize_field("params", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for UpdateMarketMapParams { + #[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 = UpdateMarketMapParams; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.UpdateMarketMapParams") + } + + 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(UpdateMarketMapParams { + params: params__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.UpdateMarketMapParams", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index df3ecfe30d..a1fe62fa3b 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -1072,6 +1072,28 @@ mod tests { } } + fn genesis_fees() -> raw::GenesisFees { + raw::GenesisFees { + transfer: Some(FeeComponents::::new(12, 0).to_raw()), + rollup_data_submission: Some( + FeeComponents::::new(32, 1).to_raw(), + ), + init_bridge_account: Some(FeeComponents::::new(48, 0).to_raw()), + bridge_lock: Some(FeeComponents::::new(12, 1).to_raw()), + bridge_unlock: Some(FeeComponents::::new(12, 0).to_raw()), + bridge_sudo_change: Some(FeeComponents::::new(24, 0).to_raw()), + ics20_withdrawal: Some(FeeComponents::::new(24, 0).to_raw()), + ibc_relay: Some(FeeComponents::::new(0, 0).to_raw()), + validator_update: Some(FeeComponents::::new(0, 0).to_raw()), + fee_asset_change: Some(FeeComponents::::new(0, 0).to_raw()), + fee_change: Some(FeeComponents::::new(0, 0).to_raw()), + ibc_relayer_change: Some(FeeComponents::::new(0, 0).to_raw()), + sudo_address_change: Some(FeeComponents::::new(0, 0).to_raw()), + ibc_sudo_change: Some(FeeComponents::::new(0, 0).to_raw()), + price_feed: Some(FeeComponents::::new(0, 0).to_raw()), + } + } + fn proto_genesis_state() -> raw::GenesisAppState { use crate::connect::{ oracle::v2::{ @@ -1115,25 +1137,7 @@ mod tests { outbound_ics20_transfers_enabled: true, }), allowed_fee_assets: vec!["nria".into()], - fees: Some(raw::GenesisFees { - transfer: Some(FeeComponents::::new(12, 0).to_raw()), - rollup_data_submission: Some( - FeeComponents::::new(32, 1).to_raw(), - ), - init_bridge_account: Some(FeeComponents::::new(48, 0).to_raw()), - bridge_lock: Some(FeeComponents::::new(12, 1).to_raw()), - bridge_unlock: Some(FeeComponents::::new(12, 0).to_raw()), - bridge_sudo_change: Some(FeeComponents::::new(24, 0).to_raw()), - ics20_withdrawal: Some(FeeComponents::::new(24, 0).to_raw()), - ibc_relay: Some(FeeComponents::::new(0, 0).to_raw()), - validator_update: Some(FeeComponents::::new(0, 0).to_raw()), - fee_asset_change: Some(FeeComponents::::new(0, 0).to_raw()), - fee_change: Some(FeeComponents::::new(0, 0).to_raw()), - ibc_relayer_change: Some(FeeComponents::::new(0, 0).to_raw()), - sudo_address_change: Some(FeeComponents::::new(0, 0).to_raw()), - ibc_sudo_change: Some(FeeComponents::::new(0, 0).to_raw()), - price_feed: Some(FeeComponents::::new(0, 0).to_raw()), - }), + fees: Some(genesis_fees()), connect: Some( ConnectGenesis { market_map: market_map::v2::GenesisState { diff --git a/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs b/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs index fe382499bf..242fd288c1 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs @@ -9,6 +9,7 @@ use std::fmt::{ use super::{ Action, ActionName, + MarketMapChange, PriceFeed, }; @@ -17,7 +18,10 @@ impl Action { match self { Action::SudoAddressChange(_) | Action::IbcSudoChange(_) => Group::UnbundleableSudo, - Action::IbcRelayerChange(_) | Action::FeeChange(_) | Action::FeeAssetChange(_) => { + Action::IbcRelayerChange(_) + | Action::FeeChange(_) + | Action::FeeAssetChange(_) + | Action::PriceFeed(PriceFeed::MarketMap(MarketMapChange::Params(_))) => { Group::BundleableSudo } @@ -32,7 +36,9 @@ impl Action { | Action::BridgeLock(_) | Action::BridgeUnlock(_) | Action::Ibc(_) - | Action::PriceFeed(PriceFeed::Oracle(_)) => Group::BundleableGeneral, + | Action::PriceFeed( + PriceFeed::Oracle(_) | PriceFeed::MarketMap(MarketMapChange::Markets(_)), + ) => Group::BundleableGeneral, } } } diff --git a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs index bb7fbcc2ce..e710991d05 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs @@ -11,9 +11,17 @@ use prost::Name as _; use super::raw; use crate::{ - connect::types::v2::{ - CurrencyPair, - CurrencyPairError, + connect::{ + market_map::v2::{ + Market, + MarketError, + Params, + ParamsError, + }, + types::v2::{ + CurrencyPair, + CurrencyPairError, + }, }, primitive::v1::{ asset::{ @@ -2102,6 +2110,7 @@ impl Protobuf for FeeChange { #[derive(Debug, Clone)] pub enum PriceFeed { Oracle(CurrencyPairsChange), + MarketMap(MarketMapChange), } impl Protobuf for PriceFeed { @@ -2117,6 +2126,12 @@ impl Protobuf for PriceFeed { value: Some(raw), } } + PriceFeed::MarketMap(market_map_change) => { + let raw = raw::price_feed::Value::MarketMap(market_map_change.into_raw()); + Self::Raw { + value: Some(raw), + } + } } } @@ -2139,6 +2154,11 @@ impl Protobuf for PriceFeed { .map_err(Self::Error::oracle)?; Ok(Self::Oracle(currency_pairs_change)) } + Some(raw::price_feed::Value::MarketMap(market_map_change)) => { + let market_map_change = MarketMapChange::try_from_raw(market_map_change) + .map_err(Self::Error::market_map)?; + Ok(Self::MarketMap(market_map_change)) + } None => Err(Self::Error::unset()), } } @@ -2168,6 +2188,11 @@ impl PriceFeedError { fn oracle(err: CurrencyPairsChangeError) -> Self { Self(PriceFeedErrorKind::Oracle(err)) } + + #[must_use] + fn market_map(err: MarketMapChangeError) -> Self { + Self(PriceFeedErrorKind::MarketMap(err)) + } } #[derive(Debug, thiserror::Error)] @@ -2176,6 +2201,8 @@ enum PriceFeedErrorKind { Unset, #[error(transparent)] Oracle(CurrencyPairsChangeError), + #[error(transparent)] + MarketMap(MarketMapChangeError), } #[derive(Debug, Clone)] @@ -2368,3 +2395,234 @@ impl From> for FeeChange { FeeChange::PriceFeed(fee) } } + +#[derive(Debug, Clone)] +pub enum MarketMapChange { + Markets(ChangeMarkets), + Params(UpdateMarketMapParams), +} + +impl Protobuf for MarketMapChange { + type Error = MarketMapChangeError; + type Raw = raw::MarketMapChange; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + match &raw.value { + Some(raw::market_map_change::Value::Markets(change_markets)) => Ok(Self::Markets( + ChangeMarkets::try_from_raw_ref(change_markets) + .map_err(MarketMapChangeError::invalid_change_markets_action)?, + )), + Some(raw::market_map_change::Value::Params(update_market_map_params)) => { + Ok(Self::Params( + UpdateMarketMapParams::try_from_raw_ref(update_market_map_params) + .map_err(MarketMapChangeError::invalid_update_market_map_params_action)?, + )) + } + None => Err(MarketMapChangeError::missing_value()), + } + } + + fn to_raw(&self) -> Self::Raw { + let value = match self { + Self::Markets(change_markets) => { + raw::market_map_change::Value::Markets(change_markets.to_raw()) + } + Self::Params(update_market_map_params) => { + raw::market_map_change::Value::Params(update_market_map_params.to_raw()) + } + }; + Self::Raw { + value: Some(value), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct MarketMapChangeError(MarketMapChangeErrorKind); + +impl MarketMapChangeError { + #[must_use] + pub fn invalid_change_markets_action(err: ChangeMarketsError) -> Self { + Self(MarketMapChangeErrorKind::InvalidChangeMarketsAction(err)) + } + + #[must_use] + pub fn invalid_update_market_map_params_action(err: UpdateMarketMapParamsError) -> Self { + Self(MarketMapChangeErrorKind::InvalidUpdateMarketMapParamsAction(err)) + } + + #[must_use] + pub fn missing_value() -> Self { + Self(MarketMapChangeErrorKind::MissingValue) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum MarketMapChangeErrorKind { + #[error(transparent)] + InvalidChangeMarketsAction(#[from] ChangeMarketsError), + #[error(transparent)] + InvalidUpdateMarketMapParamsAction(#[from] UpdateMarketMapParamsError), + #[error("missing market map change value")] + MissingValue, +} + +/// Takes a list of markets and either creates, updates, or removes them depending on its variant. +/// Must be signed by an address included in the market map [`Params`]' `market_authorities`. +/// - **Create:** Creates the markets in the market map. If no market map is found, one will be +/// created. If any of the markets to create already exist, this action will err. +/// - **Update:** Updates the markets in the market map, matching based on `Ticker.currency_pair`). +/// If no market map is found, or any market is missing a counterpart in the map, this action will +/// err. +/// - **Remove:** Removes the markets from the market map. If a market is not found in the map, it +/// will be ignored. +#[derive(Debug, Clone)] +pub enum ChangeMarkets { + Create(Vec), + Update(Vec), + Remove(Vec), +} + +impl Protobuf for ChangeMarkets { + type Error = ChangeMarketsError; + type Raw = raw::ChangeMarkets; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + match &raw.action { + Some(raw::change_markets::Action::Create(markets)) => Ok(Self::Create( + markets + .markets + .iter() + .map(|market| Market::try_from_raw(market.clone())) + .collect::>() + .map_err(ChangeMarketsError::invalid_market)?, + )), + Some(raw::change_markets::Action::Update(markets)) => Ok(Self::Update( + markets + .markets + .iter() + .map(|market| Market::try_from_raw(market.clone())) + .collect::>() + .map_err(ChangeMarketsError::invalid_market)?, + )), + Some(raw::change_markets::Action::Remove(markets)) => Ok(Self::Remove( + markets + .markets + .iter() + .map(|market| Market::try_from_raw(market.clone())) + .collect::>() + .map_err(ChangeMarketsError::invalid_market)?, + )), + None => Err(ChangeMarketsError::missing_markets()), + } + } + + fn to_raw(&self) -> Self::Raw { + let action = match self { + Self::Create(markets) => raw::change_markets::Action::Create(raw::Markets { + markets: markets + .iter() + .map(|market| Market::into_raw(market.clone())) + .collect(), + }), + Self::Update(markets) => raw::change_markets::Action::Update(raw::Markets { + markets: markets + .iter() + .map(|market| Market::into_raw(market.clone())) + .collect(), + }), + Self::Remove(markets) => raw::change_markets::Action::Remove(raw::Markets { + markets: markets + .iter() + .map(|market| Market::into_raw(market.clone())) + .collect(), + }), + }; + Self::Raw { + action: Some(action), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct ChangeMarketsError(ChangeMarketsErrorKind); + +impl ChangeMarketsError { + #[must_use] + pub fn invalid_market(err: MarketError) -> Self { + Self(ChangeMarketsErrorKind::InvalidMarket(err)) + } + + #[must_use] + pub fn missing_markets() -> Self { + Self(ChangeMarketsErrorKind::MissingMarkets) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ChangeMarketsErrorKind { + #[error("invalid market in market list")] + InvalidMarket(#[from] MarketError), + #[error("change market action contained no markets to change")] + MissingMarkets, +} + +/// Updates the market map Params, which contains the market authority addresses as well as an admin +/// address. This will execute whether there are params in the state already or not. Must be signed +/// by the sequencer network authority sudo address. +#[derive(Debug, Clone)] +pub struct UpdateMarketMapParams { + /// The new parameters for the `connect/marketmap` module. + pub params: Params, +} + +impl Protobuf for UpdateMarketMapParams { + type Error = UpdateMarketMapParamsError; + type Raw = raw::UpdateMarketMapParams; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let params = Params::try_from_raw( + raw.params + .clone() + .ok_or(UpdateMarketMapParamsError::missing_params())?, + ) + .map_err(UpdateMarketMapParamsError::invalid_params)?; + Ok(Self { + params, + }) + } + + fn to_raw(&self) -> Self::Raw { + Self::Raw { + params: Some(self.params.clone().into_raw()), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct UpdateMarketMapParamsError(UpdateMarketMapParamsErrorKind); + +impl UpdateMarketMapParamsError { + #[must_use] + pub fn missing_params() -> Self { + Self(UpdateMarketMapParamsErrorKind::MissingParams) + } + + #[must_use] + pub fn invalid_params(err: ParamsError) -> Self { + Self(UpdateMarketMapParamsErrorKind::InvalidParams(err)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum UpdateMarketMapParamsErrorKind { + #[error("missing params")] + MissingParams, + #[error("invalid params")] + InvalidParams(#[from] ParamsError), + #[error("authority string could not be parsed to address")] + AuthorityParse(#[from] AddressError), +} diff --git a/crates/astria-sequencer-utils/src/genesis_example.rs b/crates/astria-sequencer-utils/src/genesis_example.rs index 426e5265bd..87d38c1fd4 100644 --- a/crates/astria-sequencer-utils/src/genesis_example.rs +++ b/crates/astria-sequencer-utils/src/genesis_example.rs @@ -12,15 +12,19 @@ use astria_core::{ IbcParameters, }, connect::{ - marketmap, - marketmap::v2::{ - Market, - MarketMap, + marketmap::{ + self, + v2::{ + Market, + MarketMap, + }, }, - oracle, - oracle::v2::{ - CurrencyPairGenesis, - QuotePrice, + oracle::{ + self, + v2::{ + CurrencyPairGenesis, + QuotePrice, + }, }, types::v2::CurrencyPair, }, diff --git a/crates/astria-sequencer/src/action_handler/impls/fee_change.rs b/crates/astria-sequencer/src/action_handler/impls/fee_change.rs index 7551e6d80a..bd71943754 100644 --- a/crates/astria-sequencer/src/action_handler/impls/fee_change.rs +++ b/crates/astria-sequencer/src/action_handler/impls/fee_change.rs @@ -187,6 +187,11 @@ mod tests { test_fee_change_action::().await; } + #[tokio::test] + async fn price_feed_fee_change_action_executes_as_expected() { + test_fee_change_action::().await; + } + async fn test_fee_change_action<'a, F>() where F: FeeHandler, diff --git a/crates/astria-sequencer/src/action_handler/impls/price_feed.rs b/crates/astria-sequencer/src/action_handler/impls/price_feed.rs index 283a5ea46b..2a5aa1dae5 100644 --- a/crates/astria-sequencer/src/action_handler/impls/price_feed.rs +++ b/crates/astria-sequencer/src/action_handler/impls/price_feed.rs @@ -1,5 +1,6 @@ use astria_core::{ connect::{ + market_map::v2::MarketMap, oracle::v2::CurrencyPairState, types::v2::{ CurrencyPair, @@ -7,24 +8,39 @@ use astria_core::{ }, }, protocol::transaction::v1::action::{ + ChangeMarkets, CurrencyPairsChange, + MarketMapChange, PriceFeed, + UpdateMarketMapParams, }, }; use astria_eyre::eyre::{ + bail, ensure, + eyre, OptionExt as _, Result, WrapErr as _, }; use async_trait::async_trait; -use cnidarium::StateWrite; +use cnidarium::{ + StateRead, + StateWrite, +}; +use indexmap::IndexMap; use tracing::debug; use crate::{ action_handler::ActionHandler, + address::StateReadExt as _, + app::StateReadExt as _, + authority::StateReadExt as _, connect::{ - market_map::state_ext::StateReadExt as _, + market_map::state_ext::{ + StateReadExt as _, + StateWriteExt as _, + }, oracle::state_ext::{ StateReadExt as _, StateWriteExt as _, @@ -40,21 +56,6 @@ impl ActionHandler for PriceFeed { } async fn check_and_execute(&self, state: S) -> Result<()> { - // TODO: should we use the market map admin here, or a different admin? - let admin = state - .get_params() - .await? - .ok_or_eyre("market map params not set")? - .admin; - let from = state - .get_transaction_context() - .expect("transaction source must be present in state when executing an action") - .address_bytes(); - ensure!( - from == admin.bytes(), - "only the market map admin can add currency pairs" - ); - match self { PriceFeed::Oracle(CurrencyPairsChange::Addition(currency_pairs)) => { check_and_execute_currency_pairs_addition(state, currency_pairs).await @@ -62,6 +63,12 @@ impl ActionHandler for PriceFeed { PriceFeed::Oracle(CurrencyPairsChange::Removal(currency_pairs)) => { check_and_execute_currency_pairs_removal(state, currency_pairs).await } + PriceFeed::MarketMap(MarketMapChange::Markets(change_markets_action)) => { + check_and_execute_change_markets(state, change_markets_action).await + } + PriceFeed::MarketMap(MarketMapChange::Params(update_market_map_params)) => { + check_and_execute_update_market_map_params(state, update_market_map_params).await + } } } } @@ -70,6 +77,8 @@ async fn check_and_execute_currency_pairs_addition( mut state: S, currency_pairs: &[CurrencyPair], ) -> Result<()> { + validate_signer_is_admin(&state).await?; + let mut next_currency_pair_id = state .get_next_currency_pair_id() .await @@ -118,6 +127,8 @@ async fn check_and_execute_currency_pairs_removal( mut state: S, currency_pairs: &[CurrencyPair], ) -> Result<()> { + validate_signer_is_admin(&state).await?; + let mut num_currency_pairs = state .get_num_currency_pairs() .await @@ -144,11 +155,129 @@ async fn check_and_execute_currency_pairs_removal( .wrap_err("failed to put number of currency pairs") } +async fn check_and_execute_change_markets( + mut state: S, + change_markets_action: &ChangeMarkets, +) -> Result<()> { + // check that the signer of the transaction is a market authority + let from = state + .try_base_prefixed( + &state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action") + .address_bytes(), + ) + .await + .wrap_err("failed to convert signer address to base prefixed address")?; + let market_authorities = state + .get_params() + .await? + .ok_or_eyre("market map params not found in state")? + .market_authorities; + ensure!( + market_authorities.contains(&from), + "address {from} is not a market authority" + ); + + // create a new market map if one does not already exist + let mut market_map = state + .get_market_map() + .await + .wrap_err("failed to get market map")? + .unwrap_or(MarketMap { + markets: IndexMap::new(), + }); + match change_markets_action { + ChangeMarkets::Create(create_markets) => { + for market in create_markets { + let ticker_key = market.ticker.currency_pair.to_string(); + if market_map.markets.contains_key(&ticker_key) { + bail!("market for ticker {ticker_key} already exists"); + } + market_map.markets.insert(ticker_key, market.clone()); + } + } + ChangeMarkets::Update(update_markets) => { + if market_map.markets.is_empty() { + bail!("market map is empty"); + } + for market in update_markets { + let ticker_key = market.ticker.currency_pair.to_string(); + *market_map.markets.get_mut(&ticker_key).ok_or_else(|| { + eyre!("market for ticker {ticker_key} not found in market map") + })? = market.clone(); + } + } + ChangeMarkets::Remove(remove_markets) => { + for key in remove_markets { + market_map + .markets + .shift_remove(&key.ticker.currency_pair.to_string()); + } + } + }; + + state + .put_market_map(market_map) + .wrap_err("failed to put market map into state")?; + + // update the last updated height for the market map + state + .put_market_map_last_updated_height( + state + .get_block_height() + .await + .wrap_err("failed to get block height")?, + ) + .wrap_err("failed to update latest market map height")?; + Ok(()) +} + +async fn check_and_execute_update_market_map_params( + mut state: S, + update_market_map_params: &UpdateMarketMapParams, +) -> Result<()> { + let from = &state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action") + .address_bytes(); + let sudo_address = state + .get_sudo_address() + .await + .wrap_err("failed to get sudo address from state")?; + ensure!(sudo_address == *from, "signer is not the sudo key"); + state + .put_params(update_market_map_params.params.clone()) + .wrap_err("failed to put params into state")?; + Ok(()) +} + +async fn validate_signer_is_admin(state: S) -> Result<()> { + // TODO: should we use the market map admin here, or a different admin? + let admin = state + .get_params() + .await? + .ok_or_eyre("market map params not set")? + .admin; + let from = state + .get_transaction_context() + .expect("transaction source must be present in state when executing an action") + .address_bytes(); + ensure!( + from == admin.bytes(), + "only the market map admin can add currency pairs" + ); + Ok(()) +} + #[cfg(test)] mod test { use astria_core::{ connect::{ - market_map::v2::Params, + market_map::v2::{ + Market, + Params, + }, oracle::v2::CurrencyPairState, types::v2::CurrencyPairId, }, @@ -159,9 +288,21 @@ mod test { use super::*; use crate::{ - app::test_utils::get_alice_signing_key, - benchmark_and_test_utils::astria_address, - connect::market_map::state_ext::StateWriteExt as _, + accounts::AddressBytes as _, + address::StateWriteExt as _, + app::{ + test_utils::get_alice_signing_key, + StateWriteExt as _, + }, + authority::StateWriteExt as _, + benchmark_and_test_utils::{ + astria_address, + ASTRIA_PREFIX, + }, + test_utils::{ + example_ticker_from_currency_pair, + example_ticker_with_metadata, + }, transaction::{ StateWriteExt, TransactionContext, @@ -295,4 +436,414 @@ mod test { assert_eq!(state.get_num_currency_pairs().await.unwrap(), 1); } + + #[tokio::test] + async fn create_markets_executes_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + state.put_block_height(1).unwrap(); + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let ticker = example_ticker_with_metadata(String::new()); + let market = Market { + ticker: ticker.clone(), + provider_configs: vec![], + }; + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Create(vec![ + market.clone(), + ]))); + action.check_and_execute(&mut state).await.unwrap(); + let market_map = state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 1); + assert_eq!( + *market_map + .markets + .get(&ticker.currency_pair.to_string()) + .unwrap(), + market, + ); + assert_eq!(state.get_market_map_last_updated_height().await.unwrap(), 1); + } + + #[tokio::test] + async fn change_markets_fails_if_authority_is_invalid() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + let params = Params { + market_authorities: vec![], // should fail even though the authority address is admin + admin: authority_address, + }; + state.put_params(params).unwrap(); + + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Create(vec![]))); + let res = action.check_and_execute(&mut state).await.unwrap_err(); + assert!(res.to_string().contains(&format!( + "address {authority_address} is not a market authority" + ))); + assert!(state.get_market_map().await.unwrap().is_none()); + } + + #[tokio::test] + async fn change_markets_fails_if_params_not_found() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: [0; 20], + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Create(vec![]))); + let res = action.check_and_execute(&mut state).await.unwrap_err(); + assert!( + res.to_string() + .contains("market map params not found in state") + ); + assert!(state.get_market_map().await.unwrap().is_none()); + } + + #[tokio::test] + async fn remove_markets_executes_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_block_height(1).unwrap(); + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + let ticker = example_ticker_with_metadata(String::new()); + + let mut markets = IndexMap::new(); + markets.insert( + ticker.currency_pair.to_string(), + Market { + ticker: ticker.clone(), + provider_configs: vec![], + }, + ); + + state + .put_market_map(MarketMap { + markets, + }) + .unwrap(); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Remove(vec![ + Market { + ticker, + provider_configs: vec![], + }, + ]))); + action.check_and_execute(&mut state).await.unwrap(); + let market_map = state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 0); + assert_eq!(state.get_market_map_last_updated_height().await.unwrap(), 1); + } + + #[tokio::test] + async fn remove_markets_skips_missing_markets() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_block_height(1).unwrap(); + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + let ticker = example_ticker_with_metadata(String::new()); + let market = Market { + ticker: ticker.clone(), + provider_configs: vec![], + }; + + let mut markets = IndexMap::new(); + markets.insert(ticker.currency_pair.to_string(), market.clone()); + + state + .put_market_map(MarketMap { + markets, + }) + .unwrap(); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Remove(vec![ + Market { + ticker: example_ticker_from_currency_pair("DIFBASE", "DIFQUOTE", String::new()), + provider_configs: vec![], + }, + ]))); + action.check_and_execute(&mut state).await.unwrap(); + let market_map = state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 1); + assert_eq!( + market_map.markets.get(&ticker.currency_pair.to_string()), + Some(&market) + ); + assert_eq!(state.get_market_map_last_updated_height().await.unwrap(), 1); + } + + #[tokio::test] + async fn update_markets_executes_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_block_height(1).unwrap(); + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + let ticker_1 = example_ticker_with_metadata("ticker_1".to_string()); + let market_1 = Market { + ticker: ticker_1.clone(), + provider_configs: vec![], + }; + + let mut markets = IndexMap::new(); + markets.insert(ticker_1.currency_pair.to_string(), market_1.clone()); + let initial_market_map = MarketMap { + markets, + }; + state.put_market_map(initial_market_map).unwrap(); + let market_map = state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 1); + assert_eq!( + *market_map + .markets + .get(&ticker_1.currency_pair.to_string()) + .unwrap(), + market_1, + ); + + let ticker = example_ticker_with_metadata("ticker".to_string()); + let market = Market { + ticker: ticker.clone(), + provider_configs: vec![], + }; + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Update(vec![ + market.clone(), + ]))); + + action.check_and_execute(&mut state).await.unwrap(); + let market_map = state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 1); + assert_eq!( + *market_map + .markets + .get(&ticker.currency_pair.to_string()) + .unwrap(), + market, + ); + assert_eq!(state.get_market_map_last_updated_height().await.unwrap(), 1); + } + + #[tokio::test] + async fn update_markets_fails_if_market_is_not_in_market_map() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + let ticker = example_ticker_with_metadata("ticker".to_string()); + let market = Market { + ticker: ticker.clone(), + provider_configs: vec![], + }; + + let different_ticker = example_ticker_from_currency_pair( + "difbase", + "difquote", + "different ticker".to_string(), + ); + let different_market = Market { + ticker: different_ticker.clone(), + provider_configs: vec![], + }; + let mut market_map = MarketMap { + markets: IndexMap::new(), + }; + market_map.markets.insert( + different_ticker.currency_pair.to_string(), + different_market.clone(), + ); + state.put_market_map(market_map).unwrap(); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Update(vec![ + market.clone(), + ]))); + + let res = action.check_and_execute(&mut state).await.unwrap_err(); + assert!(res.to_string().contains(&format!( + "market for ticker {} not found in market map", + ticker.currency_pair + ))); + } + + #[tokio::test] + async fn update_markets_fails_if_market_map_is_not_in_state() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_base_prefix(ASTRIA_PREFIX.to_string()).unwrap(); + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + state.put_params(params).unwrap(); + + let action = PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Update(vec![]))); + + let res = action.check_and_execute(&mut state).await.unwrap_err(); + assert!(res.to_string().contains("market map is empty")); + } + + #[tokio::test] + async fn update_market_map_params_executes_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + + state.put_sudo_address(authority_address).unwrap(); + + state.put_transaction_context(TransactionContext { + address_bytes: *authority_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + assert!(state.get_params().await.unwrap().is_none()); + + let expected_params = Params { + market_authorities: vec![authority_address], + admin: authority_address, + }; + let action = PriceFeed::MarketMap(MarketMapChange::Params(UpdateMarketMapParams { + params: expected_params.clone(), + })); + action.check_and_execute(&mut state).await.unwrap(); + let actual_params = state + .get_params() + .await + .unwrap() + .expect("params should be present"); + assert_eq!(actual_params, expected_params); + } + + #[tokio::test] + async fn update_market_map_params_fails_if_signer_is_invalid() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = cnidarium::StateDelta::new(snapshot); + + let authority_address = astria_address(&[0; 20]); + let invalid_address = astria_address(&[1; 20]); + + state.put_sudo_address(authority_address).unwrap(); + + state.put_transaction_context(TransactionContext { + address_bytes: *invalid_address.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let action = PriceFeed::MarketMap(MarketMapChange::Params(UpdateMarketMapParams { + params: Params { + market_authorities: vec![invalid_address], + admin: invalid_address, + }, + })); + let res = action.check_and_execute(&mut state).await.unwrap_err(); + assert!(res.to_string().contains("signer is not the sudo key")); + } } 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 4cfa2e0d8f..b59f3b68df 100644 --- a/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs +++ b/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs @@ -128,7 +128,7 @@ pub(crate) fn proto_genesis_state() }, last_updated: 0, params: Params { - market_authorities: vec![], + market_authorities: vec![astria_address_from_hex_string(ALICE_ADDRESS)], admin: astria_address_from_hex_string(ALICE_ADDRESS), }, } diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_at_genesis.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_at_genesis.snap index 080589c205..fa8a4d17b2 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_at_genesis.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_at_genesis.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 25, - 202, - 16, - 32, - 252, - 173, + 95, + 197, + 243, + 41, + 242, + 162, + 116, + 124, + 1, + 220, 102, - 131, - 153, - 236, - 185, - 158, - 85, 213, - 241, - 154, - 205, - 152, - 116, - 249, - 83, - 162, - 101, - 198, - 178, - 72, + 189, + 186, + 169, 175, - 194, - 82, - 48, - 144, - 202 + 138, + 234, + 86, + 44, + 22, + 188, + 225, + 44, + 235, + 215, + 166, + 138, + 53, + 244, + 212, + 242 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_execute_every_action.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_execute_every_action.snap index cab8ccc371..aac2b7ef8a 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_execute_every_action.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_execute_every_action.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 141, - 135, - 210, - 187, - 173, - 122, - 103, - 225, - 89, - 154, - 223, - 152, - 90, - 227, - 149, + 124, + 74, + 163, + 170, + 5, + 56, + 191, + 69, + 24, + 148, + 7, + 146, + 196, + 252, + 115, 121, - 220, - 2, - 219, - 82, - 18, - 59, - 44, - 41, + 226, + 111, + 47, + 120, + 47, + 101, + 97, + 149, 238, - 7, - 64, - 70, - 108, - 227, - 35, - 213 + 149, + 178, + 219, + 22, + 129, + 135, + 154 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_finalize_block.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_finalize_block.snap index 21721cb60a..834d117be8 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_finalize_block.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_hash_finalize_block.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 208, - 98, - 88, - 248, - 190, - 250, - 213, - 195, - 138, - 18, - 42, + 91, + 191, + 157, + 77, + 222, + 167, + 8, + 48, + 21, + 205, + 102, + 193, 240, - 54, - 98, + 160, 29, - 183, 96, - 171, - 138, - 18, - 254, - 228, - 141, - 157, - 74, - 11, - 45, - 225, - 86, - 63, - 244, - 103 + 247, + 190, + 81, + 159, + 251, + 223, + 37, + 236, + 188, + 151, + 211, + 82, + 175, + 238, + 62, + 134 ] diff --git a/crates/astria-sequencer/src/app/tests_breaking_changes.rs b/crates/astria-sequencer/src/app/tests_breaking_changes.rs index 228ac7f24b..51d50ee2ec 100644 --- a/crates/astria-sequencer/src/app/tests_breaking_changes.rs +++ b/crates/astria-sequencer/src/app/tests_breaking_changes.rs @@ -16,7 +16,13 @@ use std::{ }; use astria_core::{ - connect::types::v2::CurrencyPair, + connect::{ + market_map::v2::{ + Market, + Params, + }, + types::v2::CurrencyPair, + }, primitive::v1::{ Address, RollupId, @@ -28,12 +34,15 @@ use astria_core::{ BridgeLock, BridgeSudoChange, BridgeUnlock, + ChangeMarkets, CurrencyPairsChange, IbcRelayerChange, IbcSudoChange, + MarketMapChange, PriceFeed, RollupDataSubmission, Transfer, + UpdateMarketMapParams, ValidatorUpdate, }, Action, @@ -59,6 +68,7 @@ use crate::{ default_genesis_accounts, initialize_app_with_storage, proto_genesis_state, + ALICE_ADDRESS, BOB_ADDRESS, CAROL_ADDRESS, }, @@ -77,6 +87,7 @@ use crate::{ ASTRIA_PREFIX, }, bridge::StateWriteExt as _, + test_utils::example_ticker_from_currency_pair, }; #[tokio::test] @@ -183,6 +194,7 @@ async fn app_execute_transaction_with_every_action_snapshot() { let alice = get_alice_signing_key(); let bridge = get_bridge_signing_key(); let bridge_address = astria_address(&bridge.address_bytes()); + let alice_address = astria_address_from_hex_string(ALICE_ADDRESS); let bob_address = astria_address_from_hex_string(BOB_ADDRESS); let carol_address = astria_address_from_hex_string(CAROL_ADDRESS); @@ -242,6 +254,39 @@ async fn app_execute_transaction_with_every_action_snapshot() { } .into(), Action::ValidatorUpdate(update.clone()), + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Create(vec![ + Market { + ticker: example_ticker_from_currency_pair( + "testAssetOne", + "testAssetTwo", + "create market".to_string(), + ), + provider_configs: vec![], + }, + ]))) + .into(), + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Update(vec![ + Market { + ticker: example_ticker_from_currency_pair( + "testAssetOne", + "testAssetTwo", + "update market".to_string(), + ), + provider_configs: vec![], + }, + ]))) + .into(), + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Remove(vec![ + Market { + ticker: example_ticker_from_currency_pair( + "testAssetOne", + "testAssetTwo", + "remove market".to_string(), + ), + provider_configs: vec![], + }, + ]))) + .into(), ]) .chain_id("test") .try_build() @@ -255,6 +300,13 @@ async fn app_execute_transaction_with_every_action_snapshot() { FeeAssetChange::Addition("test-0".parse().unwrap()).into(), FeeAssetChange::Addition("test-1".parse().unwrap()).into(), FeeAssetChange::Removal("test-0".parse().unwrap()).into(), + PriceFeed::MarketMap(MarketMapChange::Params(UpdateMarketMapParams { + params: Params { + market_authorities: vec![bob_address, carol_address], + admin: alice_address, + }, + })) + .into(), ]) .nonce(1) .chain_id("test") diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index e3c2a2812c..064d4d53fa 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -4,7 +4,14 @@ use std::{ }; use astria_core::{ - connect::types::v2::CurrencyPair, + connect::{ + market_map::v2::{ + Market, + MarketMap, + Params, + }, + types::v2::CurrencyPair, + }, crypto::SigningKey, primitive::v1::{ asset, @@ -18,13 +25,16 @@ use astria_core::{ action::{ BridgeLock, BridgeUnlock, + ChangeMarkets, CurrencyPairsChange, IbcRelayerChange, IbcSudoChange, + MarketMapChange, PriceFeed, RollupDataSubmission, SudoAddressChange, Transfer, + UpdateMarketMapParams, ValidatorUpdate, }, Action, @@ -40,6 +50,7 @@ use cnidarium::{ StateDelta, }; use futures::StreamExt as _; +use indexmap::IndexMap; use super::test_utils::get_alice_signing_key; use crate::{ @@ -50,6 +61,7 @@ use crate::{ }, app::{ benchmark_and_test_utils::{ + ALICE_ADDRESS, BOB_ADDRESS, CAROL_ADDRESS, }, @@ -59,7 +71,10 @@ use crate::{ }, InvalidNonce, }, - authority::StateReadExt as _, + authority::{ + StateReadExt as _, + StateWriteExt, + }, benchmark_and_test_utils::{ astria_address, astria_address_from_hex_string, @@ -71,13 +86,23 @@ use crate::{ StateReadExt as _, StateWriteExt as _, }, - connect::oracle::state_ext::StateReadExt, + connect::{ + market_map::state_ext::{ + StateReadExt as _, + StateWriteExt as _, + }, + oracle::state_ext::StateReadExt, + }, fees::{ StateReadExt as _, StateWriteExt as _, }, ibc::StateReadExt as _, - test_utils::calculate_rollup_data_submission_fee_from_state, + test_utils::{ + calculate_rollup_data_submission_fee_from_state, + example_ticker_from_currency_pair, + example_ticker_with_metadata, + }, utils::create_deposit_event, }; @@ -1383,3 +1408,221 @@ async fn test_app_execute_transaction_add_and_remove_currency_pairs() { app.state.currency_pairs().collect().await; assert_eq!(currency_pairs.len(), 0); } + +#[tokio::test] +async fn create_markets_executes_as_expected() { + let mut app = initialize_app(None, vec![]).await; + + let alice_signing_key = get_alice_signing_key(); + + let ticker_1 = example_ticker_with_metadata("create market 1".to_string()); + let market_1 = Market { + ticker: ticker_1.clone(), + provider_configs: vec![], + }; + + let ticker_2 = example_ticker_from_currency_pair("USDC", "TIA", "create market 2".to_string()); + let market_2 = Market { + ticker: ticker_2.clone(), + provider_configs: vec![], + }; + + let create_markets_action = + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Create(vec![ + market_1.clone(), + market_2.clone(), + ]))); + + let tx = TransactionBody::builder() + .actions(vec![create_markets_action.into()]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice_signing_key)); + app.execute_transaction(signed_tx).await.unwrap(); + + let market_map = app.state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 2); + assert_eq!( + market_map.markets.get(&ticker_1.currency_pair.to_string()), + Some(&market_1) + ); + assert_eq!( + market_map.markets.get(&ticker_2.currency_pair.to_string()), + Some(&market_2) + ); +} + +#[tokio::test] +async fn update_markets_executes_as_expected() { + let mut app = initialize_app(None, vec![]).await; + let mut state_tx = StateDelta::new(app.state.clone()); + + let alice_signing_key = get_alice_signing_key(); + + let ticker_1 = example_ticker_with_metadata("create market 1".to_string()); + let market_1 = Market { + ticker: ticker_1.clone(), + provider_configs: vec![], + }; + let ticker_2 = example_ticker_from_currency_pair("USDC", "TIA", "create market 2".to_string()); + let market_2 = Market { + ticker: ticker_2.clone(), + provider_configs: vec![], + }; + + let mut market_map = MarketMap { + markets: IndexMap::new(), + }; + + market_map + .markets + .insert(ticker_1.currency_pair.to_string(), market_1); + market_map + .markets + .insert(ticker_2.currency_pair.to_string(), market_2.clone()); + + state_tx.put_market_map(market_map).unwrap(); + app.apply(state_tx); + + // market_3 should replace market_1, since they share the same currency pair + let ticker_3 = example_ticker_with_metadata("update market 1 to market 2".to_string()); + let market_3 = Market { + ticker: ticker_3.clone(), + provider_configs: vec![], + }; + + let update_markets_action = + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Update(vec![ + market_3.clone(), + ]))); + + let tx = TransactionBody::builder() + .actions(vec![update_markets_action.into()]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice_signing_key)); + app.execute_transaction(signed_tx).await.unwrap(); + + let market_map = app.state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 2); + assert_eq!( + market_map.markets.get(&ticker_1.currency_pair.to_string()), + Some(&market_3) + ); + assert_eq!( + market_map.markets.get(&ticker_2.currency_pair.to_string()), + Some(&market_2) + ); + assert_eq!( + market_map.markets.get(&ticker_3.currency_pair.to_string()), + Some(&market_3) + ); +} + +#[tokio::test] +async fn remove_markets_executes_as_expected() { + let mut app = initialize_app(None, vec![]).await; + let mut state_tx = StateDelta::new(app.state.clone()); + + let alice_signing_key = get_alice_signing_key(); + + let ticker_1 = example_ticker_with_metadata("create market 1".to_string()); + let market_1 = Market { + ticker: ticker_1.clone(), + provider_configs: vec![], + }; + let ticker_2 = example_ticker_from_currency_pair("USDC", "TIA", "create market 2".to_string()); + let market_2 = Market { + ticker: ticker_2.clone(), + provider_configs: vec![], + }; + + let mut market_map = MarketMap { + markets: IndexMap::new(), + }; + + market_map + .markets + .insert(ticker_1.currency_pair.to_string(), market_1); + market_map + .markets + .insert(ticker_2.currency_pair.to_string(), market_2.clone()); + + state_tx.put_market_map(market_map).unwrap(); + app.apply(state_tx); + + let remove_markets_action = + PriceFeed::MarketMap(MarketMapChange::Markets(ChangeMarkets::Remove(vec![ + Market { + ticker: ticker_1.clone(), + provider_configs: vec![], + }, + ]))); + + let tx = TransactionBody::builder() + .actions(vec![remove_markets_action.into()]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice_signing_key)); + app.execute_transaction(signed_tx).await.unwrap(); + + let market_map = app.state.get_market_map().await.unwrap().unwrap(); + assert_eq!(market_map.markets.len(), 1); + assert!( + market_map + .markets + .get(&ticker_1.currency_pair.to_string()) + .is_none() + ); + assert_eq!( + market_map.markets.get(&ticker_2.currency_pair.to_string()), + Some(&market_2) + ); +} + +#[tokio::test] +async fn update_market_map_params_executes_as_expected() { + let mut app = initialize_app(None, vec![]).await; + let mut state_tx = StateDelta::new(app.state.clone()); + + let alice_signing_key = get_alice_signing_key(); + let alice_address = astria_address_from_hex_string(ALICE_ADDRESS); + let bob_address = astria_address_from_hex_string(BOB_ADDRESS); + let carol_address = astria_address_from_hex_string(CAROL_ADDRESS); + + let params_1 = Params { + market_authorities: vec![alice_address, bob_address], + admin: alice_address, + }; + state_tx.put_params(params_1.clone()).unwrap(); + state_tx.put_sudo_address(alice_address).unwrap(); + app.apply(state_tx); + + let params_2 = Params { + market_authorities: vec![bob_address, carol_address], + admin: alice_address, + }; + let update_market_map_params_action = + PriceFeed::MarketMap(MarketMapChange::Params(UpdateMarketMapParams { + params: params_2.clone(), + })); + + let tx = TransactionBody::builder() + .actions(vec![update_market_map_params_action.into()]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice_signing_key)); + app.execute_transaction(signed_tx).await.unwrap(); + + let params = app.state.get_params().await.unwrap().unwrap(); + assert_ne!(params, params_1); + assert_eq!(params, params_2); +} diff --git a/crates/astria-sequencer/src/connect/market_map/component.rs b/crates/astria-sequencer/src/connect/market_map/component.rs index 337d53b479..7c7e13a238 100644 --- a/crates/astria-sequencer/src/connect/market_map/component.rs +++ b/crates/astria-sequencer/src/connect/market_map/component.rs @@ -25,9 +25,6 @@ impl Component for MarketMapComponent { #[instrument(name = "MarketMapComponent::init_chain", skip_all, err)] async fn init_chain(mut state: S, app_state: &Self::AppState) -> Result<()> { if let Some(connect) = app_state.connect() { - // TODO: put market map authorities 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")?; diff --git a/crates/astria-sequencer/src/test_utils.rs b/crates/astria-sequencer/src/test_utils.rs index d9d7934239..9e80a7683d 100644 --- a/crates/astria-sequencer/src/test_utils.rs +++ b/crates/astria-sequencer/src/test_utils.rs @@ -1,4 +1,14 @@ +use std::str::FromStr as _; + use astria_core::{ + connect::{ + market_map::v2::Ticker, + types::v2::{ + Base, + CurrencyPair, + Quote, + }, + }, primitive::v1::{ Address, Bech32, @@ -47,3 +57,33 @@ pub(crate) async fn calculate_rollup_data_submission_fee_from_state< ) .expect("fee addition should not overflow") } + +pub(crate) fn example_ticker_with_metadata(metadata: String) -> Ticker { + Ticker { + currency_pair: CurrencyPair::from_parts( + Base::from_str("BTC").unwrap(), + Quote::from_str("USD").unwrap(), + ), + decimals: 2, + min_provider_count: 2, + enabled: true, + metadata_json: metadata, + } +} + +pub(crate) fn example_ticker_from_currency_pair( + base: &str, + quote: &str, + metadata: String, +) -> Ticker { + Ticker { + currency_pair: CurrencyPair::from_parts( + Base::from_str(base).unwrap(), + Quote::from_str(quote).unwrap(), + ), + decimals: 2, + min_provider_count: 2, + enabled: true, + metadata_json: metadata, + } +} diff --git a/proto/protocolapis/astria/protocol/fees/v1/types.proto b/proto/protocolapis/astria/protocol/fees/v1/types.proto index 9ee981729b..7f520addcd 100644 --- a/proto/protocolapis/astria/protocol/fees/v1/types.proto +++ b/proto/protocolapis/astria/protocol/fees/v1/types.proto @@ -84,6 +84,21 @@ message PriceFeedFeeComponents { astria.primitive.v1.Uint128 multiplier = 2; } +message ChangeMarketsFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message UpdateMarketMapParamsFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message RemoveMarketAuthoritiesFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + // Response to a transaction fee ABCI query. message TransactionFeeResponse { uint64 height = 2; diff --git a/proto/protocolapis/astria/protocol/transaction/v1/action.proto b/proto/protocolapis/astria/protocol/transaction/v1/action.proto index a88010d9f9..054bcfde9e 100644 --- a/proto/protocolapis/astria/protocol/transaction/v1/action.proto +++ b/proto/protocolapis/astria/protocol/transaction/v1/action.proto @@ -6,6 +6,8 @@ import "astria/primitive/v1/types.proto"; import "astria/protocol/fees/v1/types.proto"; import "astria_vendored/penumbra/core/component/ibc/v1/ibc.proto"; import "astria_vendored/tendermint/abci/types.proto"; +import "connect/marketmap/v2/market.proto"; +import "connect/marketmap/v2/params.proto"; import "connect/types/v2/currency_pair.proto"; message Action { @@ -244,6 +246,7 @@ message IbcSudoChange { message PriceFeed { oneof value { CurrencyPairsChange oracle = 1; + MarketMapChange market_map = 2; } } @@ -258,3 +261,30 @@ message CurrencyPairsChange { message CurrencyPairs { repeated connect.types.v2.CurrencyPair pairs = 1; } + +message MarketMapChange { + oneof value { + ChangeMarkets markets = 1; + UpdateMarketMapParams params = 2; + } +} + +// Either creates, updates existing, or removes markets. +message ChangeMarkets { + oneof action { + Markets create = 1; + Markets update = 2; + Markets remove = 3; + } +} + +// A list of markets for creation, updating, or removal. +message Markets { + repeated connect.marketmap.v2.Market markets = 1; +} + +// Updates the market map parameters. +message UpdateMarketMapParams { + // Params defines the new parameters for the x/marketmap module. + connect.marketmap.v2.Params params = 1; +}