Skip to main content

mx_core/
bigint.rs

1//! Big integer helpers for Go `BigIntCaster` compatibility.
2
3use crate::error::CoreError;
4use num_bigint::{BigInt, BigUint, Sign};
5use prost::bytes::Bytes;
6
7/// Encodes a [`BigInt`] using Go's `BigIntCaster` format.
8///
9/// The wire format is sign-byte-plus-magnitude:
10/// - `nil` is represented separately by callers as `[0]`
11/// - zero is encoded as `[0, 0]`
12/// - positive values are encoded as `[0, ..magnitude_be]`
13/// - negative values are encoded as `[1, ..magnitude_be]`
14#[must_use]
15pub fn encode_big_int_caster(value: &BigInt) -> Bytes {
16    let (sign, magnitude) = value.to_bytes_be();
17    if magnitude.is_empty() {
18        return Bytes::from_static(&[0, 0]);
19    }
20
21    let sign_byte = match sign {
22        Sign::Minus => 1,
23        Sign::NoSign | Sign::Plus => 0,
24    };
25
26    let mut encoded = Vec::with_capacity(magnitude.len() + 1);
27    encoded.push(sign_byte);
28    encoded.extend_from_slice(&magnitude);
29    Bytes::from(encoded)
30}
31
32/// Decodes bytes encoded with Go's `BigIntCaster` format.
33///
34/// Returns:
35/// - `Ok(None)` for the Go `nil` representation `[0]`
36/// - `Ok(Some(_))` for zero and non-zero encoded values
37/// - `Err(_)` for malformed encodings
38pub fn decode_big_int_caster(bytes: &[u8]) -> Result<Option<BigInt>, CoreError> {
39    match bytes.len() {
40        0 => Err(CoreError::InvalidBigIntEncoding(
41            "empty buffer is not a valid BigIntCaster value".to_owned(),
42        )),
43        1 => {
44            if bytes[0] == 0 {
45                Ok(None)
46            } else {
47                Err(CoreError::InvalidBigIntEncoding(format!(
48                    "single-byte encoding must be nil marker 0x00, got 0x{:02x}",
49                    bytes[0]
50                )))
51            }
52        }
53        _ => {
54            let magnitude = BigUint::from_bytes_be(&bytes[1..]);
55            let value = match bytes[0] {
56                0 => BigInt::from_biguint(Sign::Plus, magnitude),
57                1 => BigInt::from_biguint(Sign::Minus, magnitude),
58                sign => {
59                    return Err(CoreError::InvalidBigIntEncoding(format!(
60                        "invalid sign byte 0x{sign:02x}"
61                    )));
62                }
63            };
64
65            Ok(Some(value))
66        }
67    }
68}
69
70/// Parses an unsigned integer string into the Go `BigIntCaster` wire format.
71///
72/// This is the unsigned subset used by transaction value fields and similar amount-like values.
73///
74/// Accepted formats:
75/// - Decimal: `"12345"`
76/// - Hex: `"0x3039"`
77pub fn parse_big_uint(value: &str) -> Result<Bytes, CoreError> {
78    let trimmed = value.trim();
79    let number = if let Some(hex_body) = trimmed.strip_prefix("0x") {
80        BigUint::parse_bytes(hex_body.as_bytes(), 16)
81    } else {
82        BigUint::parse_bytes(trimmed.as_bytes(), 10)
83    };
84    let num = number.ok_or_else(|| CoreError::InvalidNumeric(value.to_owned()))?;
85
86    Ok(encode_big_int_caster(&BigInt::from_biguint(
87        Sign::Plus,
88        num,
89    )))
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_encode_big_int_caster_zero() {
98        let encoded = encode_big_int_caster(&BigInt::from(0));
99        assert_eq!(encoded.as_ref(), &[0, 0]);
100    }
101
102    #[test]
103    fn test_encode_big_int_caster_positive() {
104        let encoded = encode_big_int_caster(&BigInt::from(12345));
105        assert_eq!(encoded.as_ref(), &[0, 0x30, 0x39]);
106    }
107
108    #[test]
109    fn test_encode_big_int_caster_negative() {
110        let encoded = encode_big_int_caster(&BigInt::from(-12345));
111        assert_eq!(encoded.as_ref(), &[1, 0x30, 0x39]);
112    }
113
114    #[test]
115    fn test_decode_big_int_caster_nil() {
116        assert_eq!(decode_big_int_caster(&[0]).unwrap(), None);
117    }
118
119    #[test]
120    fn test_decode_big_int_caster_zero() {
121        assert_eq!(
122            decode_big_int_caster(&[0, 0]).unwrap(),
123            Some(BigInt::from(0))
124        );
125    }
126
127    #[test]
128    fn test_decode_big_int_caster_positive() {
129        assert_eq!(
130            decode_big_int_caster(&[0, 0x30, 0x39]).unwrap(),
131            Some(BigInt::from(12345))
132        );
133    }
134
135    #[test]
136    fn test_decode_big_int_caster_negative() {
137        assert_eq!(
138            decode_big_int_caster(&[1, 0x30, 0x39]).unwrap(),
139            Some(BigInt::from(-12345))
140        );
141    }
142
143    #[test]
144    fn test_decode_big_int_caster_invalid_sign() {
145        let err = decode_big_int_caster(&[2, 3, 4]).unwrap_err();
146        assert!(matches!(err, CoreError::InvalidBigIntEncoding(_)));
147    }
148
149    #[test]
150    fn test_parse_big_uint_zero() {
151        let zero = parse_big_uint("0").unwrap();
152        assert_eq!(zero.as_ref(), &[0, 0]);
153    }
154
155    #[test]
156    fn test_parse_big_uint_decimal() {
157        let val = parse_big_uint("1000000000000000").unwrap();
158        assert!(!val.is_empty());
159        assert_eq!(val[0], 0);
160    }
161
162    #[test]
163    fn test_parse_big_uint_hex() {
164        let val = parse_big_uint("0x3039").unwrap();
165        assert_eq!(val.as_ref(), &[0, 0x30, 0x39]);
166    }
167
168    #[test]
169    fn test_parse_big_uint_invalid() {
170        let result = parse_big_uint("not_a_number");
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_parse_big_uint_whitespace() {
176        let val = parse_big_uint("  123  ").unwrap();
177        assert_eq!(val.as_ref(), &[0, 123]);
178    }
179}