use std::{fmt, string::FromUtf8Error};
use base64::Engine;
use bech32::{Bech32, Hrp};
use num_bigint::{BigInt, BigUint, Sign};
use prost::bytes::Bytes;
use serde::ser::{SerializeMap, Serializer as _};
use serde::{Deserialize, Serialize};
use crate::generated::proto::Transaction as ProtoTransaction;
const ERD_HRP: Hrp = Hrp::parse_unchecked("erd");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub nonce: u64,
pub value: String,
pub receiver: String,
pub sender: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_username: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver_username: Option<String>,
#[serde(rename = "gasPrice")]
pub gas_price: u64,
#[serde(rename = "gasLimit")]
pub gas_limit: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<String>,
#[serde(rename = "chainID")]
pub chain_id: String,
pub version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guardian: Option<String>,
#[serde(
rename = "guardianSignature",
default,
skip_serializing_if = "Option::is_none"
)]
pub guardian_signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relayer: Option<String>,
#[serde(
rename = "relayerSignature",
default,
skip_serializing_if = "Option::is_none"
)]
pub relayer_signature: Option<String>,
}
#[derive(Debug)]
pub enum ConversionError {
InvalidBech32(String),
InvalidAddressLength(usize),
InvalidNumeric(String),
InvalidHex(String),
InvalidBase64(String),
InvalidUtf8(FromUtf8Error),
InvalidBigIntEncoding(String),
Serialization(String),
Bech32Encode(bech32::EncodeError),
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidBech32(err) => write!(f, "invalid bech32 address: {err}"),
Self::InvalidAddressLength(len) => {
write!(f, "invalid address length: expected 32 bytes, got {len}")
}
Self::InvalidNumeric(value) => write!(f, "invalid numeric value: {value}"),
Self::InvalidHex(err) => write!(f, "invalid hex: {err}"),
Self::InvalidBase64(err) => write!(f, "invalid base64: {err}"),
Self::InvalidUtf8(err) => write!(f, "invalid utf-8: {err}"),
Self::InvalidBigIntEncoding(err) => write!(f, "invalid BigIntCaster encoding: {err}"),
Self::Serialization(err) => write!(f, "serialization failed: {err}"),
Self::Bech32Encode(err) => write!(f, "bech32 encode failed: {err}"),
}
}
}
impl std::error::Error for ConversionError {}
impl From<FromUtf8Error> for ConversionError {
fn from(value: FromUtf8Error) -> Self {
Self::InvalidUtf8(value)
}
}
impl From<bech32::EncodeError> for ConversionError {
fn from(value: bech32::EncodeError) -> Self {
Self::Bech32Encode(value)
}
}
impl TryFrom<&Transaction> for ProtoTransaction {
type Error = ConversionError;
fn try_from(tx: &Transaction) -> Result<Self, Self::Error> {
Ok(Self {
nonce: tx.nonce,
value: parse_big_uint(&tx.value)?,
rcv_addr: decode_bech32(&tx.receiver)?,
rcv_user_name: decode_optional_base64(tx.receiver_username.as_deref())?,
snd_addr: decode_bech32(&tx.sender)?,
snd_user_name: decode_optional_base64(tx.sender_username.as_deref())?,
gas_price: tx.gas_price,
gas_limit: tx.gas_limit,
data: decode_data_field(tx.data.as_deref())?,
chain_id: Bytes::copy_from_slice(tx.chain_id.as_bytes()),
version: tx.version,
signature: decode_optional_hex(tx.signature.as_deref())?,
options: tx.options.unwrap_or_default(),
guardian_addr: decode_optional_bech32(tx.guardian.as_deref())?,
guardian_signature: decode_optional_hex(tx.guardian_signature.as_deref())?,
relayer_addr: decode_optional_bech32(tx.relayer.as_deref())?,
relayer_signature: decode_optional_hex(tx.relayer_signature.as_deref())?,
})
}
}
impl TryFrom<Transaction> for ProtoTransaction {
type Error = ConversionError;
fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
Self::try_from(&tx)
}
}
impl Transaction {
pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
let data_len = self.data.as_ref().map_or(0, String::len);
let mut buf = Vec::with_capacity(256 + data_len);
let mut serializer = serde_json::Serializer::new(&mut buf);
let mut map = serializer.serialize_map(None).map_err(serialize_err)?;
map.serialize_entry("nonce", &self.nonce)
.map_err(serialize_err)?;
map.serialize_entry("value", &self.value)
.map_err(serialize_err)?;
map.serialize_entry("receiver", &self.receiver)
.map_err(serialize_err)?;
map.serialize_entry("sender", &self.sender)
.map_err(serialize_err)?;
if let Some(sender_username) = &self.sender_username
&& !sender_username.is_empty()
{
map.serialize_entry("senderUsername", sender_username)
.map_err(serialize_err)?;
}
if let Some(receiver_username) = &self.receiver_username
&& !receiver_username.is_empty()
{
map.serialize_entry("receiverUsername", receiver_username)
.map_err(serialize_err)?;
}
map.serialize_entry("gasPrice", &self.gas_price)
.map_err(serialize_err)?;
map.serialize_entry("gasLimit", &self.gas_limit)
.map_err(serialize_err)?;
if let Some(data) = &self.data {
map.serialize_entry("data", data).map_err(serialize_err)?;
}
map.serialize_entry("chainID", &self.chain_id)
.map_err(serialize_err)?;
map.serialize_entry("version", &self.version)
.map_err(serialize_err)?;
if let Some(options) = &self.options {
map.serialize_entry("options", options)
.map_err(serialize_err)?;
}
if let Some(guardian) = &self.guardian {
map.serialize_entry("guardian", guardian)
.map_err(serialize_err)?;
}
if let Some(relayer) = &self.relayer {
map.serialize_entry("relayer", relayer)
.map_err(serialize_err)?;
}
map.end().map_err(serialize_err)?;
Ok(buf)
}
}
impl TryFrom<&ProtoTransaction> for Transaction {
type Error = ConversionError;
fn try_from(tx: &ProtoTransaction) -> Result<Self, Self::Error> {
let value = decode_big_int_caster(&tx.value)?
.map_or_else(|| "0".to_owned(), |value| value.to_str_radix(10));
Ok(Self {
nonce: tx.nonce,
value,
receiver: encode_required_bech32(&tx.rcv_addr)?,
sender: encode_required_bech32(&tx.snd_addr)?,
sender_username: encode_optional_base64(&tx.snd_user_name),
receiver_username: encode_optional_base64(&tx.rcv_user_name),
gas_price: tx.gas_price,
gas_limit: tx.gas_limit,
data: encode_optional_base64(&tx.data),
chain_id: String::from_utf8(tx.chain_id.to_vec())?,
version: tx.version,
options: (tx.options != 0).then_some(tx.options),
guardian: encode_optional_bech32(&tx.guardian_addr)?,
guardian_signature: encode_optional_hex(&tx.guardian_signature),
signature: encode_optional_hex(&tx.signature),
relayer: encode_optional_bech32(&tx.relayer_addr)?,
relayer_signature: encode_optional_hex(&tx.relayer_signature),
})
}
}
impl TryFrom<ProtoTransaction> for Transaction {
type Error = ConversionError;
fn try_from(tx: ProtoTransaction) -> Result<Self, Self::Error> {
Self::try_from(&tx)
}
}
impl ProtoTransaction {
pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
Transaction::try_from(self)?.signing_bytes()
}
}
fn parse_big_uint(value: &str) -> Result<Bytes, ConversionError> {
let trimmed = value.trim();
let number = if let Some(hex_body) = trimmed.strip_prefix("0x") {
BigUint::parse_bytes(hex_body.as_bytes(), 16)
} else {
BigUint::parse_bytes(trimmed.as_bytes(), 10)
};
let num = number.ok_or_else(|| ConversionError::InvalidNumeric(value.to_owned()))?;
Ok(encode_big_int_caster(&BigInt::from_biguint(
Sign::Plus,
num,
)))
}
fn encode_big_int_caster(value: &BigInt) -> Bytes {
let (sign, magnitude) = value.to_bytes_be();
if magnitude.is_empty() {
return Bytes::from_static(&[0, 0]);
}
let mut encoded = Vec::with_capacity(magnitude.len() + 1);
encoded.push(match sign {
Sign::Minus => 1,
Sign::NoSign | Sign::Plus => 0,
});
encoded.extend_from_slice(&magnitude);
Bytes::from(encoded)
}
fn decode_big_int_caster(bytes: &[u8]) -> Result<Option<BigInt>, ConversionError> {
match bytes.len() {
0 => Err(ConversionError::InvalidBigIntEncoding(
"empty buffer is not a valid BigIntCaster value".to_owned(),
)),
1 => {
if bytes[0] == 0 {
Ok(None)
} else {
Err(ConversionError::InvalidBigIntEncoding(format!(
"single-byte encoding must be nil marker 0x00, got 0x{:02x}",
bytes[0]
)))
}
}
_ => {
let magnitude = BigUint::from_bytes_be(&bytes[1..]);
let value = match bytes[0] {
0 => BigInt::from_biguint(Sign::Plus, magnitude),
1 => BigInt::from_biguint(Sign::Minus, magnitude),
sign => {
return Err(ConversionError::InvalidBigIntEncoding(format!(
"invalid sign byte 0x{sign:02x}"
)));
}
};
Ok(Some(value))
}
}
}
fn decode_bech32(addr: &str) -> Result<Bytes, ConversionError> {
let (_hrp, raw) =
bech32::decode(addr).map_err(|e| ConversionError::InvalidBech32(e.to_string()))?;
if raw.len() != 32 {
return Err(ConversionError::InvalidAddressLength(raw.len()));
}
Ok(Bytes::from(raw))
}
fn decode_optional_bech32(addr: Option<&str>) -> Result<Bytes, ConversionError> {
match addr {
Some(a) if !a.trim().is_empty() => decode_bech32(a),
_ => Ok(Bytes::new()),
}
}
fn decode_optional_hex(value: Option<&str>) -> Result<Bytes, ConversionError> {
match value {
Some(s) if !s.trim().is_empty() => hex::decode(s.trim())
.map(Bytes::from)
.map_err(|e| ConversionError::InvalidHex(e.to_string())),
_ => Ok(Bytes::new()),
}
}
fn decode_optional_base64(value: Option<&str>) -> Result<Bytes, ConversionError> {
match value {
Some(s) if !s.trim().is_empty() => base64::engine::general_purpose::STANDARD
.decode(s.trim())
.map(Bytes::from)
.map_err(|e| ConversionError::InvalidBase64(e.to_string())),
_ => Ok(Bytes::new()),
}
}
fn decode_data_field(value: Option<&str>) -> Result<Bytes, ConversionError> {
match value {
Some(s) => match base64::engine::general_purpose::STANDARD.decode(s.trim()) {
Ok(bytes) => Ok(Bytes::from(bytes)),
Err(_) => hex::decode(s.trim())
.map(Bytes::from)
.map_err(|e| ConversionError::InvalidHex(e.to_string())),
},
None => Ok(Bytes::new()),
}
}
fn encode_required_bech32(bytes: &[u8]) -> Result<String, ConversionError> {
if bytes.len() != 32 {
return Err(ConversionError::InvalidAddressLength(bytes.len()));
}
Ok(bech32::encode::<Bech32>(ERD_HRP, bytes)?)
}
fn encode_optional_bech32(bytes: &[u8]) -> Result<Option<String>, ConversionError> {
if bytes.is_empty() {
return Ok(None);
}
encode_required_bech32(bytes).map(Some)
}
fn encode_optional_hex(bytes: &[u8]) -> Option<String> {
(!bytes.is_empty()).then(|| hex::encode(bytes))
}
fn encode_optional_base64(bytes: &[u8]) -> Option<String> {
(!bytes.is_empty()).then(|| base64::engine::general_purpose::STANDARD.encode(bytes))
}
fn serialize_err(err: serde_json::Error) -> ConversionError {
ConversionError::Serialization(err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_json_tx() -> Transaction {
Transaction {
nonce: 42,
value: "1000000000000000000".to_owned(),
receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l".to_owned(),
sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th".to_owned(),
sender_username: Some("c2VuZGVy".to_owned()),
receiver_username: Some("cmVjZWl2ZXI=".to_owned()),
gas_price: 1_000_000_000,
gas_limit: 50_000,
data: Some("dGVzdA==".to_owned()),
chain_id: "1".to_owned(),
version: 2,
options: Some(1),
guardian: None,
guardian_signature: None,
signature: Some("ab".repeat(64)),
relayer: None,
relayer_signature: None,
}
}
#[test]
fn json_to_proto_converts_fields() {
let tx = make_json_tx();
let proto = ProtoTransaction::try_from(&tx).unwrap();
assert_eq!(proto.nonce, 42);
assert_eq!(proto.gas_price, 1_000_000_000);
assert_eq!(proto.gas_limit, 50_000);
assert_eq!(proto.chain_id.as_ref(), b"1");
assert_eq!(proto.data.as_ref(), b"test");
assert_eq!(proto.snd_addr.len(), 32);
assert_eq!(proto.rcv_addr.len(), 32);
assert_eq!(proto.value[0], 0);
}
#[test]
fn proto_to_json_roundtrip() {
let tx = make_json_tx();
let proto = ProtoTransaction::try_from(&tx).unwrap();
let roundtrip = Transaction::try_from(&proto).unwrap();
assert_eq!(roundtrip.nonce, tx.nonce);
assert_eq!(roundtrip.value, tx.value);
assert_eq!(roundtrip.receiver, tx.receiver);
assert_eq!(roundtrip.sender, tx.sender);
assert_eq!(roundtrip.chain_id, tx.chain_id);
assert_eq!(roundtrip.data, Some("dGVzdA==".to_owned()));
assert_eq!(roundtrip.signature, tx.signature);
}
#[test]
fn proto_to_json_zero_value_maps_to_zero_string() {
let proto = ProtoTransaction {
value: Bytes::from_static(&[0, 0]),
chain_id: Bytes::from_static(b"1"),
rcv_addr: decode_bech32(
"erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l",
)
.unwrap(),
snd_addr: decode_bech32(
"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
)
.unwrap(),
..Default::default()
};
let json = Transaction::try_from(&proto).unwrap();
assert_eq!(json.value, "0");
}
#[test]
fn signing_bytes_field_order_matches_protocol() {
let tx = make_json_tx();
let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
let fields: Vec<&str> = json_str
.trim_matches(|c| c == '{' || c == '}')
.split(',')
.map(|s| s.split(':').next().unwrap().trim().trim_matches('"'))
.collect();
assert_eq!(fields[0], "nonce");
assert_eq!(fields[1], "value");
assert_eq!(fields[2], "receiver");
assert_eq!(fields[3], "sender");
assert_eq!(fields[4], "senderUsername");
assert_eq!(fields[5], "receiverUsername");
assert_eq!(fields[6], "gasPrice");
assert_eq!(fields[7], "gasLimit");
assert_eq!(fields[8], "data");
assert_eq!(fields[9], "chainID");
assert_eq!(fields[10], "version");
assert_eq!(fields[11], "options");
}
#[test]
fn signing_bytes_omit_signatures_and_include_relayer_when_present() {
let mut tx = make_json_tx();
tx.guardian =
Some("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8".to_owned());
tx.guardian_signature = Some("cd".repeat(64));
tx.relayer =
Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());
tx.relayer_signature = Some("ef".repeat(64));
let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
assert!(json_str.contains("\"relayer\":"));
assert!(json_str.contains("\"guardian\":"));
assert!(!json_str.contains("signature"));
assert!(!json_str.contains("guardianSignature"));
assert!(!json_str.contains("relayerSignature"));
}
#[test]
fn signing_bytes_omits_empty_usernames() {
let mut tx = make_json_tx();
tx.sender_username = Some(String::new());
tx.receiver_username = Some(String::new());
tx.relayer =
Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());
let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
assert!(json_str.contains("\"relayer\":"));
assert!(!json_str.contains("senderUsername"));
assert!(!json_str.contains("receiverUsername"));
}
#[test]
fn proto_signing_bytes_match_json_signing_bytes() {
let tx = make_json_tx();
let proto = ProtoTransaction::try_from(&tx).unwrap();
assert_eq!(proto.signing_bytes().unwrap(), tx.signing_bytes().unwrap());
}
}