mx-core 0.1.0

Core utilities for MultiversX Rust services.
Documentation
//! Big integer helpers for Go `BigIntCaster` compatibility.

use crate::error::CoreError;
use num_bigint::{BigInt, BigUint, Sign};
use prost::bytes::Bytes;

/// Encodes a [`BigInt`] using Go's `BigIntCaster` format.
///
/// The wire format is sign-byte-plus-magnitude:
/// - `nil` is represented separately by callers as `[0]`
/// - zero is encoded as `[0, 0]`
/// - positive values are encoded as `[0, ..magnitude_be]`
/// - negative values are encoded as `[1, ..magnitude_be]`
#[must_use]
pub 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 sign_byte = match sign {
        Sign::Minus => 1,
        Sign::NoSign | Sign::Plus => 0,
    };

    let mut encoded = Vec::with_capacity(magnitude.len() + 1);
    encoded.push(sign_byte);
    encoded.extend_from_slice(&magnitude);
    Bytes::from(encoded)
}

/// Decodes bytes encoded with Go's `BigIntCaster` format.
///
/// Returns:
/// - `Ok(None)` for the Go `nil` representation `[0]`
/// - `Ok(Some(_))` for zero and non-zero encoded values
/// - `Err(_)` for malformed encodings
pub fn decode_big_int_caster(bytes: &[u8]) -> Result<Option<BigInt>, CoreError> {
    match bytes.len() {
        0 => Err(CoreError::InvalidBigIntEncoding(
            "empty buffer is not a valid BigIntCaster value".to_owned(),
        )),
        1 => {
            if bytes[0] == 0 {
                Ok(None)
            } else {
                Err(CoreError::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(CoreError::InvalidBigIntEncoding(format!(
                        "invalid sign byte 0x{sign:02x}"
                    )));
                }
            };

            Ok(Some(value))
        }
    }
}

/// Parses an unsigned integer string into the Go `BigIntCaster` wire format.
///
/// This is the unsigned subset used by transaction value fields and similar amount-like values.
///
/// Accepted formats:
/// - Decimal: `"12345"`
/// - Hex: `"0x3039"`
pub fn parse_big_uint(value: &str) -> Result<Bytes, CoreError> {
    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(|| CoreError::InvalidNumeric(value.to_owned()))?;

    Ok(encode_big_int_caster(&BigInt::from_biguint(
        Sign::Plus,
        num,
    )))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_encode_big_int_caster_zero() {
        let encoded = encode_big_int_caster(&BigInt::from(0));
        assert_eq!(encoded.as_ref(), &[0, 0]);
    }

    #[test]
    fn test_encode_big_int_caster_positive() {
        let encoded = encode_big_int_caster(&BigInt::from(12345));
        assert_eq!(encoded.as_ref(), &[0, 0x30, 0x39]);
    }

    #[test]
    fn test_encode_big_int_caster_negative() {
        let encoded = encode_big_int_caster(&BigInt::from(-12345));
        assert_eq!(encoded.as_ref(), &[1, 0x30, 0x39]);
    }

    #[test]
    fn test_decode_big_int_caster_nil() {
        assert_eq!(decode_big_int_caster(&[0]).unwrap(), None);
    }

    #[test]
    fn test_decode_big_int_caster_zero() {
        assert_eq!(
            decode_big_int_caster(&[0, 0]).unwrap(),
            Some(BigInt::from(0))
        );
    }

    #[test]
    fn test_decode_big_int_caster_positive() {
        assert_eq!(
            decode_big_int_caster(&[0, 0x30, 0x39]).unwrap(),
            Some(BigInt::from(12345))
        );
    }

    #[test]
    fn test_decode_big_int_caster_negative() {
        assert_eq!(
            decode_big_int_caster(&[1, 0x30, 0x39]).unwrap(),
            Some(BigInt::from(-12345))
        );
    }

    #[test]
    fn test_decode_big_int_caster_invalid_sign() {
        let err = decode_big_int_caster(&[2, 3, 4]).unwrap_err();
        assert!(matches!(err, CoreError::InvalidBigIntEncoding(_)));
    }

    #[test]
    fn test_parse_big_uint_zero() {
        let zero = parse_big_uint("0").unwrap();
        assert_eq!(zero.as_ref(), &[0, 0]);
    }

    #[test]
    fn test_parse_big_uint_decimal() {
        let val = parse_big_uint("1000000000000000").unwrap();
        assert!(!val.is_empty());
        assert_eq!(val[0], 0);
    }

    #[test]
    fn test_parse_big_uint_hex() {
        let val = parse_big_uint("0x3039").unwrap();
        assert_eq!(val.as_ref(), &[0, 0x30, 0x39]);
    }

    #[test]
    fn test_parse_big_uint_invalid() {
        let result = parse_big_uint("not_a_number");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_big_uint_whitespace() {
        let val = parse_big_uint("  123  ").unwrap();
        assert_eq!(val.as_ref(), &[0, 123]);
    }
}