Skip to content
Closed
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
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ prost-types = "0.13"
prost = "0.13"
pbkdf2 = {version = "0.12"}
hmac = {version = "0.12"}
rand = {version = "0.8"}
rust_decimal = "1.36"
secp256k1 = {version = "0.30", features = ["global-context"]}
rand = {version = "0.9"}
rust_decimal = "1.40"
secp256k1 = {version = "0.31", features = ["global-context"]}
tonic = {version = "0.12", features = ["gzip"]}
bytes = "1.8"
bytes = "1.11"
log = "0.4"
tokio = {version = "1", features=["time"]}
clarity = {version = "1.5", optional = true}
sha3 = {version = "0.10", optional = true}

cosmos-sdk-proto = {package = "cosmos-sdk-proto-althea", version = "0.20"}
althea_proto = {version="0.10", optional=true}
althea_proto = {version="0.9", optional=true}

[dev-dependencies]
rand = "0.8"
rand = "0.9"
env_logger = "0.11"
actix-rt = "2.10"
actix-rt = "2.11"


[features]
Expand Down
69 changes: 69 additions & 0 deletions src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ impl Address {
Ok(())
}

/// Changes the `prefix` field to modify the resulting Bech32 `hrp`, returns the updated Address
pub fn re_prefix<T: Into<String>>(&self, prefix: T) -> Result<Address, AddressError> {
match self {
Address::Base(base_address) => Ok(Address::Base(BaseAddress {
bytes: base_address.bytes,
prefix: ArrayString::new(&prefix.into())?,
})),
Address::Derived(derived_address) => Ok(Address::Derived(DerivedAddress {
bytes: derived_address.bytes,
prefix: ArrayString::new(&prefix.into())?,
})),
}
}

