eth-abi

repository·main·Indexed 42 days ago

https://github.com/apeworx/eth-abi

Python utilities for working with Ethereum ABI definitions, primarily focused on encoding and decoding data between Python types and Solidity's binary ABI format. It provides tools for parsing and normalizing ABI type strings, managing custom types via ABIRegistry, and supporting complex structures like nested dynamic arrays.

Tokens
13K
Snippets
36
Records
60
Agent score
53%

What's inside eth-abi

  1. Overview of eth-abi

    main
    eth-abi provides Python utilities specifically designed for working with Ethereum Contract Interface (ABI) definitions. Its primary use cases are encoding data into ABI format and decoding ABI-encoded data back into Python types.
  2. Extend encoding and decoding with BaseEncoder and BaseDecoder

    main
    For custom ABI logic, you can implement or interact with eth_abi.encoding.BaseEncoder and eth_abi.decoding.BaseDecoder. These serve as the foundational classes for the library's codec system.
  3. Handle malformed UTF-8 strings during decoding

    main

    By default, the StringDecoder uses errors='strict', which causes a UnicodeDecodeError if a string contains bytes that are not valid UTF-8.

    To handle malformed strings, you can register new type names that use a different handle_string_errors strategy. This is done by registering the standard TextStringEncoder paired with a StringDecoder configured with one of the following Python error handling options:

    • surrogateescape
    • ignore
    • replace
    • backslashreplace

    Note: This customization is only available for decoding operations.

    from eth_abi import decode, encode
    from eth_abi.decoding import StringDecoder
    from eth_abi.encoding import TextStringEncoder
    from eth_abi.registry import registry
    
    # Setup a malformed string for testing
    test_string = encode(["string"], ["cat"])
    bad_string = test_string[:65] + b"\xff" + test_string[66:]
    
    # Register custom handlers
    registry.register(
        "surrogateescape_string",
        TextStringEncoder,
        StringDecoder(handle_string_errors="surrogateescape")
    )
    registry.register(
        "backslashreplace_string",
        TextStringEncoder,
        StringDecoder(handle_string_errors="backslashreplace")
    )
    
    # Use the custom types to decode without error
    assert decode(["surrogateescape_string"], bad_string) == ("c\udcfft",)
    assert decode(["backslashreplace_string"], bad_string) == ("c\\xfft",)
    
    # Cleanup
    registry.unregister('surrogateescape_string')
    registry.unregister('backslashreplace_string')
  4. Use a custom stream class for decoding

    main

    To customize the internal stream behavior used during the decoding process, you can subclass ABIDecoder and override the stream_class attribute. You can then compose this with ABIEncoder to create a custom codec that uses your specialized stream class.

    from eth_abi.codec import ABIEncoder, ABIDecoder
    from eth_abi.registry import registry
    
    class MyStream:
        # Custom stream behavior implementation
        pass
    
    class MyDecoder(ABIDecoder):
        stream_class = MyStream
    
    class MyCodec(ABIEncoder, MyDecoder):
        pass
    
    codec = MyCodec(registry)
  5. Copy and modify an existing registry

    main

    To create a specialized version of the default registry (e.g., to remove a specific type like address), use the .copy() method on the default registry. This ensures that the original default_registry remains intact and unaffected by your modifications. You then pass the copied registry to a new ABICodec instance.

    from eth_abi.codec import ABICodec
    from eth_abi.registry import registry as default_registry
    
    # Clone the default registry to avoid side effects
    registry = default_registry.copy()
    registry.unregister('address')
    
    # Create a codec using the modified copy
    codec = ABICodec(registry)
    
    # The default registry remains functional
    default_codec = ABICodec(default_registry)
    assert default_codec.encode(['address'], ['0x' + 'ff' * 20]) == b'\x00' * 12 + b'\xff' * 20
  6. Use a custom registry with ABICodec

    main

    If you need to add new types or modify existing type support without affecting the global default registry, you can create a new ABIRegistry instance, register your custom coders, and pass that registry to an ABICodec instance. This allows you to use the standard encoding and decoding API with your custom type definitions.

    from eth_abi.codec import ABICodec
    from eth_abi.exceptions import EncodingError, DecodingError
    from eth_abi.registry import ABIRegistry
    
    # Define and register the coders
    NULL_ENCODING = b'\x00' * 32
    
    def encode_null(x):
        if x is not None:
            raise EncodingError('Unsupported value')
        return NULL_ENCODING
    
    def decode_null(stream):
        if stream.read(32) != NULL_ENCODING:
            raise DecodingError('Not enough data or wrong data')
        return None
    
    registry = ABIRegistry()
    registry.register('null', encode_null, decode_null)
    
    # Use the custom registry with ABICodec
    codec = ABICodec(registry)
    
    # Encoding/Decoding works with the custom type
    assert codec.encode(['null'], [None]) == NULL_ENCODING
    (decoded_null_val,) = codec.decode(['null'], NULL_ENCODING)
    assert decoded_null_val is None
  7. Register complex ABI types using BaseEncoder and BaseDecoder

    main

    For types that require parameters extracted from the type string (e.g., null2 where 2 is a width), you should register custom classes that inherit from BaseEncoder and BaseDecoder.

    To implement this pattern:

    1. Matcher Function: Instead of a literal string, pass a function to registry.register that returns True if the type string matches (e.g., lambda x: x.startswith('null')).
    2. from_type_str Class Method: Implement this method on both classes to parse the type string and return an instance of the class with the necessary settings.
    3. Settings via __init__: Any keyword arguments passed to the constructor via from_type_str are automatically assigned to class properties of the same name (inherited from BaseCoder).
    4. encode and decode: Implement these methods to perform the actual logic using the instance's settings.
    5. validate_value: For BaseEncoder subclasses, implement this method to allow is_encodable to check compatibility without performing a full encoding.
    from eth_abi.decoding import BaseDecoder
    from eth_abi.encoding import BaseEncoder
    from eth_abi.exceptions import EncodingError, DecodingError
    from eth_abi.registry import registry
    from eth_abi import encode, decode
    
    NULL_ENCODING = b'\x00' * 32
    
    class EncodeNull(BaseEncoder):
        word_width = None
    
        @classmethod
        def from_type_str(cls, type_str, registry):
            # Extract width from 'null2' -> 2
            word_width = int(type_str[4:])
            return cls(word_width=word_width)
    
        def encode(self, value):
            self.validate_value(value)
            return NULL_ENCODING * self.word_width
    
        def validate_value(self, value):
            if value is not None:
                raise EncodingError('Unsupported value')
    
    class DecodeNull(BaseDecoder):
        word_width = None
    
        @classmethod
        def from_type_str(cls, type_str, registry):
            word_width = int(type_str[4:])
            return cls(word_width=word_width)
    
        def decode(self, stream):
            byts = stream.read(32 * self.word_width)
            if byts != NULL_ENCODING * self.word_width:
                raise DecodingError('Not enough data or wrong data')
            return None
    
    # Register using a matcher function and a label for easy unregistering
    registry.register(
        lambda x: x.startswith('null'),
        EncodeNull,
        DecodeNull,
        label='null',
    )
    
    # Usage
    assert encode(['null2'], [None]) == NULL_ENCODING * 2
    (decoded_null_val,) = decode(['null2'], NULL_ENCODING * 2)
    assert decoded_null_val is None
    
    # Cleanup
    registry.unregister('null')
  8. Encode and decode nested dynamic arrays

    main

    The eth-abi library supports the Solidity ABIv2 encoding format for nested dynamic arrays. You can encode and decode complex data types where arrays are nested within each other or contain dynamic types. Supported types include:

    • Nested arrays: int[][]
    • Arrays of dynamic types: string[]
    • Fixed-size arrays of dynamic types: string[2]
  9. Customize ABI types using the Registry with simple callables

    main

    The eth-abi library uses a central registry to route encoding and decoding operations. You can add support for new, simple ABI types by registering two callables (one for encoding and one for decoding) to a specific type string. This works for exact matches and when the type appears within compound types (like tuples).

    Note: The registry will raise an exception if more than one registration matches a query. Ensure your custom types do not conflict with existing ones.

    from eth_abi.exceptions import EncodingError, DecodingError
    from eth_abi.registry import registry
    from eth_abi import encode, decode
    
    # 1. Define the logic
    NULL_ENCODING = b'\x00' * 32
    
    def encode_null(x):
        if x is not None:
            raise EncodingError('Unsupported value')
        return NULL_ENCODING
    
    def decode_null(stream):
        if stream.read(32) != NULL_ENCODING:
            raise DecodingError('Not enough data or wrong data')
        return None
    
    # 2. Register the type string
    registry.register('null', encode_null, decode_null)
    
    # 3. Use it
    assert encode(['null'], [None]) == NULL_ENCODING
    (decoded_null_val,) = decode(['null'], NULL_ENCODING)
    assert decoded_null_val is None
    
    # Cleanup
    registry.unregister('null')