Skip to content

Commit

Permalink
feat(sequencer): query full denomination from asset ID (#1067)
Browse files Browse the repository at this point in the history
## Summary
implement ABCI query to get full denomination from an asset ID, if it
exists in the latest sequencer state.

## Background
useful for block explorers/UIs as txs only have asset IDs, not the full
denom.

## Changes
- implement ABCI query to get full denomination from an asset ID, if it
exists in the latest sequencer state

## Testing
unit tests

## Related Issues

closes #1053
  • Loading branch information
noot committed May 24, 2024
1 parent c7cee97 commit 1860dec
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 16 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.

5 changes: 5 additions & 0 deletions crates/astria-core/src/generated/mod.rs

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

3 changes: 3 additions & 0 deletions crates/astria-core/src/protocol/abci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ impl AbciErrorCode {
pub const TRANSACTION_TOO_LARGE: Self = Self(5);
pub const INSUFFICIENT_FUNDS: Self = Self(6);
pub const INVALID_CHAIN_ID: Self = Self(7);
pub const VALUE_NOT_FOUND: Self = Self(8);
}

impl AbciErrorCode {
Expand All @@ -31,6 +32,7 @@ impl AbciErrorCode {
5 => "the provided transaction was too large".into(),
6 => "insufficient funds".into(),
7 => "the provided chain id was invalid".into(),
8 => "the requested value was not found".into(),
other => format!("unknown non-zero abci error code: {other}").into(),
}
}
Expand Down Expand Up @@ -58,6 +60,7 @@ impl From<NonZeroU32> for AbciErrorCode {
5 => Self::TRANSACTION_TOO_LARGE,
6 => Self::INSUFFICIENT_FUNDS,
7 => Self::INVALID_CHAIN_ID,
8 => Self::VALUE_NOT_FOUND,
other => Self(other),
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/astria-core/src/protocol/asset/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod v1alpha1;

use crate::generated::protocol::asset::v1alpha1 as raw;
62 changes: 62 additions & 0 deletions crates/astria-core/src/protocol/asset/v1alpha1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use super::raw;
use crate::primitive::v1::asset::Denom;

/// The sequencer response to a denomination request for a given asset ID.
#[derive(Clone, Debug, PartialEq)]
pub struct DenomResponse {
pub height: u64,
pub denom: Denom,
}

impl DenomResponse {
/// Converts a protobuf [`raw::DenomResponse`] to an astria
/// native [`DenomResponse`].
#[must_use]
pub fn from_raw(proto: &raw::DenomResponse) -> Self {
let raw::DenomResponse {
height,
denom,
} = proto;
Self {
height: *height,
denom: denom.clone().into(),
}
}

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

impl raw::DenomResponse {
/// Converts an astria native [`DenomResponse`] to a
/// protobuf [`raw::DenomResponse`].
#[must_use]
pub fn from_native(native: DenomResponse) -> Self {
let DenomResponse {
height,
denom,
} = native;
Self {
height,
denom: denom.to_string(),
}
}

/// Converts a protobuf [`raw::DenomResponse`] to an astria
/// native [`DenomResponse`].
#[must_use]
pub fn into_native(self) -> DenomResponse {
DenomResponse::from_raw(&self)
}

/// Converts a protobuf [`raw::DenomResponse`] to an astria
/// native [`DenomResponse`] by allocating a new [`v1alpha1::DenomResponse`].
#[must_use]
pub fn to_native(&self) -> DenomResponse {
self.clone().into_native()
}
}
1 change: 1 addition & 0 deletions crates/astria-core/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::primitive::v1::RollupId;

pub mod abci;
pub mod account;
pub mod asset;
pub mod transaction;

#[cfg(any(feature = "test-utils", test))]
Expand Down
6 changes: 5 additions & 1 deletion crates/astria-sequencer/src/accounts/state_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ pub(crate) trait StateReadExt: StateRead {
continue;
}

let denom = self.get_ibc_asset(asset_id).await?;
let denom = self
.get_ibc_asset(asset_id)
.await
.context("failed to get ibc asset denom")?
.context("asset denom not found when user has balance of it; this is a bug")?;
balances.push(AssetBalance {
denom,
balance,
Expand Down
1 change: 1 addition & 0 deletions crates/astria-sequencer/src/asset/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod query;
pub(crate) mod state_ext;

use std::sync::OnceLock;
Expand Down
108 changes: 108 additions & 0 deletions crates/astria-sequencer/src/asset/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use anyhow::Context as _;
use astria_core::{
primitive::v1::asset,
protocol::abci::AbciErrorCode,
};
use cnidarium::Storage;
use prost::Message as _;
use tendermint::abci::{
request,
response,
};

use crate::{
asset::state_ext::StateReadExt as _,
state_ext::StateReadExt,
};

// Retrieve the full asset denomination given the asset ID.
//
// Example:
// `abci-cli query --path=asset/denom/<DENOM_ID>`
pub(crate) async fn denom_request(
storage: Storage,
request: request::Query,
params: Vec<(String, String)>,
) -> response::Query {
use astria_core::protocol::asset::v1alpha1::DenomResponse;

// use the latest snapshot, as this is a lookup of id->denom
let snapshot = storage.latest_snapshot();
let asset_id = match preprocess_request(&params) {
Ok(asset_id) => asset_id,
Err(err_rsp) => return err_rsp,
};

let height = match snapshot.get_block_height().await {
Ok(height) => height,
Err(err) => {
return response::Query {
code: AbciErrorCode::INTERNAL_ERROR.into(),
info: AbciErrorCode::INTERNAL_ERROR.to_string(),
log: format!("failed getting block height: {err:#}"),
..response::Query::default()
};
}
};

let maybe_denom = match snapshot.get_ibc_asset(asset_id).await {
Ok(maybe_denom) => maybe_denom,
Err(err) => {
return response::Query {
code: AbciErrorCode::INTERNAL_ERROR.into(),
info: AbciErrorCode::INTERNAL_ERROR.to_string(),
log: format!("failed to retrieve denomination `{asset_id}`: {err:#}"),
..response::Query::default()
};
}
};

let Some(denom) = maybe_denom else {
return response::Query {
code: AbciErrorCode::VALUE_NOT_FOUND.into(),
info: AbciErrorCode::VALUE_NOT_FOUND.to_string(),
log: format!("failed to retrieve value for denomination ID`{asset_id}`"),
..response::Query::default()
};
};

let payload = DenomResponse {
height,
denom,
}
.into_raw()
.encode_to_vec()
.into();

let height = tendermint::block::Height::try_from(height).expect("height must fit into an i64");
response::Query {
code: tendermint::abci::Code::Ok,
key: request.path.into_bytes().into(),
value: payload,
height,
..response::Query::default()
}
}

fn preprocess_request(params: &[(String, String)]) -> anyhow::Result<asset::Id, response::Query> {
let Some(asset_id) = params.iter().find_map(|(k, v)| (k == "id").then_some(v)) else {
return Err(response::Query {
code: AbciErrorCode::INVALID_PARAMETER.into(),
info: AbciErrorCode::INVALID_PARAMETER.to_string(),
log: "path did not contain asset ID parameter".into(),
..response::Query::default()
});
};
let asset_id = hex::decode(asset_id)
.context("failed decoding hex encoded bytes")
.and_then(|addr| {
asset::Id::try_from_slice(&addr).context("failed constructing asset ID from bytes")
})
.map_err(|err| response::Query {
code: AbciErrorCode::INVALID_PARAMETER.into(),
info: AbciErrorCode::INVALID_PARAMETER.to_string(),
log: format!("asset ID could not be constructed from provided parameter: {err:#}"),
..response::Query::default()
})?;
Ok(asset_id)
}
25 changes: 16 additions & 9 deletions crates/astria-sequencer/src/asset/state_ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use anyhow::{
bail,
Context as _,
Result,
};
Expand Down Expand Up @@ -42,19 +41,19 @@ pub(crate) trait StateReadExt: StateRead {
}

#[instrument(skip(self))]
async fn get_ibc_asset(&self, id: asset::Id) -> Result<Denom> {
async fn get_ibc_asset(&self, id: asset::Id) -> Result<Option<Denom>> {
let Some(bytes) = self
.get_raw(&asset_storage_key(id))
.await
.context("failed reading raw asset from state")?
else {
bail!("asset not found");
return Ok(None);
};

let DenominationTrace(denom_str) =
DenominationTrace::try_from_slice(&bytes).context("invalid asset bytes")?;
let denom: Denom = denom_str.into();
Ok(denom)
Ok(Some(denom))
}
}

Expand Down Expand Up @@ -94,11 +93,14 @@ mod test {

let asset = Id::from_denom("asset");

// gets for non existing assets fail
state
.get_ibc_asset(asset)
.await
.expect_err("gets for non existing ibc assets should fail");
// gets for non existing assets should return none
assert_eq!(
state
.get_ibc_asset(asset)
.await
.expect("getting non existing asset should not fail"),
None
);
}

#[tokio::test]
Expand Down Expand Up @@ -147,6 +149,7 @@ mod test {
state
.get_ibc_asset(denom.id())
.await
.unwrap()
.expect("an ibc asset was written and must exist inside the database"),
denom,
"stored ibc asset was not what was expected"
Expand All @@ -168,6 +171,7 @@ mod test {
state
.get_ibc_asset(denom.id())
.await
.unwrap()
.expect("an ibc asset was written and must exist inside the database"),
denom,
"stored ibc asset was not what was expected"
Expand All @@ -182,6 +186,7 @@ mod test {
state
.get_ibc_asset(denom_1.id())
.await
.unwrap()
.expect("an additional ibc asset was written and must exist inside the database"),
denom_1,
"additional ibc asset was not what was expected"
Expand All @@ -190,6 +195,7 @@ mod test {
state
.get_ibc_asset(denom.id())
.await
.unwrap()
.expect("an ibc asset was written and must exist inside the database"),
denom,
"original ibc asset was not what was expected"
Expand All @@ -214,6 +220,7 @@ mod test {
state
.get_ibc_asset(id_key)
.await
.unwrap()
.expect("an ibc asset was written and must exist inside the database")
.id(),
id_key,
Expand Down
4 changes: 3 additions & 1 deletion crates/astria-sequencer/src/ibc/ics20_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ async fn refund_tokens_check<S: StateRead>(
denom = state
.get_ibc_asset(denom.id())
.await
.context("failed to get denom trace from asset id")?;
.context("failed to get denom trace from asset id")?
.context("denom for given asset id not found in state")?;
}

let is_source = !is_prefixed(source_port, source_channel, &denom);
Expand Down Expand Up @@ -396,6 +397,7 @@ async fn convert_denomination_if_ibc_prefixed<S: StateReadExt>(
.get_ibc_asset(id_bytes.into())
.await
.context("failed to get denom trace from asset id")?
.context("denom for given asset id not found in state")?
} else {
packet_denom
};
Expand Down
Loading

0 comments on commit 1860dec

Please sign in to comment.