lib/eth/abi.rb



# Copyright (c) 2016-2023 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 -*-

require "konstructor"

require "eth/abi/event"
require "eth/abi/type"

# Provides the {Eth} module.
module Eth

  # Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI).
  # ref: https://docs.soliditylang.org/en/develop/abi-spec.html
  module Abi
    extend self

    # Provides a special encoding error if anything fails to encode.
    class EncodingError < StandardError; end

    # Provides a special decoding error if anything fails to decode.
    class DecodingError < StandardError; end

    # Provides a special out-of-bounds error for values.
    class ValueOutOfBounds < StandardError; end

    # Encodes Application Binary Interface (ABI) data. It accepts multiple
    # arguments and encodes using the head/tail mechanism.
    #
    # @param types [Array] types to be ABI-encoded.
    # @param args [Array] values to be ABI-encoded.
    # @return [String] the encoded ABI data.
    def encode(types, args)

      # parse all types
      parsed_types = types.map { |t| Type === t ? t : Type.parse(t) }

      # prepare the "head"
      head_size = (0...args.size)
        .map { |i| parsed_types[i].size or 32 }
        .reduce(0, &:+)
      head, tail = "", ""

      # encode types and arguments
      args.each_with_index do |arg, i|
        if parsed_types[i].dynamic?
          head += encode_type Type.size_type, head_size + tail.size
          tail += encode_type parsed_types[i], arg
        else
          head += encode_type parsed_types[i], arg
        end
      end

      # return the encoded ABI blob
      return "#{head}#{tail}"
    end

    # Encodes a specific value, either static or dynamic.
    #
    # @param type [Eth::Abi::Type] type to be encoded.
    # @param arg [String|Number] value to be encoded.
    # @return [String] the encoded type.
    # @raise [EncodingError] if value does not match type.
    def encode_type(type, arg)
      if %w(string bytes).include? type.base_type and type.sub_type.empty? and type.dimensions.empty?
        raise EncodingError, "Argument must be a String" unless arg.instance_of? String

        # encodes strings and bytes
        size = encode_type Type.size_type, arg.size
        padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
        return "#{size}#{arg}#{padding}"
      elsif type.base_type == "tuple" && type.dimensions.size == 1 && type.dimensions[0] != 0
        result = ""
        result += encode_struct_offsets(type.nested_sub, arg)
        result += arg.map { |x| encode_type(type.nested_sub, x) }.join
        result
      elsif type.dynamic? && arg.is_a?(Array)

        # encodes dynamic-sized arrays
        head, tail = "", ""
        head += encode_type(Type.size_type, arg.size)
        nested_sub = type.nested_sub
        nested_sub_size = type.nested_sub.size

        # calculate offsets
        if %w(string bytes).include?(type.base_type) && type.sub_type.empty?
          offset = 0
          arg.size.times do |i|
            if i == 0
              offset = arg.size * 32
            else
              number_of_words = ((arg[i - 1].size + 32 - 1) / 32).floor
              total_bytes_length = number_of_words * 32
              offset += total_bytes_length + 32
            end

            head += encode_type(Type.size_type, offset)
          end
        elsif nested_sub.base_type == "tuple" && nested_sub.dynamic?
          head += encode_struct_offsets(nested_sub, arg)
        end

        arg.size.times do |i|
          head += encode_type nested_sub, arg[i]
        end
        return "#{head}#{tail}"
      else
        if type.dimensions.empty?

          # encode a primitive type
          return encode_primitive_type type, arg
        else

          # encode static-size arrays
          return arg.map { |x| encode_type(type.nested_sub, x) }.join
        end
      end
    end

    # Encodes primitive types.
    #
    # @param type [Eth::Abi::Type] type to be encoded.
    # @param arg [String|Number] value to be encoded.
    # @return [String] the encoded primitive type.
    # @raise [EncodingError] if value does not match type.
    # @raise [ValueOutOfBounds] if value is out of bounds for type.
    # @raise [EncodingError] if encoding fails for type.
    def encode_primitive_type(type, arg)
      case type.base_type
      when "uint"
        return encode_uint arg, type
      when "bool"
        return encode_bool arg
      when "int"
        return encode_int arg, type
      when "ureal", "ufixed"
        return encode_ufixed arg, type
      when "real", "fixed"
        return encode_fixed arg, type
      when "string", "bytes"
        return encode_bytes arg, type
      when "tuple"
        return encode_tuple arg, type
      when "hash"
        return encode_hash arg, type
      when "address"
        return encode_address arg
      else
        raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
      end
    end

    # Decodes Application Binary Interface (ABI) data. It accepts multiple
    # arguments and decodes using the head/tail mechanism.
    #
    # @param types [Array] the ABI to be decoded.
    # @param data [String] ABI data to be decoded.
    # @return [Array] the decoded ABI data.
    def decode(types, data)

      # accept hex abi but decode it first
      data = Util.hex_to_bin data if Util.hex? data

      # parse all types
      parsed_types = types.map { |t| Type.parse(t) }

      # prepare output data
      outputs = [nil] * types.size
      start_positions = [nil] * types.size + [data.size]
      pos = 0
      parsed_types.each_with_index do |t, i|
        if t.dynamic?

          # record start position for dynamic type
          start_positions[i] = Util.deserialize_big_endian_to_int(data[pos, 32])
          j = i - 1
          while j >= 0 and start_positions[j].nil?
            start_positions[j] = start_positions[i]
            j -= 1
          end
          pos += 32
        else

          # get data directly for static types
          outputs[i] = data[pos, t.size]
          pos += t.size
        end
      end

      # add start position equal the length of the entire data
      j = types.size - 1
      while j >= 0 and start_positions[j].nil?
        start_positions[j] = start_positions[types.size]
        j -= 1
      end
      raise DecodingError, "Not enough data for head" unless pos <= data.size

      # add dynamic types
      parsed_types.each_with_index do |t, i|
        if t.dynamic?
          offset, next_offset = start_positions[i, 2]
          outputs[i] = data[offset...next_offset]
        end
      end

      # return the decoded ABI types and data
      return parsed_types.zip(outputs).map { |(type, out)| decode_type(type, out) }
    end

    # Decodes a specific value, either static or dynamic.
    #
    # @param type [Eth::Abi::Type] type to be decoded.
    # @param arg [String] encoded type data string.
    # @return [String] the decoded data for the type.
    # @raise [DecodingError] if decoding fails for type.
    def decode_type(type, arg)
      if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
        l = Util.deserialize_big_endian_to_int arg[0, 32]
        data = arg[32..-1]
        raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)

        # decoded strings and bytes
        return data[0, l]
      elsif type.dynamic?
        l = Util.deserialize_big_endian_to_int arg[0, 32]
        nested_sub = type.nested_sub

        # ref https://github.com/ethereum/tests/issues/691
        raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.dynamic?

        # decoded dynamic-sized arrays
        return (0...l).map { |i| decode_type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
      elsif !type.dimensions.empty?
        l = type.dimensions.last[0]
        nested_sub = type.nested_sub

        # decoded static-size arrays
        return (0...l).map { |i| decode_type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
      else

        # decoded primitive types
        return decode_primitive_type type, arg
      end
    end

    # Decodes primitive types.
    #
    # @param type [Eth::Abi::Type] type to be decoded.
    # @param data [String] encoded primitive type data string.
    # @return [String] the decoded data for the type.
    # @raise [DecodingError] if decoding fails for type.
    def decode_primitive_type(type, data)
      case type.base_type
      when "address"

        # decoded address with 0x-prefix
        return "0x#{Util.bin_to_hex data[12..-1]}"
      when "string", "bytes"
        if type.sub_type.empty?
          size = Util.deserialize_big_endian_to_int data[0, 32]

          # decoded dynamic-sized array
          return data[32..-1][0, size]
        else

          # decoded static-sized array
          return data[0, type.sub_type.to_i]
        end
      when "hash"

        # decoded hash
        return data[(32 - type.sub_type.to_i), type.sub_type.to_i]
      when "uint"

        # decoded unsigned integer
        return Util.deserialize_big_endian_to_int data
      when "int"
        u = Util.deserialize_big_endian_to_int data
        i = u >= 2 ** (type.sub_type.to_i - 1) ? (u - 2 ** type.sub_type.to_i) : u

        # decoded integer
        return i
      when "ureal", "ufixed"
        high, low = type.sub_type.split("x").map(&:to_i)

        # decoded unsigned fixed point numeric
        return Util.deserialize_big_endian_to_int(data) * 1.0 / 2 ** low
      when "real", "fixed"
        high, low = type.sub_type.split("x").map(&:to_i)
        u = Util.deserialize_big_endian_to_int data
        i = u >= 2 ** (high + low - 1) ? (u - 2 ** (high + low)) : u

        # decoded fixed point numeric
        return i * 1.0 / 2 ** low
      when "bool"

        # decoded boolean
        return data[-1] == Constant::BYTE_ONE
      else
        raise DecodingError, "Unknown primitive type: #{type.base_type}"
      end
    end

    # Build event signature string from ABI interface.
    #
    # @param interface [Hash] ABI event interface.
    # @return [String] interface signature string.
    def signature(interface)
      name = interface.fetch("name")
      inputs = interface.fetch("inputs", [])
      types = inputs.map { |i| i.fetch("type") }
      "#{name}(#{types.join(",")})"
    end

    private

    # Properly encodes unsigned integers.
    def encode_uint(arg, type)
      raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
      raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::UINT_MAX or arg < Constant::UINT_MIN
      real_size = type.sub_type.to_i
      i = arg.to_i
      raise ValueOutOfBounds, arg unless i >= 0 and i < 2 ** real_size
      return Util.zpad_int i
    end

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

    # Properly encodes booleans.
    def encode_bool(arg)
      raise EncodingError, "Argument is not bool: #{arg}" unless arg.instance_of? TrueClass or arg.instance_of? FalseClass
      return Util.zpad_int(arg ? 1 : 0)
    end

    # Properly encodes unsigned fixed-point numbers.
    def encode_ufixed(arg, type)
      raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
      high, low = type.sub_type.split("x").map(&:to_i)
      raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
      return Util.zpad_int((arg * 2 ** low).to_i)
    end

    # Properly encodes signed fixed-point numbers.
    def encode_fixed(arg, type)
      raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
      high, low = type.sub_type.split("x").map(&:to_i)
      raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
      i = (arg * 2 ** low).to_i
      return Util.zpad_int(i % 2 ** (high + low))
    end

    # Properly encodes byte-strings.
    def encode_bytes(arg, type)
      raise EncodingError, "Expecting String: #{arg}" unless arg.instance_of? String
      arg = handle_hex_string arg, type

      if type.sub_type.empty?
        size = Util.zpad_int arg.size
        padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)

        # variable length string/bytes
        return "#{size}#{arg}#{padding}"
      else
        raise ValueOutOfBounds, arg unless arg.size <= type.sub_type.to_i
        padding = Constant::BYTE_ZERO * (32 - arg.size)

        # fixed length string/bytes
        return "#{arg}#{padding}"
      end
    end

    # Properly encodes tuples.
    def encode_tuple(arg, type)
      raise EncodingError, "Expecting Hash: #{arg}" unless arg.instance_of? Hash
      raise EncodingError, "Expecting #{type.components.size} elements: #{arg}" unless arg.size == type.components.size

      static_size = 0
      type.components.each_with_index do |component, i|
        if type.components[i].dynamic?
          static_size += 32
        else
          static_size += Util.ceil32(type.components[i].size || 0)
        end
      end

      dynamic_offset = static_size
      offsets_and_static_values = []
      dynamic_values = []

      type.components.each_with_index do |component, i|
        component_type = type.components[i]
        if component_type.dynamic?
          offsets_and_static_values << encode_type(Type.size_type, dynamic_offset)
          dynamic_value = encode_type(component_type, arg.is_a?(Array) ? arg[i] : arg[component_type.name])
          dynamic_values << dynamic_value
          dynamic_offset += dynamic_value.size
        else
          offsets_and_static_values << encode_type(component_type, arg.is_a?(Array) ? arg[i] : arg[component_type.name])
        end
      end

      offsets_and_static_values.join + dynamic_values.join
    end

    # Properly encode struct offsets.
    def encode_struct_offsets(type, arg)
      result = ""
      offset = arg.size
      tails_encoding = arg.map { |a| encode_type(type, a) }
      arg.size.times do |i|
        if i == 0
          offset *= 32
        else
          offset += tails_encoding[i - 1].size
        end
        offset_string = encode_type(Type.size_type, offset)
        result += offset_string
      end
      result
    end

    # Properly encodes hash-strings.
    def encode_hash(arg, type)
      size = type.sub_type.to_i
      raise EncodingError, "Argument too long: #{arg}" unless size > 0 and size <= 32
      if arg.is_a? Integer

        # hash from integer
        return Util.zpad_int arg
      elsif arg.size == size

        # hash from encoded hash
        return Util.zpad arg, 32
      elsif arg.size == size * 2

        # hash from hexa-decimal hash
        return Util.zpad_hex arg
      else
        raise EncodingError, "Could not parse hash: #{arg}"
      end
    end

    # Properly encodes addresses.
    def encode_address(arg)
      if arg.is_a? Integer

        # address from integer
        return Util.zpad_int arg
      elsif arg.size == 20

        # address from encoded address
        return Util.zpad arg, 32
      elsif arg.size == 40

        # address from hexa-decimal address with 0x prefix
        return Util.zpad_hex arg
      elsif arg.size == 42 and arg[0, 2] == "0x"

        # address from hexa-decimal address
        return Util.zpad_hex arg[2..-1]
      else
        raise EncodingError, "Could not parse address: #{arg}"
      end
    end

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

        # 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.
        return Util.hex_to_bin arg
      else

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