1use std::{fmt, string::FromUtf8Error};
2
3use base64::Engine;
4use bech32::{Bech32, Hrp};
5use num_bigint::{BigInt, BigUint, Sign};
6use prost::bytes::Bytes;
7use serde::ser::{SerializeMap, Serializer as _};
8use serde::{Deserialize, Serialize};
9
10use crate::generated::proto::Transaction as ProtoTransaction;
11
12const ERD_HRP: Hrp = Hrp::parse_unchecked("erd");
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Transaction {
18 pub nonce: u64,
20 pub value: String,
22 pub receiver: String,
24 pub sender: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub sender_username: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub receiver_username: Option<String>,
32 #[serde(rename = "gasPrice")]
34 pub gas_price: u64,
35 #[serde(rename = "gasLimit")]
37 pub gas_limit: u64,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub data: Option<String>,
41 #[serde(rename = "chainID")]
43 pub chain_id: String,
44 pub version: u32,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub options: Option<u32>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub guardian: Option<String>,
52 #[serde(
54 rename = "guardianSignature",
55 default,
56 skip_serializing_if = "Option::is_none"
57 )]
58 pub guardian_signature: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub signature: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub relayer: Option<String>,
65 #[serde(
67 rename = "relayerSignature",
68 default,
69 skip_serializing_if = "Option::is_none"
70 )]
71 pub relayer_signature: Option<String>,
72}
73
74#[derive(Debug)]
76pub enum ConversionError {
77 InvalidBech32(String),
78 InvalidAddressLength(usize),
79 InvalidNumeric(String),
80 InvalidHex(String),
81 InvalidBase64(String),
82 InvalidUtf8(FromUtf8Error),
83 InvalidBigIntEncoding(String),
84 Serialization(String),
85 Bech32Encode(bech32::EncodeError),
86}
87
88impl fmt::Display for ConversionError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::InvalidBech32(err) => write!(f, "invalid bech32 address: {err}"),
92 Self::InvalidAddressLength(len) => {
93 write!(f, "invalid address length: expected 32 bytes, got {len}")
94 }
95 Self::InvalidNumeric(value) => write!(f, "invalid numeric value: {value}"),
96 Self::InvalidHex(err) => write!(f, "invalid hex: {err}"),
97 Self::InvalidBase64(err) => write!(f, "invalid base64: {err}"),
98 Self::InvalidUtf8(err) => write!(f, "invalid utf-8: {err}"),
99 Self::InvalidBigIntEncoding(err) => write!(f, "invalid BigIntCaster encoding: {err}"),
100 Self::Serialization(err) => write!(f, "serialization failed: {err}"),
101 Self::Bech32Encode(err) => write!(f, "bech32 encode failed: {err}"),
102 }
103 }
104}
105
106impl std::error::Error for ConversionError {}
107
108impl From<FromUtf8Error> for ConversionError {
109 fn from(value: FromUtf8Error) -> Self {
110 Self::InvalidUtf8(value)
111 }
112}
113
114impl From<bech32::EncodeError> for ConversionError {
115 fn from(value: bech32::EncodeError) -> Self {
116 Self::Bech32Encode(value)
117 }
118}
119
120impl TryFrom<&Transaction> for ProtoTransaction {
121 type Error = ConversionError;
122
123 fn try_from(tx: &Transaction) -> Result<Self, Self::Error> {
124 Ok(Self {
125 nonce: tx.nonce,
126 value: parse_big_uint(&tx.value)?,
127 rcv_addr: decode_bech32(&tx.receiver)?,
128 rcv_user_name: decode_optional_base64(tx.receiver_username.as_deref())?,
129 snd_addr: decode_bech32(&tx.sender)?,
130 snd_user_name: decode_optional_base64(tx.sender_username.as_deref())?,
131 gas_price: tx.gas_price,
132 gas_limit: tx.gas_limit,
133 data: decode_data_field(tx.data.as_deref())?,
134 chain_id: Bytes::copy_from_slice(tx.chain_id.as_bytes()),
135 version: tx.version,
136 signature: decode_optional_hex(tx.signature.as_deref())?,
137 options: tx.options.unwrap_or_default(),
138 guardian_addr: decode_optional_bech32(tx.guardian.as_deref())?,
139 guardian_signature: decode_optional_hex(tx.guardian_signature.as_deref())?,
140 relayer_addr: decode_optional_bech32(tx.relayer.as_deref())?,
141 relayer_signature: decode_optional_hex(tx.relayer_signature.as_deref())?,
142 })
143 }
144}
145
146impl TryFrom<Transaction> for ProtoTransaction {
147 type Error = ConversionError;
148
149 fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
150 Self::try_from(&tx)
151 }
152}
153
154impl Transaction {
155 pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
157 let data_len = self.data.as_ref().map_or(0, String::len);
158 let mut buf = Vec::with_capacity(256 + data_len);
159 let mut serializer = serde_json::Serializer::new(&mut buf);
160 let mut map = serializer.serialize_map(None).map_err(serialize_err)?;
161
162 map.serialize_entry("nonce", &self.nonce)
163 .map_err(serialize_err)?;
164 map.serialize_entry("value", &self.value)
165 .map_err(serialize_err)?;
166 map.serialize_entry("receiver", &self.receiver)
167 .map_err(serialize_err)?;
168 map.serialize_entry("sender", &self.sender)
169 .map_err(serialize_err)?;
170
171 if let Some(sender_username) = &self.sender_username
172 && !sender_username.is_empty()
173 {
174 map.serialize_entry("senderUsername", sender_username)
175 .map_err(serialize_err)?;
176 }
177 if let Some(receiver_username) = &self.receiver_username
178 && !receiver_username.is_empty()
179 {
180 map.serialize_entry("receiverUsername", receiver_username)
181 .map_err(serialize_err)?;
182 }
183
184 map.serialize_entry("gasPrice", &self.gas_price)
185 .map_err(serialize_err)?;
186 map.serialize_entry("gasLimit", &self.gas_limit)
187 .map_err(serialize_err)?;
188 if let Some(data) = &self.data {
189 map.serialize_entry("data", data).map_err(serialize_err)?;
190 }
191 map.serialize_entry("chainID", &self.chain_id)
192 .map_err(serialize_err)?;
193 map.serialize_entry("version", &self.version)
194 .map_err(serialize_err)?;
195 if let Some(options) = &self.options {
196 map.serialize_entry("options", options)
197 .map_err(serialize_err)?;
198 }
199 if let Some(guardian) = &self.guardian {
200 map.serialize_entry("guardian", guardian)
201 .map_err(serialize_err)?;
202 }
203 if let Some(relayer) = &self.relayer {
204 map.serialize_entry("relayer", relayer)
205 .map_err(serialize_err)?;
206 }
207 map.end().map_err(serialize_err)?;
208
209 Ok(buf)
210 }
211}
212
213impl TryFrom<&ProtoTransaction> for Transaction {
214 type Error = ConversionError;
215
216 fn try_from(tx: &ProtoTransaction) -> Result<Self, Self::Error> {
217 let value = decode_big_int_caster(&tx.value)?
218 .map_or_else(|| "0".to_owned(), |value| value.to_str_radix(10));
219
220 Ok(Self {
221 nonce: tx.nonce,
222 value,
223 receiver: encode_required_bech32(&tx.rcv_addr)?,
224 sender: encode_required_bech32(&tx.snd_addr)?,
225 sender_username: encode_optional_base64(&tx.snd_user_name),
226 receiver_username: encode_optional_base64(&tx.rcv_user_name),
227 gas_price: tx.gas_price,
228 gas_limit: tx.gas_limit,
229 data: encode_optional_base64(&tx.data),
230 chain_id: String::from_utf8(tx.chain_id.to_vec())?,
231 version: tx.version,
232 options: (tx.options != 0).then_some(tx.options),
233 guardian: encode_optional_bech32(&tx.guardian_addr)?,
234 guardian_signature: encode_optional_hex(&tx.guardian_signature),
235 signature: encode_optional_hex(&tx.signature),
236 relayer: encode_optional_bech32(&tx.relayer_addr)?,
237 relayer_signature: encode_optional_hex(&tx.relayer_signature),
238 })
239 }
240}
241
242impl TryFrom<ProtoTransaction> for Transaction {
243 type Error = ConversionError;
244
245 fn try_from(tx: ProtoTransaction) -> Result<Self, Self::Error> {
246 Self::try_from(&tx)
247 }
248}
249
250impl ProtoTransaction {
251 pub fn signing_bytes(&self) -> Result<Vec<u8>, ConversionError> {
253 Transaction::try_from(self)?.signing_bytes()
254 }
255}
256
257fn parse_big_uint(value: &str) -> Result<Bytes, ConversionError> {
258 let trimmed = value.trim();
259 let number = if let Some(hex_body) = trimmed.strip_prefix("0x") {
260 BigUint::parse_bytes(hex_body.as_bytes(), 16)
261 } else {
262 BigUint::parse_bytes(trimmed.as_bytes(), 10)
263 };
264 let num = number.ok_or_else(|| ConversionError::InvalidNumeric(value.to_owned()))?;
265 Ok(encode_big_int_caster(&BigInt::from_biguint(
266 Sign::Plus,
267 num,
268 )))
269}
270
271fn encode_big_int_caster(value: &BigInt) -> Bytes {
272 let (sign, magnitude) = value.to_bytes_be();
273 if magnitude.is_empty() {
274 return Bytes::from_static(&[0, 0]);
275 }
276
277 let mut encoded = Vec::with_capacity(magnitude.len() + 1);
278 encoded.push(match sign {
279 Sign::Minus => 1,
280 Sign::NoSign | Sign::Plus => 0,
281 });
282 encoded.extend_from_slice(&magnitude);
283 Bytes::from(encoded)
284}
285
286fn decode_big_int_caster(bytes: &[u8]) -> Result<Option<BigInt>, ConversionError> {
287 match bytes.len() {
288 0 => Err(ConversionError::InvalidBigIntEncoding(
289 "empty buffer is not a valid BigIntCaster value".to_owned(),
290 )),
291 1 => {
292 if bytes[0] == 0 {
293 Ok(None)
294 } else {
295 Err(ConversionError::InvalidBigIntEncoding(format!(
296 "single-byte encoding must be nil marker 0x00, got 0x{:02x}",
297 bytes[0]
298 )))
299 }
300 }
301 _ => {
302 let magnitude = BigUint::from_bytes_be(&bytes[1..]);
303 let value = match bytes[0] {
304 0 => BigInt::from_biguint(Sign::Plus, magnitude),
305 1 => BigInt::from_biguint(Sign::Minus, magnitude),
306 sign => {
307 return Err(ConversionError::InvalidBigIntEncoding(format!(
308 "invalid sign byte 0x{sign:02x}"
309 )));
310 }
311 };
312 Ok(Some(value))
313 }
314 }
315}
316
317fn decode_bech32(addr: &str) -> Result<Bytes, ConversionError> {
318 let (_hrp, raw) =
319 bech32::decode(addr).map_err(|e| ConversionError::InvalidBech32(e.to_string()))?;
320 if raw.len() != 32 {
321 return Err(ConversionError::InvalidAddressLength(raw.len()));
322 }
323 Ok(Bytes::from(raw))
324}
325
326fn decode_optional_bech32(addr: Option<&str>) -> Result<Bytes, ConversionError> {
327 match addr {
328 Some(a) if !a.trim().is_empty() => decode_bech32(a),
329 _ => Ok(Bytes::new()),
330 }
331}
332
333fn decode_optional_hex(value: Option<&str>) -> Result<Bytes, ConversionError> {
334 match value {
335 Some(s) if !s.trim().is_empty() => hex::decode(s.trim())
336 .map(Bytes::from)
337 .map_err(|e| ConversionError::InvalidHex(e.to_string())),
338 _ => Ok(Bytes::new()),
339 }
340}
341
342fn decode_optional_base64(value: Option<&str>) -> Result<Bytes, ConversionError> {
343 match value {
344 Some(s) if !s.trim().is_empty() => base64::engine::general_purpose::STANDARD
345 .decode(s.trim())
346 .map(Bytes::from)
347 .map_err(|e| ConversionError::InvalidBase64(e.to_string())),
348 _ => Ok(Bytes::new()),
349 }
350}
351
352fn decode_data_field(value: Option<&str>) -> Result<Bytes, ConversionError> {
353 match value {
354 Some(s) => match base64::engine::general_purpose::STANDARD.decode(s.trim()) {
355 Ok(bytes) => Ok(Bytes::from(bytes)),
356 Err(_) => hex::decode(s.trim())
357 .map(Bytes::from)
358 .map_err(|e| ConversionError::InvalidHex(e.to_string())),
359 },
360 None => Ok(Bytes::new()),
361 }
362}
363
364fn encode_required_bech32(bytes: &[u8]) -> Result<String, ConversionError> {
365 if bytes.len() != 32 {
366 return Err(ConversionError::InvalidAddressLength(bytes.len()));
367 }
368 Ok(bech32::encode::<Bech32>(ERD_HRP, bytes)?)
369}
370
371fn encode_optional_bech32(bytes: &[u8]) -> Result<Option<String>, ConversionError> {
372 if bytes.is_empty() {
373 return Ok(None);
374 }
375 encode_required_bech32(bytes).map(Some)
376}
377
378fn encode_optional_hex(bytes: &[u8]) -> Option<String> {
379 (!bytes.is_empty()).then(|| hex::encode(bytes))
380}
381
382fn encode_optional_base64(bytes: &[u8]) -> Option<String> {
383 (!bytes.is_empty()).then(|| base64::engine::general_purpose::STANDARD.encode(bytes))
384}
385
386fn serialize_err(err: serde_json::Error) -> ConversionError {
387 ConversionError::Serialization(err.to_string())
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 fn make_json_tx() -> Transaction {
395 Transaction {
396 nonce: 42,
397 value: "1000000000000000000".to_owned(),
398 receiver: "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l".to_owned(),
399 sender: "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th".to_owned(),
400 sender_username: Some("c2VuZGVy".to_owned()),
401 receiver_username: Some("cmVjZWl2ZXI=".to_owned()),
402 gas_price: 1_000_000_000,
403 gas_limit: 50_000,
404 data: Some("dGVzdA==".to_owned()),
405 chain_id: "1".to_owned(),
406 version: 2,
407 options: Some(1),
408 guardian: None,
409 guardian_signature: None,
410 signature: Some("ab".repeat(64)),
411 relayer: None,
412 relayer_signature: None,
413 }
414 }
415
416 #[test]
417 fn json_to_proto_converts_fields() {
418 let tx = make_json_tx();
419 let proto = ProtoTransaction::try_from(&tx).unwrap();
420
421 assert_eq!(proto.nonce, 42);
422 assert_eq!(proto.gas_price, 1_000_000_000);
423 assert_eq!(proto.gas_limit, 50_000);
424 assert_eq!(proto.chain_id.as_ref(), b"1");
425 assert_eq!(proto.data.as_ref(), b"test");
426 assert_eq!(proto.snd_addr.len(), 32);
427 assert_eq!(proto.rcv_addr.len(), 32);
428 assert_eq!(proto.value[0], 0);
429 }
430
431 #[test]
432 fn proto_to_json_roundtrip() {
433 let tx = make_json_tx();
434 let proto = ProtoTransaction::try_from(&tx).unwrap();
435 let roundtrip = Transaction::try_from(&proto).unwrap();
436
437 assert_eq!(roundtrip.nonce, tx.nonce);
438 assert_eq!(roundtrip.value, tx.value);
439 assert_eq!(roundtrip.receiver, tx.receiver);
440 assert_eq!(roundtrip.sender, tx.sender);
441 assert_eq!(roundtrip.chain_id, tx.chain_id);
442 assert_eq!(roundtrip.data, Some("dGVzdA==".to_owned()));
443 assert_eq!(roundtrip.signature, tx.signature);
444 }
445
446 #[test]
447 fn proto_to_json_zero_value_maps_to_zero_string() {
448 let proto = ProtoTransaction {
449 value: Bytes::from_static(&[0, 0]),
450 chain_id: Bytes::from_static(b"1"),
451 rcv_addr: decode_bech32(
452 "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqplllst77y4l",
453 )
454 .unwrap(),
455 snd_addr: decode_bech32(
456 "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
457 )
458 .unwrap(),
459 ..Default::default()
460 };
461
462 let json = Transaction::try_from(&proto).unwrap();
463 assert_eq!(json.value, "0");
464 }
465
466 #[test]
467 fn signing_bytes_field_order_matches_protocol() {
468 let tx = make_json_tx();
469 let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
470
471 let fields: Vec<&str> = json_str
472 .trim_matches(|c| c == '{' || c == '}')
473 .split(',')
474 .map(|s| s.split(':').next().unwrap().trim().trim_matches('"'))
475 .collect();
476
477 assert_eq!(fields[0], "nonce");
478 assert_eq!(fields[1], "value");
479 assert_eq!(fields[2], "receiver");
480 assert_eq!(fields[3], "sender");
481 assert_eq!(fields[4], "senderUsername");
482 assert_eq!(fields[5], "receiverUsername");
483 assert_eq!(fields[6], "gasPrice");
484 assert_eq!(fields[7], "gasLimit");
485 assert_eq!(fields[8], "data");
486 assert_eq!(fields[9], "chainID");
487 assert_eq!(fields[10], "version");
488 assert_eq!(fields[11], "options");
489 }
490
491 #[test]
492 fn signing_bytes_omit_signatures_and_include_relayer_when_present() {
493 let mut tx = make_json_tx();
494 tx.guardian =
495 Some("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8".to_owned());
496 tx.guardian_signature = Some("cd".repeat(64));
497 tx.relayer =
498 Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());
499 tx.relayer_signature = Some("ef".repeat(64));
500
501 let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
502
503 assert!(json_str.contains("\"relayer\":"));
504 assert!(json_str.contains("\"guardian\":"));
505 assert!(!json_str.contains("signature"));
506 assert!(!json_str.contains("guardianSignature"));
507 assert!(!json_str.contains("relayerSignature"));
508 }
509
510 #[test]
511 fn signing_bytes_omits_empty_usernames() {
512 let mut tx = make_json_tx();
513 tx.sender_username = Some(String::new());
514 tx.receiver_username = Some(String::new());
515 tx.relayer =
516 Some("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7znyq426ca4qznv276".to_owned());
517
518 let json_str = String::from_utf8(tx.signing_bytes().unwrap()).unwrap();
519
520 assert!(json_str.contains("\"relayer\":"));
521 assert!(!json_str.contains("senderUsername"));
522 assert!(!json_str.contains("receiverUsername"));
523 }
524
525 #[test]
526 fn proto_signing_bytes_match_json_signing_bytes() {
527 let tx = make_json_tx();
528 let proto = ProtoTransaction::try_from(&tx).unwrap();
529
530 assert_eq!(proto.signing_bytes().unwrap(), tx.signing_bytes().unwrap());
531 }
532}