lib/eth/abi/packed/encoder.rb



# Copyright (c) 2016-2025 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# -*- encoding : ascii-8bit -*-

# Provides the {Eth} module.
module Eth

  # Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI).
  module Abi

    # Encapsulates the module for non-standard packed encoding used in Solidity.
    module Packed

      # Provides a utility module to assist encoding ABIs.
      module Encoder
        extend self

        # Encodes a specific value, either static or dynamic in non-standard
        # packed encoding mode.
        #
        # @param type [Eth::Abi::Type] type to be encoded.
        # @param arg [String|Number] value to be encoded.
        # @return [String] the packed encoded type.
        # @raise [EncodingError] if value does not match type.
        # @raise [ArgumentError] if encoding fails for type.
        def type(type, arg)
          case type
          when /^uint(\d+)$/
            uint(arg, $1.to_i / 8)
          when /^int(\d+)$/
            int(arg, $1.to_i / 8)
          when "bool"
            bool(arg)
          when /^ureal(\d+)x(\d+)$/, /^ufixed(\d+)x(\d+)$/
            ufixed(arg, $1.to_i / 8, $2.to_i)
          when /^real(\d+)x(\d+)$/, /^fixed(\d+)x(\d+)$/
            fixed(arg, $1.to_i / 8, $2.to_i)
          when "string"
            string(arg)
          when /^bytes(\d+)$/
            bytes(arg, $1.to_i)
          when "bytes"
            string(arg)
          when /^tuple\((.+)\)$/
            tuple($1.split(","), arg)
          when /^hash(\d+)$/
            hash(arg, $1.to_i / 8)
          when "address"
            address(arg)
          when /^(.+)\[\]$/
            array($1, arg)
          when /^(.+)\[(\d+)\]$/
            fixed_array($1, arg, $2.to_i)
          else
            raise EncodingError, "Unhandled type: #{type}"
          end
        end

        private

        # Properly encodes signed integers.
        def uint(value, byte_size)
          raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
          raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::UINT_MAX or value < Constant::UINT_MIN
          i = value.to_i
          Util.zpad_int i, byte_size
        end

        # Properly encodes signed integers.
        def int(value, byte_size)
          raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
          raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::INT_MAX or value < Constant::INT_MIN
          real_size = byte_size * 8
          i = value.to_i % 2 ** real_size
          Util.zpad_int i, byte_size
        end

        # Properly encodes booleans.
        def bool(value)
          raise EncodingError, "Argument is not bool: #{value}" unless value.instance_of? TrueClass or value.instance_of? FalseClass
          (value ? "\x01" : "\x00").b
        end

        # Properly encodes unsigned fixed-point numbers.
        def ufixed(value, byte_size, decimals)
          raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
          raise ValueOutOfBounds, value unless value >= 0 and value < 2 ** decimals
          scaled_value = (value * (10 ** decimals)).to_i
          uint(scaled_value, byte_size)
        end

        # Properly encodes signed fixed-point numbers.
        def fixed(value, byte_size, decimals)
          raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
          raise ValueOutOfBounds, value unless value >= -2 ** (decimals - 1) and value < 2 ** (decimals - 1)
          scaled_value = (value * (10 ** decimals)).to_i
          int(scaled_value, byte_size)
        end

        # Properly encodes byte(-string)s.
        def bytes(value, length)
          raise EncodingError, "Expecting String: #{value}" unless value.instance_of? String
          value = handle_hex_string value, length
          raise ArgumentError, "Value must be a string of length #{length}" unless value.is_a?(String) && value.bytesize == length
          value.b
        end

        # Properly encodes (byte-)strings.
        def string(value)
          raise ArgumentError, "Value must be a string" unless value.is_a?(String)
          value.b
        end

        # Properly encodes tuples.
        def tuple(types, values)
          Abi.solidity_packed(types, values)
        end

        # Properly encodes hash-strings.
        def hash(value, byte_size)
          raise EncodingError, "Argument too long: #{value}" unless byte_size > 0 and byte_size <= 32
          hash_bytes = handle_hex_string value, byte_size
          hash_bytes.b
        end

        # Properly encodes addresses.
        def address(value)
          if value.is_a? Address

            # from checksummed address with 0x prefix
            Util.zpad_hex value.to_s[2..-1], 20
          elsif value.is_a? Integer

            # address from integer
            Util.zpad_int value, 20
          elsif value.size == 20

            # address from encoded address
            Util.zpad value, 20
          elsif value.size == 40

            # address from hexadecimal address
            Util.zpad_hex value, 20
          elsif value.size == 42 and value[0, 2] == "0x"

            # address from hexadecimal address with 0x prefix
            Util.zpad_hex value[2..-1], 20
          else
            raise EncodingError, "Could not parse address: #{value}"
          end
        end

        # Properly encodes dynamic-sized arrays.
        def array(type, values)
          values.map { |value| type(type, value) }.join.b
        end

        # Properly encodes fixed-size arrays.
        def fixed_array(type, values, size)
          raise ArgumentError, "Array size does not match" unless values.size == size
          array(type, values)
        end

        # The ABI encoder needs to be able to determine between a hex `"123"`
        # and a binary `"123"` string.
        def handle_hex_string(val, len)
          if Util.prefixed? val or
             (len === val.size / 2 and Util.hex? val)

            # There is no way telling whether a string is hex or binary with certainty
            # in Ruby. Therefore, we assume a `0x` prefix to indicate a hex string.
            # Additionally, if the string size is exactly the double of the expected
            # binary size, we can assume a hex value.
            Util.hex_to_bin val
          else

            # Everything else will be assumed binary or raw string.
            val.b
          end
        end
      end
    end
  end
end