From 1e1d25297757699e8c68f260d37de865d4210bcd Mon Sep 17 00:00:00 2001 From: c0nsol3 Date: Sun, 26 Jun 2022 19:34:29 +0200 Subject: [PATCH] feat: align vote transaction with core --- solar_crypto/constants.py | 2 +- solar_crypto/transactions/builder/vote.py | 38 +++++++++++++-- tests/conftest.py | 10 ++-- tests/transactions/builder/test_vote.py | 46 ++++++++++++++----- tests/transactions/deserializers/test_vote.py | 21 ++------- 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/solar_crypto/constants.py b/solar_crypto/constants.py index 6f5bd03..99b6e1e 100644 --- a/solar_crypto/constants.py +++ b/solar_crypto/constants.py @@ -50,7 +50,7 @@ SOLAR_TRANSACTION_FEES = { SOLAR_TRANSACTION_BURN: 0, - SOLAR_TRANSACTION_VOTE: 100000000, + SOLAR_TRANSACTION_VOTE: 9000000, } diff --git a/solar_crypto/transactions/builder/vote.py b/solar_crypto/transactions/builder/vote.py index 4a33947..23025cf 100644 --- a/solar_crypto/transactions/builder/vote.py +++ b/solar_crypto/transactions/builder/vote.py @@ -1,9 +1,11 @@ import re import typing +from decimal import Decimal from functools import cmp_to_key from math import trunc from solar_crypto.constants import SOLAR_TRANSACTION_VOTE, TRANSACTION_TYPE_GROUP +from solar_crypto.exceptions import SolarInvalidTransaction from solar_crypto.transactions.builder.base import BaseTransactionBuilder @@ -19,7 +21,9 @@ def __init__(self): def set_votes( self, - votes: typing.Union[typing.List[str], typing.Dict[str, typing.Union[int, float]]] = dict, + votes: typing.Union[ + typing.List[str], typing.Dict[str, typing.Union[int, float, Decimal]] + ] = dict, ): """Set votes @@ -30,24 +34,34 @@ def set_votes( if isinstance(votes, list): vote_list = filter(lambda vote: not vote.startswith("-"), votes) - vote_list = list(map(lambda vote: vote[1:], vote_list)) + vote_list = list( + map(lambda vote: vote[1:] if vote.startswith("+") else vote, vote_list) + ) + + if len(vote_list) > 53: + raise SolarInvalidTransaction("Unable to vote for more than 53 delegates") if len(vote_list) == 0: self.transaction.asset["votes"] = {} return - weight = round((trunc((100 / len(vote_list)) * 100) / 100) * 100) + weight = trunc(((((100 / len(vote_list))) * 100) / 100) * 100) remainder = 10000 for vote in vote_list: vote_object[vote] = weight / 100 remainder -= weight - for index in range(remainder): + for index in range(int(remainder)): key = list(vote_object.keys())[index] vote_object[key] = round((vote_object[key] + 0.01) * 100) / 100 votes = vote_object + else: + for key, val in votes.items(): + votes[key] = val + + validate(votes) if votes: nr_of_votes = len(votes.keys()) @@ -57,6 +71,22 @@ def set_votes( self.transaction.asset["votes"] = votes +def validate(votes): + for value in votes.values(): + if not valid_precision(value): + raise SolarInvalidTransaction("Only two decimal places are allowed.") + + +def valid_precision(value, max_precision=2): + if isinstance(value, Decimal): + if abs(value.as_tuple().exponent) <= max_precision: + return True + elif isinstance(value, float) or isinstance(value, str): + if str(value)[::-1].find(".") <= max_precision: + return True + return False + + def cmp(a: typing.List[typing.Union[int, str]], b: typing.List[typing.Union[int, str]]): """ Compare two alphanum keys diff --git a/tests/conftest.py b/tests/conftest.py index 3d3886c..83e3ed3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -306,11 +306,11 @@ def transaction_type_vote(): "type": 2, "nonce": 1, "senderPublicKey": "037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c", - "fee": 100000000, - "asset": {"votes": {"deadlock": 50.33}}, - "signature": "4b3f1fc10a11302109b123decceadcf5601053079919f90f7e0c28d502771a0e19553b8019579762373a957eb7876474fcfc5d12d6ea898205f54e62ce3f2c7c", # noqa + "fee": 9000000, + "asset": {"votes": {"deadlock": 64.7, "fun": 35.3}}, + "signature": "b1b484cb3b781b0af8f82f8639c52f25f637100d8adadcdde380c07924ada1cc1c1f5b6107cce1ff86b87ab66806ca1a8ae716e3555198cc7fa324a70f697b54", # noqa "amount": 0, - "id": "a7789f2eb62bae9782db053b045ed35a4475147c0b9bab5f964d69af2422a9ac", - "serialized": "ff031e0200000002000100000000000000037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c00e1f50500000000000108646561646c6f636ba9134b3f1fc10a11302109b123decceadcf5601053079919f90f7e0c28d502771a0e19553b8019579762373a957eb7876474fcfc5d12d6ea898205f54e62ce3f2c7c", # noqa + "id": "73a9a7a58c1060b8dd0c92cfe5c9bf305cfd9c387888aab92ae4e30770c5ff68", + "serialized": "ff031e0200000002000100000000000000037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c4054890000000000000208646561646c6f636b46190366756eca0db1b484cb3b781b0af8f82f8639c52f25f637100d8adadcdde380c07924ada1cc1c1f5b6107cce1ff86b87ab66806ca1a8ae716e3555198cc7fa324a70f697b54", # noqa } return data diff --git a/tests/transactions/builder/test_vote.py b/tests/transactions/builder/test_vote.py index 9bba105..9159d4d 100644 --- a/tests/transactions/builder/test_vote.py +++ b/tests/transactions/builder/test_vote.py @@ -1,16 +1,28 @@ from collections import OrderedDict +import pytest + from solar_crypto.configuration.network import set_network from solar_crypto.constants import SOLAR_TRANSACTION_VOTE, TRANSACTION_TYPE_GROUP +from solar_crypto.exceptions import SolarInvalidTransaction from solar_crypto.networks.testnet import Testnet from solar_crypto.transactions.builder.vote import Vote, sort_votes set_network(Testnet) -def test_vote_transaction(): +def test_vote_transaction_input_error(): vote = {"deadlock": 50.3333} + transaction = Vote() + with pytest.raises(SolarInvalidTransaction) as e: + transaction.set_votes(vote) + assert str(e.value) == "Only two decimal places are allowed." + + +def test_vote_transaction_multiple_votes(): + vote = {"fun": 35.3, "deadlock": 64.7} + transaction = Vote() transaction.set_votes(vote) transaction.set_nonce(1) @@ -22,47 +34,57 @@ def test_vote_transaction(): assert transaction_dict["asset"]["votes"] == vote assert transaction_dict["type"] is SOLAR_TRANSACTION_VOTE assert transaction_dict["typeGroup"] == TRANSACTION_TYPE_GROUP.SOLAR.value - assert transaction_dict["fee"] == 100000000 + assert transaction_dict["fee"] == 9000000 transaction.verify() # if no exception is raised, it means the transaction is valid -def test_vote_transaction_multiple_votes(): - vote = {"fun": 35.3, "deadlock": 64.7} - +def test_vote_transaction_using_empty_list(): transaction = Vote() - transaction.set_votes(vote) + transaction.set_votes(["-awesome"]) transaction.set_nonce(1) transaction.sign("testing") transaction_dict = transaction.to_dict() assert transaction_dict["nonce"] == 1 assert transaction_dict["signature"] - assert transaction_dict["asset"]["votes"] == vote + assert transaction_dict["asset"]["votes"] == {} assert transaction_dict["type"] is SOLAR_TRANSACTION_VOTE assert transaction_dict["typeGroup"] == TRANSACTION_TYPE_GROUP.SOLAR.value - assert transaction_dict["fee"] == 100000000 + assert transaction_dict["fee"] == 9000000 transaction.verify() # if no exception is raised, it means the transaction is valid -def test_vote_transaction_using_list(): +def test_vote_transaction_using_list_ok(): transaction = Vote() - transaction.set_votes(["-awesome"]) + transaction.set_votes(["awesome", "possom", "flossom"]) transaction.set_nonce(1) transaction.sign("testing") transaction_dict = transaction.to_dict() assert transaction_dict["nonce"] == 1 assert transaction_dict["signature"] - assert transaction_dict["asset"]["votes"] == {} + assert OrderedDict(transaction_dict["asset"]["votes"]) == OrderedDict({ + "awesome": 33.34, + "flossom": 33.33, + "possom": 33.33, + }) assert transaction_dict["type"] is SOLAR_TRANSACTION_VOTE assert transaction_dict["typeGroup"] == TRANSACTION_TYPE_GROUP.SOLAR.value - assert transaction_dict["fee"] == 100000000 + assert transaction_dict["fee"] == 9000000 transaction.verify() # if no exception is raised, it means the transaction is valid +def test_vote_transaction_using_list_more_than_53(): + transaction = Vote() + + with pytest.raises(SolarInvalidTransaction) as e: + transaction.set_votes([str(x) for x in range(55)]) + assert str(e.value) == "Unable to vote for more than 53 delegates" + + def test_sort_votes(): votes = { "ddd": 50, diff --git a/tests/transactions/deserializers/test_vote.py b/tests/transactions/deserializers/test_vote.py index feb06de..109d751 100644 --- a/tests/transactions/deserializers/test_vote.py +++ b/tests/transactions/deserializers/test_vote.py @@ -2,7 +2,7 @@ def test_vote_deserializer(): - serialized = "ff031e0200000002000100000000000000037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c00e1f50500000000000108646561646c6f636ba9138ee49974901f9818d56459d1817c41fc2d51204abc07cea05b72cae00cffaf314057f06b009ce6a2f99c8774af171f209fac1a9ce15abb429648ea22321765cb" # noqa + serialized = "ff031e0200000002000100000000000000037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c4054890000000000000208646561646c6f636b46190366756eca0db1b484cb3b781b0af8f82f8639c52f25f637100d8adadcdde380c07924ada1cc1c1f5b6107cce1ff86b87ab66806ca1a8ae716e3555198cc7fa324a70f697b54" # noqa deserializer = Deserializer(serialized) actual = deserializer.deserialize() @@ -11,29 +11,16 @@ def test_vote_deserializer(): assert actual.typeGroup == 2 assert actual.type == 2 assert actual.amount == 0 - assert actual.fee == 100000000 + assert actual.fee == 9000000 assert actual.nonce == 1 - assert actual.asset["votes"] == {"deadlock": 50.33} + assert actual.asset["votes"] == {"deadlock": 64.7, "fun": 35.3} assert ( actual.senderPublicKey == "037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c" ) # noqa assert ( actual.signature - == "8ee49974901f9818d56459d1817c41fc2d51204abc07cea05b72cae00cffaf314057f06b009ce6a2f99c8774af171f209fac1a9ce15abb429648ea22321765cb" + == "b1b484cb3b781b0af8f82f8639c52f25f637100d8adadcdde380c07924ada1cc1c1f5b6107cce1ff86b87ab66806ca1a8ae716e3555198cc7fa324a70f697b54" ) actual.verify() - - -def test_vote_deserializer_multiple_votes(): - serialized = "ff031e0200000002000100000000000000037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c00e1f5050000000000020366756eca0d08646561646c6f636b46196300f3ff5eff1a2e083ab31762089ad95bebb3de8ee451442a5fd945c2d7773cfe5dfc1528d0c03533677b88cd9e4421395716f78e9f7519343d2fb43ef1ca8d" - - deserializer = Deserializer(serialized) - actual = deserializer.deserialize() - - assert actual.asset["votes"] == {"fun": 35.3, "deadlock": 64.7} - assert ( - actual.senderPublicKey - == "037fde73baaa48eb75c013fe9ff52a74a096d48b9978351bdcb5b72331ca37487c" - ) # noqa