lib/eth/abi/function.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

    # Provides a module to decode transaction input data.
    module Function
      extend self

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

      # Compute selector for ABI function interface.
      #
      # @param interface [Hash] ABI function interface.
      # @return [String] a hex-string selector.
      def selector(interface)
        sig = signature(interface)
        Util.prefix_hex(Util.bin_to_hex(Util.keccak256(sig))[0, 8])
      end

      # Gets the input type for functions.
      #
      # @param input [Hash] function input.
      # @return [String] input type.
      def type(input)
        if input["type"] == "tuple"
          "(#{input["components"].map { |c| type(c) }.join(",")})"
        elsif input["type"] == "enum"
          "uint8"
        else
          input["type"]
        end
      end

      # A decoded function call.
      class CallDescription
        # The function ABI interface used to decode the call.
        attr_accessor :function_interface

        # The positional arguments of the call.
        attr_accessor :args

        # The named arguments of the call.
        attr_accessor :kwargs

        # The function selector.
        attr_accessor :selector

        # Creates a description object for a decoded function call.
        #
        # @param function_interface [Hash] function ABI type.
        # @param selector [String] function selector hex-string.
        # @param args [Array] decoded positional arguments.
        # @param kwargs [Hash] decoded keyword arguments.
        def initialize(function_interface, selector, args, kwargs)
          @function_interface = function_interface
          @selector = selector
          @args = args
          @kwargs = kwargs
        end

        # The function name. (e.g. transfer)
        def name
          @name ||= function_interface.fetch("name")
        end

        # The function signature. (e.g. transfer(address,uint256))
        def signature
          @signature ||= Function.signature(function_interface)
        end
      end

      # Decodes a transaction input with a set of ABI interfaces.
      #
      # @param interfaces [Array] function ABI types.
      # @param data [String] transaction input data.
      # @return [CallDescription, nil] a CallDescription object or nil if selector unknown.
      def decode(interfaces, data)
        data = Util.remove_hex_prefix(data)
        selector = Util.prefix_hex(data[0, 8])
        payload = Util.prefix_hex(data[8..] || "")

        selector_to_interfaces = Hash[interfaces.map { |i| [selector(i), i] }]
        if (interface = selector_to_interfaces[selector])
          inputs = interface.fetch("inputs", [])
          types = inputs.map { |i| type(i) }
          args = Abi.decode(types, payload)
          kwargs = {}
          inputs.each_with_index do |input, i|
            name = input.fetch("name", "")
            kwargs[name.to_sym] = args[i] unless name.empty?
          end
          CallDescription.new(interface, selector, args, kwargs)
        end
      end
    end
  end
end