lib/eth/contract.rb



# Copyright (c) 2016-2022 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 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.
    #
    # @param name [String] contract name.
    # @param bin [String] contract bin string.
    # @param abi [String] contract abi string.
    def initialize(name, bin, abi)
      @name = name
      @bin = bin
      @abi = abi
      @constructor_inputs, @functions, @events = parse_abi(abi)
    end

    # Creates a contract wrapper.
    #
    # @param file [String] solidity file path.
    # @param bin [String] contract bin string.
    # @param abi [String] contract abi string.
    # @param address [String] contract address.
    # @param name [String] name of contract.
    # @param contract_index [Number] specify contract.
    # @return [Eth::Contract::Object] Returns the class of the smart contract.
    # @raise [JSON::ParserError] if the json format is wrong.
    # @raise [ArgumentError] if argument is incorrect.
    def self.create(file: nil, bin: nil, abi: nil, address: nil, name: nil, contract_index: nil)
      if File.exist?(file.to_s)
        contracts = Eth::Contract::Initializer.new(file).build_all
        raise "No contracts compiled" if contracts.empty?
        if contract_index
          contract = contracts[contract_index].class_object.new
        else
          contract = contracts.first.class_object.new
        end
      elsif ![name, bin, abi].include? nil
        begin
          abi = abi.is_a?(Array) ? abi : JSON.parse(abi)
        rescue JSON::ParserError => e
          raise e
        end
        contract = Eth::Contract.new(name, bin, abi)
        contract.build
        contract = contract.class_object.new
      else
        raise ArgumentError, "The argument is incorrect."
      end
      contract.address = address
      contract
    end

    # Set the address of the smart contract
    def address=(addr)
      @address = addr.nil? ? nil : Eth::Address.new(addr).address
      @events.each do |event|
        event.set_address(@address)
      end
    end

    # Create 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
        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