diff --git a/crates/astria-core/src/connect/oracle.rs b/crates/astria-core/src/connect/oracle.rs index bfed0016dd..70f1bea115 100644 --- a/crates/astria-core/src/connect/oracle.rs +++ b/crates/astria-core/src/connect/oracle.rs @@ -94,7 +94,7 @@ pub mod v2 { #[derive(Debug, Clone, PartialEq)] pub struct CurrencyPairState { - pub price: QuotePrice, + pub price: Option, pub nonce: CurrencyPairNonce, pub id: CurrencyPairId, } @@ -121,14 +121,11 @@ pub mod v2 { /// - 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 + let price = raw .price .map(QuotePrice::try_from_raw) .transpose() - .map_err(CurrencyPairStateError::quote_price_parse_error)? - else { - return Err(CurrencyPairStateError::missing_price()); - }; + .map_err(CurrencyPairStateError::quote_price_parse_error)?; let nonce = CurrencyPairNonce::new(raw.nonce); let id = CurrencyPairId::new(raw.id); Ok(Self { @@ -141,7 +138,7 @@ pub mod v2 { #[must_use] pub fn into_raw(self) -> raw::CurrencyPairState { raw::CurrencyPairState { - price: Some(self.price.into_raw()), + price: self.price.map(QuotePrice::into_raw), nonce: self.nonce.get(), id: self.id.get(), } @@ -153,11 +150,6 @@ pub mod v2 { 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)) @@ -166,8 +158,6 @@ pub mod v2 { #[derive(Debug, thiserror::Error)] enum CurrencyPairStateErrorKind { - #[error("missing price")] - MissingPrice, #[error("failed to parse quote price")] QuotePriceParseError(#[source] QuotePriceError), } @@ -175,7 +165,7 @@ pub mod v2 { #[derive(Debug, Clone)] pub struct CurrencyPairGenesis { pub currency_pair: CurrencyPair, - pub currency_pair_price: QuotePrice, + pub currency_pair_price: Option, pub id: CurrencyPairId, pub nonce: CurrencyPairNonce, } @@ -201,7 +191,7 @@ pub mod v2 { } #[must_use] - pub fn currency_pair_price(&self) -> &QuotePrice { + pub fn currency_pair_price(&self) -> &Option { &self.currency_pair_price } @@ -232,13 +222,11 @@ pub mod v2 { .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 currency_pair_price = raw + .currency_pair_price + .map(QuotePrice::try_from_raw) + .transpose() + .map_err(CurrencyPairGenesisError::currency_pair_price)?; let id = CurrencyPairId::new(raw.id); let nonce = CurrencyPairNonce::new(raw.nonce); @@ -254,7 +242,7 @@ pub mod v2 { 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()), + currency_pair_price: self.currency_pair_price.map(QuotePrice::into_raw), id: self.id.get(), nonce: self.nonce.get(), } diff --git a/crates/astria-core/src/connect/types.rs b/crates/astria-core/src/connect/types.rs index d97f4b9896..13a110fc53 100644 --- a/crates/astria-core/src/connect/types.rs +++ b/crates/astria-core/src/connect/types.rs @@ -261,6 +261,14 @@ pub mod v2 { quote: self.quote.0, } } + + #[must_use] + pub fn to_raw(&self) -> raw::CurrencyPair { + raw::CurrencyPair { + base: self.base.0.clone(), + quote: self.quote.0.clone(), + } + } } impl TryFrom for CurrencyPair { 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 8d03de6cd6..7fec6ae51a 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1.rs @@ -223,6 +223,36 @@ impl ::prost::Name for IbcSudoChangeFeeComponents { ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddCurrencyPairsFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for AddCurrencyPairsFeeComponents { + const NAME: &'static str = "AddCurrencyPairsFeeComponents"; + 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 RemoveCurrencyPairsFeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for RemoveCurrencyPairsFeeComponents { + const NAME: &'static str = "RemoveCurrencyPairsFeeComponents"; + 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 90c71b03f9..b7fe2cf023 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 @@ -1,3 +1,111 @@ +impl serde::Serialize for AddCurrencyPairsFeeComponents { + #[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.AddCurrencyPairsFeeComponents", 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 AddCurrencyPairsFeeComponents { + #[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 = AddCurrencyPairsFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.AddCurrencyPairsFeeComponents") + } + + 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(AddCurrencyPairsFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.AddCurrencyPairsFeeComponents", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for BridgeLockFeeComponents { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -1078,6 +1186,114 @@ impl<'de> serde::Deserialize<'de> for InitBridgeAccountFeeComponents { deserializer.deserialize_struct("astria.protocol.fees.v1.InitBridgeAccountFeeComponents", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for RemoveCurrencyPairsFeeComponents { + #[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.RemoveCurrencyPairsFeeComponents", 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 RemoveCurrencyPairsFeeComponents { + #[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 = RemoveCurrencyPairsFeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.RemoveCurrencyPairsFeeComponents") + } + + 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(RemoveCurrencyPairsFeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.RemoveCurrencyPairsFeeComponents", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for RollupDataSubmissionFeeComponents { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result 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 991867b813..93d8b795d0 100644 --- a/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.genesis.v1.rs @@ -147,6 +147,14 @@ pub struct GenesisFees { pub validator_update: ::core::option::Option< super::super::fees::v1::ValidatorUpdateFeeComponents, >, + #[prost(message, optional, tag = "15")] + pub add_currency_pairs: ::core::option::Option< + super::super::fees::v1::AddCurrencyPairsFeeComponents, + >, + #[prost(message, optional, tag = "16")] + pub remove_currency_pairs: ::core::option::Option< + super::super::fees::v1::RemoveCurrencyPairsFeeComponents, + >, } impl ::prost::Name for GenesisFees { const NAME: &'static str = "GenesisFees"; 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 9ca59c33ca..acfcfa6c8a 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 @@ -643,6 +643,12 @@ impl serde::Serialize for GenesisFees { if self.validator_update.is_some() { len += 1; } + if self.add_currency_pairs.is_some() { + len += 1; + } + if self.remove_currency_pairs.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("astria.protocol.genesis.v1.GenesisFees", len)?; if let Some(v) = self.bridge_lock.as_ref() { struct_ser.serialize_field("bridgeLock", v)?; @@ -686,6 +692,12 @@ impl serde::Serialize for GenesisFees { if let Some(v) = self.validator_update.as_ref() { struct_ser.serialize_field("validatorUpdate", v)?; } + if let Some(v) = self.add_currency_pairs.as_ref() { + struct_ser.serialize_field("addCurrencyPairs", v)?; + } + if let Some(v) = self.remove_currency_pairs.as_ref() { + struct_ser.serialize_field("removeCurrencyPairs", v)?; + } struct_ser.end() } } @@ -723,6 +735,10 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { "transfer", "validator_update", "validatorUpdate", + "add_currency_pairs", + "addCurrencyPairs", + "remove_currency_pairs", + "removeCurrencyPairs", ]; #[allow(clippy::enum_variant_names)] @@ -741,6 +757,8 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { SudoAddressChange, Transfer, ValidatorUpdate, + AddCurrencyPairs, + RemoveCurrencyPairs, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -776,6 +794,8 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), "transfer" => Ok(GeneratedField::Transfer), "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), + "addCurrencyPairs" | "add_currency_pairs" => Ok(GeneratedField::AddCurrencyPairs), + "removeCurrencyPairs" | "remove_currency_pairs" => Ok(GeneratedField::RemoveCurrencyPairs), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -809,6 +829,8 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { let mut sudo_address_change__ = None; let mut transfer__ = None; let mut validator_update__ = None; + let mut add_currency_pairs__ = None; + let mut remove_currency_pairs__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::BridgeLock => { @@ -895,6 +917,18 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { } validator_update__ = map_.next_value()?; } + GeneratedField::AddCurrencyPairs => { + if add_currency_pairs__.is_some() { + return Err(serde::de::Error::duplicate_field("addCurrencyPairs")); + } + add_currency_pairs__ = map_.next_value()?; + } + GeneratedField::RemoveCurrencyPairs => { + if remove_currency_pairs__.is_some() { + return Err(serde::de::Error::duplicate_field("removeCurrencyPairs")); + } + remove_currency_pairs__ = map_.next_value()?; + } } } Ok(GenesisFees { @@ -912,6 +946,8 @@ impl<'de> serde::Deserialize<'de> for GenesisFees { sudo_address_change: sudo_address_change__, transfer: transfer__, validator_update: validator_update__, + add_currency_pairs: add_currency_pairs__, + remove_currency_pairs: remove_currency_pairs__, }) } } 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 0b38628394..27d3782a5c 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs @@ -3,7 +3,7 @@ pub struct Action { #[prost( oneof = "action::Value", - tags = "1, 2, 11, 12, 13, 14, 21, 22, 50, 51, 52, 53, 55, 56" + tags = "1, 2, 11, 12, 13, 14, 21, 22, 50, 51, 52, 53, 55, 56, 71, 72" )] pub value: ::core::option::Option, } @@ -31,7 +31,7 @@ pub mod action { Ibc(::penumbra_proto::core::component::ibc::v1::IbcRelay), #[prost(message, tag = "22")] Ics20Withdrawal(super::Ics20Withdrawal), - /// POA sudo actions are defined on 50-60 + /// POA sudo actions are defined on 50-70 #[prost(message, tag = "50")] SudoAddressChange(super::SudoAddressChange), #[prost(message, tag = "51")] @@ -46,6 +46,11 @@ pub mod action { FeeChange(super::FeeChange), #[prost(message, tag = "56")] IbcSudoChange(super::IbcSudoChange), + /// Oracle actions are defined on 71-80 + #[prost(message, tag = "71")] + AddCurrencyPairs(super::AddCurrencyPairs), + #[prost(message, tag = "72")] + RemoveCurrencyPairs(super::RemoveCurrencyPairs), } } impl ::prost::Name for Action { @@ -402,7 +407,7 @@ pub struct FeeChange { /// the new fee components values #[prost( oneof = "fee_change::FeeComponents", - tags = "1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14" + tags = "1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16" )] pub fee_components: ::core::option::Option, } @@ -442,6 +447,12 @@ pub mod fee_change { Transfer(super::super::super::fees::v1::TransferFeeComponents), #[prost(message, tag = "14")] ValidatorUpdate(super::super::super::fees::v1::ValidatorUpdateFeeComponents), + #[prost(message, tag = "15")] + AddCurrencyPairs(super::super::super::fees::v1::AddCurrencyPairsFeeComponents), + #[prost(message, tag = "16")] + RemoveCurrencyPairs( + super::super::super::fees::v1::RemoveCurrencyPairsFeeComponents, + ), } } impl ::prost::Name for FeeChange { @@ -464,6 +475,36 @@ impl ::prost::Name for IbcSudoChange { ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AddCurrencyPairs { + #[prost(message, repeated, tag = "1")] + pub pairs: ::prost::alloc::vec::Vec< + super::super::super::super::connect::types::v2::CurrencyPair, + >, +} +impl ::prost::Name for AddCurrencyPairs { + const NAME: &'static str = "AddCurrencyPairs"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RemoveCurrencyPairs { + #[prost(message, repeated, tag = "1")] + pub pairs: ::prost::alloc::vec::Vec< + super::super::super::super::connect::types::v2::CurrencyPair, + >, +} +impl ::prost::Name for RemoveCurrencyPairs { + const NAME: &'static str = "RemoveCurrencyPairs"; + 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 4c77666d12..a17822602b 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 @@ -54,6 +54,12 @@ impl serde::Serialize for Action { action::Value::IbcSudoChange(v) => { struct_ser.serialize_field("ibcSudoChange", v)?; } + action::Value::AddCurrencyPairs(v) => { + struct_ser.serialize_field("addCurrencyPairs", v)?; + } + action::Value::RemoveCurrencyPairs(v) => { + struct_ser.serialize_field("removeCurrencyPairs", v)?; + } } } struct_ser.end() @@ -92,6 +98,10 @@ impl<'de> serde::Deserialize<'de> for Action { "feeChange", "ibc_sudo_change", "ibcSudoChange", + "add_currency_pairs", + "addCurrencyPairs", + "remove_currency_pairs", + "removeCurrencyPairs", ]; #[allow(clippy::enum_variant_names)] @@ -110,6 +120,8 @@ impl<'de> serde::Deserialize<'de> for Action { FeeAssetChange, FeeChange, IbcSudoChange, + AddCurrencyPairs, + RemoveCurrencyPairs, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -145,6 +157,8 @@ impl<'de> serde::Deserialize<'de> for Action { "feeAssetChange" | "fee_asset_change" => Ok(GeneratedField::FeeAssetChange), "feeChange" | "fee_change" => Ok(GeneratedField::FeeChange), "ibcSudoChange" | "ibc_sudo_change" => Ok(GeneratedField::IbcSudoChange), + "addCurrencyPairs" | "add_currency_pairs" => Ok(GeneratedField::AddCurrencyPairs), + "removeCurrencyPairs" | "remove_currency_pairs" => Ok(GeneratedField::RemoveCurrencyPairs), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -263,6 +277,20 @@ impl<'de> serde::Deserialize<'de> for Action { return Err(serde::de::Error::duplicate_field("ibcSudoChange")); } value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::IbcSudoChange) +; + } + GeneratedField::AddCurrencyPairs => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("addCurrencyPairs")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::AddCurrencyPairs) +; + } + GeneratedField::RemoveCurrencyPairs => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("removeCurrencyPairs")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::RemoveCurrencyPairs) ; } } @@ -275,6 +303,97 @@ impl<'de> serde::Deserialize<'de> for Action { deserializer.deserialize_struct("astria.protocol.transaction.v1.Action", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for AddCurrencyPairs { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.pairs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.transaction.v1.AddCurrencyPairs", len)?; + if !self.pairs.is_empty() { + struct_ser.serialize_field("pairs", &self.pairs)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for AddCurrencyPairs { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pairs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Pairs, + } + 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 { + "pairs" => Ok(GeneratedField::Pairs), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = AddCurrencyPairs; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.AddCurrencyPairs") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pairs__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Pairs => { + if pairs__.is_some() { + return Err(serde::de::Error::duplicate_field("pairs")); + } + pairs__ = Some(map_.next_value()?); + } + } + } + Ok(AddCurrencyPairs { + pairs: pairs__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.AddCurrencyPairs", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for BridgeLock { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -945,6 +1064,12 @@ impl serde::Serialize for FeeChange { fee_change::FeeComponents::ValidatorUpdate(v) => { struct_ser.serialize_field("validatorUpdate", v)?; } + fee_change::FeeComponents::AddCurrencyPairs(v) => { + struct_ser.serialize_field("addCurrencyPairs", v)?; + } + fee_change::FeeComponents::RemoveCurrencyPairs(v) => { + struct_ser.serialize_field("removeCurrencyPairs", v)?; + } } } struct_ser.end() @@ -984,6 +1109,10 @@ impl<'de> serde::Deserialize<'de> for FeeChange { "transfer", "validator_update", "validatorUpdate", + "add_currency_pairs", + "addCurrencyPairs", + "remove_currency_pairs", + "removeCurrencyPairs", ]; #[allow(clippy::enum_variant_names)] @@ -1002,6 +1131,8 @@ impl<'de> serde::Deserialize<'de> for FeeChange { SudoAddressChange, Transfer, ValidatorUpdate, + AddCurrencyPairs, + RemoveCurrencyPairs, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -1037,6 +1168,8 @@ impl<'de> serde::Deserialize<'de> for FeeChange { "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), "transfer" => Ok(GeneratedField::Transfer), "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), + "addCurrencyPairs" | "add_currency_pairs" => Ok(GeneratedField::AddCurrencyPairs), + "removeCurrencyPairs" | "remove_currency_pairs" => Ok(GeneratedField::RemoveCurrencyPairs), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -1155,6 +1288,20 @@ impl<'de> serde::Deserialize<'de> for FeeChange { return Err(serde::de::Error::duplicate_field("validatorUpdate")); } fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::ValidatorUpdate) +; + } + GeneratedField::AddCurrencyPairs => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("addCurrencyPairs")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::AddCurrencyPairs) +; + } + GeneratedField::RemoveCurrencyPairs => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("removeCurrencyPairs")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::RemoveCurrencyPairs) ; } } @@ -1919,6 +2066,97 @@ impl<'de> serde::Deserialize<'de> for InitBridgeAccount { deserializer.deserialize_struct("astria.protocol.transaction.v1.InitBridgeAccount", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for RemoveCurrencyPairs { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.pairs.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.transaction.v1.RemoveCurrencyPairs", len)?; + if !self.pairs.is_empty() { + struct_ser.serialize_field("pairs", &self.pairs)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for RemoveCurrencyPairs { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pairs", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Pairs, + } + 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 { + "pairs" => Ok(GeneratedField::Pairs), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = RemoveCurrencyPairs; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.RemoveCurrencyPairs") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pairs__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Pairs => { + if pairs__.is_some() { + return Err(serde::de::Error::duplicate_field("pairs")); + } + pairs__ = Some(map_.next_value()?); + } + } + } + Ok(RemoveCurrencyPairs { + pairs: pairs__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.RemoveCurrencyPairs", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for RollupDataSubmission { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/astria-core/src/protocol/fees/v1.rs b/crates/astria-core/src/protocol/fees/v1.rs index fb5571e9b9..22e103306a 100644 --- a/crates/astria-core/src/protocol/fees/v1.rs +++ b/crates/astria-core/src/protocol/fees/v1.rs @@ -14,6 +14,7 @@ use crate::{ generated::astria::protocol::fees::v1 as raw, primitive::v1::asset, protocol::transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -23,6 +24,7 @@ use crate::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -108,6 +110,8 @@ impl_protobuf_for_fee_components!( FeeComponents => raw::FeeChangeFeeComponents, FeeComponents => raw::SudoAddressChangeFeeComponents, FeeComponents => raw::IbcSudoChangeFeeComponents, + FeeComponents => raw::AddCurrencyPairsFeeComponents, + FeeComponents => raw::RemoveCurrencyPairsFeeComponents, ); pub struct FeeComponents { diff --git a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state.snap b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state.snap index 21acc3a521..004306d1e7 100644 --- a/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state.snap +++ b/crates/astria-core/src/protocol/genesis/snapshots/astria_core__protocol__genesis__v1__tests__genesis_state.snap @@ -131,6 +131,14 @@ expression: genesis_state() "validatorUpdate": { "base": {}, "multiplier": {} + }, + "addCurrencyPairs": { + "base": {}, + "multiplier": {} + }, + "removeCurrencyPairs": { + "base": {}, + "multiplier": {} } }, "connect": { diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index f5383d1d7e..283bbcb10d 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -26,6 +26,7 @@ use crate::{ FeeComponents, }, transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -35,6 +36,7 @@ use crate::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -763,6 +765,8 @@ pub struct GenesisFees { pub ibc_relayer_change: Option>, pub sudo_address_change: Option>, pub ibc_sudo_change: Option>, + pub add_currency_pairs: Option>, + pub remove_currency_pairs: Option>, } impl Protobuf for GenesisFees { @@ -789,6 +793,8 @@ impl Protobuf for GenesisFees { ibc_relayer_change, sudo_address_change, ibc_sudo_change, + add_currency_pairs, + remove_currency_pairs, } = raw; let rollup_data_submission = rollup_data_submission .clone() @@ -875,6 +881,18 @@ impl Protobuf for GenesisFees { .transpose() .map_err(|e| FeesError::fee_components("ibc_sudo_change", e))?; + let add_currency_pairs = add_currency_pairs + .clone() + .map(FeeComponents::::try_from_raw) + .transpose() + .map_err(|e| FeesError::fee_components("add_currency_pairs", e))?; + + let remove_currency_pairs = remove_currency_pairs + .clone() + .map(FeeComponents::::try_from_raw) + .transpose() + .map_err(|e| FeesError::fee_components("remove_currency_pairs", e))?; + Ok(Self { rollup_data_submission, transfer, @@ -890,6 +908,8 @@ impl Protobuf for GenesisFees { ibc_relayer_change, sudo_address_change, ibc_sudo_change, + add_currency_pairs, + remove_currency_pairs, }) } @@ -909,6 +929,8 @@ impl Protobuf for GenesisFees { ibc_relayer_change, sudo_address_change, ibc_sudo_change, + add_currency_pairs, + remove_currency_pairs, } = self; Self::Raw { transfer: transfer.map(|act| FeeComponents::::to_raw(&act)), @@ -934,6 +956,10 @@ impl Protobuf for GenesisFees { .map(|act| FeeComponents::::to_raw(&act)), ibc_sudo_change: ibc_sudo_change .map(|act| FeeComponents::::to_raw(&act)), + add_currency_pairs: add_currency_pairs + .map(|act| FeeComponents::::to_raw(&act)), + remove_currency_pairs: remove_currency_pairs + .map(|act| FeeComponents::::to_raw(&act)), } } } @@ -1120,6 +1146,10 @@ mod tests { 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()), + add_currency_pairs: Some(FeeComponents::::new(0, 0).to_raw()), + remove_currency_pairs: Some( + FeeComponents::::new(0, 0).to_raw(), + ), }), connect: Some( ConnectGenesis { @@ -1135,14 +1165,14 @@ mod tests { currency_pair_genesis: vec![CurrencyPairGenesis { id: CurrencyPairId::new(1), nonce: CurrencyPairNonce::new(0), - currency_pair_price: QuotePrice { + currency_pair_price: Some(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(), 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 c145176803..0cb105d3ca 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 @@ -11,6 +11,7 @@ use penumbra_ibc::IbcRelay; use super::{ Action, ActionName, + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -20,6 +21,7 @@ use super::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -55,6 +57,8 @@ impl_belong_to_group!( (FeeAssetChange, Group::BundleableSudo), (IbcRelay, Group::BundleableGeneral), (IbcSudoChange, Group::UnbundleableSudo), + (AddCurrencyPairs, Group::BundleableGeneral), + (RemoveCurrencyPairs, Group::BundleableGeneral), ); impl Action { @@ -74,6 +78,8 @@ impl Action { Action::FeeAssetChange(_) => FeeAssetChange::GROUP, Action::Ibc(_) => IbcRelay::GROUP, Action::IbcSudoChange(_) => IbcSudoChange::GROUP, + Action::AddCurrencyPairs(_) => AddCurrencyPairs::GROUP, + Action::RemoveCurrencyPairs(_) => RemoveCurrencyPairs::GROUP, } } } 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 5c2b93745b..95d6a007d6 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs @@ -11,6 +11,10 @@ use prost::Name as _; use super::raw; use crate::{ + connect::types::v2::{ + CurrencyPair, + CurrencyPairError, + }, primitive::v1::{ asset::{ self, @@ -51,6 +55,8 @@ pub enum Action { BridgeUnlock(BridgeUnlock), BridgeSudoChange(BridgeSudoChange), FeeChange(FeeChange), + AddCurrencyPairs(AddCurrencyPairs), + RemoveCurrencyPairs(RemoveCurrencyPairs), } impl Protobuf for Action { @@ -75,6 +81,8 @@ impl Protobuf for Action { Action::BridgeUnlock(act) => Value::BridgeUnlock(act.to_raw()), Action::BridgeSudoChange(act) => Value::BridgeSudoChange(act.to_raw()), Action::FeeChange(act) => Value::FeeChange(act.to_raw()), + Action::AddCurrencyPairs(act) => Value::AddCurrencyPairs(act.to_raw()), + Action::RemoveCurrencyPairs(act) => Value::RemoveCurrencyPairs(act.to_raw()), }; raw::Action { value: Some(kind), @@ -148,6 +156,12 @@ impl Protobuf for Action { Value::FeeChange(act) => { Self::FeeChange(FeeChange::try_from_raw_ref(&act).map_err(Error::fee_change)?) } + Value::AddCurrencyPairs(act) => Self::AddCurrencyPairs( + AddCurrencyPairs::try_from_raw(act).map_err(Error::add_currency_pairs)?, + ), + Value::RemoveCurrencyPairs(act) => Self::RemoveCurrencyPairs( + RemoveCurrencyPairs::try_from_raw(act).map_err(Error::remove_currency_pairs)?, + ), }; Ok(action) } @@ -258,6 +272,18 @@ impl From for Action { } } +impl From for Action { + fn from(value: AddCurrencyPairs) -> Self { + Self::AddCurrencyPairs(value) + } +} + +impl From for Action { + fn from(value: RemoveCurrencyPairs) -> Self { + Self::RemoveCurrencyPairs(value) + } +} + impl From for raw::Action { fn from(value: Action) -> Self { value.into_raw() @@ -295,6 +321,8 @@ impl ActionName for Action { Action::BridgeUnlock(_) => "BridgeUnlock", Action::BridgeSudoChange(_) => "BridgeSudoChange", Action::FeeChange(_) => "FeeChange", + Action::AddCurrencyPairs(_) => "AddCurrencyPairs", + Action::RemoveCurrencyPairs(_) => "RemoveCurrencyPairs", } } } @@ -363,6 +391,14 @@ impl Error { fn fee_change(inner: FeeChangeError) -> Self { Self(ActionErrorKind::FeeChange(inner)) } + + fn add_currency_pairs(inner: AddCurrencyPairsError) -> Self { + Self(ActionErrorKind::AddCurrencyPairs(inner)) + } + + fn remove_currency_pairs(inner: RemoveCurrencyPairsError) -> Self { + Self(ActionErrorKind::RemoveCurrencyPairs(inner)) + } } #[derive(Debug, thiserror::Error)] @@ -397,6 +433,10 @@ enum ActionErrorKind { BridgeSudoChange(#[source] BridgeSudoChangeError), #[error("fee change action was not valid")] FeeChange(#[source] FeeChangeError), + #[error("add currency pairs action was not valid")] + AddCurrencyPairs(#[source] AddCurrencyPairsError), + #[error("remove currency pairs action was not valid")] + RemoveCurrencyPairs(#[source] RemoveCurrencyPairsError), } #[derive(Debug, thiserror::Error)] @@ -1943,6 +1983,8 @@ pub enum FeeChange { IbcRelayerChange(FeeComponents), SudoAddressChange(FeeComponents), IbcSudoChange(FeeComponents), + AddCurrencyPairs(FeeComponents), + RemoveCurrencyPairs(FeeComponents), } impl Protobuf for FeeChange { @@ -1995,6 +2037,12 @@ impl Protobuf for FeeChange { Self::IbcSudoChange(fee_change) => { raw::fee_change::FeeComponents::IbcSudoChange(fee_change.to_raw()) } + Self::AddCurrencyPairs(fee_change) => { + raw::fee_change::FeeComponents::AddCurrencyPairs(fee_change.to_raw()) + } + Self::RemoveCurrencyPairs(fee_change) => { + raw::fee_change::FeeComponents::RemoveCurrencyPairs(fee_change.to_raw()) + } }), } } @@ -2065,11 +2113,157 @@ impl Protobuf for FeeChange { Some(raw::fee_change::FeeComponents::IbcSudoChange(fee_change)) => Self::IbcSudoChange( FeeComponents::::try_from_raw_ref(fee_change)?, ), + Some(raw::fee_change::FeeComponents::AddCurrencyPairs(fee_change)) => { + Self::AddCurrencyPairs(FeeComponents::::try_from_raw_ref( + fee_change, + )?) + } + Some(raw::fee_change::FeeComponents::RemoveCurrencyPairs(fee_change)) => { + Self::RemoveCurrencyPairs(FeeComponents::::try_from_raw_ref( + fee_change, + )?) + } None => return Err(FeeChangeError::field_unset("fee_components")), }) } } +#[derive(Debug, Clone)] +pub struct AddCurrencyPairs { + pub pairs: Vec, +} + +impl Protobuf for AddCurrencyPairs { + type Error = AddCurrencyPairsError; + type Raw = raw::AddCurrencyPairs; + + #[must_use] + fn into_raw(self) -> raw::AddCurrencyPairs { + raw::AddCurrencyPairs { + pairs: self.pairs.into_iter().map(CurrencyPair::into_raw).collect(), + } + } + + #[must_use] + fn to_raw(&self) -> raw::AddCurrencyPairs { + raw::AddCurrencyPairs { + pairs: self.pairs.iter().map(CurrencyPair::to_raw).collect(), + } + } + + /// Convert from a raw, unchecked protobuf [`raw::AddCurrencyPairsAction`]. + /// + /// # Errors + /// + /// - if any of the `pairs` field is invalid + fn try_from_raw(proto: raw::AddCurrencyPairs) -> Result { + let pairs = proto + .pairs + .into_iter() + .map(CurrencyPair::try_from_raw) + .collect::>() + .map_err(AddCurrencyPairsError::invalid_currency_pair)?; + Ok(Self { + pairs, + }) + } + + /// Convert from a reference to a raw, unchecked protobuf [`raw::AddCurrencyPairsAction`]. + /// + /// # Errors + /// + /// - if any of the `pairs` field is invalid + fn try_from_raw_ref(proto: &raw::AddCurrencyPairs) -> Result { + Self::try_from_raw(proto.clone()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct AddCurrencyPairsError(AddCurrencyPairsErrorKind); + +impl AddCurrencyPairsError { + #[must_use] + fn invalid_currency_pair(err: CurrencyPairError) -> Self { + Self(AddCurrencyPairsErrorKind::InvalidCurrencyPair(err)) + } +} + +#[derive(Debug, thiserror::Error)] +enum AddCurrencyPairsErrorKind { + #[error("a currency pair was invalid")] + InvalidCurrencyPair(#[from] CurrencyPairError), +} + +#[derive(Debug, Clone)] +pub struct RemoveCurrencyPairs { + pub pairs: Vec, +} + +impl Protobuf for RemoveCurrencyPairs { + type Error = RemoveCurrencyPairsError; + type Raw = raw::RemoveCurrencyPairs; + + #[must_use] + fn into_raw(self) -> raw::RemoveCurrencyPairs { + raw::RemoveCurrencyPairs { + pairs: self.pairs.into_iter().map(CurrencyPair::into_raw).collect(), + } + } + + #[must_use] + fn to_raw(&self) -> raw::RemoveCurrencyPairs { + raw::RemoveCurrencyPairs { + pairs: self.pairs.iter().map(CurrencyPair::to_raw).collect(), + } + } + + /// Convert from a raw, unchecked protobuf [`raw::RemoveCurrencyPairsAction`]. + /// + /// # Errors + /// + /// - if any of the `pairs` field is invalid + fn try_from_raw(proto: raw::RemoveCurrencyPairs) -> Result { + let pairs = proto + .pairs + .into_iter() + .map(CurrencyPair::try_from_raw) + .collect::>() + .map_err(RemoveCurrencyPairsError::invalid_currency_pair)?; + Ok(Self { + pairs, + }) + } + + /// Convert from a reference to a raw, unchecked protobuf [`raw::RemoveCurrencyPairsAction`]. + /// + /// # Errors + /// + /// - if any of the `pairs` field is invalid + fn try_from_raw_ref( + proto: &raw::RemoveCurrencyPairs, + ) -> Result { + Self::try_from_raw(proto.clone()) + } +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct RemoveCurrencyPairsError(RemoveCurrencyPairsErrorKind); + +impl RemoveCurrencyPairsError { + #[must_use] + fn invalid_currency_pair(err: CurrencyPairError) -> Self { + Self(RemoveCurrencyPairsErrorKind::InvalidCurrencyPair(err)) + } +} + +#[derive(Debug, thiserror::Error)] +enum RemoveCurrencyPairsErrorKind { + #[error("a currency pair was invalid")] + InvalidCurrencyPair(#[from] CurrencyPairError), +} + impl From> for FeeChange { fn from(fee: FeeComponents) -> Self { FeeChange::Transfer(fee) @@ -2153,3 +2347,15 @@ impl From> for FeeChange { FeeChange::IbcSudoChange(fee) } } + +impl From> for FeeChange { + fn from(fee: FeeComponents) -> Self { + FeeChange::AddCurrencyPairs(fee) + } +} + +impl From> for FeeChange { + fn from(fee: FeeComponents) -> Self { + FeeChange::RemoveCurrencyPairs(fee) + } +} diff --git a/crates/astria-sequencer-utils/src/genesis_example.rs b/crates/astria-sequencer-utils/src/genesis_example.rs index b84f29a70e..60e8906b19 100644 --- a/crates/astria-sequencer-utils/src/genesis_example.rs +++ b/crates/astria-sequencer-utils/src/genesis_example.rs @@ -33,6 +33,7 @@ use astria_core::{ GenesisAppState, }, transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -42,6 +43,7 @@ use astria_core::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -243,6 +245,8 @@ fn proto_genesis_state() -> astria_core::generated::astria::protocol::genesis::v 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()), + add_currency_pairs: Some(FeeComponents::::new(0, 0).to_raw()), + remove_currency_pairs: Some(FeeComponents::::new(0, 0).to_raw()), }), } } diff --git a/crates/astria-sequencer/src/action_handler/impls/add_currency_pairs.rs b/crates/astria-sequencer/src/action_handler/impls/add_currency_pairs.rs new file mode 100644 index 0000000000..048bdc86f3 --- /dev/null +++ b/crates/astria-sequencer/src/action_handler/impls/add_currency_pairs.rs @@ -0,0 +1,187 @@ +use astria_core::{ + connect::{ + oracle::v2::CurrencyPairState, + types::v2::CurrencyPairNonce, + }, + protocol::transaction::v1::action::AddCurrencyPairs, +}; +use astria_eyre::eyre::{ + ensure, + OptionExt as _, + Result, + WrapErr as _, +}; +use async_trait::async_trait; +use cnidarium::StateWrite; +use tracing::debug; + +use crate::{ + action_handler::ActionHandler, + connect::{ + market_map::state_ext::StateReadExt as _, + oracle::state_ext::{ + StateReadExt as _, + StateWriteExt as _, + }, + }, + transaction::StateReadExt as _, +}; + +#[async_trait] +impl ActionHandler for AddCurrencyPairs { + async fn check_stateless(&self) -> Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut 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" + ); + + let mut next_currency_pair_id = state + .get_next_currency_pair_id() + .await + .wrap_err("failed to get next currency pair id")?; + let mut num_currency_pairs = state + .get_num_currency_pairs() + .await + .wrap_err("failed to get number of currency pairs")?; + + for pair in &self.pairs { + if state + .get_currency_pair_state(pair) + .await + .wrap_err("failed to get currency pair state")? + .is_some() + { + debug!("currency pair {} already exists, skipping", pair); + continue; + } + + let currency_pair_state = CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: next_currency_pair_id, + }; + state + .put_currency_pair_state(pair.clone(), currency_pair_state) + .wrap_err("failed to put currency pair state")?; + num_currency_pairs = num_currency_pairs + .checked_add(1) + .ok_or_eyre("overflow when incrementing number of currency pairs")?; + next_currency_pair_id = next_currency_pair_id + .increment() + .ok_or_eyre("overflow when incrementing next currency pair id")?; + } + + state + .put_next_currency_pair_id(next_currency_pair_id) + .wrap_err("failed to put next currency pair id")?; + state + .put_num_currency_pairs(num_currency_pairs) + .wrap_err("failed to put number of currency pairs")?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use astria_core::{ + connect::{ + market_map::v2::Params, + oracle::v2::CurrencyPairState, + types::v2::CurrencyPairId, + }, + primitive::v1::TransactionId, + protocol::transaction::v1::action::AddCurrencyPairs, + }; + use cnidarium::StateDelta; + + use super::*; + use crate::{ + app::test_utils::get_alice_signing_key, + benchmark_and_test_utils::astria_address, + connect::market_map::state_ext::StateWriteExt as _, + transaction::{ + StateWriteExt, + TransactionContext, + }, + }; + + #[tokio::test] + async fn add_currency_pairs_with_duplicate() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let alice = get_alice_signing_key(); + let alice_address = astria_address(&alice.address_bytes()); + state.put_transaction_context(TransactionContext { + address_bytes: alice.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + state + .put_params(Params { + market_authorities: vec![], + admin: alice_address, + }) + .unwrap(); + state + .put_next_currency_pair_id(CurrencyPairId::new(0)) + .unwrap(); + state.put_num_currency_pairs(0).unwrap(); + + let pairs = vec![ + "BTC/USD".parse().unwrap(), + "ETH/USD".parse().unwrap(), + "BTC/USD".parse().unwrap(), + ]; + let action = AddCurrencyPairs { + pairs: pairs.clone(), + }; + action.check_and_execute(&mut state).await.unwrap(); + + assert_eq!( + state + .get_currency_pair_state(&pairs[0]) + .await + .unwrap() + .unwrap(), + CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: CurrencyPairId::new(0), + } + ); + assert_eq!( + state + .get_currency_pair_state(&pairs[1]) + .await + .unwrap() + .unwrap(), + CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: CurrencyPairId::new(1), + } + ); + assert_eq!( + state.get_next_currency_pair_id().await.unwrap(), + CurrencyPairId::new(2) + ); + assert_eq!(state.get_num_currency_pairs().await.unwrap(), 2); + } +} 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 f066ded9ea..2d93464d51 100644 --- a/crates/astria-sequencer/src/action_handler/impls/fee_change.rs +++ b/crates/astria-sequencer/src/action_handler/impls/fee_change.rs @@ -77,6 +77,12 @@ impl ActionHandler for FeeChange { Self::IbcSudoChange(fees) => state .put_fees(*fees) .wrap_err("failed to put ibc sudo change fees"), + Self::AddCurrencyPairs(fees) => state + .put_fees(*fees) + .wrap_err("failed to put add currency pairs fees"), + Self::RemoveCurrencyPairs(fees) => state + .put_fees(*fees) + .wrap_err("failed to put remove currency pairs fees"), } } } diff --git a/crates/astria-sequencer/src/action_handler/impls/mod.rs b/crates/astria-sequencer/src/action_handler/impls/mod.rs index c273cc26ff..040c3c8c05 100644 --- a/crates/astria-sequencer/src/action_handler/impls/mod.rs +++ b/crates/astria-sequencer/src/action_handler/impls/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod add_currency_pairs; pub(crate) mod bridge_lock; pub(crate) mod bridge_sudo_change; pub(crate) mod bridge_unlock; @@ -7,6 +8,7 @@ pub(crate) mod ibc_relayer_change; pub(crate) mod ibc_sudo_change; pub(crate) mod ics20_withdrawal; pub(crate) mod init_bridge_account; +pub(crate) mod remove_currency_pairs; pub(crate) mod rollup_data_submission; pub(crate) mod sudo_address_change; #[cfg(test)] diff --git a/crates/astria-sequencer/src/action_handler/impls/remove_currency_pairs.rs b/crates/astria-sequencer/src/action_handler/impls/remove_currency_pairs.rs new file mode 100644 index 0000000000..b717c7378a --- /dev/null +++ b/crates/astria-sequencer/src/action_handler/impls/remove_currency_pairs.rs @@ -0,0 +1,170 @@ +use astria_core::protocol::transaction::v1::action::RemoveCurrencyPairs; +use astria_eyre::eyre::{ + ensure, + OptionExt as _, + Result, + WrapErr as _, +}; +use async_trait::async_trait; +use cnidarium::StateWrite; + +use crate::{ + action_handler::ActionHandler, + connect::{ + market_map::state_ext::StateReadExt as _, + oracle::state_ext::{ + StateReadExt as _, + StateWriteExt, + }, + }, + transaction::StateReadExt as _, +}; + +#[async_trait] +impl ActionHandler for RemoveCurrencyPairs { + async fn check_stateless(&self) -> Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut 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" + ); + + let mut num_currency_pairs = state + .get_num_currency_pairs() + .await + .wrap_err("failed to get number of currency pairs")?; + ensure!( + num_currency_pairs >= self.pairs.len() as u64, + "cannot remove more currency pairs than exist", + ); + + for pair in &self.pairs { + if state + .remove_currency_pair(pair) + .await + .wrap_err("failed to delete currency pair")? + { + num_currency_pairs = num_currency_pairs + .checked_sub(1) + .ok_or_eyre("failed to decrement number of currency pairs")?; + } + } + + state + .put_num_currency_pairs(num_currency_pairs) + .wrap_err("failed to put number of currency pairs")?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use astria_core::{ + connect::{ + market_map::v2::Params, + oracle::v2::CurrencyPairState, + types::v2::{ + CurrencyPair, + CurrencyPairId, + CurrencyPairNonce, + }, + }, + primitive::v1::TransactionId, + protocol::transaction::v1::action::RemoveCurrencyPairs, + }; + use cnidarium::StateDelta; + + use super::*; + use crate::{ + app::test_utils::get_alice_signing_key, + benchmark_and_test_utils::astria_address, + connect::{ + market_map::state_ext::StateWriteExt as _, + oracle::state_ext::StateWriteExt as _, + }, + transaction::{ + StateWriteExt, + TransactionContext, + }, + }; + + #[tokio::test] + async fn remove_currency_pairs_with_duplicate() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let alice = get_alice_signing_key(); + let alice_address = astria_address(&alice.address_bytes()); + state.put_transaction_context(TransactionContext { + address_bytes: alice.address_bytes(), + transaction_id: TransactionId::new([0; 32]), + position_in_transaction: 0, + }); + + let pairs: Vec = vec![ + "BTC/USD".parse().unwrap(), + "ETH/USD".parse().unwrap(), + "BTC/USD".parse().unwrap(), + ]; + + state + .put_params(Params { + market_authorities: vec![], + admin: alice_address, + }) + .unwrap(); + state.put_num_currency_pairs(3).unwrap(); + state + .put_currency_pair_state( + pairs[0].clone(), + CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: CurrencyPairId::new(0), + }, + ) + .unwrap(); + state + .put_currency_pair_state( + pairs[1].clone(), + CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: CurrencyPairId::new(1), + }, + ) + .unwrap(); + state + .put_currency_pair_state( + "TIA/USD".parse().unwrap(), + CurrencyPairState { + price: None, + nonce: CurrencyPairNonce::new(0), + id: CurrencyPairId::new(2), + }, + ) + .unwrap(); + + let action = RemoveCurrencyPairs { + pairs: pairs.clone(), + }; + action.check_and_execute(&mut state).await.unwrap(); + + assert_eq!(state.get_num_currency_pairs().await.unwrap(), 1); + } +} diff --git a/crates/astria-sequencer/src/action_handler/impls/transaction.rs b/crates/astria-sequencer/src/action_handler/impls/transaction.rs index a5b66bf4fd..efe0ffdd47 100644 --- a/crates/astria-sequencer/src/action_handler/impls/transaction.rs +++ b/crates/astria-sequencer/src/action_handler/impls/transaction.rs @@ -141,6 +141,14 @@ impl ActionHandler for Transaction { .check_stateless() .await .wrap_err("stateless check failed for BridgeSudoChange action")?, + Action::AddCurrencyPairs(act) => act + .check_stateless() + .await + .wrap_err("stateless check failed for AddCurrencyPairs action")?, + Action::RemoveCurrencyPairs(act) => act + .check_stateless() + .await + .wrap_err("stateless check failed for RemoveCurrencyPairs action")?, } } Ok(()) @@ -267,6 +275,12 @@ impl ActionHandler for Transaction { Action::BridgeSudoChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing bridge sudo change")?, + Action::AddCurrencyPairs(act) => check_execute_and_pay_fees(act, &mut state) + .await + .wrap_err("failed executing add currency pairs")?, + Action::RemoveCurrencyPairs(act) => check_execute_and_pay_fees(act, &mut state) + .await + .wrap_err("failed executing remove currency pairs")?, } } 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 7ec8062d45..ceeeb57486 100644 --- a/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs +++ b/crates/astria-sequencer/src/app/benchmark_and_test_utils.rs @@ -18,6 +18,7 @@ use astria_core::{ GenesisAppState, }, transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -27,6 +28,7 @@ use astria_core::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -91,6 +93,8 @@ pub(crate) fn default_fees() -> astria_core::protocol::genesis::v1::GenesisFees ibc_relayer_change: Some(FeeComponents::::new(0, 0)), sudo_address_change: Some(FeeComponents::::new(0, 0)), ibc_sudo_change: Some(FeeComponents::::new(0, 0)), + add_currency_pairs: Some(FeeComponents::::new(0, 0)), + remove_currency_pairs: Some(FeeComponents::::new(0, 0)), } } @@ -140,7 +144,7 @@ pub(crate) fn proto_genesis_state() } } -pub(crate) fn genesis_state() -> GenesisAppState { +pub(crate) fn get_test_genesis_state() -> GenesisAppState { proto_genesis_state().try_into().unwrap() } @@ -159,7 +163,7 @@ pub(crate) async fn initialize_app_with_storage( .await .unwrap(); - let genesis_state = genesis_state.unwrap_or_else(self::genesis_state); + let genesis_state = genesis_state.unwrap_or_else(get_test_genesis_state); app.init_chain( storage.clone(), @@ -275,6 +279,7 @@ pub(crate) fn mock_state_put_account_nonce( state.put_account_nonce(address, nonce).unwrap(); } +#[expect(clippy::too_many_lines, reason = "this is needed for test set up")] pub(crate) async fn mock_state_getter() -> StateDelta { let storage = cnidarium::TempStorage::new().await.unwrap(); let snapshot = storage.latest_snapshot(); @@ -389,6 +394,18 @@ pub(crate) async fn mock_state_getter() -> StateDelta { .wrap_err("failed to initiate ibc sudo change fee components") .unwrap(); + let add_currency_pairs_fees = FeeComponents::::new(0, 0); + state + .put_fees(add_currency_pairs_fees) + .wrap_err("failed to initiate add currency pairs fee components") + .unwrap(); + + let remove_currency_pairs_fees = FeeComponents::::new(0, 0); + state + .put_fees(remove_currency_pairs_fees) + .wrap_err("failed to initiate remove currency pairs fee components") + .unwrap(); + // put denoms as allowed fee asset state.put_allowed_fee_asset(&denom_0()).unwrap(); 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 f6c9833e83..94ae75bee4 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 @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 78 expression: app.app_hash.as_bytes() --- [ + 131, + 167, + 88, + 101, + 218, + 254, + 19, + 102, + 147, + 63, + 205, + 234, + 31, + 196, + 47, + 222, + 20, + 84, 53, - 138, - 44, - 156, - 111, - 243, - 242, - 225, + 174, + 172, + 189, + 174, + 154, + 139, + 244, + 239, + 59, + 70, 251, - 114, - 33, - 182, - 11, - 173, - 107, - 58, - 204, - 168, - 230, - 127, - 25, - 143, - 211, - 168, - 91, - 18, - 167, - 229, - 221, - 230, - 107, - 85 + 95, + 70 ] 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 b8bbde8d49..091c59ce73 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 @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 353 expression: app.app_hash.as_bytes() --- [ - 36, + 153, + 195, 17, - 125, - 110, + 228, 25, - 11, - 235, - 211, - 94, + 148, + 197, + 236, + 36, + 26, + 230, + 230, + 127, + 10, 235, - 13, - 113, - 63, - 150, - 42, - 51, - 123, - 133, - 22, - 198, - 180, - 136, - 152, - 182, - 124, - 193, - 188, - 48, + 212, + 172, + 12, + 247, + 81, + 31, + 207, 9, - 215, - 118, - 202 + 46, + 5, + 124, + 216, + 184, + 167, + 175, + 212, + 152 ] 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 051cc94ebd..24b0fb555c 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 @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 160 expression: app.app_hash.as_bytes() --- [ - 16, - 234, - 26, - 31, - 30, - 140, - 161, - 180, 247, - 182, - 185, - 153, - 253, - 236, + 161, + 143, + 121, + 170, 69, - 32, - 60, - 164, - 33, - 20, - 135, - 188, - 98, - 107, - 198, - 164, - 95, - 223, - 50, - 124, + 5, + 151, + 193, + 59, + 28, 146, - 122 + 221, + 210, + 102, + 243, + 221, + 51, + 5, + 90, + 88, + 208, + 47, + 18, + 92, + 248, + 211, + 103, + 156, + 26, + 127, + 58 ] diff --git a/crates/astria-sequencer/src/app/tests_breaking_changes.rs b/crates/astria-sequencer/src/app/tests_breaking_changes.rs index 1e2b186712..709bcfc437 100644 --- a/crates/astria-sequencer/src/app/tests_breaking_changes.rs +++ b/crates/astria-sequencer/src/app/tests_breaking_changes.rs @@ -11,10 +11,12 @@ use std::{ collections::HashMap, + str::FromStr as _, sync::Arc, }; use astria_core::{ + connect::types::v2::CurrencyPair, primitive::v1::{ Address, RollupId, @@ -23,11 +25,13 @@ use astria_core::{ genesis::v1::Account, transaction::v1::{ action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, IbcRelayerChange, IbcSudoChange, + RemoveCurrencyPairs, RollupDataSubmission, Transfer, ValidatorUpdate, @@ -361,6 +365,36 @@ async fn app_execute_transaction_with_every_action_snapshot() { let signed_tx = Arc::new(tx_bridge.sign(&bridge)); app.execute_transaction(signed_tx).await.unwrap(); + let currency_pair_tia = CurrencyPair::from_str("TIA/USD").unwrap(); + let currency_pair_eth = CurrencyPair::from_str("ETH/USD").unwrap(); + let tx = TransactionBody::builder() + .actions(vec![ + AddCurrencyPairs { + pairs: vec![currency_pair_tia.clone(), currency_pair_eth.clone()], + } + .into(), + ]) + .chain_id("test") + .nonce(4) + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.sign(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + + let tx = TransactionBody::builder() + .actions(vec![ + RemoveCurrencyPairs { + pairs: vec![currency_pair_tia.clone()], + } + .into(), + ]) + .chain_id("test") + .nonce(5) + .try_build() + .unwrap(); + let signed_tx = Arc::new(tx.sign(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + let sudo_address = app.state.get_sudo_address().await.unwrap(); app.end_block(1, &sudo_address).await.unwrap(); diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 7f5d778e36..c4790b130e 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -1,6 +1,10 @@ -use std::sync::Arc; +use std::{ + str::FromStr, + sync::Arc, +}; use astria_core::{ + connect::types::v2::CurrencyPair, crypto::SigningKey, primitive::v1::{ asset, @@ -12,10 +16,12 @@ use astria_core::{ genesis::v1::GenesisAppState, transaction::v1::{ action::{ + AddCurrencyPairs, BridgeLock, BridgeUnlock, IbcRelayerChange, IbcSudoChange, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -33,6 +39,7 @@ use cnidarium::{ ArcStateDeltaExt as _, StateDelta, }; +use futures::StreamExt as _; use super::test_utils::get_alice_signing_key; use crate::{ @@ -64,6 +71,7 @@ use crate::{ StateReadExt as _, StateWriteExt as _, }, + connect::oracle::state_ext::StateReadExt, fees::{ StateReadExt as _, StateWriteExt as _, @@ -1325,3 +1333,59 @@ async fn ensure_all_event_attributes_are_indexed() { ); }); } + +#[tokio::test] +async fn test_app_execute_transaction_add_and_remove_currency_pairs() { + let alice = get_alice_signing_key(); + let alice_address = astria_address(&alice.address_bytes()); + + let mut app = initialize_app(Some(genesis_state()), vec![]).await; + + let currency_pair = CurrencyPair::from_str("TIA/USD").unwrap(); + + let tx = TransactionBody::builder() + .actions(vec![ + AddCurrencyPairs { + pairs: vec![currency_pair.clone()], + } + .into(), + ]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + assert_eq!( + app.state.get_account_nonce(&alice_address).await.unwrap(), + 1 + ); + + let currency_pairs: Vec> = + app.state.currency_pairs().collect().await; + assert_eq!(currency_pairs.len(), 1); + assert_eq!(currency_pairs[0].as_ref().unwrap(), ¤cy_pair); + + let tx = TransactionBody::builder() + .actions(vec![ + RemoveCurrencyPairs { + pairs: vec![currency_pair.clone()], + } + .into(), + ]) + .chain_id("test") + .nonce(1) + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + assert_eq!( + app.state.get_account_nonce(&alice_address).await.unwrap(), + 2 + ); + + let currency_pairs: Vec> = + app.state.currency_pairs().collect().await; + assert_eq!(currency_pairs.len(), 0); +} diff --git a/crates/astria-sequencer/src/app/vote_extension.rs b/crates/astria-sequencer/src/app/vote_extension.rs index 12ca11af30..4a73ef235e 100644 --- a/crates/astria-sequencer/src/app/vote_extension.rs +++ b/crates/astria-sequencer/src/app/vote_extension.rs @@ -123,10 +123,9 @@ impl Handler { 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 max_num_currency_pairs = DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state) + .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, @@ -225,10 +224,9 @@ impl ProposalHandler { )); } - let max_num_currency_pairs = - DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state, true) - .await - .wrap_err("failed to get max number of currency pairs")?; + let max_num_currency_pairs = DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state) + .await + .wrap_err("failed to get max number of currency pairs")?; let mut all_currency_pair_ids = HashSet::new(); for vote in &mut extended_commit_info.votes { @@ -310,10 +308,9 @@ impl ProposalHandler { .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")?; + let max_num_currency_pairs = DefaultCurrencyPairStrategy::get_max_num_currency_pairs(state) + .await + .wrap_err("failed to get max number of currency pairs")?; let mut all_currency_pair_ids = HashSet::new(); for vote in &extended_commit_info.votes { @@ -854,14 +851,14 @@ mod test { for (pair, pair_id) in [pair_0(), pair_1(), pair_2()] { let pair_state = CurrencyPairState { - price: QuotePrice { + price: Some(QuotePrice { price: Price::new(123), block_timestamp: Timestamp { seconds: 4, nanos: 5, }, block_height: 1, - }, + }), nonce: CurrencyPairNonce::new(1), id: pair_id, }; diff --git a/crates/astria-sequencer/src/connect/oracle/component.rs b/crates/astria-sequencer/src/connect/oracle/component.rs index 8cfc1824f1..a9c9b6c2fe 100644 --- a/crates/astria-sequencer/src/connect/oracle/component.rs +++ b/crates/astria-sequencer/src/connect/oracle/component.rs @@ -47,9 +47,6 @@ impl Component for OracleComponent { 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(()) } diff --git a/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs b/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs index 3e21daddc8..27bc777ce3 100644 --- a/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs +++ b/crates/astria-sequencer/src/connect/oracle/currency_pair_strategy.rs @@ -27,23 +27,14 @@ impl DefaultCurrencyPairStrategy { 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 + pub(crate) async fn get_max_num_currency_pairs(state: &S) -> Result { + // unlike the skip implementation, we don't need to track removed currency pairs + // from the previous block as we execute our transactions during the proposal phase, + // before vote extensions are broadcast. thus by the time we're making our VE, we + // already have the updated state for that block. + 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) - } + .wrap_err("failed to get number of currency pairs") } } diff --git a/crates/astria-sequencer/src/connect/oracle/state_ext.rs b/crates/astria-sequencer/src/connect/oracle/state_ext.rs index 19b16f543a..1d2428130a 100644 --- a/crates/astria-sequencer/src/connect/oracle/state_ext.rs +++ b/crates/astria-sequencer/src/connect/oracle/state_ext.rs @@ -195,21 +195,6 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("invalid number of currency pairs bytes") } - #[instrument(skip_all)] - async fn get_num_removed_currency_pairs(&self) -> Result { - let Some(bytes) = self - .get_raw(keys::NUM_REMOVED_CURRENCY_PAIRS) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading number of removed currency pairs from state")? - else { - return Ok(0); - }; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::Count::try_from(value).map(u64::from)) - .wrap_err("invalid number of removed currency pairs bytes") - } - #[instrument(skip_all)] async fn get_currency_pair_state( &self, @@ -230,6 +215,21 @@ pub(crate) trait StateReadExt: StateRead { }) .wrap_err("invalid currency pair state bytes") } + + #[instrument(skip_all)] + async fn get_next_currency_pair_id(&self) -> Result { + let Some(bytes) = self + .get_raw(keys::NEXT_CURRENCY_PAIR_ID) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading next currency pair id from state")? + else { + return Ok(CurrencyPairId::new(0)); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| storage::CurrencyPairId::try_from(value).map(CurrencyPairId::from)) + .wrap_err("invalid next currency pair id bytes") + } } impl StateReadExt for T {} @@ -245,15 +245,6 @@ pub(crate) trait StateWriteExt: StateWrite { Ok(()) } - #[instrument(skip_all)] - fn put_num_removed_currency_pairs(&mut self, num_removed_currency_pairs: u64) -> Result<()> { - let bytes = StoredValue::from(storage::Count::from(num_removed_currency_pairs)) - .serialize() - .wrap_err("failed to serialize number of removed currency pairs")?; - self.put_raw(keys::NUM_REMOVED_CURRENCY_PAIRS.to_string(), bytes); - Ok(()) - } - #[instrument(skip_all)] fn put_currency_pair_state( &mut self, @@ -292,21 +283,22 @@ pub(crate) trait StateWriteExt: StateWrite { .await .wrap_err("failed to get currency pair state")? { - state.price = price; + state.price = Some(price); state.nonce = state .nonce .increment() .ok_or_eyre("increment nonce overflowed")?; state } else { - let id = get_next_currency_pair_id(self) + 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, + price: Some(price), nonce: CurrencyPairNonce::new(0), id, } @@ -314,25 +306,25 @@ pub(crate) trait StateWriteExt: StateWrite { self.put_currency_pair_state(currency_pair, state) .wrap_err("failed to put currency pair state") } + + #[instrument(skip_all)] + async fn remove_currency_pair(&mut self, currency_pair: &CurrencyPair) -> Result { + let Some(id) = self + .get_currency_pair_id(currency_pair) + .await + .wrap_err("failed to get currency pair ID")? + else { + return Ok(false); + }; + self.delete(keys::currency_pair_to_id(currency_pair)); + self.delete(keys::id_to_currency_pair(id)); + self.delete(keys::currency_pair_state(currency_pair)); + Ok(true) + } } impl StateWriteExt for T {} -#[instrument(skip_all)] -async fn get_next_currency_pair_id(state: &T) -> Result { - let Some(bytes) = state - .get_raw(keys::NEXT_CURRENCY_PAIR_ID) - .await - .map_err(anyhow_to_eyre) - .wrap_err("failed reading next currency pair id from state")? - else { - return Ok(CurrencyPairId::new(0)); - }; - StoredValue::deserialize(&bytes) - .and_then(|value| storage::CurrencyPairId::try_from(value).map(CurrencyPairId::from)) - .wrap_err("invalid next currency pair id bytes") -} - #[instrument(skip_all)] fn put_currency_pair_id( state: &mut T, @@ -391,14 +383,14 @@ mod tests { fn currency_pair_state(id: u64, nonce: u64) -> CurrencyPairState { CurrencyPairState { - price: QuotePrice { + price: Some(QuotePrice { price: Price::new(123), block_timestamp: Timestamp { seconds: 4, nanos: 5, }, block_height: nonce.checked_add(10).unwrap(), - }, + }), nonce: CurrencyPairNonce::new(nonce), id: CurrencyPairId::new(id), } @@ -557,33 +549,4 @@ mod tests { .expect("should not error"); assert_eq!(2, retrieved_count); } - - #[tokio::test] - async fn should_put_and_get_num_removed_currency_pairs() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // Getting should return `0` when no count is stored. - assert_eq!(state.get_num_removed_currency_pairs().await.unwrap(), 0); - - // Putting a count should succeed. - state.put_num_removed_currency_pairs(1).unwrap(); - - // Getting the stored count should succeed. - let retrieved_count = state - .get_num_removed_currency_pairs() - .await - .expect("should not error"); - assert_eq!(1, retrieved_count); - - // Putting a new count should overwrite the first. - state.put_num_removed_currency_pairs(2).unwrap(); - - let retrieved_count = state - .get_num_removed_currency_pairs() - .await - .expect("should not error"); - assert_eq!(2, retrieved_count); - } } diff --git a/crates/astria-sequencer/src/connect/oracle/storage/keys.rs b/crates/astria-sequencer/src/connect/oracle/storage/keys.rs index b37e2493ac..bb81fdf22c 100644 --- a/crates/astria-sequencer/src/connect/oracle/storage/keys.rs +++ b/crates/astria-sequencer/src/connect/oracle/storage/keys.rs @@ -16,8 +16,6 @@ pub(in crate::connect::oracle) const CURRENCY_PAIR_STATE_PREFIX: &str = "connect/oracle/currency_pair_state/"; pub(in crate::connect::oracle) const NUM_CURRENCY_PAIRS: &str = "connect/oracle/num_currency_pairs"; -pub(in crate::connect::oracle) const NUM_REMOVED_CURRENCY_PAIRS: &str = - "connect/oracle/num_removed_currency_pairs"; pub(in crate::connect::oracle) const NEXT_CURRENCY_PAIR_ID: &str = "connect/oracle/next_currency_pair_id"; @@ -83,7 +81,6 @@ mod tests { #[test] fn keys_should_not_change() { insta::assert_snapshot!("num_currency_pairs_key", NUM_CURRENCY_PAIRS); - insta::assert_snapshot!("num_removed_currency_pairs_key", NUM_REMOVED_CURRENCY_PAIRS); insta::assert_snapshot!("next_currency_pair_id_key", NEXT_CURRENCY_PAIR_ID); insta::assert_snapshot!( "currency_pair_to_id_key", @@ -102,7 +99,6 @@ mod tests { #[test] fn keys_should_have_component_prefix() { assert!(NUM_CURRENCY_PAIRS.starts_with(COMPONENT_PREFIX)); - assert!(NUM_REMOVED_CURRENCY_PAIRS.starts_with(COMPONENT_PREFIX)); assert!(NEXT_CURRENCY_PAIR_ID.starts_with(COMPONENT_PREFIX)); assert!(currency_pair_to_id(¤cy_pair()).starts_with(COMPONENT_PREFIX)); assert!(id_to_currency_pair(CurrencyPairId::new(9)).starts_with(COMPONENT_PREFIX)); diff --git a/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs b/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs index 98b3cd7f6f..6dc19ad75f 100644 --- a/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs +++ b/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs @@ -77,7 +77,7 @@ impl From for DomainQuotePrice { #[derive(Debug, BorshSerialize, BorshDeserialize)] pub(in crate::connect::oracle) struct CurrencyPairState { - price: QuotePrice, + price: Option, nonce: u64, id: CurrencyPairId, } @@ -85,7 +85,7 @@ pub(in crate::connect::oracle) struct CurrencyPairState { impl From for CurrencyPairState { fn from(state: DomainCurrencyPairState) -> Self { CurrencyPairState { - price: QuotePrice::from(state.price), + price: state.price.map(QuotePrice::from), nonce: state.nonce.get(), id: CurrencyPairId::from(state.id), } @@ -95,7 +95,7 @@ impl From for CurrencyPairState { impl From for DomainCurrencyPairState { fn from(state: CurrencyPairState) -> Self { Self { - price: DomainQuotePrice::from(state.price), + price: state.price.map(DomainQuotePrice::from), nonce: CurrencyPairNonce::new(state.nonce), id: DomainCurrencyPairId::from(state.id), } diff --git a/crates/astria-sequencer/src/fees/component.rs b/crates/astria-sequencer/src/fees/component.rs index e22e4480d7..9b9120e051 100644 --- a/crates/astria-sequencer/src/fees/component.rs +++ b/crates/astria-sequencer/src/fees/component.rs @@ -130,6 +130,20 @@ impl Component for FeesComponent { .wrap_err("failed to store ibc sudo change fee components")?; } + let add_currency_pairs_fees = app_state.fees().add_currency_pairs; + if let Some(add_currency_pairs_fees) = add_currency_pairs_fees { + state + .put_fees(add_currency_pairs_fees) + .wrap_err("failed to store add currency pairs fee components")?; + } + + let remove_currency_pairs_fees = app_state.fees().remove_currency_pairs; + if let Some(remove_currency_pairs_fees) = remove_currency_pairs_fees { + state + .put_fees(remove_currency_pairs_fees) + .wrap_err("failed to store remove currency pairs fee components")?; + } + Ok(()) } diff --git a/crates/astria-sequencer/src/fees/mod.rs b/crates/astria-sequencer/src/fees/mod.rs index ba6557d522..d055e47f4a 100644 --- a/crates/astria-sequencer/src/fees/mod.rs +++ b/crates/astria-sequencer/src/fees/mod.rs @@ -3,6 +3,7 @@ use astria_core::{ protocol::{ fees::v1::FeeComponents, transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -12,6 +13,7 @@ use astria_core::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -458,6 +460,50 @@ impl FeeHandler for IbcRelay { } } +impl FeeHandler for AddCurrencyPairs { + fn name() -> &'static str { + ::Raw::NAME + } + + fn full_name() -> String { + ::full_name() + } + + fn snake_case_name() -> &'static str { + "add_currency_pairs" + } + + fn variable_component(&self) -> u128 { + 0 + } + + fn fee_asset(&self) -> Option<&asset::Denom> { + None + } +} + +impl FeeHandler for RemoveCurrencyPairs { + fn name() -> &'static str { + ::Raw::NAME + } + + fn full_name() -> String { + ::full_name() + } + + fn snake_case_name() -> &'static str { + "remove_currency_pairs" + } + + fn variable_component(&self) -> u128 { + 0 + } + + fn fee_asset(&self) -> Option<&asset::Denom> { + None + } +} + /// Returns a modified byte length of the deposit event. Length is calculated with reasonable values /// for all fields except `asset` and `destination_chain_address`, ergo it may not be representative /// of on-wire length. diff --git a/crates/astria-sequencer/src/fees/query.rs b/crates/astria-sequencer/src/fees/query.rs index d4b28dc768..00473f18fa 100644 --- a/crates/astria-sequencer/src/fees/query.rs +++ b/crates/astria-sequencer/src/fees/query.rs @@ -15,6 +15,7 @@ use astria_core::{ fees::v1::FeeComponents, transaction::v1::{ action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -24,6 +25,7 @@ use astria_core::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -299,6 +301,10 @@ pub(crate) async fn get_fees_for_transaction( OnceCell::new(); let fee_asset_change_fees: OnceCell>> = OnceCell::new(); let fee_change_fees: OnceCell>> = OnceCell::new(); + let add_currency_pairs_fees: OnceCell>> = + OnceCell::new(); + let remove_currency_pairs_fees: OnceCell>> = + OnceCell::new(); let mut fees_by_asset = HashMap::new(); for action in tx.actions() { @@ -359,6 +365,14 @@ pub(crate) async fn get_fees_for_transaction( let fees = get_or_init_fees(state, &fee_change_fees).await?; calculate_and_add_fees(act, &mut fees_by_asset, fees); } + Action::AddCurrencyPairs(act) => { + let fees = get_or_init_fees(state, &add_currency_pairs_fees).await?; + calculate_and_add_fees(act, &mut fees_by_asset, fees); + } + Action::RemoveCurrencyPairs(act) => { + let fees = get_or_init_fees(state, &remove_currency_pairs_fees).await?; + calculate_and_add_fees(act, &mut fees_by_asset, fees); + } } } Ok(fees_by_asset) diff --git a/crates/astria-sequencer/src/fees/storage/values.rs b/crates/astria-sequencer/src/fees/storage/values.rs index 138b2f8cbd..edf6b3ca86 100644 --- a/crates/astria-sequencer/src/fees/storage/values.rs +++ b/crates/astria-sequencer/src/fees/storage/values.rs @@ -1,6 +1,7 @@ use astria_core::protocol::{ fees::v1::FeeComponents as DomainFeeComponents, transaction::v1::action::{ + AddCurrencyPairs, BridgeLock, BridgeSudoChange, BridgeUnlock, @@ -10,6 +11,7 @@ use astria_core::protocol::{ IbcSudoChange, Ics20Withdrawal, InitBridgeAccount, + RemoveCurrencyPairs, RollupDataSubmission, SudoAddressChange, Transfer, @@ -46,6 +48,8 @@ enum ValueImpl { IbcRelayerChangeFees(FeeComponents), IbcSudoChangeFees(FeeComponents), SudoAddressChangeFees(FeeComponents), + AddCurrencyPairsFees(FeeComponents), + RemoveCurrencyPairsFees(FeeComponents), } macro_rules! impl_from_for_fee_storage { @@ -109,4 +113,6 @@ impl_from_for_fee_storage!( DomainFeeComponents => IbcRelayerChangeFees, DomainFeeComponents => IbcSudoChangeFees, DomainFeeComponents => SudoAddressChangeFees, + DomainFeeComponents => AddCurrencyPairsFees, + DomainFeeComponents => RemoveCurrencyPairsFees, ); diff --git a/crates/astria-sequencer/src/grpc/connect.rs b/crates/astria-sequencer/src/grpc/connect.rs index 2ebb6c77ac..ea4efa7a8e 100644 --- a/crates/astria-sequencer/src/grpc/connect.rs +++ b/crates/astria-sequencer/src/grpc/connect.rs @@ -206,7 +206,9 @@ impl OracleService for SequencerServer { }; Ok(Response::new(GetPriceResponse { - price: Some(state.price.into_raw()), + price: state + .price + .map(astria_core::connect::oracle::v2::QuotePrice::into_raw), nonce: state.nonce.get(), id: state.id.get(), decimals: market.ticker.decimals, @@ -260,7 +262,9 @@ impl OracleService for SequencerServer { }; prices.push(GetPriceResponse { - price: Some(state.price.into_raw()), + price: state + .price + .map(astria_core::connect::oracle::v2::QuotePrice::into_raw), nonce: state.nonce.get(), id: state.id.get(), decimals: market.ticker.decimals, diff --git a/crates/astria-sequencer/src/service/mempool/tests.rs b/crates/astria-sequencer/src/service/mempool/tests.rs index b85f41df7c..631bfc827e 100644 --- a/crates/astria-sequencer/src/service/mempool/tests.rs +++ b/crates/astria-sequencer/src/service/mempool/tests.rs @@ -13,7 +13,7 @@ use tendermint::{ use crate::{ app::{ - benchmark_and_test_utils::genesis_state, + benchmark_and_test_utils::get_test_genesis_state, test_utils::MockTxBuilder, App, }, @@ -38,7 +38,7 @@ async fn future_nonces_are_accepted() { app.init_chain( storage.clone(), - genesis_state(), + get_test_genesis_state(), vec![], "test".to_string(), 0, @@ -76,7 +76,7 @@ async fn rechecks_pass() { app.init_chain( storage.clone(), - genesis_state(), + get_test_genesis_state(), vec![], "test".to_string(), 0, @@ -122,7 +122,7 @@ async fn can_reinsert_after_recheck_fail() { app.init_chain( storage.clone(), - genesis_state(), + get_test_genesis_state(), vec![], "test".to_string(), 0, @@ -178,7 +178,7 @@ async fn recheck_adds_non_tracked_tx() { app.init_chain( storage.clone(), - genesis_state(), + get_test_genesis_state(), vec![], "test".to_string(), 0, diff --git a/crates/astria-sequencer/src/transaction/checks.rs b/crates/astria-sequencer/src/transaction/checks.rs index bff86e838e..58bdc60772 100644 --- a/crates/astria-sequencer/src/transaction/checks.rs +++ b/crates/astria-sequencer/src/transaction/checks.rs @@ -114,7 +114,9 @@ pub(crate) async fn get_total_transaction_cost( | Action::Ibc(_) | Action::IbcRelayerChange(_) | Action::FeeAssetChange(_) - | Action::FeeChange(_) => { + | Action::FeeChange(_) + | Action::AddCurrencyPairs(_) + | Action::RemoveCurrencyPairs(_) => { continue; } } diff --git a/proto/protocolapis/astria/protocol/fees/v1/types.proto b/proto/protocolapis/astria/protocol/fees/v1/types.proto index 1d3edc2701..5f5cdedefd 100644 --- a/proto/protocolapis/astria/protocol/fees/v1/types.proto +++ b/proto/protocolapis/astria/protocol/fees/v1/types.proto @@ -79,6 +79,16 @@ message IbcSudoChangeFeeComponents { astria.primitive.v1.Uint128 multiplier = 2; } +message AddCurrencyPairsFeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + +message RemoveCurrencyPairsFeeComponents { + 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/genesis/v1/types.proto b/proto/protocolapis/astria/protocol/genesis/v1/types.proto index 2fb05eb0ea..d6eb4f26bf 100644 --- a/proto/protocolapis/astria/protocol/genesis/v1/types.proto +++ b/proto/protocolapis/astria/protocol/genesis/v1/types.proto @@ -59,6 +59,8 @@ message GenesisFees { astria.protocol.fees.v1.SudoAddressChangeFeeComponents sudo_address_change = 12; astria.protocol.fees.v1.TransferFeeComponents transfer = 13; astria.protocol.fees.v1.ValidatorUpdateFeeComponents validator_update = 14; + astria.protocol.fees.v1.AddCurrencyPairsFeeComponents add_currency_pairs = 15; + astria.protocol.fees.v1.RemoveCurrencyPairsFeeComponents remove_currency_pairs = 16; } message ConnectGenesis { diff --git a/proto/protocolapis/astria/protocol/transaction/v1/action.proto b/proto/protocolapis/astria/protocol/transaction/v1/action.proto index 26cab7579d..03177e55cd 100644 --- a/proto/protocolapis/astria/protocol/transaction/v1/action.proto +++ b/proto/protocolapis/astria/protocol/transaction/v1/action.proto @@ -6,6 +6,7 @@ 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/types/v2/currency_pair.proto"; message Action { oneof value { @@ -23,13 +24,17 @@ message Action { astria_vendored.penumbra.core.component.ibc.v1.IbcRelay ibc = 21; Ics20Withdrawal ics20_withdrawal = 22; - // POA sudo actions are defined on 50-60 + // POA sudo actions are defined on 50-70 SudoAddressChange sudo_address_change = 50; astria_vendored.tendermint.abci.ValidatorUpdate validator_update = 51; IbcRelayerChange ibc_relayer_change = 52; FeeAssetChange fee_asset_change = 53; FeeChange fee_change = 55; IbcSudoChange ibc_sudo_change = 56; + + // Oracle actions are defined on 71-80 + AddCurrencyPairs add_currency_pairs = 71; + RemoveCurrencyPairs remove_currency_pairs = 72; } } @@ -228,9 +233,19 @@ message FeeChange { astria.protocol.fees.v1.SudoAddressChangeFeeComponents sudo_address_change = 12; astria.protocol.fees.v1.TransferFeeComponents transfer = 13; astria.protocol.fees.v1.ValidatorUpdateFeeComponents validator_update = 14; + astria.protocol.fees.v1.AddCurrencyPairsFeeComponents add_currency_pairs = 15; + astria.protocol.fees.v1.RemoveCurrencyPairsFeeComponents remove_currency_pairs = 16; } } message IbcSudoChange { astria.primitive.v1.Address new_address = 1; } + +message AddCurrencyPairs { + repeated connect.types.v2.CurrencyPair pairs = 1; +} + +message RemoveCurrencyPairs { + repeated connect.types.v2.CurrencyPair pairs = 1; +}