diff --git a/crypto/enums/abi_function.py b/crypto/enums/abi_function.py index c0cad256..7d8f14c7 100644 --- a/crypto/enums/abi_function.py +++ b/crypto/enums/abi_function.py @@ -3,6 +3,7 @@ class AbiFunction(Enum): VOTE = 'vote' UNVOTE = 'unvote' + MULTIPAYMENT = 'pay' USERNAME_REGISTRATION = 'registerUsername' USERNAME_RESIGNATION = 'resignUsername' VALIDATOR_REGISTRATION = 'registerValidator' diff --git a/crypto/enums/contract_abi_type.py b/crypto/enums/contract_abi_type.py index a9eefb1d..36d17956 100644 --- a/crypto/enums/contract_abi_type.py +++ b/crypto/enums/contract_abi_type.py @@ -3,4 +3,5 @@ class ContractAbiType(Enum): CUSTOM = 'custom' CONSENSUS = 'consensus' + MULTIPAYMENT = 'multipayment' USERNAMES = 'usernames' diff --git a/crypto/enums/contract_addresses.py b/crypto/enums/contract_addresses.py index ac0b0510..c7379dec 100644 --- a/crypto/enums/contract_addresses.py +++ b/crypto/enums/contract_addresses.py @@ -2,5 +2,5 @@ class ContractAddresses(Enum): CONSENSUS = '0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1' - MULTIPAYMENT = '0x83769BeEB7e5405ef0B7dc3C66C43E3a51A6d27f' + MULTIPAYMENT = '0x00EFd0D4639191C49908A7BddbB9A11A994A8527' USERNAMES = '0x2c1DE3b4Dbb4aDebEbB5dcECAe825bE2a9fc6eb6' diff --git a/crypto/transactions/builder/multipayment_builder.py b/crypto/transactions/builder/multipayment_builder.py new file mode 100644 index 00000000..19ffbfab --- /dev/null +++ b/crypto/transactions/builder/multipayment_builder.py @@ -0,0 +1,26 @@ +from typing import Optional +from crypto.enums.contract_addresses import ContractAddresses +from crypto.transactions.builder.base import AbstractTransactionBuilder +from crypto.transactions.types.multipayment import Multipayment + +class MultipaymentBuilder(AbstractTransactionBuilder): + def __init__(self, data: Optional[dict] = None): + super().__init__(data) + + self.transaction.data['pay'] = [[], []] + + self.recipient_address(ContractAddresses.MULTIPAYMENT.value) + self.transaction.refresh_payload_data() + + def pay(self, address: str, amount: str): + self.transaction.data['pay'][0].append(address) + self.transaction.data['pay'][1].append(amount) + + self.transaction.refresh_payload_data() + + self.transaction.data['value'] = str(int(self.transaction.data['value']) + int(amount)) + + return self + + def get_transaction_instance(self, data: dict): + return Multipayment(data) diff --git a/crypto/transactions/deserializer.py b/crypto/transactions/deserializer.py index 125292ab..6e1cbb67 100644 --- a/crypto/transactions/deserializer.py +++ b/crypto/transactions/deserializer.py @@ -3,6 +3,7 @@ from crypto.enums.constants import Constants from crypto.enums.contract_abi_type import ContractAbiType from crypto.transactions.types.abstract_transaction import AbstractTransaction +from crypto.transactions.types.multipayment import Multipayment from crypto.transactions.types.transfer import Transfer from crypto.transactions.types.evm_call import EvmCall from crypto.transactions.types.username_registration import UsernameRegistration @@ -59,6 +60,12 @@ def deserialize(self) -> AbstractTransaction: return transaction def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: + multipayment_payload_data = self.decode_payload(data, ContractAbiType.MULTIPAYMENT) + if multipayment_payload_data is not None: + function_name = multipayment_payload_data.get('functionName') + if function_name == AbiFunction.MULTIPAYMENT.value: + return Multipayment(data, multipayment_payload_data) + if data['value'] != '0': return Transfer(data) @@ -66,13 +73,13 @@ def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: if consensus_payload_data is not None: function_name = consensus_payload_data.get('functionName') if function_name == AbiFunction.VOTE.value: - return Vote(data) + return Vote(data, consensus_payload_data) if function_name == AbiFunction.UNVOTE.value: return Unvote(data) if function_name == AbiFunction.VALIDATOR_REGISTRATION.value: - return ValidatorRegistration(data) + return ValidatorRegistration(data, consensus_payload_data) if function_name == AbiFunction.VALIDATOR_RESIGNATION.value: return ValidatorResignation(data) @@ -81,7 +88,7 @@ def guess_transaction_from_data(self, data: dict) -> AbstractTransaction: if username_payload_data is not None: function_name = username_payload_data.get('functionName') if function_name == AbiFunction.USERNAME_REGISTRATION.value: - return UsernameRegistration(data) + return UsernameRegistration(data, username_payload_data) if function_name == AbiFunction.USERNAME_RESIGNATION.value: return UsernameResignation(data) diff --git a/crypto/transactions/types/multipayment.py b/crypto/transactions/types/multipayment.py new file mode 100644 index 00000000..4bb6dcf8 --- /dev/null +++ b/crypto/transactions/types/multipayment.py @@ -0,0 +1,24 @@ +from typing import Optional +from crypto.enums.contract_abi_type import ContractAbiType +from crypto.transactions.types.abstract_transaction import AbstractTransaction +from crypto.utils.abi_encoder import AbiEncoder +from crypto.enums.abi_function import AbiFunction + +class Multipayment(AbstractTransaction): + def __init__(self, data: Optional[dict] = None, payload: Optional[dict] = None): + data = data or {} + if payload is None: + payload = self.decode_payload(data, ContractAbiType.MULTIPAYMENT) + + if payload: + data['pay'] = payload.get('args', []) + + super().__init__(data) + + def get_payload(self) -> str: + if 'pay' not in self.data: + return '' + + encoder = AbiEncoder(ContractAbiType.MULTIPAYMENT) + + return encoder.encode_function_call(AbiFunction.MULTIPAYMENT.value, self.data['pay']) diff --git a/crypto/transactions/types/username_registration.py b/crypto/transactions/types/username_registration.py index 12fb21a0..3fcd8583 100644 --- a/crypto/transactions/types/username_registration.py +++ b/crypto/transactions/types/username_registration.py @@ -5,9 +5,11 @@ from crypto.enums.abi_function import AbiFunction class UsernameRegistration(AbstractTransaction): - def __init__(self, data: Optional[dict] = None): + def __init__(self, data: Optional[dict] = None, payload: Optional[dict] = None): data = data or {} - payload = self.decode_payload(data, ContractAbiType.USERNAMES) + if payload is None: + payload = self.decode_payload(data, ContractAbiType.USERNAMES) + if payload: data['username'] = payload.get('args', [None])[0] if payload.get('args') else None diff --git a/crypto/transactions/types/validator_registration.py b/crypto/transactions/types/validator_registration.py index ecaf86ee..28123468 100644 --- a/crypto/transactions/types/validator_registration.py +++ b/crypto/transactions/types/validator_registration.py @@ -5,9 +5,12 @@ from crypto.utils.transaction_utils import TransactionUtils class ValidatorRegistration(AbstractTransaction): - def __init__(self, data: Optional[dict] = None): + def __init__(self, data: Optional[dict] = None, payload: Optional[dict] = None): data = data or {} - payload = self.decode_payload(data) + + if payload is None: + payload = self.decode_payload(data) + if payload: data['validatorPublicKey'] = TransactionUtils.parse_hex_from_str(payload.get('args', [None])[0]) if payload.get('args') else None super().__init__(data) diff --git a/crypto/transactions/types/vote.py b/crypto/transactions/types/vote.py index 27124ef0..261f2984 100644 --- a/crypto/transactions/types/vote.py +++ b/crypto/transactions/types/vote.py @@ -4,9 +4,11 @@ from crypto.enums.abi_function import AbiFunction class Vote(AbstractTransaction): - def __init__(self, data: Optional[dict] = None): + def __init__(self, data: Optional[dict] = None, payload: Optional[dict] = None): data = data or {} - payload = self.decode_payload(data) + if payload is None: + payload = self.decode_payload(data) + if payload: data['vote'] = payload.get('args', [None])[0] if payload.get('args') else None diff --git a/crypto/utils/abi/json/Abi.Multipayment.json b/crypto/utils/abi/json/Abi.Multipayment.json new file mode 100644 index 00000000..b2ab88ce --- /dev/null +++ b/crypto/utils/abi/json/Abi.Multipayment.json @@ -0,0 +1,110 @@ +{ + "abi": [ + { + "type": "function", + "name": "pay", + "inputs": [ + { + "name": "recipients", + "type": "address[]", + "internalType": "address payable[]" + }, + { + "name": "amounts", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { "type": "error", "name": "FailedToSendEther", "inputs": [] }, + { "type": "error", "name": "InvalidValue", "inputs": [] }, + { + "type": "error", + "name": "RecipientsAndAmountsMismatch", + "inputs": [] + } + ], + "bytecode": { + "object": "0x6080604052348015600e575f5ffd5b506102c98061001c5f395ff3fe60806040526004361061001d575f3560e01c8063084ce70814610021575b5f5ffd5b61003461002f3660046101c1565b610036565b005b828114610056576040516366d5293b60e11b815260040160405180910390fd5b5f805b8281101561008f578383828181106100735761007361022d565b90506020020135826100859190610241565b9150600101610059565b508034146100b057604051632a9ffab760e21b815260040160405180910390fd5b5f5b84811015610171575f8686838181106100cd576100cd61022d565b90506020020160208101906100e29190610266565b6001600160a01b03168585848181106100fd576100fd61022d565b905060200201356040515f6040518083038185875af1925050503d805f8114610141576040519150601f19603f3d011682016040523d82523d5f602084013e610146565b606091505b505090508061016857604051630dcf35db60e41b815260040160405180910390fd5b506001016100b2565b505050505050565b5f5f83601f840112610189575f5ffd5b50813567ffffffffffffffff8111156101a0575f5ffd5b6020830191508360208260051b85010111156101ba575f5ffd5b9250929050565b5f5f5f5f604085870312156101d4575f5ffd5b843567ffffffffffffffff8111156101ea575f5ffd5b6101f687828801610179565b909550935050602085013567ffffffffffffffff811115610215575f5ffd5b61022187828801610179565b95989497509550505050565b634e487b7160e01b5f52603260045260245ffd5b8082018082111561026057634e487b7160e01b5f52601160045260245ffd5b92915050565b5f60208284031215610276575f5ffd5b81356001600160a01b038116811461028c575f5ffd5b939250505056fea2646970667358221220bfee9113d4628767f3e4ea5baeb21f9c3bd88ea4c440d0a915dae090d37cd9a664736f6c634300081b0033", + "sourceMap": "81:836:23:-:0;;;;;;;;;;;;;;;;;;;", + "linkReferences": {} + }, + "deployedBytecode": { + "object": "0x60806040526004361061001d575f3560e01c8063084ce70814610021575b5f5ffd5b61003461002f3660046101c1565b610036565b005b828114610056576040516366d5293b60e11b815260040160405180910390fd5b5f805b8281101561008f578383828181106100735761007361022d565b90506020020135826100859190610241565b9150600101610059565b508034146100b057604051632a9ffab760e21b815260040160405180910390fd5b5f5b84811015610171575f8686838181106100cd576100cd61022d565b90506020020160208101906100e29190610266565b6001600160a01b03168585848181106100fd576100fd61022d565b905060200201356040515f6040518083038185875af1925050503d805f8114610141576040519150601f19603f3d011682016040523d82523d5f602084013e610146565b606091505b505090508061016857604051630dcf35db60e41b815260040160405180910390fd5b506001016100b2565b505050505050565b5f5f83601f840112610189575f5ffd5b50813567ffffffffffffffff8111156101a0575f5ffd5b6020830191508360208260051b85010111156101ba575f5ffd5b9250929050565b5f5f5f5f604085870312156101d4575f5ffd5b843567ffffffffffffffff8111156101ea575f5ffd5b6101f687828801610179565b909550935050602085013567ffffffffffffffff811115610215575f5ffd5b61022187828801610179565b95989497509550505050565b634e487b7160e01b5f52603260045260245ffd5b8082018082111561026057634e487b7160e01b5f52601160045260245ffd5b92915050565b5f60208284031215610276575f5ffd5b81356001600160a01b038116811461028c575f5ffd5b939250505056fea2646970667358221220bfee9113d4628767f3e4ea5baeb21f9c3bd88ea4c440d0a915dae090d37cd9a664736f6c634300081b0033", + "sourceMap": "81:836:23:-:0;;;;;;;;;;;;;;;;;;;;;209:706;;;;;;:::i;:::-;;:::i;:::-;;;318:35;;;314:103;;376:30;;-1:-1:-1;;;376:30:23;;;;;;;;;;;314:103;492:13;;519:89;539:18;;;519:89;;;587:7;;595:1;587:10;;;;;;;:::i;:::-;;;;;;;578:19;;;;;:::i;:::-;;-1:-1:-1;559:3:23;;519:89;;;;634:5;621:9;:18;617:70;;662:14;;-1:-1:-1;;;662:14:23;;;;;;;;;;;617:70;702:9;697:212;717:21;;;697:212;;;760:9;774:10;;785:1;774:13;;;;;;;:::i;:::-;;;;;;;;;;;;;;:::i;:::-;-1:-1:-1;;;;;774:18:23;800:7;;808:1;800:10;;;;;;;:::i;:::-;;;;;;;774:41;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;759:56;;;834:4;829:70;;865:19;;-1:-1:-1;;;865:19:23;;;;;;;;;;;829:70;-1:-1:-1;740:3:23;;697:212;;;;304:611;209:706;;;;:::o;14:375:25:-;85:8;95:6;149:3;142:4;134:6;130:17;126:27;116:55;;167:1;164;157:12;116:55;-1:-1:-1;190:20:25;;233:18;222:30;;219:50;;;265:1;262;255:12;219:50;302:4;294:6;290:17;278:29;;362:3;355:4;345:6;342:1;338:14;330:6;326:27;322:38;319:47;316:67;;;379:1;376;369:12;316:67;14:375;;;;;:::o;394:792::-;524:6;532;540;548;601:2;589:9;580:7;576:23;572:32;569:52;;;617:1;614;607:12;569:52;657:9;644:23;690:18;682:6;679:30;676:50;;;722:1;719;712:12;676:50;761:78;831:7;822:6;811:9;807:22;761:78;:::i;:::-;858:8;;-1:-1:-1;735:104:25;-1:-1:-1;;946:2:25;931:18;;918:32;975:18;962:32;;959:52;;;1007:1;1004;997:12;959:52;1046:80;1118:7;1107:8;1096:9;1092:24;1046:80;:::i;:::-;394:792;;;;-1:-1:-1;1145:8:25;-1:-1:-1;;;;394:792:25:o;1191:127::-;1252:10;1247:3;1243:20;1240:1;1233:31;1283:4;1280:1;1273:15;1307:4;1304:1;1297:15;1323:222;1388:9;;;1409:10;;;1406:133;;;1461:10;1456:3;1452:20;1449:1;1442:31;1496:4;1493:1;1486:15;1524:4;1521:1;1514:15;1406:133;1323:222;;;;:::o;1550:294::-;1617:6;1670:2;1658:9;1649:7;1645:23;1641:32;1638:52;;;1686:1;1683;1676:12;1638:52;1712:23;;-1:-1:-1;;;;;1764:31:25;;1754:42;;1744:70;;1810:1;1807;1800:12;1744:70;1833:5;1550:294;-1:-1:-1;;;1550:294:25:o", + "linkReferences": {} + }, + "methodIdentifiers": { "pay(address[],uint256[])": "084ce708" }, + "rawMetadata": "{\"compiler\":{\"version\":\"0.8.27+commit.40a35a09\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"FailedToSendEther\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidValue\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RecipientsAndAmountsMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address payable[]\",\"name\":\"recipients\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"amounts\",\"type\":\"uint256[]\"}],\"name\":\"pay\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/multi-payment/MultiPayment.sol\":\"MultiPayment\"},\"evmVersion\":\"shanghai\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@contracts/=src/\",\":@forge-std/=lib/forge-std/src/\",\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":ds-test/=lib/openzeppelin-contracts-upgradeable/lib/forge-std/lib/ds-test/src/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\",\":solidity-stringutils/=lib/openzeppelin-foundry-upgrades/lib/solidity-stringutils/\"]},\"sources\":{\"src/multi-payment/MultiPayment.sol\":{\"keccak256\":\"0x804a5823ffdb1866bc5f42a0fe3688c8e153da92e279c7b79aa3d73472a1ce35\",\"license\":\"GNU GENERAL PUBLIC LICENSE\",\"urls\":[\"bzz-raw://43df2e2ee0c0a76dd4ee8bd6cc81d65c80fb1aaada5e925d129a91de434d2c52\",\"dweb:/ipfs/QmRj4GTC5R6QTcaBxmghrH3ABuDXXwvHpi4yYjd1e8VduW\"]}},\"version\":1}", + "metadata": { + "compiler": { "version": "0.8.27+commit.40a35a09" }, + "language": "Solidity", + "output": { + "abi": [ + { "inputs": [], "type": "error", "name": "FailedToSendEther" }, + { "inputs": [], "type": "error", "name": "InvalidValue" }, + { + "inputs": [], + "type": "error", + "name": "RecipientsAndAmountsMismatch" + }, + { + "inputs": [ + { + "internalType": "address payable[]", + "name": "recipients", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function", + "name": "pay" + } + ], + "devdoc": { "kind": "dev", "methods": {}, "version": 1 }, + "userdoc": { "kind": "user", "methods": {}, "version": 1 } + }, + "settings": { + "remappings": [ + "@contracts/=src/", + "@forge-std/=lib/forge-std/src/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", + "ds-test/=lib/openzeppelin-contracts-upgradeable/lib/forge-std/lib/ds-test/src/", + "erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/", + "forge-std/=lib/forge-std/src/", + "halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/", + "openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/", + "openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/", + "openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/", + "solidity-stringutils/=lib/openzeppelin-foundry-upgrades/lib/solidity-stringutils/" + ], + "optimizer": { "enabled": true, "runs": 200 }, + "metadata": { "bytecodeHash": "ipfs" }, + "compilationTarget": { + "src/multi-payment/MultiPayment.sol": "MultiPayment" + }, + "evmVersion": "shanghai", + "libraries": {} + }, + "sources": { + "src/multi-payment/MultiPayment.sol": { + "keccak256": "0x804a5823ffdb1866bc5f42a0fe3688c8e153da92e279c7b79aa3d73472a1ce35", + "urls": [ + "bzz-raw://43df2e2ee0c0a76dd4ee8bd6cc81d65c80fb1aaada5e925d129a91de434d2c52", + "dweb:/ipfs/QmRj4GTC5R6QTcaBxmghrH3ABuDXXwvHpi4yYjd1e8VduW" + ], + "license": "GNU GENERAL PUBLIC LICENSE" + } + }, + "version": 1 + }, + "id": 23 +} diff --git a/crypto/utils/abi_base.py b/crypto/utils/abi_base.py index d810c551..96731cd3 100644 --- a/crypto/utils/abi_base.py +++ b/crypto/utils/abi_base.py @@ -39,7 +39,8 @@ def strip_hex_prefix(self, hex_str): def is_valid_address(self, address): # Compute the checksum address and compare computed_checksum_address = get_checksum_address(address.lower()) - return address == computed_checksum_address + + return address.lower() == computed_checksum_address.lower() def keccak256(self, input_str): k = keccak.new(digest_bits=256) @@ -62,6 +63,9 @@ def __contract_abi_path(self, abi_type: ContractAbiType, path: Optional[str] = N if abi_type == ContractAbiType.CONSENSUS: return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.Consensus.json') + if abi_type == ContractAbiType.MULTIPAYMENT: + return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.Multipayment.json') + if abi_type == ContractAbiType.USERNAMES: return os.path.join(os.path.dirname(__file__), 'abi/json', 'Abi.Usernames.json') diff --git a/crypto/utils/abi_decoder.py b/crypto/utils/abi_decoder.py index 53466a4d..1bf9ddcf 100644 --- a/crypto/utils/abi_decoder.py +++ b/crypto/utils/abi_decoder.py @@ -13,8 +13,10 @@ def decode_function_data(self, data): abi_item = self.find_function_by_selector(function_selector) if not abi_item: raise Exception('Function selector not found in ABI: ' + function_selector) + encoded_params = data[8:] decoded_params = self.decode_abi_parameters(abi_item['inputs'], encoded_params) + return { 'functionName': abi_item['name'], 'args': decoded_params, @@ -27,11 +29,13 @@ def find_function_by_selector(self, selector): function_selector = self.strip_hex_prefix(self.keccak256(function_signature))[0:8] if function_selector == selector: return item + return None def decode_abi_parameters(self, params, data): if not data and len(params) > 0: raise Exception('No data to decode') + bytes_data = binascii.unhexlify(data) cursor = 0 values = [] @@ -39,6 +43,7 @@ def decode_abi_parameters(self, params, data): value, consumed = self.decode_parameter(bytes_data, cursor, param) cursor += consumed values.append(value) + return values def decode_parameter(self, bytes_data, offset, param): @@ -47,26 +52,35 @@ def decode_parameter(self, bytes_data, offset, param): if array_components: length, base_type = array_components param['type'] = base_type + return self.decode_array(bytes_data, offset, param, length) + if type_ == 'address': return self.decode_address(bytes_data, offset) + if type_ == 'bool': return self.decode_bool(bytes_data, offset) + if type_ == 'string': return self.decode_string(bytes_data, offset) + if type_ == 'bytes': return self.decode_dynamic_bytes(bytes_data, offset) + match = re.match(r'^bytes(\d+)$', type_) if match: size = int(match.group(1)) return self.decode_fixed_bytes(bytes_data, offset, size) + match = re.match(r'^(u?int)(\d+)$', type_) if match: signed = match.group(1) == 'int' - bits = int(match.group(2)) + return self.decode_number(bytes_data, offset, signed) + if type_ == 'tuple': return self.decode_tuple(bytes_data, offset, param) + raise Exception('Unsupported type: ' + type_) @staticmethod @@ -75,18 +89,21 @@ def decode_address(bytes_data, offset): address_bytes = data[12:32] address = '0x' + address_bytes.hex() address = get_checksum_address(address) + return address, 32 @staticmethod def decode_bool(bytes_data, offset): data = bytes_data[offset:offset+32] value = int.from_bytes(data, byteorder='big') != 0 + return value, 32 @staticmethod def decode_number(bytes_data, offset, signed): data = bytes_data[offset:offset+32] value = int.from_bytes(data, byteorder='big', signed=signed) + return value, 32 @classmethod @@ -96,6 +113,7 @@ def decode_string(cls, bytes_data, offset): length = cls.read_uint(bytes_data, string_offset) string_data = bytes_data[string_offset+32:string_offset+32+length] value = string_data.decode('utf-8') + return value, 32 def decode_dynamic_bytes(self, bytes_data, offset): @@ -104,6 +122,7 @@ def decode_dynamic_bytes(self, bytes_data, offset): length = self.read_uint(bytes_data, bytes_offset) bytes_data_value = bytes_data[bytes_offset+32:bytes_offset+32+length] value = '0x' + bytes_data_value.hex() + return value, 32 def decode_fixed_bytes(self, bytes_data, offset, size): @@ -118,7 +137,7 @@ def decode_array(self, bytes_data, offset, param, length): if length is None: data_offset = self.read_uint(bytes_data, offset) - array_offset = offset + data_offset + array_offset = data_offset array_length = self.read_uint(bytes_data, array_offset) cursor = array_offset + 32 else: @@ -126,10 +145,11 @@ def decode_array(self, bytes_data, offset, param, length): cursor = offset values = [] - for i in range(array_length): + for _ in range(array_length): value, consumed = self.decode_parameter(bytes_data, cursor, element_type) cursor += consumed values.append(value) + return values, 32 def decode_tuple(self, bytes_data, offset, param): @@ -142,9 +162,11 @@ def decode_tuple(self, bytes_data, offset, param): cursor += consumed name = component.get('name', '') values[name] = value + return values, 32 @staticmethod def read_uint(bytes_data, offset): data = bytes_data[offset:offset+32] + return int.from_bytes(data, byteorder='big') diff --git a/tests/transactions/builder/test_multipayment_builder.py b/tests/transactions/builder/test_multipayment_builder.py new file mode 100644 index 00000000..f9823f7a --- /dev/null +++ b/tests/transactions/builder/test_multipayment_builder.py @@ -0,0 +1,76 @@ +from crypto.transactions.builder.multipayment_builder import MultipaymentBuilder + +def test_it_should_sign_it_with_a_passphrase(passphrase, load_transaction_fixture): + fixture = load_transaction_fixture('multipayment') + + builder = ( + MultipaymentBuilder() + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .pay('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', '100000') + .pay('0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763', '200000') + .sign(passphrase) + ) + + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + + assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['id'] == fixture['data']['id'] + assert builder.verify() + +def test_it_should_handle_single_recipient(passphrase, load_transaction_fixture): + fixture = load_transaction_fixture('multipayment-single') + + builder = ( + MultipaymentBuilder() + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .pay('0x6F0182a0cc707b055322CcF6d4CB6a5Aff1aEb22', '100000') + .sign(passphrase) + ) + + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + + assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['id'] == fixture['data']['id'] + assert builder.verify() + +def test_it_should_handle_empty_payment(passphrase, load_transaction_fixture): + fixture = load_transaction_fixture('multipayment-empty') + + builder = ( + MultipaymentBuilder() + .gas_price(fixture['data']['gasPrice']) + .nonce(fixture['data']['nonce']) + .network(fixture['data']['network']) + .gas_limit(fixture['data']['gasLimit']) + .sign(passphrase) + ) + + assert builder.transaction.data['gasPrice'] == fixture['data']['gasPrice'] + assert builder.transaction.data['nonce'] == fixture['data']['nonce'] + assert builder.transaction.data['network'] == fixture['data']['network'] + assert builder.transaction.data['gasLimit'] == fixture['data']['gasLimit'] + assert builder.transaction.data['v'] == fixture['data']['v'] + assert builder.transaction.data['r'] == fixture['data']['r'] + assert builder.transaction.data['s'] == fixture['data']['s'] + + assert builder.transaction.serialize().hex() == fixture['serialized'] + assert builder.transaction.data['id'] == fixture['data']['id'] + assert builder.verify() diff --git a/tests/transactions/builder/test_validator_registration_builder.py b/tests/transactions/builder/test_validator_registration_builder.py index 87573baf..73221b44 100644 --- a/tests/transactions/builder/test_validator_registration_builder.py +++ b/tests/transactions/builder/test_validator_registration_builder.py @@ -29,7 +29,7 @@ def test_validator_registration_transaction(passphrase, validator_public_key, lo assert builder.transaction.data['id'] == fixture['data']['id'] assert builder.verify() -def test_validator_registration_transaction_with_default_recipient_address(passphrase, load_transaction_fixture): +def test_validator_registration_transaction_with_default_recipient_address(passphrase, validator_public_key, load_transaction_fixture): fixture = load_transaction_fixture('validator-registration') builder = ( @@ -38,7 +38,7 @@ def test_validator_registration_transaction_with_default_recipient_address(passp .nonce(fixture['data']['nonce']) .network(fixture['data']['network']) .gas_limit(fixture['data']['gasLimit']) - .validator_public_key('954f46d6097a1d314e900e66e11e0dad0a57cd03e04ec99f0dedd1c765dcb11e6d7fa02e22cf40f9ee23d9cc1c0624bd') + .validator_public_key(validator_public_key) .sign(passphrase) ) diff --git a/tests/transactions/test_deserializer.py b/tests/transactions/test_deserializer.py index f5b1d895..ef732299 100644 --- a/tests/transactions/test_deserializer.py +++ b/tests/transactions/test_deserializer.py @@ -1,4 +1,5 @@ from crypto.transactions.deserializer import Deserializer +from crypto.transactions.types.multipayment import Multipayment from crypto.transactions.types.transfer import Transfer from crypto.transactions.types.username_registration import UsernameRegistration from crypto.transactions.types.username_resignation import UsernameResignation @@ -61,19 +62,18 @@ def test_deserialize_username_resignation(load_transaction_fixture): assert isinstance(transaction, UsernameResignation) +def test_deserialize_multipayment(load_transaction_fixture): + fixture = load_transaction_fixture('multipayment') + transaction = assert_deserialized(fixture, ['id', 'nonce', 'gasPrice', 'gasLimit', 'value', 'v', 'r', 's']) + + assert isinstance(transaction, Multipayment) + def test_parse_number(): assert Deserializer.parse_number('0x01') == 1 assert Deserializer.parse_number('0x0100') == 256 assert Deserializer.parse_number('0x010000') == 65536 assert Deserializer.parse_number('0x') == 0 -def test_parse_big_number(): - assert Deserializer.parse_big_number('0x01') == '1' - assert Deserializer.parse_big_number('0x0100') == '256' - assert Deserializer.parse_big_number('0x010000') == '65536' - assert Deserializer.parse_big_number('0x') == '0' - assert Deserializer.parse_big_number('0x52B7D2DCC80CD2E4000000') == '100000000000000000000000000' - def test_parse_hex(): assert Deserializer.parse_hex('0x01') == '01' assert Deserializer.parse_hex('0x0100') == '0100' @@ -81,6 +81,13 @@ def test_parse_hex(): assert Deserializer.parse_hex('0x') == '' assert Deserializer.parse_hex('0x52B7D2DCC80CD2E4000000') == '52B7D2DCC80CD2E4000000' +def test_parse_big_number(): + assert Deserializer.parse_big_number('0x01') == '1' + assert Deserializer.parse_big_number('0x0100') == '256' + assert Deserializer.parse_big_number('0x010000') == '65536' + assert Deserializer.parse_big_number('0x') == '0' + assert Deserializer.parse_big_number('0x52B7D2DCC80CD2E4000000') == '100000000000000000000000000' + def test_parse_address(): assert Deserializer.parse_address('0x52B7D2DCC80CD2E4000000') == '0x52B7D2DCC80CD2E4000000' - assert Deserializer.parse_address('0x') == None + assert Deserializer.parse_address('0x') is None diff --git a/tests/utils/test_abi_base.py b/tests/utils/test_abi_base.py new file mode 100644 index 00000000..0705883a --- /dev/null +++ b/tests/utils/test_abi_base.py @@ -0,0 +1,8 @@ +from crypto.utils.abi_base import AbiBase + + +def test_it_should_validate_uppercase_addresses(): + assert AbiBase().is_valid_address('0xC3bBE9B1CeE1ff85Ad72b87414B0E9B7F2366763') is True + +def test_it_should_validate_lowercase_addresses(): + assert AbiBase().is_valid_address('0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763') is True diff --git a/tests/utils/test_abi_encoder.py b/tests/utils/test_abi_encoder.py index 7b68d162..96f60531 100644 --- a/tests/utils/test_abi_encoder.py +++ b/tests/utils/test_abi_encoder.py @@ -10,3 +10,15 @@ def test_encode_vote_function_call(): encoded_data = encoder.encode_function_call(function_name, args) assert encoded_data == expected_encoded_data + +def test_encode_address(): + assert AbiEncoder().encode_address('0xC3bBE9B1CeE1ff85Ad72b87414B0E9B7F2366763') == { + 'dynamic': False, + 'encoded': '0x000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763', + } + + # lowercase + assert AbiEncoder().encode_address('0xc3bbe9b1cee1ff85ad72b87414b0e9b7f2366763') == { + 'dynamic': False, + 'encoded': '0x000000000000000000000000c3bbe9b1cee1ff85ad72b87414b0e9b7f2366763', + }