lib/eth/contract.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 -*-

require "forwardable"

# Provides the {Eth} module.
module Eth

  # Provides classes to access smart contracts
  class Contract
    attr_reader :address
    attr_accessor :key
    attr_accessor :gas_limit, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :nonce
    attr_accessor :bin, :name, :abi, :class_object
    attr_accessor :events, :functions, :constructor_inputs

    # Constructor of the {Eth::Contract} class.
    #
    # **Note**, do not use this directly. Use
    # {from_abi}, {from_bin}, or {from_file}!
    #
    # @param name [String] contract name.
    # @param bin [String] contract bin string.
    # @param abi [String] contract abi string.
    def initialize(name, bin, abi)

      # The contract name will be the class name and needs title casing.
      _name = name.dup
      _name[0] = name[0].upcase

      @name = _name
      @bin = bin
      @abi = abi
      @constructor_inputs, @functions, @events = parse_abi(abi)
    end

    # Creates a contract wrapper from a Solidity file.
    #
    # @param file [String] solidity file path.
    # @param contract_index [Number] specify contract.
    # @return [Eth::Contract::Object] Returns the class of the smart contract.
    # @raise [ArgumentError] if the file path is empty or no contracts were compiled.
    def self.from_file(file:, contract_index: 0)
      raise ArgumentError, "Cannot find the contract at #{file.to_s}!" if !File.exist?(file.to_s)
      contracts = Eth::Contract::Initializer.new(file).build_all
      raise ArgumentError, "No contracts compiled." if contracts.empty?
      contracts[contract_index].class_object.new
    end

    # Creates a contract wrapper from ABI and address.
    #
    # @param abi [String] contract abi string.
    # @param address [String] contract address.
    # @param name [String] name of contract.
    # @return [Eth::Contract::Object] Returns the class of the smart contract.
    # @raise [JSON::ParserError] if the json format is wrong.
    # @raise [ArgumentError] if ABI, address, or name is missing.
    def self.from_abi(abi:, address:, name:)
      abi = abi.is_a?(Array) ? abi : JSON.parse(abi)
      contract = Eth::Contract.new(name, nil, abi)
      contract.build
      contract = contract.class_object.new
      contract.address = address
      contract
    end

    # Creates a contract wrapper from binary and ABI.
    #
    # @param bin [String] contract bin string.
    # @param abi [String] contract abi string.
    # @param name [String] name of contract.
    # @return [Eth::Contract::Object] Returns the class of the smart contract.
    # @raise [JSON::ParserError] if the json format is wrong.
    # @raise [ArgumentError] if ABI, binary, or name is missing.
    def self.from_bin(bin:, abi:, name:)
      abi = abi.is_a?(Array) ? abi : JSON.parse(abi)
      contract = Eth::Contract.new(name, bin, abi)
      contract.build
      contract.class_object.new
    end

    # Sets the address of the smart contract.
    #
    # @param addr [String|Eth::Address] contract address string.
    def address=(addr)
      if addr.is_a? Eth::Address
        @address = addr.to_s
      else
        @address = Eth::Address.new(addr).to_s
      end
      @events.each do |event|
        event.set_address(@address)
      end
    end

    # Create meta classes for smart contracts.
    def build
      class_name = @name
      parent = self
      class_methods = Class.new do
        extend Forwardable
        def_delegators :parent, :key, :key=
        def_delegators :parent, :name, :abi, :bin
        def_delegators :parent, :gas_limit, :gas_price, :gas_limit=, :gas_price=, :nonce, :nonce=
        def_delegators :parent, :max_fee_per_gas, :max_fee_per_gas=, :max_priority_fee_per_gas, :max_priority_fee_per_gas=
        def_delegators :parent, :events
        def_delegators :parent, :address, :address=
        def_delegator :parent, :functions
        def_delegator :parent, :constructor_inputs
        define_method :parent do
          parent
        end
      end
      Eth::Contract.send(:remove_const, class_name) if Eth::Contract.const_defined?(class_name, false)
      Eth::Contract.const_set(class_name, class_methods)
      @class_object = class_methods
    end

    private

    def parse_abi(abi)
      constructor = abi.detect { |x| x["type"] == "constructor" }
      if !constructor.nil?
        constructor_inputs = constructor["inputs"].map { |input| Eth::Contract::FunctionInput.new(input) }
      else
        constructor_inputs = []
      end
      functions = abi.select { |x| x["type"] == "function" }.map { |fun| Eth::Contract::Function.new(fun) }
      events = abi.select { |x| x["type"] == "event" }.map { |evt| Eth::Contract::Event.new(evt) }
      [constructor_inputs, functions, events]
    end
  end
end

# Load the contract/* libraries
require "eth/contract/event"
require "eth/contract/function"
require "eth/contract/function_input"
require "eth/contract/function_output"
require "eth/contract/initializer"