/// Returns the underlying `bytes` buffer as a slice
pub fn get_bytes(&self) -> &[u8] {
match self {
Expand All @@ -167,6 +181,25 @@ impl Address {
}
}

// Ethermint address conversion, note there's no way to determine from an address how the signers wallet is configured, if you convert Cosmos addressed
// used with a non-eth signing scheme using this method that public key goes to a completely different address and the user will never be able to retrieve
// funds sent to that address, so use caution when making assumptions.
#[cfg(feature = "ethermint")]
impl TryInto<EthAddress> for &Address {
type Error = clarity::error::Error;

fn try_into(self) -> Result<EthAddress, Self::Error> {
EthAddress::from_slice(self.get_bytes())
}
}

#[cfg(feature = "ethermint")]
impl From<EthAddress> for Address {
fn from(value: EthAddress) -> Self {
Address::from_slice(value.as_bytes(), DEFAULT_PREFIX).unwrap()
}
}

impl FromStr for Address {
type Err = AddressError;

Expand Down Expand Up @@ -296,3 +329,39 @@ fn test_address_conversion() {
let eth_address = cosmos_address_to_eth_address(test).unwrap();
let _cosmos_address = eth_address_to_cosmos_address(eth_address, None).unwrap();
}

#[cfg(feature = "ethermint")]
#[test]
fn test_trait_conversions() {
use std::convert::TryInto;

// Test TryInto<EthAddress> for &Address
let test: Address = "cosmos1vlms2r8f6x7yxjh3ynyzc7ckarqd8a96ckjvrp"
.parse()
.unwrap();
let eth_address: EthAddress = (&test).try_into().unwrap();

// Test From<EthAddress> for Address
let cosmos_address: Address = eth_address.into();

// Verify the roundtrip works
assert_eq!(test.get_bytes(), cosmos_address.get_bytes());
}

#[test]
fn test_re_prefix() {
let test: Address = "cosmos1vlms2r8f6x7yxjh3ynyzc7ckarqd8a96ckjvrp"
.parse()
.unwrap();

// Test re_prefix with a different prefix
let osmosis_address = test.re_prefix("osmo").unwrap();
assert_eq!(osmosis_address.to_string(), "osmo1vlms2r8f6x7yxjh3ynyzc7ckarqd8a96sdpu4n");

// Verify the underlying bytes are the same
assert_eq!(test.get_bytes(), osmosis_address.get_bytes());

// Test re_prefix with the same prefix
let cosmos_address = test.re_prefix("cosmos").unwrap();
assert_eq!(test.to_string(), cosmos_address.to_string());
}
5 changes: 3 additions & 2 deletions src/client/gov/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ impl Contact {
.await
}

#[allow(clippy::too_many_arguments)]
/// Provides an interface for submitting msg-based governance proposals
pub async fn create_gov_proposal(
&self,
Expand All @@ -192,8 +193,8 @@ impl Contact {
proposer: our_address.to_string(),
initial_deposit: vec![deposit.into()],
expedited: false,
summary: summary,
title: title,
summary,
title,
};

let msg = Msg::new(MSG_SUBMIT_PROPOSAL_TYPE_URL, proposal);
Expand Down
23 changes: 17 additions & 6 deletions src/client/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::error::CosmosGrpcError;
use crate::msg::Msg;
use crate::private_key::PrivateKey;
use crate::utils::check_for_sdk_error;
use crate::utils::get_txhash;
use crate::MessageArgs;
#[cfg(feature = "althea")]
use althea_proto::althea::microtx::v1::MsgMicrotx;
Expand Down Expand Up @@ -92,14 +93,22 @@ impl Contact {
) -> Result<TransactionResponse, CosmosGrpcError> {
let mut txrpc =
timeout(self.get_timeout(), TxServiceClient::connect(self.get_url())).await??;
let response = timeout(

let txid = get_txhash(msg.clone());
let response = match timeout(
self.get_timeout(),
txrpc.broadcast_tx(BroadcastTxRequest {
tx_bytes: msg,
mode: mode.into(),
}),
)
.await??;
.await
{
Ok(res) => res?,
// broadcasting the signed tx, if we return here the signed tx bytes MAY have been
// published so we must return an error containing the txid for the caller to check
Err(_) => return Err(CosmosGrpcError::TimeoutErrorSigned { txid }),
};
let response = response.into_inner().tx_response.unwrap();
// checks only for sdk errors, other types will not be handled
check_for_sdk_error(&response)?;
Expand Down Expand Up @@ -266,7 +275,7 @@ impl Contact {
let fee_amount = fee_amount.unwrap_or_default();
let mut txrpc =
timeout(self.get_timeout(), TxServiceClient::connect(self.get_url())).await??;
let max_gas = max_gas.map_or_else(|| 10_000_000, |v| v.clamp(0, 9223372036854775807)) as u64;
let max_gas = max_gas.map_or_else(|| 10_000_000, |v| v.clamp(0, 9223372036854775807));

let fee_obj = Fee {
amount: fee_amount.to_vec(),
Expand Down Expand Up @@ -368,7 +377,7 @@ impl Contact {
/// * `private_key` - A private key used to sign and send the transaction
/// # Examples
/// ```rust
/// use althea_proto::microtx::v1::MsgMicrotx;
/// use althea_proto::althea::microtx::v1::MsgMicrotx;
/// use cosmos_sdk_proto::cosmos::tx::v1beta1::BroadcastMode;
/// use deep_space::{Coin, client::Contact, Fee, MessageArgs, Msg, CosmosPrivateKey, PrivateKey, PublicKey};
/// use std::time::Duration;
Expand All @@ -386,7 +395,7 @@ impl Contact {
/// let contact = Contact::new("https:://your-grpc-server", Duration::from_secs(5), "prefix").unwrap();
/// let duration = Duration::from_secs(30);
/// // future must be awaited in tokio runtime
/// contact.send_microtx(coin.clone(), Some(fee), address, Some(duration), private_key);
/// contact.send_microtx(coin.clone(), Some(fee), address, Some(duration), Some(5), private_key);
/// ```
pub async fn send_microtx(
&self,
Expand Down Expand Up @@ -437,11 +446,12 @@ impl Contact {
}
Err(CosmosGrpcError::RequestError { error }) => match error.code() {
TonicCode::NotFound | TonicCode::Unknown | TonicCode::InvalidArgument => {}
_ => {
v => {
return Err(CosmosGrpcError::TransactionFailed {
tx: response.into(),
time: Instant::now() - start,
sdk_error: None,
tonic_code: Some(v),
});
}
},
Expand All @@ -453,6 +463,7 @@ impl Contact {
tx: response.into(),
time: timeout,
sdk_error: None,
tonic_code: None,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions src/coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ impl From<Coin> for ProtoCoin {
}
}


/// Fee represents everything about a Cosmos transaction fee, including the gas limit
/// who pays, and how much of an arbitrary number of Coin structs.
#[derive(Serialize, Debug, Default, Clone, Deserialize, Eq, PartialEq, Hash)]
Expand Down
31 changes: 30 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub enum CosmosGrpcError {
tx: TxResponse,
time: Duration,
sdk_error: Option<SdkErrorCode>,
tonic_code: Option<tonic::Code>,
},
InsufficientFees {
fee_info: FeeInfo,
Expand All @@ -56,9 +57,29 @@ pub enum CosmosGrpcError {
max: u64,
required: u64,
},
/// This timeout error occurs when a transaction has been signed but we
/// have not seen it appear on chain yet. We return the txid so the caller
/// can check if it made it into the chain or not.
/// DEVELOPER: you must be cautious not to ? away a timeout error that needs the txid
TimeoutErrorSigned {
txid: String,
},
/// This timeout error occurs when no transaction has been signed and therefore we have
/// complete confidence that no transaction has been broadcast
TimeoutError,
}

impl CosmosGrpcError {
/// Returns the txid if this error is associated with a signed and potential published transaction
pub fn get_txid(&self) -> Option<String> {
match self {
CosmosGrpcError::TimeoutErrorSigned { txid } => Some(txid.clone()),
CosmosGrpcError::TransactionFailed { tx, .. } => Some(tx.txhash.clone()),
_ => None,
}
}
}

impl Display for CosmosGrpcError {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Expand Down Expand Up @@ -98,12 +119,14 @@ impl Display for CosmosGrpcError {
tx,
time,
sdk_error,
tonic_code,
} => {
write!(
f,
"CosmosGrpc Transaction {:?} {:?} did not enter chain in {}ms",
"CosmosGrpc Transaction {:?} {:?} {:?} did not enter chain in {}ms",
tx,
sdk_error,
tonic_code,
time.as_millis()
)
}
Expand All @@ -120,6 +143,12 @@ impl Display for CosmosGrpcError {
)
}
CosmosGrpcError::TimeoutError => write!(f, "Timed out"),
CosmosGrpcError::TimeoutErrorSigned { txid } => {
write!(
f,
"Timed out waiting for tx to appear on chain. Txid: {txid}"
)
}
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/mnemonic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl Mnemonic {
/// Create a new [Mnemonic] in the specified language from the given entropy.
/// Entropy must be a multiple of 32 bits (4 bytes) and 128-256 bits in length.
pub fn from_entropy_in(language: Language, entropy: &[u8]) -> Result<Mnemonic, Bip39Error> {
if entropy.len() % 4 != 0 {
if !entropy.len().is_multiple_of(4) {
return Err(Bip39Error::BadEntropyBitCount(entropy.len() * 8));
}

Expand Down Expand Up @@ -89,12 +89,12 @@ impl Mnemonic {
/// Generate a new Mnemonic in the given language.
/// For the different supported word counts, see documentation on [Mnemonoc].
pub fn generate_in(language: Language, word_count: usize) -> Result<Mnemonic, Bip39Error> {
if word_count < 6 || word_count % 6 != 0 || word_count > 24 {
if word_count < 6 || !word_count.is_multiple_of(6) || word_count > 24 {
return Err(Bip39Error::BadWordCount(word_count));
}

let entropy_bytes = (word_count / 3) * 4;
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
let mut entropy = vec![0u8; entropy_bytes];
rand::RngCore::fill_bytes(&mut rng, &mut entropy);
Mnemonic::from_entropy_in(language, &entropy)
Expand All @@ -109,7 +109,7 @@ impl Mnemonic {
/// Static method to validate a mnemonic in a given language.
pub fn validate_in(language: Language, s: &str) -> Result<(), Bip39Error> {
let words: Vec<&str> = s.split_whitespace().collect();
if words.len() < 6 || words.len() % 6 != 0 || words.len() > 24 {
if words.len() < 6 || !words.len().is_multiple_of(6) || words.len() > 24 {
return Err(Bip39Error::BadWordCount(words.len()));
}

Expand Down
Loading