-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manged decimal #172
Manged decimal #172
Changes from 4 commits
78a37de
744acb8
e619b45
b6752ec
6a37cdb
e1f3a41
9663816
761a123
8d6bd4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
from decimal import Decimal | ||
from pathlib import Path | ||
from types import SimpleNamespace | ||
from typing import Optional | ||
|
||
from multiversx_sdk.abi.abi import Abi | ||
from multiversx_sdk.abi.abi_definition import ParameterDefinition | ||
from multiversx_sdk.abi.abi_definition import AbiDefinition, ParameterDefinition | ||
from multiversx_sdk.abi.address_value import AddressValue | ||
from multiversx_sdk.abi.biguint_value import BigUIntValue | ||
from multiversx_sdk.abi.bytes_value import BytesValue | ||
|
@@ -19,6 +20,8 @@ | |
from multiversx_sdk.abi.variadic_values import VariadicValues | ||
from multiversx_sdk.core.address import Address | ||
|
||
from multiversx_sdk.abi.managed_decimal_value import ManagedDecimalValue | ||
|
||
testdata = Path(__file__).parent.parent / "testutils" / "testdata" | ||
|
||
|
||
|
@@ -318,3 +321,36 @@ def test_decode_endpoint_output_parameters_multisig_get_pending_action_full_info | |
Address.from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").get_public_key(), | ||
Address.from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx").get_public_key(), | ||
] | ||
|
||
|
||
def test_managed_decimals(): | ||
abi_definition = AbiDefinition.from_dict({ | ||
"endpoints": [{ | ||
"name": "foo", | ||
"inputs": [ | ||
{ | ||
"type": "ManagedDecimal<8>" | ||
}, | ||
{ | ||
"type": "ManagedDecimal<usize>" | ||
} | ||
], | ||
"outputs": [] | ||
}] | ||
}) | ||
|
||
abi = Abi(abi_definition) | ||
endpoint = abi.endpoints_prototypes_by_name["foo"] | ||
|
||
first_input = endpoint.input_parameters[0] | ||
second_input = endpoint.input_parameters[1] | ||
|
||
assert isinstance(first_input, ManagedDecimalValue) | ||
assert not first_input.is_variable | ||
assert first_input.scale == 8 | ||
assert first_input.value == Decimal(0) | ||
|
||
assert isinstance(second_input, ManagedDecimalValue) | ||
assert second_input.is_variable | ||
assert second_input.scale == 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thus, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed. |
||
assert second_input.value == Decimal(0) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
from decimal import Decimal | ||
from pathlib import Path | ||
|
||
import pytest | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the PR description, please also reference the PRs that brought the implementation in JS. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
from multiversx_sdk.abi.abi import Abi, AbiDefinition | ||
from multiversx_sdk.abi.managed_decimal_value import ManagedDecimalValue | ||
from multiversx_sdk.accounts.account import Account | ||
from multiversx_sdk.network_providers.proxy_network_provider import ProxyNetworkProvider | ||
from multiversx_sdk.smart_contracts.smart_contract_controller import SmartContractController | ||
from multiversx_sdk.testutils.wallets import load_wallets | ||
|
||
|
||
@pytest.mark.skip("Requires localnet") | ||
class TestLocalnetInteraction: | ||
wallets = load_wallets() | ||
alice = wallets["alice"] | ||
testdata = Path(__file__).parent.parent / "testutils" / "testdata" | ||
|
||
def test_managed_decimal(self): | ||
abi_definition = AbiDefinition.from_dict( | ||
{ | ||
"endpoints": [ | ||
{ | ||
"name": "returns_egld_decimal", | ||
"mutability": "mutable", | ||
"payableInTokens": ["EGLD"], | ||
"inputs": [], | ||
"outputs": [{"type": "ManagedDecimal<18>"}], | ||
}, | ||
{ | ||
"name": "managed_decimal_addition", | ||
"mutability": "mutable", | ||
"inputs": [ | ||
{"name": "first", "type": "ManagedDecimal<2>"}, | ||
{"name": "second", "type": "ManagedDecimal<2>"}, | ||
], | ||
"outputs": [{"type": "ManagedDecimal<2>"}], | ||
}, | ||
{ | ||
"name": "managed_decimal_ln", | ||
"mutability": "mutable", | ||
"inputs": [{"name": "x", "type": "ManagedDecimal<9>"}], | ||
"outputs": [{"type": "ManagedDecimalSigned<9>"}], | ||
}, | ||
{ | ||
"name": "managed_decimal_addition_var", | ||
"mutability": "mutable", | ||
"inputs": [ | ||
{"name": "first", "type": "ManagedDecimal<usize>"}, | ||
{"name": "second", "type": "ManagedDecimal<usize>"}, | ||
], | ||
"outputs": [{"type": "ManagedDecimal<usize>"}], | ||
}, | ||
{ | ||
"name": "managed_decimal_ln_var", | ||
"mutability": "mutable", | ||
"inputs": [{"name": "x", "type": "ManagedDecimal<usize>"}], | ||
"outputs": [{"type": "ManagedDecimalSigned<9>"}], | ||
}, | ||
] | ||
} | ||
) | ||
|
||
abi = Abi(abi_definition) | ||
|
||
proxy = ProxyNetworkProvider("http://localhost:7950") | ||
sc_controller = SmartContractController( | ||
chain_id="localnet", | ||
network_provider=proxy, | ||
abi=abi, | ||
) | ||
|
||
alice = Account(self.alice.secret_key) | ||
alice.nonce = proxy.get_account(alice.address).nonce | ||
|
||
# deploy contract | ||
deploy_tx = sc_controller.create_transaction_for_deploy( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
bytecode=self.testdata / "basic-features.wasm", | ||
gas_limit=600_000_000, | ||
arguments=[], | ||
) | ||
|
||
deploy_tx_hash = proxy.send_transaction(deploy_tx) | ||
deploy_outcome = sc_controller.await_completed_deploy(deploy_tx_hash) | ||
assert deploy_outcome.return_code == "ok" | ||
|
||
contract = deploy_outcome.contracts[0].address | ||
|
||
# return egld decimals | ||
return_egld_transaction = sc_controller.create_transaction_for_execute( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
contract=contract, | ||
gas_limit=100_000_000, | ||
function="returns_egld_decimal", | ||
arguments=[], | ||
native_transfer_amount=1, | ||
) | ||
|
||
tx_hash = proxy.send_transaction(return_egld_transaction) | ||
outcome = sc_controller.await_completed_execute(tx_hash) | ||
assert outcome.return_code == "ok" | ||
assert len(outcome.values) == 1 | ||
assert outcome.values[0] == Decimal("0.000000000000000001") | ||
|
||
# addition with const decimals | ||
addition_transaction = sc_controller.create_transaction_for_execute( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
contract=contract, | ||
gas_limit=100_000_000, | ||
function="managed_decimal_addition", | ||
arguments=[ManagedDecimalValue("2.5", 2), ManagedDecimalValue("2.7", 2)], | ||
) | ||
|
||
tx_hash = proxy.send_transaction(addition_transaction) | ||
outcome = sc_controller.await_completed_execute(tx_hash) | ||
assert outcome.return_code == "ok" | ||
assert len(outcome.values) == 1 | ||
assert outcome.values[0] == Decimal("5.2") | ||
|
||
# log | ||
md_ln_transaction = sc_controller.create_transaction_for_execute( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
contract=contract, | ||
gas_limit=100_000_000, | ||
function="managed_decimal_ln", | ||
arguments=[ManagedDecimalValue("23", 9)], | ||
) | ||
|
||
tx_hash = proxy.send_transaction(md_ln_transaction) | ||
outcome = sc_controller.await_completed_execute(tx_hash) | ||
assert outcome.return_code == "ok" | ||
assert len(outcome.values) == 1 | ||
assert outcome.values[0] == Decimal("3.135553845") | ||
|
||
# addition var decimals | ||
addition_var_transaction = sc_controller.create_transaction_for_execute( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
contract=contract, | ||
gas_limit=50_000_000, | ||
function="managed_decimal_addition_var", | ||
arguments=[ManagedDecimalValue("4", 2, True), ManagedDecimalValue("5", 2, True)], | ||
) | ||
|
||
tx_hash = proxy.send_transaction(addition_var_transaction) | ||
outcome = sc_controller.await_completed_execute(tx_hash) | ||
assert outcome.return_code == "ok" | ||
assert len(outcome.values) == 1 | ||
assert outcome.values[0] == Decimal("9") | ||
|
||
# ln var | ||
ln_var_transaction = sc_controller.create_transaction_for_execute( | ||
sender=alice, | ||
nonce=alice.get_nonce_then_increment(), | ||
contract=contract, | ||
gas_limit=50_000_000, | ||
function="managed_decimal_ln_var", | ||
arguments=[ManagedDecimalValue("23", 9, True)], | ||
) | ||
|
||
tx_hash = proxy.send_transaction(ln_var_transaction) | ||
outcome = sc_controller.await_completed_execute(tx_hash) | ||
assert outcome.return_code == "ok" | ||
assert len(outcome.values) == 1 | ||
assert outcome.values[0] == Decimal("3.135553845") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import io | ||
from decimal import ROUND_DOWN, Decimal | ||
from typing import Any, Union | ||
|
||
from multiversx_sdk.abi.bigint_value import BigIntValue | ||
from multiversx_sdk.abi.constants import U32_SIZE_IN_BYTES | ||
from multiversx_sdk.abi.shared import read_bytes_exactly | ||
from multiversx_sdk.abi.small_int_values import U32Value | ||
|
||
|
||
class ManagedDecimalSignedValue: | ||
def __init__(self, value: Union[int, str] = 0, scale: int = 0, is_variable: bool = False): | ||
self.value = Decimal(value) | ||
self.scale = scale | ||
self.is_variable = is_variable | ||
|
||
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.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
self.value = value.value | ||
|
||
if self.is_variable: | ||
self.scale = value.scale | ||
else: | ||
self.value = self._convert_to_decimal(value) | ||
|
||
def get_payload(self) -> Decimal: | ||
return self.value | ||
|
||
def encode_top_level(self, writer: io.BytesIO): | ||
self.encode_nested(writer) | ||
|
||
def encode_nested(self, writer: io.BytesIO): | ||
raw_value = BigIntValue(self._convert_value_to_int()) | ||
if self.is_variable: | ||
raw_value.encode_nested(writer) | ||
U32Value(self.scale).encode_nested(writer) | ||
else: | ||
raw_value.encode_top_level(writer) | ||
|
||
def decode_top_level(self, data: bytes): | ||
if not data: | ||
self.value = Decimal(0) | ||
self.scale = 0 | ||
return | ||
|
||
bigint = BigIntValue() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, it is. renamed |
||
scale = U32Value() | ||
|
||
if self.is_variable: | ||
# read biguint value length in bytes | ||
big_int_size = self._unsigned_from_bytes(data[:U32_SIZE_IN_BYTES]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed. |
||
|
||
# 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]) | ||
|
||
# remove biguintvalue; data contains only scale | ||
data = data[big_int_size:] | ||
|
||
# read scale | ||
scale.decode_top_level(data) | ||
self.scale = scale.get_payload() | ||
else: | ||
bigint.decode_top_level(data) | ||
|
||
self.value = self._convert_to_decimal(bigint.get_payload()) | ||
|
||
def decode_nested(self, reader: io.BytesIO): | ||
length = self._unsigned_from_bytes(read_bytes_exactly(reader, U32_SIZE_IN_BYTES)) | ||
payload = read_bytes_exactly(reader, length) | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this help? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it did. refactored the method |
||
return f"{value_str[:-self.scale]}.{value_str[-self.scale:]}" | ||
|
||
def get_precision(self) -> int: | ||
return len(str(self._convert_value_to_int()).lstrip("0")) | ||
|
||
def _unsigned_from_bytes(self, data: bytes) -> int: | ||
return int.from_bytes(data, byteorder="big", signed=False) | ||
|
||
def _convert_value_to_int(self) -> int: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this help?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it did. refactored this method, as well. |
||
scaled_value: Decimal = self.value * (10**self.scale) | ||
return int(scaled_value.quantize(Decimal("1."), rounding=ROUND_DOWN)) | ||
|
||
def _convert_to_decimal(self, value: Union[int, str]) -> Decimal: | ||
return Decimal(value) / (10**self.scale) | ||
|
||
def __eq__(self, other: object) -> bool: | ||
if not isinstance(other, ManagedDecimalSignedValue): | ||
return False | ||
return self.value == other.value and self.scale == other.scale |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be a bit more compact: if that return object, else return object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, done