diff --git a/multiversx_sdk/abi/abi.py b/multiversx_sdk/abi/abi.py index 5e884ab2..cf8a1a15 100644 --- a/multiversx_sdk/abi/abi.py +++ b/multiversx_sdk/abi/abi.py @@ -323,13 +323,11 @@ def _create_prototype(self, type_formula: TypeFormula) -> Any: scale = type_formula.type_parameters[0].name if scale == "usize": - scale = 0 - is_variable = True + return ManagedDecimalValue(scale=0, is_variable=True) else: - scale = int(scale) - is_variable = False + return ManagedDecimalValue(scale=int(scale), is_variable=False) - return ManagedDecimalValue(scale=scale, is_variable=is_variable) + if name == "ManagedDecimalSigned": scale = type_formula.type_parameters[0].name diff --git a/multiversx_sdk/abi/abi_test.py b/multiversx_sdk/abi/abi_test.py index 88cbaec3..61e7a9d2 100644 --- a/multiversx_sdk/abi/abi_test.py +++ b/multiversx_sdk/abi/abi_test.py @@ -354,3 +354,45 @@ def test_managed_decimals(): assert second_input.is_variable assert second_input.scale == 0 assert second_input.value == Decimal(0) + + +def test_encode_decode_managed_decimals(): + abi_definition = AbiDefinition.from_dict( + { + "endpoints": [ + { + "name": "dummy", + "inputs": [{"type": "ManagedDecimal<18>"}], + "outputs": [], + }, + { + "name": "foo", + "inputs": [{"name": "x", "type": "ManagedDecimal"}], + "outputs": [{"type": "ManagedDecimalSigned<9>"}], + }, + { + "name": "foobar", + "inputs": [{"name": "x", "type": "ManagedDecimal"}], + "outputs": [{"type": "ManagedDecimal"}], + }, + ] + } + ) + + abi = Abi(abi_definition) + + values = abi.encode_endpoint_input_parameters("dummy", [1]) + assert values[0].hex() == "01" + + values = abi.encode_endpoint_input_parameters("foo", [ManagedDecimalValue(7, 2, True)]) + assert values[0].hex() == "0702" + + values = abi.decode_endpoint_output_parameters("foo", [bytes.fromhex("07")]) + assert values[0].get_payload() == Decimal("7") + assert values[0].scale == Decimal("7") + assert values[0].to_string() == "7.000000000" + + values = abi.decode_endpoint_output_parameters("foobar", [bytes.fromhex("0700000003")]) + assert values[0].get_payload() == Decimal("7") + assert values[0].scale == Decimal("3") + assert values[0].to_string() == "7.000" diff --git a/multiversx_sdk/abi/managed_decimal_signed_value.py b/multiversx_sdk/abi/managed_decimal_signed_value.py index 2d524a6f..5eb67718 100644 --- a/multiversx_sdk/abi/managed_decimal_signed_value.py +++ b/multiversx_sdk/abi/managed_decimal_signed_value.py @@ -1,5 +1,5 @@ import io -from decimal import ROUND_DOWN, Decimal +from decimal import Decimal from typing import Any, Union from multiversx_sdk.abi.bigint_value import BigIntValue @@ -17,7 +17,7 @@ def __init__(self, value: Union[int, str] = 0, scale: int = 0, is_variable: bool def set_payload(self, value: Any): if isinstance(value, ManagedDecimalSignedValue): if self.is_variable != value.is_variable: - raise Exception("Cannot set payload! Both ManagedDecimalValues should be variable.") + raise Exception("Cannot set payload! Both managed decimal values should be variable.") self.value = value.value @@ -46,29 +46,29 @@ def decode_top_level(self, data: bytes): self.scale = 0 return - bigint = BigIntValue() + value = BigIntValue() scale = U32Value() if self.is_variable: # read biguint value length in bytes - big_int_size = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES]) + value_length = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES]) # remove biguint length; data is only biguint value and scale data = data[U32_SIZE_IN_BYTES:] # read biguint value - bigint.decode_top_level(data[:big_int_size]) + value.decode_top_level(data[:value_length]) # remove biguintvalue; data contains only scale - data = data[big_int_size:] + data = data[value_length:] # read scale scale.decode_top_level(data) self.scale = scale.get_payload() else: - bigint.decode_top_level(data) + value.decode_top_level(data) - self.value = self._convert_to_decimal(bigint.get_payload()) + self.value = self._convert_to_decimal(value.get_payload()) def decode_nested(self, reader: io.BytesIO): length = self._unsigned_from_bytes(read_bytes_exactly(reader, U32_SIZE_IN_BYTES)) @@ -76,23 +76,18 @@ def decode_nested(self, reader: io.BytesIO): self.decode_top_level(payload) def to_string(self) -> str: - value_str = str(self._convert_value_to_int()) - if self.scale == 0: - return value_str - if len(value_str) <= self.scale: - # If the value is smaller than the scale, prepend zeros - value_str = "0" * (self.scale - len(value_str) + 1) + value_str - return f"{value_str[:-self.scale]}.{value_str[-self.scale:]}" + scaled_value = self._convert_value_to_int() + return f"{scaled_value / (10 ** self.scale):.{self.scale}f}" def get_precision(self) -> int: - return len(str(self._convert_value_to_int()).lstrip("0")) + value_str = f"{self.value:.{self.scale}f}" + return len(value_str.replace(".", "")) def _unsigned_from_bytes(self, data: bytes) -> int: return int.from_bytes(data, byteorder="big", signed=False) def _convert_value_to_int(self) -> int: - scaled_value: Decimal = self.value * (10**self.scale) - return int(scaled_value.quantize(Decimal("1."), rounding=ROUND_DOWN)) + return int(self.value.scaleb(self.scale)) def _convert_to_decimal(self, value: Union[int, str]) -> Decimal: return Decimal(value) / (10**self.scale) diff --git a/multiversx_sdk/abi/managed_decimal_value.py b/multiversx_sdk/abi/managed_decimal_value.py index 917bca85..14eccc77 100644 --- a/multiversx_sdk/abi/managed_decimal_value.py +++ b/multiversx_sdk/abi/managed_decimal_value.py @@ -17,7 +17,7 @@ def __init__(self, value: Union[int, str] = 0, scale: int = 0, is_variable: bool def set_payload(self, value: Any): if isinstance(value, ManagedDecimalValue): if self.is_variable != value.is_variable: - raise Exception("Cannot set payload! Both ManagedDecimalValues should be variable.") + raise Exception("Cannot set payload! Both managed decimal values should be variable.") self.value = value.value @@ -46,29 +46,29 @@ def decode_top_level(self, data: bytes): self.scale = 0 return - biguint = BigUIntValue() + value = BigUIntValue() scale = U32Value() if self.is_variable: # read biguint value length in bytes - big_uint_size = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES]) + value_length = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES]) # remove biguint length; data is only biguint value and scale data = data[U32_SIZE_IN_BYTES:] # read biguint value - biguint.decode_top_level(data[:big_uint_size]) + value.decode_top_level(data[:value_length]) # remove biguintvalue; data contains only scale - data = data[big_uint_size:] + data = data[value_length:] # read scale scale.decode_top_level(data) self.scale = scale.get_payload() else: - biguint.decode_top_level(data) + value.decode_top_level(data) - self.value = self._convert_to_decimal(biguint.get_payload()) + self.value = self._convert_to_decimal(value.get_payload()) def decode_nested(self, reader: io.BytesIO): length = self._unsigned_from_bytes(read_bytes_exactly(reader, U32_SIZE_IN_BYTES)) @@ -76,23 +76,18 @@ def decode_nested(self, reader: io.BytesIO): self.decode_top_level(payload) def to_string(self) -> str: - value_str = str(self._convert_value_to_int()) - if self.scale == 0: - return value_str - if len(value_str) <= self.scale: - # If the value is smaller than the scale, prepend zeros - value_str = "0" * (self.scale - len(value_str) + 1) + value_str - return f"{value_str[:-self.scale]}.{value_str[-self.scale:]}" + scaled_value = self._convert_value_to_int() + return f"{scaled_value / (10 ** self.scale):.{self.scale}f}" def get_precision(self) -> int: - return len(str(self._convert_value_to_int()).lstrip("0")) + value_str = f"{self.value:.{self.scale}f}" + return len(value_str.replace(".", "")) def _unsigned_from_bytes(self, data: bytes) -> int: return int.from_bytes(data, byteorder="big", signed=False) def _convert_value_to_int(self) -> int: - scaled_value: Decimal = self.value * (10**self.scale) - return int(scaled_value.quantize(Decimal("1."), rounding=ROUND_DOWN)) + return int(self.value.scaleb(self.scale)) def _convert_to_decimal(self, value: Union[int, str]) -> Decimal: return Decimal(value) / (10**self.scale) diff --git a/multiversx_sdk/abi/managed_decimal_value_test.py b/multiversx_sdk/abi/managed_decimal_value_test.py index 32555234..cd1f01d3 100644 --- a/multiversx_sdk/abi/managed_decimal_value_test.py +++ b/multiversx_sdk/abi/managed_decimal_value_test.py @@ -1,13 +1,13 @@ from decimal import Decimal from multiversx_sdk.abi.managed_decimal_value import ManagedDecimalValue +from multiversx_sdk.abi.managed_decimal_signed_value import ManagedDecimalSignedValue class TestManagedDecimalValueTest: def test_expected_values(self): value = ManagedDecimalValue(1, 2) - assert not value.is_variable assert value.get_precision() == 3 assert value.to_string() == "1.00" @@ -33,9 +33,62 @@ def test_expected_values(self): assert value.to_string() == "2.70" assert value.get_payload() == Decimal("2.7") + value = ManagedDecimalValue(value="0.000000000000000001", scale=18) + assert value.get_precision() == 19 + assert value.to_string() == "0.000000000000000001" + assert value.get_payload() == Decimal("0.000000000000000001") + def test_compare_values(self): value = ManagedDecimalValue(1, 2) assert value != ManagedDecimalValue(2, 2) assert value != ManagedDecimalValue(1, 3) assert value == ManagedDecimalValue(1, 2) + + +class TestManagedDecimalSignedValueTest: + + def test_expected_values(self): + value = ManagedDecimalSignedValue(1, 2) + assert not value.is_variable + assert value.get_precision() == 3 + assert value.to_string() == "1.00" + assert value.get_payload() == Decimal(1) + + value = ManagedDecimalSignedValue(-1, 2) + assert not value.is_variable + assert value.get_precision() == 4 + assert value.to_string() == "-1.00" + assert value.get_payload() == Decimal(-1) + + value = ManagedDecimalSignedValue("1.234", 3) + assert value.get_precision() == 4 + assert value.to_string() == "1.234" + assert value.get_payload() == Decimal("1.234") + + value = ManagedDecimalSignedValue("1.3", 2) + assert value.get_precision() == 3 + assert value.to_string() == "1.30" + assert value.get_payload() == Decimal("1.3") + + value = ManagedDecimalSignedValue(13, 2) + assert value.get_precision() == 4 + assert value.to_string() == "13.00" + assert value.get_payload() == Decimal(13) + + value = ManagedDecimalSignedValue("2.7", 2) + assert value.get_precision() == 3 + assert value.to_string() == "2.70" + assert value.get_payload() == Decimal("2.7") + + value = ManagedDecimalSignedValue(value="0.000000000000000001", scale=18) + assert value.get_precision() == 19 + assert value.to_string() == "0.000000000000000001" + assert value.get_payload() == Decimal("0.000000000000000001") + + def test_compare_values(self): + value = ManagedDecimalSignedValue(1, 2) + + assert value != ManagedDecimalSignedValue(2, 2) + assert value != ManagedDecimalSignedValue(1, 3) + assert value == ManagedDecimalSignedValue(1, 2)