Skip to content

Commit

Permalink
feat(sequencer): add allowed_fee_asset_ids abci query and `sequence…
Browse files Browse the repository at this point in the history
…r_client` support (#1127)

## Summary
This adds the `allowed_fee_asset_ids` ABCI info query and the required
`sequencer_client` methods to use it.

## Background
This is required for the bridge implementation and other off-chain
services to check against local configs on startup.

## Changes
- [x] add `FeeAssetsResponse` to
`protocolapis/astria/protocol/asset/types.proto`
- [x] add native type to `astria-core::protocol::asset
- [x] add query function to `astria-core::protocol::asset::query`
- [x] add query function to query router
- [x] add unit tests
    - [x] response into/from raw
    - [x] query
- [x] missing unit tests for `DenomResponse::into_raw/from_raw`
(unrelated but this was on the way and took 2 seconds)

### sequencer client
- [x] add query method to the client extension crate
- [x] should the `try_from_raw` failure return a different error kind
than `abci_query_deserialization`
- [x] blackbox? integration? test

## Testing
- [x] unit tests for raw protobuf types to native types conversions
- [x] unit test for the `sequencer`-side logic for the abci query
- [x] blackbox/integration test for the `sequencer_client`
  • Loading branch information
itamarreif authored Jun 4, 2024
1 parent 3a968fb commit f0acb1c
Show file tree
Hide file tree
Showing 8 changed files with 456 additions and 6 deletions.
16 changes: 16 additions & 0 deletions crates/astria-core/src/generated/astria.protocol.asset.v1alpha1.rs

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

211 changes: 210 additions & 1 deletion crates/astria-core/src/protocol/asset/v1alpha1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use super::raw;
use crate::primitive::v1::asset::Denom;
use crate::primitive::v1::asset::{
self,
Denom,
IncorrectAssetIdLength,
};

/// The sequencer response to a denomination request for a given asset ID.
#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -60,3 +64,208 @@ impl raw::DenomResponse {
self.clone().into_native()
}
}

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct AllowedFeeAssetIdsResponseError(AllowedFeeAssetIdsResponseErrorKind);

#[derive(Debug, thiserror::Error)]
enum AllowedFeeAssetIdsResponseErrorKind {
#[error("failed to convert asset ID")]
IncorrectAssetIdLength(#[source] IncorrectAssetIdLength),
}

impl AllowedFeeAssetIdsResponseError {
fn incorrect_asset_id_length(inner: IncorrectAssetIdLength) -> Self {
Self(AllowedFeeAssetIdsResponseErrorKind::IncorrectAssetIdLength(
inner,
))
}
}

#[derive(Debug, PartialEq, Clone)]
pub struct AllowedFeeAssetIdsResponse {
pub height: u64,
pub fee_asset_ids: Vec<asset::Id>,
}

impl AllowedFeeAssetIdsResponse {
/// Converts a protobuf [`raw::AllowedFeeAssetIdsResponse`] to an astria
/// native [`AllowedFeeAssetIdsResponse`].
///
/// # Errors
/// - If one of the serialized asset IDs cannot be converted to a [`asset::Id`].
pub fn try_from_raw(
proto: &raw::AllowedFeeAssetIdsResponse,
) -> Result<Self, AllowedFeeAssetIdsResponseError> {
let raw::AllowedFeeAssetIdsResponse {
height,
fee_asset_ids,
} = proto;
let mut assets: Vec<asset::Id> = Vec::new();

for raw_id in fee_asset_ids {
let native_id = asset::Id::try_from_slice(raw_id)
.map_err(AllowedFeeAssetIdsResponseError::incorrect_asset_id_length)?;
assets.push(native_id);
}

Ok(Self {
height: *height,
fee_asset_ids: assets,
})
}

/// Converts an astria native [`AllowedFeeAssetIdsResponse`] to a
/// protobuf [`raw::AllowedFeeAssetIdsResponse`].
#[must_use]
pub fn into_raw(self) -> raw::AllowedFeeAssetIdsResponse {
raw::AllowedFeeAssetIdsResponse::from_native(self)
}
}

impl raw::AllowedFeeAssetIdsResponse {
/// Converts an astria native [`AllowedFeeAssetIdsResponse`] to a
/// protobuf [`raw::AllowedFeeAssetIdsResponse`].
#[must_use]
pub fn from_native(native: AllowedFeeAssetIdsResponse) -> Self {
let AllowedFeeAssetIdsResponse {
height,
fee_asset_ids,
} = native;
let raw_assets = fee_asset_ids
.into_iter()
.map(|id| id.as_ref().to_vec().into())
.collect();
Self {
height,
fee_asset_ids: raw_assets,
}
}

/// Converts a protobuf [`raw::AllowedFeeAssetIdsResponse`] to an astria
/// native [`AllowedFeeAssetIdsResponse`].
///
/// # Errors
/// - If one of the serialized asset IDs cannot be converted to a [`asset::Id`].
pub fn try_into_native(
self,
) -> Result<AllowedFeeAssetIdsResponse, AllowedFeeAssetIdsResponseError> {
AllowedFeeAssetIdsResponse::try_from_raw(&self)
}

/// Converts a protobuf [`raw::AllowedFeeAssetIdsResponse`] to an astria
/// native [`AllowedFeeAssetIdsResponse`] by allocating a new
/// [`v1alpha1::AllowedFeeAssetIdsResponse`].
///
/// # Errors
/// - If one of the serialized asset IDs cannot be converted to a [`asset::Id`].
pub fn try_to_native(
&self,
) -> Result<AllowedFeeAssetIdsResponse, AllowedFeeAssetIdsResponseError> {
self.clone().try_into_native()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn denom_response_from_raw_is_correct() {
let raw = raw::DenomResponse {
height: 42,
denom: "nria".to_owned(),
};
let expected = DenomResponse {
height: 42,
denom: "nria".to_owned().into(),
};
let actual = DenomResponse::from_raw(&raw);
assert_eq!(expected, actual);
}

#[test]
fn denom_response_into_raw_is_correct() {
let native = DenomResponse {
height: 42,
denom: "nria".to_owned().into(),
};
let expected = raw::DenomResponse {
height: 42,
denom: "nria".to_owned(),
};
let actual = native.into_raw();
assert_eq!(expected, actual);
}

#[test]
fn denom_response_roundtrip_is_correct() {
let native = DenomResponse {
height: 42,
denom: "nria".to_owned().into(),
};
let expected = native.clone();
let actual = native.into_raw().into_native();
assert_eq!(expected, actual);
}

#[test]
fn allowed_fee_asset_ids_try_from_raw_is_correct() {
let raw = raw::AllowedFeeAssetIdsResponse {
height: 42,
fee_asset_ids: vec![
asset::Id::from_denom("asset_0").get().to_vec().into(),
asset::Id::from_denom("asset_1").get().to_vec().into(),
asset::Id::from_denom("asset_2").get().to_vec().into(),
],
};
let expected = AllowedFeeAssetIdsResponse {
height: 42,
fee_asset_ids: vec![
asset::Id::from_denom("asset_0"),
asset::Id::from_denom("asset_1"),
asset::Id::from_denom("asset_2"),
],
};
let actual = AllowedFeeAssetIdsResponse::try_from_raw(&raw).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn allowed_fee_asset_ids_into_raw_is_correct() {
let native = AllowedFeeAssetIdsResponse {
height: 42,
fee_asset_ids: vec![
asset::Id::from_denom("asset_0"),
asset::Id::from_denom("asset_1"),
asset::Id::from_denom("asset_2"),
],
};
let expected = raw::AllowedFeeAssetIdsResponse {
height: 42,
fee_asset_ids: vec![
asset::Id::from_denom("asset_0").get().to_vec().into(),
asset::Id::from_denom("asset_1").get().to_vec().into(),
asset::Id::from_denom("asset_2").get().to_vec().into(),
],
};
let actual = native.into_raw();
assert_eq!(expected, actual);
}

#[test]
fn allowed_fee_asset_ids_roundtrip_is_correct() {
let native = AllowedFeeAssetIdsResponse {
height: 42,
fee_asset_ids: vec![
asset::Id::from_denom("asset_0"),
asset::Id::from_denom("asset_1"),
asset::Id::from_denom("asset_2"),
],
};
let expected = native.clone();
let actual = native.into_raw().try_into_native().unwrap();
assert_eq!(expected, actual);
}
}
61 changes: 59 additions & 2 deletions crates/astria-sequencer-client/src/extension_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use std::{
sync::Arc,
};

use astria_core::protocol::asset::v1alpha1::AllowedFeeAssetIdsResponse;
pub use astria_core::{
primitive::v1::Address,
protocol::{
Expand Down Expand Up @@ -93,6 +94,7 @@ impl std::error::Error for Error {
match &self.inner {
ErrorKind::AbciQueryDeserialization(e) => Some(e),
ErrorKind::TendermintRpc(e) => Some(e),
ErrorKind::NativeConversion(e) => Some(e),
}
}
}
Expand All @@ -108,7 +110,7 @@ impl Error {
pub fn as_tendermint_rpc(&self) -> Option<&TendermintRpcError> {
match self.kind() {
ErrorKind::TendermintRpc(e) => Some(e),
ErrorKind::AbciQueryDeserialization(_) => None,
ErrorKind::AbciQueryDeserialization(_) | ErrorKind::NativeConversion(_) => None,
}
}

Expand All @@ -129,6 +131,16 @@ impl Error {
inner: ErrorKind::tendermint_rpc(rpc, inner),
}
}

/// Convenience function to construct `Error` containing a `DeserializationError`.
fn native_conversion(
target: &'static str,
inner: Arc<dyn std::error::Error + Send + Sync>,
) -> Self {
Self {
inner: ErrorKind::native_conversion(target, inner),
}
}
}

/// Error if deserialization of the bytes in an abci query response failed.
Expand Down Expand Up @@ -229,7 +241,7 @@ impl std::fmt::Display for DeserializationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"failed deserializing cometbft response to {}",
"failed deserializing raw protobuf response to {}",
self.target,
)
}
Expand All @@ -248,6 +260,7 @@ impl std::error::Error for DeserializationError {
pub enum ErrorKind {
AbciQueryDeserialization(AbciQueryDeserializationError),
TendermintRpc(TendermintRpcError),
NativeConversion(DeserializationError),
}

impl ErrorKind {
Expand All @@ -271,6 +284,17 @@ impl ErrorKind {
rpc,
})
}

/// Convenience method to construct a `NativeConversion` variant.
fn native_conversion(
target: &'static str,
inner: Arc<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::NativeConversion(DeserializationError {
inner,
target,
})
}
}

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -436,6 +460,39 @@ pub trait SequencerClientExt: Client {
self.get_balance(address, 0u32).await
}

/// Returns the allowed fee assets at a given height.
///
/// # Errors
///
/// - If calling tendermint `abci_query` RPC fails.
/// - If the bytes contained in the abci query response cannot be deserialized as an
/// `astria.protocol.asset.v1alpha1.AllowedFeeAssetIdsResponse`.
/// - If the raw response cannot be converted to the native type.
async fn get_allowed_fee_asset_ids(&self) -> Result<AllowedFeeAssetIdsResponse, Error> {
let path = "asset/allowed_fee_asset_ids".to_string();

let response = self
.abci_query(Some(path), vec![], Some(0u32.into()), false)
.await
.map_err(|e| Error::tendermint_rpc("abci_query", e))?;

let proto_response =
astria_core::generated::protocol::asset::v1alpha1::AllowedFeeAssetIdsResponse::decode(
&*response.value,
)
.map_err(|e| {
Error::abci_query_deserialization(
"astria.protocol.asset.v1alpha1.AllowedFeeAssetIdsResponse",
response,
e,
)
})?;
let native_response = AllowedFeeAssetIdsResponse::try_from_raw(&proto_response)
.map_err(|e| Error::native_conversion("AllowedFeeAssetIdsResponse", Arc::new(e)))?;

Ok(native_response)
}

/// Returns the nonce of the given account at the given height.
///
/// # Errors
Expand Down
Loading

0 comments on commit f0acb1c

Please sign in to comment.