#![doc = include_str!("../README.md")]
#[allow(clippy::all)]
#[allow(dead_code)]
#[allow(missing_docs)]
pub mod generated {
include!(concat!(env!("OUT_DIR"), "/mod.rs"));
}
#[cfg(feature = "serde")]
pub mod json;
use bech32::{Bech32, Hrp};
use blake2::{
Blake2b,
digest::{Digest, consts::U32},
};
type Blake2b256 = Blake2b<U32>;
#[cfg(feature = "serde")]
pub use json::Transaction as JsonTransaction;
pub use prost::Message;
const ERD_HRP: Hrp = Hrp::parse_unchecked("erd");
impl generated::proto::Transaction {
pub const HASH_SIZE: usize = 32;
#[must_use]
pub fn compute_hash(&self) -> [u8; Self::HASH_SIZE] {
let encoded = self.encode_to_vec();
Self::hash_bytes(&encoded)
}
#[must_use]
pub fn get_tx_hash(&self) -> [u8; Self::HASH_SIZE] {
self.compute_hash()
}
#[must_use]
pub fn hash_bytes(bytes: &[u8]) -> [u8; Self::HASH_SIZE] {
let digest = Blake2b256::digest(bytes);
digest.into()
}
fn bech32_address(bytes: &[u8]) -> Result<Option<String>, bech32::EncodeError> {
if bytes.is_empty() {
return Ok(None);
}
let encoded = bech32::encode::<Bech32>(ERD_HRP, bytes)?;
Ok(Some(encoded))
}
pub fn sender_address_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
Self::bech32_address(self.snd_addr.as_ref())
}
pub fn sender_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
self.sender_address_bech32()
}
pub fn receiver_address_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
Self::bech32_address(self.rcv_addr.as_ref())
}
pub fn receiver_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
self.receiver_address_bech32()
}
pub fn guardian_address_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
Self::bech32_address(self.guardian_addr.as_ref())
}
pub fn guardian_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
self.guardian_address_bech32()
}
pub fn relayer_address_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
Self::bech32_address(self.relayer_addr.as_ref())
}
pub fn relayer_bech32(&self) -> Result<Option<String>, bech32::EncodeError> {
self.relayer_address_bech32()
}
}
#[cfg(test)]
mod tests {
use super::ERD_HRP;
use super::generated::proto::Transaction;
use bech32::Bech32;
use hex::FromHex;
use prost::Message;
use prost::bytes::Bytes;
#[test]
fn hash_bytes_matches_known_digests() {
let expected_empty = <[u8; Transaction::HASH_SIZE]>::from_hex(
"0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8",
)
.unwrap();
assert_eq!(Transaction::hash_bytes(b""), expected_empty);
let expected_hello = <[u8; Transaction::HASH_SIZE]>::from_hex(
"256c83b297114d201b30179f3f0ef0cace9783622da5974326b436178aeef610",
)
.unwrap();
assert_eq!(Transaction::hash_bytes(b"hello world"), expected_hello);
}
#[test]
fn compute_hash_reencodes_transaction_like_go() {
let tx = Transaction {
nonce: 1,
value: Bytes::new(),
rcv_addr: Bytes::new(),
rcv_user_name: Bytes::new(),
snd_addr: Bytes::new(),
snd_user_name: Bytes::new(),
gas_price: 0,
gas_limit: 0,
data: Bytes::new(),
chain_id: Bytes::new(),
version: 0,
signature: Bytes::new(),
options: 0,
guardian_addr: Bytes::new(),
guardian_signature: Bytes::new(),
relayer_addr: Bytes::new(),
relayer_signature: Bytes::new(),
};
let expected = <[u8; Transaction::HASH_SIZE]>::from_hex(
"890b1d2195b2db958c0b3c02d09997776a5f7c0fc2daf30f3bf8469b841c30e9",
)
.unwrap();
let encoded = tx.encode_to_vec();
assert_eq!(encoded, [0x08, 0x01]);
assert_eq!(tx.compute_hash(), expected);
}
#[test]
fn bech32_address_helpers_encode_addresses() {
let sender_vec: Vec<u8> = (0..32).collect();
let receiver_vec: Vec<u8> = (32..64).collect();
let relayer_vec: Vec<u8> = (64..96).collect();
let tx = Transaction {
snd_addr: Bytes::from(sender_vec.clone()),
rcv_addr: Bytes::from(receiver_vec.clone()),
relayer_addr: Bytes::from(relayer_vec.clone()),
..Default::default()
};
let expected_sender = bech32::encode::<Bech32>(ERD_HRP, &sender_vec).unwrap();
let expected_receiver = bech32::encode::<Bech32>(ERD_HRP, &receiver_vec).unwrap();
let expected_relayer = bech32::encode::<Bech32>(ERD_HRP, &relayer_vec).unwrap();
assert_eq!(tx.sender_address_bech32().unwrap(), Some(expected_sender));
assert_eq!(
tx.sender_bech32().unwrap(),
tx.sender_address_bech32().unwrap()
);
assert_eq!(
tx.receiver_address_bech32().unwrap(),
Some(expected_receiver)
);
assert_eq!(
tx.receiver_bech32().unwrap(),
tx.receiver_address_bech32().unwrap()
);
assert_eq!(tx.guardian_address_bech32().unwrap(), None);
assert_eq!(
tx.guardian_bech32().unwrap(),
tx.guardian_address_bech32().unwrap()
);
assert_eq!(tx.relayer_address_bech32().unwrap(), Some(expected_relayer));
assert_eq!(
tx.relayer_bech32().unwrap(),
tx.relayer_address_bech32().unwrap()
);
}
#[test]
fn test_hash_bytes_large_input() {
let large_input: Vec<u8> = (0..=255).cycle().take(10 * 1024).collect();
let hash = Transaction::hash_bytes(&large_input);
assert_eq!(hash.len(), Transaction::HASH_SIZE);
let hash2 = Transaction::hash_bytes(&large_input);
assert_eq!(hash, hash2);
assert_ne!(hash, [0u8; 32]);
}
#[test]
fn test_hash_bytes_all_zeros() {
let zeros = vec![0u8; 1024];
let hash = Transaction::hash_bytes(&zeros);
assert_eq!(hash.len(), Transaction::HASH_SIZE);
assert_ne!(hash, [0u8; 32]);
assert_eq!(hash, Transaction::hash_bytes(&zeros));
}
#[test]
fn test_hash_bytes_all_ones() {
let ones = vec![0xFFu8; 1024];
let hash = Transaction::hash_bytes(&ones);
assert_eq!(hash.len(), Transaction::HASH_SIZE);
assert_ne!(hash, [0xFFu8; 32]);
assert_eq!(hash, Transaction::hash_bytes(&ones));
let zeros = vec![0u8; 1024];
assert_ne!(hash, Transaction::hash_bytes(&zeros));
}
#[test]
fn test_compute_hash_empty_tx() {
let tx = Transaction::default();
let hash = tx.compute_hash();
assert_eq!(hash.len(), Transaction::HASH_SIZE);
let expected_empty = <[u8; Transaction::HASH_SIZE]>::from_hex(
"0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8",
)
.unwrap();
assert_eq!(hash, expected_empty);
}
#[test]
fn test_compute_hash_full_tx() {
let tx = Transaction {
nonce: 42,
value: Bytes::from(vec![0x01, 0x00, 0x00, 0x00]), rcv_addr: Bytes::from(vec![1u8; 32]),
rcv_user_name: Bytes::from("receiver_name"),
snd_addr: Bytes::from(vec![2u8; 32]),
snd_user_name: Bytes::from("sender_name"),
gas_price: 1_000_000_000,
gas_limit: 50_000,
data: Bytes::from("transfer@01"),
chain_id: Bytes::from("1"),
version: 2,
signature: Bytes::from(vec![3u8; 64]),
options: 1,
guardian_addr: Bytes::from(vec![4u8; 32]),
guardian_signature: Bytes::from(vec![5u8; 64]),
relayer_addr: Bytes::from(vec![6u8; 32]),
relayer_signature: Bytes::from(vec![7u8; 64]),
};
let hash = tx.compute_hash();
assert_eq!(hash.len(), Transaction::HASH_SIZE);
assert_ne!(hash, [0u8; 32]);
assert_ne!(hash, Transaction::default().compute_hash());
}
#[test]
fn test_compute_hash_deterministic() {
let tx = Transaction {
nonce: 100,
value: Bytes::from(vec![0x0A]),
rcv_addr: Bytes::from(vec![0xAB; 32]),
snd_addr: Bytes::from(vec![0xCD; 32]),
gas_price: 500_000_000,
gas_limit: 100_000,
data: Bytes::from("test_data"),
chain_id: Bytes::from("T"),
version: 1,
..Default::default()
};
let hash1 = tx.compute_hash();
let hash2 = tx.compute_hash();
let hash3 = tx.compute_hash();
assert_eq!(hash1, hash2);
assert_eq!(hash2, hash3);
let tx_cloned = tx.clone();
assert_eq!(tx.compute_hash(), tx_cloned.compute_hash());
assert_eq!(tx.get_tx_hash(), tx.compute_hash());
}
#[test]
fn test_sender_address_empty() {
let tx = Transaction {
snd_addr: Bytes::new(),
..Default::default()
};
let result = tx.sender_address_bech32().unwrap();
assert_eq!(result, None);
}
#[test]
fn test_receiver_address_empty() {
let tx = Transaction {
rcv_addr: Bytes::new(),
..Default::default()
};
let result = tx.receiver_address_bech32().unwrap();
assert_eq!(result, None);
}
#[test]
fn test_guardian_address_populated() {
let guardian_bytes: Vec<u8> = (100..132).collect(); let tx = Transaction {
guardian_addr: Bytes::from(guardian_bytes.clone()),
..Default::default()
};
let result = tx.guardian_address_bech32().unwrap();
assert!(result.is_some());
let expected = bech32::encode::<Bech32>(ERD_HRP, &guardian_bytes).unwrap();
assert_eq!(result.unwrap(), expected);
assert_eq!(
tx.guardian_bech32().unwrap(),
tx.guardian_address_bech32().unwrap()
);
}
#[test]
fn test_relayer_address_populated() {
let relayer_bytes: Vec<u8> = (150..182).collect(); let tx = Transaction {
relayer_addr: Bytes::from(relayer_bytes.clone()),
..Default::default()
};
let result = tx.relayer_address_bech32().unwrap();
assert!(result.is_some());
let expected = bech32::encode::<Bech32>(ERD_HRP, &relayer_bytes).unwrap();
assert_eq!(result.unwrap(), expected);
assert_eq!(
tx.relayer_bech32().unwrap(),
tx.relayer_address_bech32().unwrap()
);
}
#[test]
fn test_all_addresses_format() {
let sender_bytes: Vec<u8> = (0..32).collect();
let receiver_bytes: Vec<u8> = (32..64).collect();
let guardian_bytes: Vec<u8> = (64..96).collect();
let relayer_bytes: Vec<u8> = (96..128).collect();
let tx = Transaction {
snd_addr: Bytes::from(sender_bytes),
rcv_addr: Bytes::from(receiver_bytes),
guardian_addr: Bytes::from(guardian_bytes),
relayer_addr: Bytes::from(relayer_bytes),
..Default::default()
};
let sender = tx.sender_bech32().unwrap().unwrap();
let receiver = tx.receiver_bech32().unwrap().unwrap();
let guardian = tx.guardian_bech32().unwrap().unwrap();
let relayer = tx.relayer_bech32().unwrap().unwrap();
assert!(
sender.starts_with("erd1"),
"Sender should start with erd1, got: {sender}"
);
assert!(
receiver.starts_with("erd1"),
"Receiver should start with erd1, got: {receiver}"
);
assert!(
guardian.starts_with("erd1"),
"Guardian should start with erd1, got: {guardian}"
);
assert!(
relayer.starts_with("erd1"),
"Relayer should start with erd1, got: {relayer}"
);
assert_eq!(sender.len(), 62);
assert_eq!(receiver.len(), 62);
assert_eq!(guardian.len(), 62);
assert_eq!(relayer.len(), 62);
}
}