From 7609bafb5a91fa1f2da80b44cc3425717e59d12b Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Wed, 6 Jul 2022 15:03:14 +0200 Subject: [PATCH] Add a Django Form Field for Hexadecimals - Refactor Keccack256FormField inheriting the new form --- gnosis/eth/django/filters.py | 57 +------------------- gnosis/eth/django/forms.py | 78 +++++++++++++++++++++++++++ gnosis/eth/django/models.py | 13 ++++- gnosis/eth/django/tests/test_forms.py | 29 +++++++++- 4 files changed, 117 insertions(+), 60 deletions(-) create mode 100644 gnosis/eth/django/forms.py diff --git a/gnosis/eth/django/filters.py b/gnosis/eth/django/filters.py index 235627bd2..5c6bc8f88 100644 --- a/gnosis/eth/django/filters.py +++ b/gnosis/eth/django/filters.py @@ -1,66 +1,11 @@ -import binascii - -from django.core.exceptions import ValidationError -from django.forms import CharField as CharFieldForm -from django.utils.translation import gettext_lazy as _ - import django_filters -from hexbytes import HexBytes - -from ..utils import fast_is_checksum_address - - -class EthereumAddressFieldForm(CharFieldForm): - default_error_messages = { - "invalid": _("Enter a valid checksummed Ethereum Address."), - } - def prepare_value(self, value): - return value - - def to_python(self, value): - value = super().to_python(value) - if value in self.empty_values: - return None - elif not fast_is_checksum_address(value): - raise ValidationError(self.error_messages["invalid"], code="invalid") - return value +from .forms import EthereumAddressFieldForm, Keccak256FieldForm class EthereumAddressFilter(django_filters.Filter): field_class = EthereumAddressFieldForm -class Keccak256FieldForm(CharFieldForm): - default_error_messages = { - "invalid": _('"%(value)s" is not a valid keccak256 hash.'), - "length": _('"%(value)s" keccak256 hash should be 32 bytes.'), - } - - def prepare_value(self, value): - return value - - def to_python(self, value): - value = super().to_python(value) - if value in self.empty_values: - return None - else: - try: - bytes_value = HexBytes(value) - if len(bytes_value) != 32: - raise ValidationError( - self.error_messages["length"], - code="length", - params={"value": value}, - ) - except (binascii.Error, ValueError): - raise ValidationError( - self.error_messages["invalid"], - code="invalid", - params={"value": value}, - ) - return value - - class Keccak256Filter(django_filters.Filter): field_class = Keccak256FieldForm diff --git a/gnosis/eth/django/forms.py b/gnosis/eth/django/forms.py new file mode 100644 index 000000000..e7a0ea497 --- /dev/null +++ b/gnosis/eth/django/forms.py @@ -0,0 +1,78 @@ +import binascii +from typing import Optional + +from django import forms +from django.core import exceptions +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ + +from hexbytes import HexBytes + +from gnosis.eth.utils import fast_is_checksum_address + + +class EthereumAddressFieldForm(forms.CharField): + default_error_messages = { + "invalid": _("Enter a valid checksummed Ethereum Address."), + } + + def prepare_value(self, value): + return value + + def to_python(self, value): + value = super().to_python(value) + if value in self.empty_values: + return None + elif not fast_is_checksum_address(value): + raise ValidationError(self.error_messages["invalid"], code="invalid") + return value + + +class HexFieldForm(forms.CharField): + default_error_messages = { + "invalid": _("Enter a valid hexadecimal."), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.empty_value = None + + def prepare_value(self, value: memoryview) -> str: + if value: + return "0x" + bytes(value).hex() + else: + return "" + + def to_python(self, value: str) -> Optional[HexBytes]: + value = value.strip() + if value in self.empty_values: + return self.empty_value + try: + return HexBytes(value) + except (binascii.Error, TypeError, ValueError): + raise exceptions.ValidationError( + self.error_messages["invalid"], + code="invalid", + params={"value": value}, + ) + + +class Keccak256FieldForm(HexFieldForm): + default_error_messages = { + "invalid": _('"%(value)s" is not a valid keccak256 hash.'), + "length": _('"%(value)s" keccak256 hash should be 32 bytes.'), + } + + def prepare_value(self, value: str) -> str: + # Keccak field already returns a hex str + return value + + def to_python(self, value) -> HexBytes: + value: Optional[HexBytes] = super().to_python(value) + if value and len(value) != 32: + raise ValidationError( + self.error_messages["length"], + code="length", + params={"value": value.hex()}, + ) + return value diff --git a/gnosis/eth/django/models.py b/gnosis/eth/django/models.py index fcc3f8008..d45129bf7 100644 --- a/gnosis/eth/django/models.py +++ b/gnosis/eth/django/models.py @@ -10,7 +10,7 @@ from hexbytes import HexBytes from ..utils import fast_bytes_to_checksum_address, fast_to_checksum_address -from .filters import EthereumAddressFieldForm, Keccak256FieldForm +from .forms import EthereumAddressFieldForm, HexFieldForm, Keccak256FieldForm from .validators import validate_checksumed_address try: @@ -140,7 +140,7 @@ class HexField(models.CharField): On Database side a CharField is used. """ - description = "Stores a hex value into an CharField" + description = "Stores a hex value into an CharField. DEPRECATED, use a BinaryField" def from_db_value(self, value, expression, connection): return self.to_python(value) @@ -177,6 +177,15 @@ def clean(self, value, model_instance): return value +class HexV2Field(models.BinaryField): + def formfield(self, **kwargs): + defaults = { + "form_class": HexFieldForm, + } + defaults.update(kwargs) + return super().formfield(**defaults) + + class Sha3HashField(HexField): description = "DEPRECATED. Use `Keccak256Field`" diff --git a/gnosis/eth/django/tests/test_forms.py b/gnosis/eth/django/tests/test_forms.py index a3cf8bfdd..0b78efd93 100644 --- a/gnosis/eth/django/tests/test_forms.py +++ b/gnosis/eth/django/tests/test_forms.py @@ -1,17 +1,22 @@ from django.forms import forms from django.test import TestCase +from hexbytes import HexBytes from web3 import Web3 -from ..filters import EthereumAddressFieldForm, Keccak256FieldForm +from ..forms import EthereumAddressFieldForm, HexFieldForm, Keccak256FieldForm class EthereumAddressForm(forms.Form): value = EthereumAddressFieldForm() +class HexForm(forms.Form): + value = HexFieldForm(required=False) + + class Keccak256Form(forms.Form): - value = Keccak256FieldForm() + value = Keccak256FieldForm(required=False) class TestForms(TestCase): @@ -35,6 +40,22 @@ def test_ethereum_address_field_form(self): ) self.assertTrue(form.is_valid()) + def test_hex_field_form(self): + form = HexForm(data={"value": "not an hexadecimal"}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors["value"], ["Enter a valid hexadecimal."]) + + form = HexForm(data={"value": "0xabcd"}) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data["value"], HexBytes("0xabcd")) + + form = HexForm(initial={"value": memoryview(bytes.fromhex("cdef"))}) + self.assertIn('value="0xcdef"', form.as_p()) + + form = HexForm(data={"value": ""}) + self.assertTrue(form.is_valid()) + self.assertIsNone(form.cleaned_data["value"]) + def test_keccak256_field_form(self): form = Keccak256Form(data={"value": "not a hash"}) self.assertFalse(form.is_valid()) @@ -50,3 +71,7 @@ def test_keccak256_field_form(self): form = Keccak256Form(data={"value": Web3.keccak(text="testing").hex()}) self.assertTrue(form.is_valid()) + + form = Keccak256Form(data={"value": ""}) + self.assertTrue(form.is_valid()) + self.assertIsNone(form.cleaned_data["value"])