diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ebe04560b..3f95eb6ca0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -188,6 +188,38 @@ jobs: run: | cargo nextest run --package astria-bridge-withdrawer -- --include-ignored + # Specifically for tools other than the protobuf and solidity compilers which + # are run as part of other steps. (only `tools/astria-address` for now). + rust-tools: + runs-on: buildjet-8vcpu-ubuntu-2204 + needs: run_checker + if: needs.run_checker.outputs.run_tests == 'true' + steps: + - uses: actions/checkout@v4 + with: + submodules: 'true' + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUSTUP_TOOLCHAIN }} + - uses: Swatinem/rust-cache@v2.7.3 + with: + cache-provider: "buildjet" + - name: Install nextest + uses: taiki-e/install-action@nextest + - name: Build tests + run: | + cargo nextest archive \ + --manifest-path tools/astria-address/Cargo.toml \ + --archive-file=archive.tar.zst \ + --release \ + --all-features \ + --all-targets \ + - name: Run tests + timeout-minutes: 20 + run: | + cargo nextest run --archive-file=archive.tar.zst + + doctest: runs-on: buildjet-8vcpu-ubuntu-2204 needs: run_checker @@ -280,7 +312,7 @@ jobs: test: if: ${{ always() && !cancelled() }} - needs: [compiles, protos-compiled, solidity-contracts-compiled, rust, doctest, clippy, lockfile, custom-lints] + needs: [compiles, protos-compiled, solidity-contracts-compiled, rust, rust-tools, doctest, clippy, lockfile, custom-lints] uses: ./.github/workflows/reusable-success.yml with: success: ${{ !contains(needs.*.result, 'failure') }} diff --git a/Cargo.lock b/Cargo.lock index b95f060418..f67dfafd3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,10 +701,10 @@ name = "astria-core" version = "0.1.0" dependencies = [ "astria-core", + "astria-core-address", "astria-merkle", "base64 0.21.7", "base64-serde", - "bech32 0.11.0", "brotli", "bytes", "celestia-tendermint", @@ -731,6 +731,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "astria-core-address" +version = "0.1.0" +dependencies = [ + "astria-core-consts", + "bech32 0.11.0", + "thiserror", +] + +[[package]] +name = "astria-core-consts" +version = "0.1.0" + [[package]] name = "astria-eyre" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d1cc181333..585f0c6b49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,9 @@ [workspace] -exclude = ["tools/protobuf-compiler", "tools/solidity-compiler"] +exclude = [ + "tools/protobuf-compiler", + "tools/solidity-compiler", + "tools/astria-address", +] members = [ "crates/astria-bridge-contracts", @@ -10,6 +14,8 @@ members = [ "crates/astria-conductor", "crates/astria-config", "crates/astria-core", + "crates/astria-core-address", + "crates/astria-core-consts", "crates/astria-eyre", "crates/astria-grpc-mock", "crates/astria-grpc-mock-test", @@ -34,6 +40,8 @@ default-members = [ "crates/astria-conductor", "crates/astria-config", "crates/astria-core", + "crates/astria-core-address", + "crates/astria-core-consts", "crates/astria-grpc-mock", "crates/astria-grpc-mock-test", "crates/astria-grpc-mock-test-codegen", diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index 0bd72886a5..13f6b8c22e 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -15,6 +15,7 @@ use astria_core::{ Action, TransactionBody, }, + Protobuf as _, }; use astria_eyre::eyre::{ self, diff --git a/crates/astria-cli/CHANGELOG.md b/crates/astria-cli/CHANGELOG.md index 09bc9674b2..94c45ef2de 100644 --- a/crates/astria-cli/CHANGELOG.md +++ b/crates/astria-cli/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed ICS20 withdrawal source when using channel with more than one port/channel combo. [#1768](https://github.com/astriaorg/astria/pull/1768) +### Removed + +- Removed `sequeuncer address` subcommand [#1803](https://github.com/astriaorg/astria/pull/1803) + ## [0.5.1] - 2024-10-23 ### Added diff --git a/crates/astria-cli/src/sequencer/address.rs b/crates/astria-cli/src/sequencer/address.rs deleted file mode 100644 index fa7dcee7cb..0000000000 --- a/crates/astria-cli/src/sequencer/address.rs +++ /dev/null @@ -1,55 +0,0 @@ -use astria_core::primitive::v1::{ - Address, - ADDRESS_LEN, -}; -use color_eyre::eyre::{ - self, - WrapErr as _, -}; - -#[derive(Debug, clap::Args)] -pub(super) struct Command { - #[command(subcommand)] - command: SubCommand, -} - -impl Command { - pub(super) fn run(self) -> eyre::Result<()> { - let SubCommand::Bech32m(bech32m) = self.command; - bech32m.run() - } -} - -#[derive(Debug, clap::Subcommand)] -enum SubCommand { - /// Returns a bech32m sequencer address given a prefix and hex-encoded byte slice - Bech32m(Bech32m), -} - -#[derive(Debug, clap::Args)] -struct Bech32m { - /// The hex formatted byte part of the bech32m address - #[arg(long)] - bytes: String, - /// The human readable prefix (Hrp) of the bech32m adress - #[arg(long, default_value = "astria")] - prefix: String, -} - -impl Bech32m { - fn run(self) -> eyre::Result<()> { - use hex::FromHex as _; - let bytes = <[u8; ADDRESS_LEN]>::from_hex(&self.bytes) - .wrap_err("failed decoding provided hex bytes")?; - let address = Address::::builder() - .array(bytes) - .prefix(&self.prefix) - .try_build() - .wrap_err( - "failed constructing a valid bech32m address from the provided hex bytes and \ - prefix", - )?; - println!("{address}"); - Ok(()) - } -} diff --git a/crates/astria-cli/src/sequencer/mod.rs b/crates/astria-cli/src/sequencer/mod.rs index 6beda8d5ec..def0079456 100644 --- a/crates/astria-cli/src/sequencer/mod.rs +++ b/crates/astria-cli/src/sequencer/mod.rs @@ -2,7 +2,6 @@ use clap::Subcommand; use color_eyre::eyre; mod account; -mod address; mod balance; mod block_height; mod bridge_account; @@ -26,7 +25,6 @@ impl Command { pub(super) async fn run(self) -> eyre::Result<()> { match self.command { SubCommand::Account(account) => account.run().await, - SubCommand::Address(address) => address.run(), SubCommand::Balance(balance) => balance.run().await, SubCommand::BlockHeight(block_height) => block_height.run().await, SubCommand::BridgeLock(bridge_lock) => bridge_lock.run().await, @@ -48,8 +46,6 @@ impl Command { enum SubCommand { /// Commands for interacting with Sequencer accounts Account(account::Command), - /// Utilities for constructing and inspecting sequencer addresses - Address(address::Command), /// Commands for interacting with Sequencer balances Balance(balance::Command), /// Commands for interacting with Sequencer block heights diff --git a/crates/astria-core-address/CHANGELOG.md b/crates/astria-core-address/CHANGELOG.md new file mode 100644 index 0000000000..4f22f52904 --- /dev/null +++ b/crates/astria-core-address/CHANGELOG.md @@ -0,0 +1,14 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Initial release. diff --git a/crates/astria-core-address/Cargo.toml b/crates/astria-core-address/Cargo.toml new file mode 100644 index 0000000000..85a2a03eaf --- /dev/null +++ b/crates/astria-core-address/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "astria-core-address" +version = "0.1.0" +edition = "2021" + +[dependencies] +astria-core-consts = { path = "../astria-core-consts" } + +thiserror = { workspace = true } + +bech32 = "0.11.0" + +[features] +unchecked-constructor = [] diff --git a/crates/astria-core-address/src/lib.rs b/crates/astria-core-address/src/lib.rs new file mode 100644 index 0000000000..3685222d87 --- /dev/null +++ b/crates/astria-core-address/src/lib.rs @@ -0,0 +1,440 @@ +use std::{ + marker::PhantomData, + str::FromStr, +}; + +pub use astria_core_consts::ADDRESS_LENGTH; + +#[derive(Debug, Hash)] +pub struct Address { + bytes: [u8; ADDRESS_LENGTH], + prefix: bech32::Hrp, + format: PhantomData, +} + +impl Clone for Address { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Address {} + +impl PartialEq for Address { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) && self.prefix.eq(&other.prefix) + } +} + +impl Eq for Address {} + +impl Address { + #[must_use = "the builder must be used to construct an address to be useful"] + pub fn builder() -> Builder { + Builder::new() + } + + #[must_use] + pub fn bytes(self) -> [u8; ADDRESS_LENGTH] { + self.bytes + } + + #[must_use] + pub fn as_bytes(&self) -> &[u8; ADDRESS_LENGTH] { + &self.bytes + } + + #[must_use] + pub fn prefix(&self) -> &str { + self.prefix.as_str() + } + + /// Converts to a new address with the given `prefix`. + /// + /// # Errors + /// Returns an error if an address with `prefix` cannot be constructed. + /// The error conditions for this are the same as for [`AddressBuilder::try_build`]. + pub fn to_prefix(&self, prefix: &str) -> Result { + Self::builder() + .array(*self.as_bytes()) + .prefix(prefix) + .try_build() + } + + /// Converts to a new address with the type argument `OtherFormat`. + /// + /// `OtherFormat` is usually [`Bech32`] or [`Bech32m`]. + #[must_use] + pub fn to_format(&self) -> Address { + Address { + bytes: self.bytes, + prefix: self.prefix, + format: PhantomData, + } + } +} + +impl Address { + /// Should only be used where the inputs have been provided by a trusted entity, e.g. read + /// from our own state store. + /// + /// Note that this function is not considered part of the public API and is subject to breaking + /// change at any time. + #[cfg(feature = "unchecked-constructor")] + #[doc(hidden)] + #[must_use] + pub fn unchecked_from_parts(bytes: [u8; ADDRESS_LENGTH], prefix: &str) -> Self { + Self { + bytes, + prefix: bech32::Hrp::parse_unchecked(prefix), + format: PhantomData, + } + } +} + +impl FromStr for Address { + type Err = Error; + + fn from_str(s: &str) -> Result { + let checked = bech32::primitives::decode::CheckedHrpstring::new::(s) + .map_err(Self::Err::decode)?; + let hrp = checked.hrp(); + Self::builder() + .with_iter(checked.byte_iter()) + .prefix(hrp.as_str()) + .try_build() + } +} + +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use bech32::EncodeError; + match bech32::encode_lower_to_fmt::(f, self.prefix, self.as_bytes()) { + Ok(()) => Ok(()), + Err(EncodeError::Fmt(err)) => Err(err), + Err(err) => panic!( + "only formatting errors are valid when encoding astria addresses; all other error \ + variants (only TooLong as of bech32-0.11.0) are guaranteed to not happen because \ + `Address` is length checked:\n{err:?}", + ), + } + } +} + +pub struct NoBytes; +pub struct NoPrefix; +pub struct WithBytes<'a, I>(WithBytesInner<'a, I>); +enum WithBytesInner<'a, I> { + Array([u8; ADDRESS_LENGTH]), + Iter(I), + Slice(std::borrow::Cow<'a, [u8]>), +} +pub struct WithPrefix<'a>(std::borrow::Cow<'a, str>); + +pub struct NoBytesIter; + +impl Iterator for NoBytesIter { + type Item = u8; + + fn next(&mut self) -> Option { + None + } +} + +impl ExactSizeIterator for NoBytesIter { + fn len(&self) -> usize { + 0 + } +} + +pub struct Builder { + bytes: TBytes, + prefix: TPrefix, + format: PhantomData, +} + +impl Builder { + const fn new() -> Self { + Self { + bytes: NoBytes, + prefix: NoPrefix, + format: PhantomData, + } + } +} + +impl Builder { + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn array( + self, + array: [u8; ADDRESS_LENGTH], + ) -> Builder, TPrefix> { + Builder { + bytes: WithBytes(WithBytesInner::Array(array)), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn slice<'a, T: Into>>( + self, + bytes: T, + ) -> Builder, TPrefix> { + Builder { + bytes: WithBytes(WithBytesInner::Slice(bytes.into())), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + fn with_iter(self, iter: T) -> Builder, TPrefix> + where + T: IntoIterator, + T::IntoIter: ExactSizeIterator, + { + Builder { + bytes: WithBytes(WithBytesInner::Iter(iter)), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn prefix<'a, T: Into>>( + self, + prefix: T, + ) -> Builder> { + Builder { + bytes: self.bytes, + prefix: WithPrefix(prefix.into()), + format: self.format, + } + } +} + +impl<'a, 'b, TFormat, TBytesIter> Builder, WithPrefix<'b>> +where + TBytesIter: IntoIterator, + TBytesIter::IntoIter: ExactSizeIterator, +{ + /// Attempts to build an address from the configured prefix and bytes. + /// + /// # Errors + /// Returns an error if one of the following conditions are violated: + /// + if the prefix shorter than 1 or longer than 83 characters, or contains characters outside + /// 33-126 of ASCII characters. + /// + if the provided bytes are not exactly 20 bytes. + pub fn try_build(self) -> Result, Error> { + let Self { + bytes: WithBytes(bytes), + prefix: WithPrefix(prefix), + format, + } = self; + let bytes = match bytes { + WithBytesInner::Array(bytes) => bytes, + WithBytesInner::Iter(bytes) => try_collect_to_array(bytes)?, + WithBytesInner::Slice(bytes) => <[u8; ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|_| Error::incorrect_length(bytes.len()))?, + }; + let prefix = bech32::Hrp::parse(&prefix).map_err(Error::invalid_prefix)?; + Ok(Address { + bytes, + prefix, + format, + }) + } +} + +fn try_collect_to_array(iter: I) -> Result<[u8; ADDRESS_LENGTH], Error> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, +{ + let iter = iter.into_iter(); + + if iter.len() != ADDRESS_LENGTH { + return Err(Error::incorrect_length(iter.len())); + } + let mut arr = [0; ADDRESS_LENGTH]; + for (left, right) in arr.iter_mut().zip(iter) { + *left = right; + } + Ok(arr) +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(ErrorKind); + +impl Error { + fn decode(source: bech32::primitives::decode::CheckedHrpstringError) -> Self { + Self(ErrorKind::Decode { + source, + }) + } + + fn invalid_prefix(source: bech32::primitives::hrp::Error) -> Self { + Self(ErrorKind::InvalidPrefix { + source, + }) + } + + fn incorrect_length(received: usize) -> Self { + Self(ErrorKind::IncorrectLength { + received, + }) + } +} + +#[derive(Debug, thiserror::Error, PartialEq)] +enum ErrorKind { + #[error("failed decoding provided string")] + Decode { + source: bech32::primitives::decode::CheckedHrpstringError, + }, + #[error("expected an address of 20 bytes, got `{received}`")] + IncorrectLength { received: usize }, + #[error("the provided prefix was not a valid bech32 human readable prefix")] + InvalidPrefix { + source: bech32::primitives::hrp::Error, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum Bech32m {} +#[derive(Clone, Copy, Debug)] +pub enum Bech32 {} +#[derive(Clone, Copy, Debug)] +pub enum NoFormat {} + +#[expect( + private_bounds, + reason = "prevent downstream implementation of this trait" +)] +pub trait Format: Sealed { + type Checksum: bech32::Checksum; +} + +impl Format for Bech32m { + type Checksum = bech32::Bech32m; +} + +impl Format for Bech32 { + type Checksum = bech32::Bech32; +} + +impl Format for NoFormat { + type Checksum = bech32::NoChecksum; +} + +trait Sealed {} +impl Sealed for Bech32m {} +impl Sealed for Bech32 {} +impl Sealed for NoFormat {} + +#[cfg(test)] +mod tests { + use super::{ + Address, + Bech32, + Bech32m, + Error, + ErrorKind, + }; + + const ASTRIA_ADDRESS_PREFIX: &str = "astria"; + const ASTRIA_COMPAT_ADDRESS_PREFIX: &str = "astriacompat"; + + #[track_caller] + fn assert_wrong_address_bytes(bad_account: &[u8]) { + let error = Address::::builder() + .slice(bad_account) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .expect_err( + "converting from an incorrectly sized byte slice succeeded where it should have \ + failed", + ); + let Error(ErrorKind::IncorrectLength { + received, + }) = error + else { + panic!("expected ErrorKind::IncorrectLength, got {error:?}"); + }; + assert_eq!(bad_account.len(), received); + } + + #[test] + fn account_of_incorrect_length_gives_error() { + assert_wrong_address_bytes(&[42; 0]); + assert_wrong_address_bytes(&[42; 19]); + assert_wrong_address_bytes(&[42; 21]); + assert_wrong_address_bytes(&[42; 100]); + } + + #[test] + fn parse_bech32m_address() { + let expected = Address::builder() + .array([42; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let actual = expected.to_string().parse::
().unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn parse_bech32_address() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let actual = expected.to_string().parse::>().unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn parsing_bech32_address_as_bech32m_fails() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let err = expected + .to_string() + .parse::>() + .expect_err("this must not work"); + match err { + Error(ErrorKind::Decode { + .. + }) => {} + other => { + panic!("expected Error(ErrorKind::Decode {{ .. }}), but got {other:?}") + } + } + } + + #[test] + fn parsing_bech32m_address_as_bech32_fails() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let err = expected + .to_string() + .parse::>() + .expect_err("this must not work"); + match err { + Error(ErrorKind::Decode { + .. + }) => {} + other => { + panic!("expected Error(ErrorKind::Decode {{ .. }}), but got {other:?}") + } + } + } +} diff --git a/crates/astria-core-consts/CHANGELOG.md b/crates/astria-core-consts/CHANGELOG.md new file mode 100644 index 0000000000..4f22f52904 --- /dev/null +++ b/crates/astria-core-consts/CHANGELOG.md @@ -0,0 +1,14 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Initial release. diff --git a/crates/astria-core-consts/Cargo.toml b/crates/astria-core-consts/Cargo.toml new file mode 100644 index 0000000000..125b069104 --- /dev/null +++ b/crates/astria-core-consts/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "astria-core-consts" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/astria-core-consts/src/lib.rs b/crates/astria-core-consts/src/lib.rs new file mode 100644 index 0000000000..1b05c26f66 --- /dev/null +++ b/crates/astria-core-consts/src/lib.rs @@ -0,0 +1,3 @@ +//! Public constants that are used throughout the Astria stack. + +pub const ADDRESS_LENGTH: usize = 20; diff --git a/crates/astria-core/CHANGELOG.md b/crates/astria-core/CHANGELOG.md index dd487eeab3..08d2e685d6 100644 --- a/crates/astria-core/CHANGELOG.md +++ b/crates/astria-core/CHANGELOG.md @@ -14,7 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release. - Added method `TracePrefixed::leading_channel` to read the left-most channel of a trace prefixed ICS20 asset [#1768](https://github.com/astriaorg/astria/pull/1768) +- Added `impl Protobuf for Address` [#1802](https://github.com/astriaorg/astria/pull/1802) + +### Changed + +- Moved definitions of address domain type to `astria-core-address` and + reexported items using the same aliases [#1802](https://github.com/astriaorg/astria/pull/1802) ### Removed - Removed method `TracePrefixed::last_channel` [#1768](https://github.com/astriaorg/astria/pull/1768) +- Removed inherent methods `Address::try_from_raw` and `Address::to_raw` + [#1802](https://github.com/astriaorg/astria/pull/1802) +- Removed `AddressBuilder::with_iter` from public interface [#1802](https://github.com/astriaorg/astria/pull/1802) diff --git a/crates/astria-core/Cargo.toml b/crates/astria-core/Cargo.toml index a7a45d5ba0..331974d9df 100644 --- a/crates/astria-core/Cargo.toml +++ b/crates/astria-core/Cargo.toml @@ -16,11 +16,11 @@ keywords = ["astria", "grpc", "rpc", "blockchain", "execution", "protobuf"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bech32 = "0.11.0" brotli = { version = "5.0.0", optional = true } celestia-types = { version = "0.1.1", optional = true } pbjson = { version = "0.6.0", optional = true } +astria-core-address = { path = "../astria-core-address" } merkle = { package = "astria-merkle", path = "../astria-merkle" } bytes = { workspace = true } @@ -58,7 +58,7 @@ brotli = ["dep:brotli"] # When enabled, this adds constructors for some types that skip the normal constructor validity # checks. It supports the case where the inputs are already deemed valid, e.g. having read them from # local storage. -unchecked-constructors = [] +unchecked-constructors = ["astria-core-address/unchecked-constructor"] [dev-dependencies] astria-core = { path = ".", features = ["serde"] } diff --git a/crates/astria-core/src/primitive/v1/mod.rs b/crates/astria-core/src/primitive/v1/mod.rs index 56cb6922b5..1a60a5bfe5 100644 --- a/crates/astria-core/src/primitive/v1/mod.rs +++ b/crates/astria-core/src/primitive/v1/mod.rs @@ -1,11 +1,15 @@ pub mod asset; pub mod u128; -use std::{ - marker::PhantomData, - str::FromStr, +pub use astria_core_address::{ + Address, + Bech32, + Bech32m, + Builder as AddressBuilder, + Error as AddressError, + Format, + ADDRESS_LENGTH as ADDRESS_LEN, }; - use base64::{ display::Base64Display, prelude::BASE64_URL_SAFE, @@ -21,12 +25,28 @@ use crate::{ Protobuf, }; -pub const ADDRESS_LEN: usize = 20; - pub const ROLLUP_ID_LEN: usize = 32; pub const TRANSACTION_ID_LEN: usize = 32; +impl Protobuf for Address { + type Error = AddressError; + type Raw = raw::Address; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let raw::Address { + bech32m, + } = raw; + bech32m.parse() + } + + fn to_raw(&self) -> Self::Raw { + raw::Address { + bech32m: self.to_string(), + } + } +} + impl Protobuf for merkle::Proof { type Error = merkle::audit::InvalidProof; type Raw = raw::Proof; @@ -254,431 +274,6 @@ pub struct IncorrectRollupIdLength { received: usize, } -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct AddressError(AddressErrorKind); - -impl AddressError { - fn decode(source: bech32::primitives::decode::CheckedHrpstringError) -> Self { - Self(AddressErrorKind::Decode { - source, - }) - } - - fn invalid_prefix(source: bech32::primitives::hrp::Error) -> Self { - Self(AddressErrorKind::InvalidPrefix { - source, - }) - } - - fn incorrect_address_length(received: usize) -> Self { - Self(AddressErrorKind::IncorrectAddressLength { - received, - }) - } -} - -#[derive(Debug, thiserror::Error, PartialEq)] -enum AddressErrorKind { - #[error("failed decoding provided string")] - Decode { - source: bech32::primitives::decode::CheckedHrpstringError, - }, - #[error("expected an address of 20 bytes, got `{received}`")] - IncorrectAddressLength { received: usize }, - #[error("the provided prefix was not a valid bech32 human readable prefix")] - InvalidPrefix { - source: bech32::primitives::hrp::Error, - }, -} - -pub struct NoBytes; -pub struct NoPrefix; -pub struct WithBytes<'a, I>(WithBytesInner<'a, I>); -enum WithBytesInner<'a, I> { - Array([u8; ADDRESS_LEN]), - Iter(I), - Slice(std::borrow::Cow<'a, [u8]>), -} -pub struct WithPrefix<'a>(std::borrow::Cow<'a, str>); - -pub struct NoBytesIter; - -impl Iterator for NoBytesIter { - type Item = u8; - - fn next(&mut self) -> Option { - None - } -} - -pub struct AddressBuilder { - bytes: TBytes, - prefix: TPrefix, - format: PhantomData, -} - -impl AddressBuilder { - const fn new() -> Self { - Self { - bytes: NoBytes, - prefix: NoPrefix, - format: PhantomData, - } - } -} - -impl AddressBuilder { - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn array( - self, - array: [u8; ADDRESS_LEN], - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Array(array)), - prefix: self.prefix, - format: self.format, - } - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn slice<'a, T: Into>>( - self, - bytes: T, - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Slice(bytes.into())), - prefix: self.prefix, - format: self.format, - } - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn with_iter>( - self, - iter: T, - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Iter(iter)), - prefix: self.prefix, - format: self.format, - } - } - - /// Use the given verification key for address generation. - /// - /// The verification key is hashed with SHA256 and the first 20 bytes are used as the address - /// bytes. - #[expect(clippy::missing_panics_doc, reason = "the conversion is infallible")] - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn verification_key( - self, - key: &crate::crypto::VerificationKey, - ) -> AddressBuilder, TPrefix> { - let hash = Sha256::digest(key.as_bytes()); - let array: [u8; ADDRESS_LEN] = hash[0..ADDRESS_LEN] - .try_into() - .expect("hash is 32 bytes long, so must always be able to convert to 20 bytes"); - self.array(array) - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn prefix<'a, T: Into>>( - self, - prefix: T, - ) -> AddressBuilder> { - AddressBuilder { - bytes: self.bytes, - prefix: WithPrefix(prefix.into()), - format: self.format, - } - } -} - -impl<'a, 'b, TFormat, TBytesIter> AddressBuilder, WithPrefix<'b>> -where - TBytesIter: IntoIterator, -{ - /// Attempts to build an address from the configured prefix and bytes. - /// - /// # Errors - /// Returns an error if one of the following conditions are violated: - /// + if the prefix shorter than 1 or longer than 83 characters, or contains characters outside - /// 33-126 of ASCII characters. - /// + if the provided bytes are not exactly 20 bytes. - pub fn try_build(self) -> Result, AddressError> { - let Self { - bytes: WithBytes(bytes), - prefix: WithPrefix(prefix), - format, - } = self; - let bytes = match bytes { - WithBytesInner::Array(bytes) => bytes, - WithBytesInner::Iter(bytes) => try_collect_to_array(bytes)?, - WithBytesInner::Slice(bytes) => <[u8; ADDRESS_LEN]>::try_from(bytes.as_ref()) - .map_err(|_| AddressError::incorrect_address_length(bytes.len()))?, - }; - let prefix = bech32::Hrp::parse(&prefix).map_err(AddressError::invalid_prefix)?; - Ok(Address { - bytes, - prefix, - format, - }) - } -} - -fn try_collect_to_array>( - iter: I, -) -> Result<[u8; ADDRESS_LEN], AddressError> { - let mut arr = [0; ADDRESS_LEN]; - let mut iter = iter.into_iter(); - let mut i = 0; - loop { - if i >= ADDRESS_LEN { - break; - } - let Some(byte) = iter.next() else { - break; - }; - arr[i] = byte; - i = i.saturating_add(1); - } - let items_in_iterator = i.saturating_add(iter.count()); - if items_in_iterator != ADDRESS_LEN { - return Err(AddressError::incorrect_address_length(items_in_iterator)); - } - Ok(arr) -} - -#[derive(Clone, Copy, Debug)] -pub enum Bech32m {} -#[derive(Clone, Copy, Debug)] -pub enum Bech32 {} -#[derive(Clone, Copy, Debug)] -pub enum NoFormat {} - -pub trait Format: private::Sealed { - type Checksum: bech32::Checksum; -} - -impl Format for Bech32m { - type Checksum = bech32::Bech32m; -} - -impl Format for Bech32 { - type Checksum = bech32::Bech32; -} - -impl Format for NoFormat { - type Checksum = bech32::NoChecksum; -} - -mod private { - pub trait Sealed {} - impl Sealed for super::Bech32m {} - impl Sealed for super::Bech32 {} - impl Sealed for super::NoFormat {} -} - -#[derive(Debug, Hash)] -pub struct Address { - bytes: [u8; ADDRESS_LEN], - prefix: bech32::Hrp, - format: PhantomData, -} - -// The serde impls need to be manually implemented for Address because they -// only work for Address which cannot be expressed using serde -// attributes. -#[cfg(feature = "serde")] -mod _serde_impls { - use serde::de::Error as _; - impl serde::Serialize for super::Address { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.to_raw().serialize(serializer) - } - } - impl<'de> serde::Deserialize<'de> for super::Address { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - super::raw::Address::deserialize(deserializer) - .and_then(|raw| raw.try_into().map_err(D::Error::custom)) - } - } -} - -impl Clone for Address { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Address {} - -impl PartialEq for Address { - fn eq(&self, other: &Self) -> bool { - self.bytes.eq(&other.bytes) && self.prefix.eq(&other.prefix) - } -} - -impl Eq for Address {} - -impl Address { - #[must_use = "the builder must be used to construct an address to be useful"] - pub fn builder() -> AddressBuilder { - AddressBuilder::::new() - } - - #[must_use] - pub fn bytes(self) -> [u8; ADDRESS_LEN] { - self.bytes - } - - #[must_use] - pub fn as_bytes(&self) -> &[u8; ADDRESS_LEN] { - &self.bytes - } - - #[must_use] - pub fn prefix(&self) -> &str { - self.prefix.as_str() - } - - /// Converts to a new address with the given `prefix`. - /// - /// # Errors - /// Returns an error if an address with `prefix` cannot be constructed. - /// The error conditions for this are the same as for [`AddressBuilder::try_build`]. - pub fn to_prefix(&self, prefix: &str) -> Result { - Self::builder() - .array(*self.as_bytes()) - .prefix(prefix) - .try_build() - } - - /// Converts to a new address with the type argument `OtherFormat`. - /// - /// `OtherFormat` is usually [`Bech32`] or [`Bech32m`]. - #[must_use] - pub fn to_format(&self) -> Address { - Address { - bytes: self.bytes, - prefix: self.prefix, - format: PhantomData, - } - } -} - -impl Address { - /// Convert [`Address`] to a [`raw::Address`]. - #[expect( - clippy::missing_panics_doc, - reason = "panics are checked to not happen" - )] - #[must_use] - pub fn to_raw(&self) -> raw::Address { - let bech32m = - bech32::encode_lower::<::Checksum>(self.prefix, self.as_bytes()) - .expect( - "should not fail because len(prefix) + len(bytes) <= 63 < BECH32M::CODELENGTH", - ); - raw::Address { - bech32m, - } - } - - #[must_use] - pub fn into_raw(self) -> raw::Address { - self.to_raw() - } - - /// Convert from protobuf to rust type an address. - /// - /// # Errors - /// - /// Returns an error if the account buffer was not 20 bytes long. - pub fn try_from_raw(raw: &raw::Address) -> Result { - let raw::Address { - bech32m, - } = raw; - bech32m.parse() - } - - /// This should only be used where the inputs have been provided by a trusted entity, e.g. read - /// from our own state store. - /// - /// Note that this function is not considered part of the public API and is subject to breaking - /// change at any time. - #[cfg(feature = "unchecked-constructors")] - #[doc(hidden)] - #[must_use] - pub fn unchecked_from_parts(bytes: [u8; ADDRESS_LEN], prefix: &str) -> Self { - Self { - bytes, - prefix: bech32::Hrp::parse_unchecked(prefix), - format: PhantomData, - } - } -} - -impl From> for raw::Address { - fn from(value: Address) -> Self { - value.into_raw() - } -} - -impl FromStr for Address { - type Err = AddressError; - - fn from_str(s: &str) -> Result { - let checked = bech32::primitives::decode::CheckedHrpstring::new::(s) - .map_err(Self::Err::decode)?; - let hrp = checked.hrp(); - Self::builder() - .with_iter(checked.byte_iter()) - .prefix(hrp.as_str()) - .try_build() - } -} - -impl TryFrom for Address { - type Error = AddressError; - - fn try_from(value: raw::Address) -> Result { - Self::try_from_raw(&value) - } -} - -impl std::fmt::Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use bech32::EncodeError; - match bech32::encode_lower_to_fmt::(f, self.prefix, self.as_bytes()) { - Ok(()) => Ok(()), - Err(EncodeError::Fmt(err)) => Err(err), - Err(err) => panic!( - "only formatting errors are valid when encoding astria addresses; all other error \ - variants (only TooLong as of bech32-0.11.0) are guaranteed to not happen because \ - `Address` is length checked:\n{err:?}", - ), - } - } -} -/// Constructs a dummy address from a given `prefix`, otherwise fail. -pub(crate) fn try_construct_dummy_address_from_prefix( - prefix: &str, -) -> Result<(), AddressError> { - Address::::builder() - .array([0u8; ADDRESS_LEN]) - .prefix(prefix) - .try_build() - .map(|_| ()) -} - /// Derive a [`merkle::Tree`] from an iterable. /// /// It is the responsibility of the caller to ensure that the iterable is @@ -799,42 +394,12 @@ enum TransactionIdErrorKind { mod tests { use super::{ Address, - AddressError, - AddressErrorKind, - Bech32m, ADDRESS_LEN, }; - use crate::primitive::v1::Bech32; + use crate::Protobuf as _; const ASTRIA_ADDRESS_PREFIX: &str = "astria"; const ASTRIA_COMPAT_ADDRESS_PREFIX: &str = "astriacompat"; - #[track_caller] - fn assert_wrong_address_bytes(bad_account: &[u8]) { - let error = Address::::builder() - .slice(bad_account) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .expect_err( - "converting from an incorrectly sized byte slice succeeded where it should have \ - failed", - ); - let AddressError(AddressErrorKind::IncorrectAddressLength { - received, - }) = error - else { - panic!("expected AddressErrorKind::IncorrectAddressLength, got {error:?}"); - }; - assert_eq!(bad_account.len(), received); - } - - #[test] - fn account_of_incorrect_length_gives_error() { - assert_wrong_address_bytes(&[42; 0]); - assert_wrong_address_bytes(&[42; 19]); - assert_wrong_address_bytes(&[42; 21]); - assert_wrong_address_bytes(&[42; 100]); - } - #[cfg(feature = "serde")] #[test] fn snapshots() { @@ -856,74 +421,6 @@ mod tests { insta::assert_snapshot!(&compat_address); } - #[test] - fn parse_bech32m_address() { - let expected = Address::builder() - .array([42; 20]) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let actual = expected.to_string().parse::
().unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn parse_bech32_address() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let actual = expected.to_string().parse::>().unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn parsing_bech32_address_as_bech32m_fails() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let err = expected - .to_string() - .parse::>() - .expect_err("this must not work"); - match err { - AddressError(AddressErrorKind::Decode { - .. - }) => {} - other => { - panic!( - "expected AddressError(AddressErrorKind::Decode {{ .. }}), but got {other:?}" - ) - } - } - } - - #[test] - fn parsing_bech32m_address_as_bech32_fails() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let err = expected - .to_string() - .parse::>() - .expect_err("this must not work"); - match err { - AddressError(AddressErrorKind::Decode { - .. - }) => {} - other => { - panic!( - "expected AddressError(AddressErrorKind::Decode {{ .. }}), but got {other:?}" - ) - } - } - } - #[test] fn can_construct_protobuf_from_address_with_maximally_sized_prefix() { // 83 is the maximal length of a hrp @@ -946,7 +443,7 @@ mod tests { .try_build() .unwrap(); let unchecked = input.into_raw(); - let roundtripped = Address::try_from_raw(&unchecked).unwrap(); + let roundtripped = Address::try_from_raw(unchecked).unwrap(); assert_eq!(input, roundtripped); assert_eq!(input.as_bytes(), roundtripped.as_bytes()); assert_eq!("astria", input.prefix()); diff --git a/crates/astria-core/src/protocol/bridge/v1/mod.rs b/crates/astria-core/src/protocol/bridge/v1/mod.rs index df30ed9068..49b3e07c90 100644 --- a/crates/astria-core/src/protocol/bridge/v1/mod.rs +++ b/crates/astria-core/src/protocol/bridge/v1/mod.rs @@ -1,13 +1,18 @@ use bytes::Bytes; use super::raw; -use crate::primitive::v1::{ - asset, - asset::denom::ParseDenomError, - Address, - AddressError, - IncorrectRollupIdLength, - RollupId, +use crate::{ + primitive::v1::{ + asset::{ + self, + denom::ParseDenomError, + }, + Address, + AddressError, + IncorrectRollupIdLength, + RollupId, + }, + Protobuf as _, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -159,9 +164,9 @@ impl BridgeAccountInfoResponse { rollup_id: RollupId::try_from_raw(rollup_id) .map_err(BridgeAccountInfoResponseError::invalid_rollup_id)?, asset, - sudo_address: Address::try_from_raw(&sudo_address) + sudo_address: Address::try_from_raw(sudo_address) .map_err(BridgeAccountInfoResponseError::invalid_sudo_address)?, - withdrawer_address: Address::try_from_raw(&withdrawer_address) + withdrawer_address: Address::try_from_raw(withdrawer_address) .map_err(BridgeAccountInfoResponseError::invalid_withdrawer_address)?, }), }) diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index 69bb2269bd..8df8f7e2bf 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -10,7 +10,6 @@ use crate::{ denom::ParseTracePrefixedError, ParseDenomError, }, - try_construct_dummy_address_from_prefix, Address, AddressError, Bech32, @@ -183,16 +182,18 @@ impl Protobuf for GenesisAppState { .as_ref() .ok_or_else(|| Self::Error::field_not_set("authority_sudo_address")) .and_then(|addr| { - Address::try_from_raw(addr).map_err(Self::Error::authority_sudo_address) + Address::try_from_raw_ref(addr).map_err(Self::Error::authority_sudo_address) })?; let ibc_sudo_address = ibc_sudo_address .as_ref() .ok_or_else(|| Self::Error::field_not_set("ibc_sudo_address")) - .and_then(|addr| Address::try_from_raw(addr).map_err(Self::Error::ibc_sudo_address))?; + .and_then(|addr| { + Address::try_from_raw_ref(addr).map_err(Self::Error::ibc_sudo_address) + })?; let ibc_relayer_addresses = ibc_relayer_addresses .iter() - .map(Address::try_from_raw) + .map(Address::try_from_raw_ref) .collect::>() .map_err(Self::Error::ibc_relayer_addresses)?; @@ -405,7 +406,7 @@ impl Protobuf for Account { let address = address .as_ref() .ok_or_else(|| AccountError::field_not_set("address")) - .and_then(|addr| Address::try_from_raw(addr).map_err(Self::Error::address))?; + .and_then(|addr| Address::try_from_raw_ref(addr).map_err(Self::Error::address))?; let balance = balance .ok_or_else(|| AccountError::field_not_set("balance")) .map(Into::into)?; @@ -481,12 +482,22 @@ impl Protobuf for AddressPrefixes { type Raw = raw::AddressPrefixes; fn try_from_raw_ref(raw: &Self::Raw) -> Result { + fn dummy_addr(prefix: &str) -> Result<(), AddressError> { + Address::::builder() + .array([0u8; crate::primitive::v1::ADDRESS_LEN]) + .prefix(prefix) + .try_build() + .map(|_| ()) + } + let Self::Raw { base, ibc_compat, } = raw; - try_construct_dummy_address_from_prefix::(base).map_err(Self::Error::base)?; - try_construct_dummy_address_from_prefix::(ibc_compat).map_err(Self::Error::base)?; + + dummy_addr::(base).map_err(Self::Error::base)?; + dummy_addr::(ibc_compat).map_err(Self::Error::base)?; + Ok(Self { base: base.to_string(), ibc_compat: ibc_compat.to_string(), 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 c4f8320f9c..80ba7d2326 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs @@ -538,7 +538,7 @@ impl Protobuf for Transfer { let Some(to) = to else { return Err(TransferError::field_not_set("to")); }; - let to = Address::try_from_raw(to).map_err(TransferError::address)?; + let to = Address::try_from_raw_ref(to).map_err(TransferError::address)?; let amount = amount.map_or(0, Into::into); let asset = asset.parse().map_err(TransferError::asset)?; let fee_asset = fee_asset.parse().map_err(TransferError::fee_asset)?; @@ -780,7 +780,7 @@ impl Protobuf for SudoAddressChange { return Err(SudoAddressChangeError::field_not_set("new_address")); }; let new_address = - Address::try_from_raw(new_address).map_err(SudoAddressChangeError::address)?; + Address::try_from_raw_ref(new_address).map_err(SudoAddressChangeError::address)?; Ok(Self { new_address, }) @@ -847,7 +847,7 @@ impl Protobuf for IbcSudoChange { return Err(IbcSudoChangeError::field_not_set("new_address")); }; let new_address = - Address::try_from_raw(new_address).map_err(IbcSudoChangeError::address)?; + Address::try_from_raw_ref(new_address).map_err(IbcSudoChangeError::address)?; Ok(Self { new_address, }) @@ -1034,7 +1034,7 @@ impl Protobuf for Ics20Withdrawal { } = proto; let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?; let return_address = Address::try_from_raw( - &return_address.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, + return_address.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, ) .map_err(Ics20WithdrawalError::return_address)?; @@ -1042,7 +1042,6 @@ impl Protobuf for Ics20Withdrawal { .ok_or(Ics20WithdrawalError::field_not_set("timeout_height"))? .into(); let bridge_address = bridge_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(Ics20WithdrawalError::invalid_bridge_address)?; @@ -1090,12 +1089,13 @@ impl Protobuf for Ics20Withdrawal { use_compat_address, } = proto; let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?; - let return_address = Address::try_from_raw( - return_address - .as_ref() - .ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, - ) - .map_err(Ics20WithdrawalError::return_address)?; + let return_address = return_address + .as_ref() + .ok_or_else(|| Ics20WithdrawalError::field_not_set("return_address")) + .and_then(|return_address| { + Address::try_from_raw_ref(return_address) + .map_err(Ics20WithdrawalError::return_address) + })?; let timeout_height = timeout_height .clone() @@ -1103,7 +1103,7 @@ impl Protobuf for Ics20Withdrawal { .into(); let bridge_address = bridge_address .as_ref() - .map(Address::try_from_raw) + .map(Address::try_from_raw_ref) .transpose() .map_err(Ics20WithdrawalError::invalid_bridge_address)?; @@ -1245,14 +1245,14 @@ impl Protobuf for IbcRelayerChange { value: Some(raw::ibc_relayer_change::Value::Addition(address)), } => { let address = - Address::try_from_raw(address).map_err(IbcRelayerChangeError::address)?; + Address::try_from_raw_ref(address).map_err(IbcRelayerChangeError::address)?; Ok(IbcRelayerChange::Addition(address)) } raw::IbcRelayerChange { value: Some(raw::ibc_relayer_change::Value::Removal(address)), } => { let address = - Address::try_from_raw(address).map_err(IbcRelayerChangeError::address)?; + Address::try_from_raw_ref(address).map_err(IbcRelayerChangeError::address)?; Ok(IbcRelayerChange::Removal(address)) } _ => Err(IbcRelayerChangeError::missing_address()), @@ -1423,13 +1423,11 @@ impl Protobuf for InitBridgeAccount { .map_err(InitBridgeAccountError::invalid_fee_asset)?; let sudo_address = proto .sudo_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(InitBridgeAccountError::invalid_sudo_address)?; let withdrawer_address = proto .withdrawer_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(InitBridgeAccountError::invalid_withdrawer_address)?; @@ -1558,7 +1556,7 @@ impl Protobuf for BridgeLock { let Some(to) = proto.to else { return Err(BridgeLockError::field_not_set("to")); }; - let to = Address::try_from_raw(&to).map_err(BridgeLockError::address)?; + let to = Address::try_from_raw(to).map_err(BridgeLockError::address)?; let amount = proto.amount.ok_or(BridgeLockError::missing_amount())?; let asset = proto .asset @@ -1704,13 +1702,13 @@ impl Protobuf for BridgeUnlock { } = proto; let to = to .ok_or_else(|| BridgeUnlockError::field_not_set("to")) - .and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::address))?; + .and_then(|to| Address::try_from_raw(to).map_err(BridgeUnlockError::address))?; let amount = amount.ok_or_else(|| BridgeUnlockError::field_not_set("amount"))?; let fee_asset = fee_asset.parse().map_err(BridgeUnlockError::fee_asset)?; let bridge_address = bridge_address .ok_or_else(|| BridgeUnlockError::field_not_set("bridge_address")) - .and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::bridge_address))?; + .and_then(|to| Address::try_from_raw(to).map_err(BridgeUnlockError::bridge_address))?; Ok(Self { to, amount: amount.into(), @@ -1825,17 +1823,15 @@ impl Protobuf for BridgeSudoChange { let Some(bridge_address) = proto.bridge_address else { return Err(BridgeSudoChangeError::field_not_set("bridge_address")); }; - let bridge_address = Address::try_from_raw(&bridge_address) + let bridge_address = Address::try_from_raw(bridge_address) .map_err(BridgeSudoChangeError::invalid_bridge_address)?; let new_sudo_address = proto .new_sudo_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(BridgeSudoChangeError::invalid_new_sudo_address)?; let new_withdrawer_address = proto .new_withdrawer_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(BridgeSudoChangeError::invalid_new_withdrawer_address)?; diff --git a/crates/astria-core/src/sequencerblock/v1/block.rs b/crates/astria-core/src/sequencerblock/v1/block.rs index 48f8907fac..c7013e4d85 100644 --- a/crates/astria-core/src/sequencerblock/v1/block.rs +++ b/crates/astria-core/src/sequencerblock/v1/block.rs @@ -1428,7 +1428,7 @@ impl Deposit { return Err(DepositError::field_not_set("bridge_address")); }; let bridge_address = - Address::try_from_raw(&bridge_address).map_err(DepositError::address)?; + Address::try_from_raw(bridge_address).map_err(DepositError::address)?; let amount = amount.ok_or(DepositError::field_not_set("amount"))?.into(); let Some(rollup_id) = rollup_id else { return Err(DepositError::field_not_set("rollup_id")); diff --git a/crates/astria-sequencer-client/src/tests/http.rs b/crates/astria-sequencer-client/src/tests/http.rs index 23f1ee481c..9fca6a4949 100644 --- a/crates/astria-sequencer-client/src/tests/http.rs +++ b/crates/astria-sequencer-client/src/tests/http.rs @@ -12,6 +12,7 @@ use astria_core::{ Transaction, TransactionBody, }, + Protobuf as _, }; use hex_literal::hex; use prost::bytes::Bytes; diff --git a/crates/astria-sequencer/src/fees/tests.rs b/crates/astria-sequencer/src/fees/tests.rs index f486683a9e..088cf62de1 100644 --- a/crates/astria-sequencer/src/fees/tests.rs +++ b/crates/astria-sequencer/src/fees/tests.rs @@ -30,6 +30,7 @@ use astria_core::{ }, }, sequencerblock::v1::block::Deposit, + Protobuf as _, }; use cnidarium::StateDelta; diff --git a/crates/astria-sequencer/src/grpc/sequencer.rs b/crates/astria-sequencer/src/grpc/sequencer.rs index 7d74f077f6..2a87c73182 100644 --- a/crates/astria-sequencer/src/grpc/sequencer.rs +++ b/crates/astria-sequencer/src/grpc/sequencer.rs @@ -188,7 +188,7 @@ impl SequencerService for SequencerServer { )); }; - let address = Address::try_from_raw(&address).map_err(|e| { + let address = Address::try_from_raw(address).map_err(|e| { info!( error = %e, "failed to parse address from request", diff --git a/tools/astria-address/CHANGELOG.md b/tools/astria-address/CHANGELOG.md new file mode 100644 index 0000000000..b433965eb6 --- /dev/null +++ b/tools/astria-address/CHANGELOG.md @@ -0,0 +1,14 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial release [#1803](https://github.com/astriaorg/astria/pull/1803) diff --git a/tools/astria-address/Cargo.lock b/tools/astria-address/Cargo.lock new file mode 100644 index 0000000000..58d7ddfa1a --- /dev/null +++ b/tools/astria-address/Cargo.lock @@ -0,0 +1,478 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "astria-address" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "astria-core-address", + "const-hex", + "const_format", + "pico-args", + "predicates", +] + +[[package]] +name = "astria-core-address" +version = "0.1.0" +dependencies = [ + "astria-core-consts", + "bech32", + "thiserror", +] + +[[package]] +name = "astria-core-consts" +version = "0.1.0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-hex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0121754e84117e65f9d90648ee6aa4882a6e63110307ab73967a4c5e7e69e586" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bitflags", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "thiserror" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/tools/astria-address/Cargo.toml b/tools/astria-address/Cargo.toml new file mode 100644 index 0000000000..ba910d94d1 --- /dev/null +++ b/tools/astria-address/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "astria-address" +version = "0.1.0" +edition = "2021" +rust-version = "1.81.0" + +[dependencies] +astria-core-address = { path = "../../crates/astria-core-address" } +const-hex = "1.13.1" +const_format = "0.2.33" +pico-args = "0.5.0" + +[dev-dependencies] +assert_cmd = "2.0.16" +predicates = "3.1.2" diff --git a/tools/astria-address/README.md b/tools/astria-address/README.md new file mode 100644 index 0000000000..9071a0ff67 --- /dev/null +++ b/tools/astria-address/README.md @@ -0,0 +1,33 @@ +# Astria Address Tool + +Construct Astria addresses given a 20 hex encoded bytes (usually obtained from +a private/public ed25519 keypair). + +This tool is intended for developers and operators and usually not needed when +interacting with the Astria network. + +## Building and usage + +```console +# From inside the `astria-address` crate +$ cargo build --release +$ target/release/astria-address --help +Astria Address Tool + +Utility to construct astria addresses given an address prefix +and 20 hex-encoded bytes. + +USAGE: + astria-address [OPTIONS] [INPUT] + +FLAGS: + -h, --help Prints help information + -c, --compat Constructs a compat address (primarily IBC communication + with chains that only support bech32 non-m addresses) + +OPTIONS: + -p, --prefix STRING Sets the prefix of the address (default: astria) + +ARGS: + The 20 bytes in hex-format +``` diff --git a/tools/astria-address/src/main.rs b/tools/astria-address/src/main.rs new file mode 100644 index 0000000000..96f38f7e26 --- /dev/null +++ b/tools/astria-address/src/main.rs @@ -0,0 +1,97 @@ +use astria_core_address as address; +use astria_core_address::{ + Address, + ADDRESS_LENGTH, +}; + +const DEFAULT_PREFIX: &str = "astria"; +const HELP: &str = const_format::formatcp!( + r"Astria Address Tool + +Utility to construct astria addresses given an address prefix +and {ADDRESS_LENGTH} hex-encoded bytes. + +USAGE: + astria-address [OPTIONS] [INPUT] + +FLAGS: + -h, --help Prints help information + -c, --compat Constructs a compat address (primarily IBC communication + with chains that only support bech32 non-m addresses) + +OPTIONS: + -p, --prefix STRING Sets the prefix of the address (default: {DEFAULT_PREFIX}) + +ARGS: + The {ADDRESS_LENGTH} bytes in hex-format +" +); + +fn main() -> Result<(), Box> { + let args = Args::parse()?; + args.run()?; + Ok(()) +} + +#[derive(Debug)] +struct Args { + compat: bool, + prefix: Option, + input: [u8; ADDRESS_LENGTH], +} + +impl Args { + fn parse() -> Result { + let mut pargs = pico_args::Arguments::from_env(); + + // XXX: little hack to move the args out of pargs and back in: + let no_args = { + let raw_args = pargs.finish(); + let no_args = raw_args.is_empty(); + pargs = pico_args::Arguments::from_vec(raw_args); + no_args + }; + if pargs.contains(["-h", "--help"]) || no_args { + print!("{}", HELP); + std::process::exit(0); + } + + let args = Self { + compat: pargs.contains(["-c", "--compat"]), + prefix: pargs.opt_value_from_str(["-p", "--prefix"])?, + input: pargs.free_from_fn(|input| const_hex::decode_to_array(input))?, + }; + + // It's up to the caller what to do with the remaining arguments. + let remaining = pargs.finish(); + if !remaining.is_empty() { + return Err(pico_args::Error::ArgumentParsingFailed { + cause: format!("unknown arguments: {remaining:?}"), + }); + } + + Ok(args) + } + + fn run(self) -> Result<(), address::Error> { + use astria_core_address::{ + Bech32, + Bech32m, + }; + let prefix = self.prefix.as_deref().unwrap_or(DEFAULT_PREFIX); + if self.compat { + let address = Address::::builder() + .array(self.input) + .prefix(prefix) + .try_build()?; + println!("{address}"); + } else { + let address = Address::::builder() + .array(self.input) + .prefix(prefix) + .try_build()?; + println!("{address}"); + } + Ok(()) + } +} diff --git a/tools/astria-address/tests/make_address.rs b/tools/astria-address/tests/make_address.rs new file mode 100644 index 0000000000..31e85511a3 --- /dev/null +++ b/tools/astria-address/tests/make_address.rs @@ -0,0 +1,52 @@ +use assert_cmd::Command; +use predicates::prelude::*; + +const INPUT_BYTES: &str = "1234567890abcdef1234567890abcdef12345678"; + +#[test] +fn input_without_options() { + let assert = Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .arg(INPUT_BYTES) + .assert(); + assert.stdout(predicate::eq( + "astria1zg69v7ys40x77y352eufp27daufrg4nc077y64\n", + )); +} + +#[test] +fn input_with_compat() { + let assert = Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .arg("--compat") + .arg(INPUT_BYTES) + .assert(); + assert.stdout(predicate::eq( + "astria1zg69v7ys40x77y352eufp27daufrg4nc6zwglh\n", + )); +} + +#[test] +fn input_with_prefix() { + let assert = Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["--prefix", "astriacompat"]) + .arg(INPUT_BYTES) + .assert(); + assert.stdout(predicate::eq( + "astriacompat1zg69v7ys40x77y352eufp27daufrg4ncd586wu\n", + )); +} + +#[test] +fn input_with_prefix_and_compat() { + let assert = Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .arg("--compat") + .args(["--prefix", "astriacompat"]) + .arg(INPUT_BYTES) + .assert(); + assert.stdout(predicate::eq( + "astriacompat1zg69v7ys40x77y352eufp27daufrg4nccghkt7\n", + )); +}