lib/eth/abi/event.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# 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 -*-

# 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 log events.
    module Event
      extend self

      # Compute topic for ABI event interface.
      #
      # @param interface [Hash] ABI event interface.
      # @return [String] a hex-string topic.
      def compute_topic(interface)
        sig = signature(interface)
        Util.prefix_hex(Util.bin_to_hex(Util.keccak256(sig)))
      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| type(i) }
        "#{name}(#{types.join(",")})"
      end

      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 event log.
      class LogDescription
        # The event ABI interface used to decode the log.
        attr_accessor :event_interface

        # The the input argument of the event.
        attr_accessor :args

        # The named input argument of the event.
        attr_accessor :kwargs

        # The topic hash.
        attr_accessor :topic

        # Decodes event log argument values.
        #
        # @param event_interface [Hash] event ABI type.
        # @param log [Hash] transaction receipt log
        def initialize(event_interface, log)
          @event_interface = event_interface

          inputs = event_interface.fetch("inputs")
          data = log.fetch("data")
          topics = log.fetch("topics", [])
          anonymous = event_interface.fetch("anonymous", false)

          @topic = topics[0] if !anonymous
          @args, @kwargs = Event.decode_log(inputs, data, topics, anonymous)
        end

        # The event name. (e.g. Transfer)
        def name
          @name ||= event_interface.fetch("name")
        end

        # The event signature. (e.g. Transfer(address,address,uint256))
        def signature
          @signature ||= Abi::Event.signature(event_interface)
        end
      end

      # Decodes a stream of receipt logs with a set of ABI interfaces.
      #
      # @param interfaces [Array] event ABI types.
      # @param logs [Array] transaction receipt logs
      # @return [Hash] an enumerator of LogDescription objects.
      def decode_logs(interfaces, logs)
        Enumerator.new do |y|
          topic_to_interfaces = Hash[interfaces.map { |i| [compute_topic(i), i] }]

          logs.each do |log|
            topic = log.fetch("topics", [])[0]
            if topic && interface = topic_to_interfaces[topic]
              y << [log, LogDescription.new(interface, log)]
            else
              y << [log, nil]
            end
          end
        end
      end

      # Decodes event log argument values.
      #
      # @param inputs [Array] event ABI types.
      # @param data [String] ABI event data to be decoded.
      # @param topics [Array] ABI event topics to be decoded.
      # @param anonymous [Boolean] If event signature is excluded from topics.
      # @return [[Array, Hash]] decoded positional arguments and decoded keyword arguments.
      # @raise [DecodingError] if decoding fails for type.
      def decode_log(inputs, data, topics, anonymous = false)
        topic_inputs, data_inputs = inputs.partition { |i| i["indexed"] }

        topic_types = topic_inputs.map do |i|
          if i["type"] == "tuple"
            Type.parse(i["type"], i["components"], i["name"])
          else
            i["type"]
          end
        end
        data_types = data_inputs.map do |i|
          if i["type"] == "tuple"
            Type.parse(i["type"], i["components"], i["name"])
          else
            i["type"]
          end
        end
        # If event is anonymous, all topics are arguments. Otherwise, the first
        # topic will be the event signature.
        if anonymous == false
          topics = topics[1..-1]
        end

        decoded_topics = topics.map.with_index { |t, i| Abi.decode([topic_types[i]], t)[0] }
        decoded_data = Abi.decode(data_types, data)

        args = []
        kwargs = {}

        inputs.each_with_index do |input, index|
          if input["indexed"]
            value = decoded_topics[topic_inputs.index(input)]
          else
            value = decoded_data[data_inputs.index(input)]
          end
          args[index] = value
          kwargs[input["name"].to_sym] = value
        end

        return args, kwargs
      end
    end
  end
end