Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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 @@ -20,6 +20,7 @@ impl AbciErrorCode {
pub const VALUE_NOT_FOUND: Self = Self(8);
pub const TRANSACTION_EXPIRED: Self = Self(9);
pub const TRANSACTION_FAILED: Self = Self(10);
pub const BAD_REQUEST: Self = Self(11);
}

impl AbciErrorCode {
Expand All @@ -37,6 +38,7 @@ impl AbciErrorCode {
8 => "the requested value was not found".into(),
9 => "the transaction expired in the app's mempool".into(),
10 => "the transaction failed to execute in prepare_proposal()".into(),
11 => "the request payload was malformed".into(),
other => format!("unknown non-zero abci error code: {other}").into(),
}
}
Expand Down Expand Up @@ -67,6 +69,7 @@ impl From<NonZeroU32> for AbciErrorCode {
8 => Self::VALUE_NOT_FOUND,
9 => Self::TRANSACTION_EXPIRED,
10 => Self::TRANSACTION_FAILED,
11 => Self::BAD_REQUEST,
other => Self(other),
}
}
Expand Down
82 changes: 81 additions & 1 deletion crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use crate::{
SigningKey,
VerificationKey,
},
primitive::v1::ADDRESS_LEN,
primitive::v1::{
asset,
ADDRESS_LEN,
},
};

pub mod action;
Expand Down Expand Up @@ -470,6 +473,83 @@ impl TransactionParams {
}
}

#[derive(Debug, Clone)]
pub struct TransactionFeeResponse {
pub height: u64,
pub fees: Vec<(asset::Denom, u128)>,
}

impl TransactionFeeResponse {
#[must_use]
pub fn into_raw(self) -> raw::TransactionFeeResponse {
raw::TransactionFeeResponse {
height: self.height,
fees: self
.fees
.into_iter()
.map(|(asset, fee)| raw::TransactionFee {
asset: asset.to_string(),
fee: Some(fee.into()),
})
.collect(),
}
}

/// Attempt to convert from a raw protobuf [`raw::TransactionFeeResponse`].
///
/// # Errors
///
/// - if the asset ID could not be converted from bytes
/// - if the fee was unset
pub fn try_from_raw(
proto: raw::TransactionFeeResponse,
) -> Result<Self, TransactionFeeResponseError> {
let raw::TransactionFeeResponse {
height,
fees,
} = proto;
let fees = fees
.into_iter()
.map(
|raw::TransactionFee {
asset,
fee,
}| {
let asset = asset.parse().map_err(TransactionFeeResponseError::asset)?;
let fee = fee.ok_or(TransactionFeeResponseError::unset_fee())?;
Ok((asset, fee.into()))
},
)
.collect::<Result<_, _>>()?;
Ok(Self {
height,
fees,
})
}
}

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

impl TransactionFeeResponseError {
fn unset_fee() -> Self {
Self(TransactionFeeResponseErrorKind::UnsetFee)
}

fn asset(inner: asset::ParseDenomError) -> Self {
Self(TransactionFeeResponseErrorKind::Asset(inner))
}
}

#[derive(Debug, thiserror::Error)]
enum TransactionFeeResponseErrorKind {
#[error("`fee` field is unset")]
UnsetFee,
#[error("failed to parse asset denom in the `assets` field")]
Asset(#[source] asset::ParseDenomError),
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
36 changes: 36 additions & 0 deletions crates/astria-sequencer-client/src/extension_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ use astria_core::protocol::{
BridgeAccountInfoResponse,
BridgeAccountLastTxHashResponse,
},
transaction::v1alpha1::{
TransactionFeeResponse,
UnsignedTransaction,
},
};
pub use astria_core::{
primitive::v1::Address,
Expand Down Expand Up @@ -610,6 +614,38 @@ pub trait SequencerClientExt: Client {
Ok(native)
}

async fn get_transaction_fee(
&self,
tx: UnsignedTransaction,
) -> Result<TransactionFeeResponse, Error> {
let path = "transaction/fee".to_string();
let data = tx.into_raw().encode_to_vec();

let response = self
.abci_query(Some(path), data, None, false)
.await
.map_err(|e| Error::tendermint_rpc("abci_query", e))?;

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

/// Submits the given transaction to the Sequencer node.
///
/// This method blocks until the transaction is checked, but not until it's committed.
Expand Down
32 changes: 32 additions & 0 deletions crates/astria-sequencer-client/src/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,38 @@ async fn get_bridge_account_last_transaction_hash() {
assert_eq!(expected_response, actual_response);
}

#[tokio::test]
async fn get_transaction_fee() {
use astria_core::generated::protocol::transaction::v1alpha1::{
TransactionFee,
TransactionFeeResponse,
};

let MockSequencer {
server,
client,
} = MockSequencer::start().await;

let expected_response = TransactionFeeResponse {
height: 10,
fees: vec![TransactionFee {
asset: "asset_0".to_string(),
fee: Some(100.into()),
}],
};

let _guard =
register_abci_query_response(&server, "transaction/fee", expected_response.clone()).await;

let actual_response = client
.get_transaction_fee(create_signed_transaction().into_unsigned())
.await
.unwrap()
.into_raw();

assert_eq!(expected_response, actual_response);
}

#[tokio::test]
async fn submit_tx_sync() {
let MockSequencer {
Expand Down
6 changes: 6 additions & 0 deletions crates/astria-sequencer/src/service/info/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ impl Info {
crate::bridge::query::bridge_account_last_tx_hash_request,
)
.context("invalid path: `bridge/account_last_tx_hash/:address`")?;
query_router
.insert(
"transaction/fee",
crate::transaction::query::transaction_fee_request,
)
.context("invalid path: `transaction/fee`")?;
query_router
.insert(
"bridge/account_info/:address",
Expand Down
Loading