class Eth::Client

network’s RPC-API endpoints (IPC or HTTP).
Provides the {Eth::Client} super-class to connect to Ethereum

def self.create(host)

Raises:
  • (ArgumentError) - in case it cannot determine the client type.

Returns:
  • (Eth::Client::Http) - an HTTP client.
  • (Eth::Client::Ipc) - an IPC client.

Parameters:
  • host (String) -- either an HTTP/S host or an IPC path.
def self.create(host)
  return Client::Ipc.new host if host.end_with? ".ipc"
  return Client::Http.new host if host.start_with? "http"
  raise ArgumentError, "Unable to detect client type!"
end

def call(contract, function, *args, **kwargs)

Returns:
  • (Object) - returns the result of the call.

Parameters:
  • **legacy (Boolean) -- enables legacy transactions (pre-EIP-1559).
  • **sender_key (Eth::Key) -- the sender private key.
  • *args () -- optional function arguments.
  • function (String) -- method name to be called.
  • contract (Eth::Contract) -- the subject contract to call.
  • *args () -- optional function arguments.
  • function (String) -- method name to be called.
  • contract (Eth::Contract) -- the subject contract to call.
  • function (String) -- method name to be called.
  • contract (Eth::Contract) -- the subject contract to call.

Overloads:
  • call(contract, function, *args, **kwargs)
  • call(contract, function, *args)
  • call(contract, function)
def call(contract, function, *args, **kwargs)
  func = contract.functions.select { |func| func.name == function }
  raise ArgumentError, "this function does not exist!" if func.nil? || func.size === 0
  selected_func = func.first
  func.each do |f|
    if f.inputs.size === args.size
      selected_func = f
    end
  end
  output = call_raw(contract, selected_func, *args, **kwargs)
  if output&.length == 1
    output[0]
  else
    output
  end
end

def call_payload(fun, args)

Encodes function call payloads.
def call_payload(fun, args)
  types = fun.inputs.map(&:parsed_type)
  encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
  Util.prefix_hex(fun.signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
end

def call_raw(contract, func, *args, **kwargs)

Other tags:
    See: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call -
def call_raw(contract, func, *args, **kwargs)
  params = {
    data: call_payload(func, args),
    to: kwargs[:address] || contract.address,
    from: kwargs[:from],
  }.compact
  raw_result = eth_call(params)["result"]
  types = func.outputs.map { |i| i.type }
  return nil if raw_result == "0x"
  Eth::Abi.decode(types, raw_result)
end

def camelize!(params)

expects Hash object
def camelize!(params)
  params.transform_keys! do |k|
    k = k.to_s.split(/_/).map(&:capitalize).join
    k[0] = k[0].downcase
    k.to_sym
  end
end

def chain_id

Returns:
  • (Integer) - the chain ID.
def chain_id
  @chain_id ||= eth_chain_id["result"].to_i 16
end

def default_account

Returns:
  • (Eth::Address) - the default account address.
def default_account
  raise ArgumentError, "The default account is not available on remote connections!" unless local? || @default_account
  @default_account ||= Address.new eth_accounts["result"].first
end

def deploy(contract, *args, **kwargs)

Raises:
  • (ArgumentError) - in case the contract does not have any source.

Returns:
  • (String) - the transaction hash.

Parameters:
  • **nonce (Integer) -- optional specific nonce for transaction.
  • **gas_limit (Integer) -- optional gas limit override for deploying the contract.
  • **legacy (Boolean) -- enables legacy transactions (pre-EIP-1559).
  • **sender_key (Eth::Key) -- the sender private key.
  • *args (optional) -- variable constructor parameter list.
  • contract (Eth::Contract) -- the contracts to deploy.
  • *args (optional) -- variable constructor parameter list.
  • contract (Eth::Contract) -- the contracts to deploy.
  • contract (Eth::Contract) -- contracts to deploy.

Overloads:
  • deploy(contract, *args, **kwargs)
  • deploy(contract, *args)
  • deploy(contract)
def deploy(contract, *args, **kwargs)
  raise ArgumentError, "Cannot deploy contract without source or binary!" if contract.bin.nil?
  raise ArgumentError, "Missing contract constructor params!" if contract.constructor_inputs.length != args.length
  data = contract.bin
  unless args.empty?
    data += encode_constructor_params(contract, args)
  end
  gas_limit = if kwargs[:gas_limit]
      kwargs[:gas_limit]
    else
      Tx.estimate_intrinsic_gas(data) + Tx::CREATE_GAS
    end
  params = {
    value: 0,
    gas_limit: gas_limit,
    chain_id: chain_id,
    data: data,
  }
  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
end

def deploy_and_wait(contract, *args, **kwargs)

Returns:
  • (String) - the contract address once it's mined.
def deploy_and_wait(contract, *args, **kwargs)
  hash = wait_for_tx(deploy(contract, *args, **kwargs))
  addr = eth_get_transaction_receipt(hash)["result"]["contractAddress"]
  contract.address = Address.new(addr).to_s
end

def encode_constructor_params(contract, args)

Encodes constructor params
def encode_constructor_params(contract, args)
  types = contract.constructor_inputs.map { |input| input.type }
  Util.bin_to_hex(Eth::Abi.encode(types, args))
end

def get_balance(address)

Returns:
  • (Integer) - the balance in Wei.

Parameters:
  • address (Eth::Address) -- the address to get the balance for.
def get_balance(address)
  eth_get_balance(address)["result"].to_i 16
end

def get_nonce(address)

Returns:
  • (Integer) - the next nonce to be used.

Parameters:
  • address (Eth::Address) -- the address to get the nonce for.
def get_nonce(address)
  eth_get_transaction_count(address, "pending")["result"].to_i 16
end

def initialize(_)

use {Client.create} intead.
Constructor for the {Eth::Client} super-class. Should not be used;
def initialize(_)
  @id = 0
  @max_priority_fee_per_gas = Tx::DEFAULT_PRIORITY_FEE
  @max_fee_per_gas = Tx::DEFAULT_GAS_PRICE
end

def is_valid_signature(contract, hash, signature, magic = "1626ba7e")

Raises:
  • (ArgumentError) - in case the contract cannot be called yet.

Returns:
  • (Boolean) - true if magic matches and signature is valid.

Parameters:
  • magic (String) -- the expected magic value (defaults to `1626ba7e`).
  • signature (String) -- the signature to be recovered by the contract.
  • hash (String) -- the message hash to be checked against the signature.
  • contract (Eth::Contract) -- a deployed contract implementing EIP-1271.
def is_valid_signature(contract, hash, signature, magic = "1626ba7e")
  raise ArgumentError, "Contract not deployed yet." if contract.address.nil?
  hash = Util.hex_to_bin hash if Util.hex? hash
  signature = Util.hex_to_bin signature if Util.hex? signature
  magic = Util.hex_to_bin magic if Util.hex? magic
  result = call(contract, "isValidSignature", hash, signature)
  result === magic
end

def local?

Allows to determine if we work with a local connectoin
def local?
  if self.instance_of? Eth::Client::Ipc
    true
  elsif self.host === "127.0.0.1" || self.host === "localhost"
    true
  else
    false
  end
end

def marshal(params)

Recursively marshals all request parameters.
def marshal(params)
  params = params.dup
  if params.is_a? Array
    params.map! { |param| marshal(param) }
  elsif params.is_a? Hash
    params = camelize!(params)
    params.transform_values! { |param| marshal(param) }
  elsif params.is_a? Numeric
    Util.prefix_hex "#{params.to_i.to_s(16)}"
  elsif params.is_a? Address
    params.to_s
  elsif Util.hex? params
    Util.prefix_hex params
  else
    params
  end
end

def next_id

Increments the request id.
def next_id
  @id += 1
end

def reset_id

Returns:
  • (Integer) - 0
def reset_id
  @id = 0
end

def resolve_ens(ens_name, registry = Ens::DEFAULT_ADDRESS, coin_type = Ens::CoinType::ETHEREUM)

Returns:
  • (Eth::Address) - the Ethereum address resolved from the ENS record.

Parameters:
  • coin_type (Integer) -- the coin type as per EIP-2304.
  • registry (String) -- the address for the ENS registry.
  • ens_name (String) -- The ENS name, e.g., `fancy.eth`.
def resolve_ens(ens_name, registry = Ens::DEFAULT_ADDRESS, coin_type = Ens::CoinType::ETHEREUM)
  ens = Ens::Resolver.new(self, registry)
  ens.resolve(ens_name, coin_type)
end

def send_command(command, args)

Prepares parameters and sends the command to the client.
def send_command(command, args)
  @block_number ||= "latest"
  args << block_number if ["eth_getBalance", "eth_call"].include? command
  payload = {
    jsonrpc: "2.0",
    method: command,
    params: marshal(args),
    id: next_id,
  }
  output = JSON.parse(send_request(payload.to_json))
  raise IOError, output["error"]["message"] unless output["error"].nil?
  output
end

def send_transaction(params, legacy, key, nonce)

Prepares a transaction to be send for the given params.
def send_transaction(params, legacy, key, nonce)
  if legacy
    params.merge!({ gas_price: max_fee_per_gas })
  else
    params.merge!({
      priority_fee: max_priority_fee_per_gas,
      max_gas_fee: max_fee_per_gas,
    })
  end
  unless key.nil?
    # use the provided key as sender and signer
    params.merge!({
      from: key.address,
      nonce: nonce || get_nonce(key.address),
    })
    tx = Eth::Tx.new(params)
    tx.sign key
    eth_send_raw_transaction(tx.hex)["result"]
  else
    # do not allow accessing accounts on remote connections
    raise ArgumentError, "The default account is not available on remote connections, please provide a :sender_key!" unless local?
    # use the default account as sender and external signer
    params.merge!({
      from: default_account,
      nonce: nonce || get_nonce(default_account),
    })
    eth_send_transaction(params)["result"]
  end
end

def transact(contract, function, *args, **kwargs)

Returns:
  • (Object) - returns the result of the transaction.

Parameters:
  • **tx_value (Integer) -- optional transaction value field filling.
  • **nonce (Integer) -- optional specific nonce for transaction.
  • **gas_limit (Integer) -- optional gas limit override for transacting with the contract.
  • **address (Eth::Address) -- contract address.
  • **legacy (Boolean) -- enables legacy transactions (pre-EIP-1559).
  • **sender_key (Eth::Key) -- the sender private key.
  • *args () -- optional function arguments.
  • function_name (String) -- method name to be executed.
  • contract (Eth::Contract) -- the subject contract to write to.
  • *args () -- optional function arguments.
  • function (String) -- method name to be executed.
  • contract (Eth::Contract) -- the subject contract to write to.
  • function (String) -- method name to be executed.
  • contract (Eth::Contract) -- the subject contract to write to.

Overloads:
  • transact(contract, function, *args, **kwargs)
  • transact(contract, function, *args)
  • transact(contract, function)
def transact(contract, function, *args, **kwargs)
  gas_limit = if kwargs[:gas_limit]
      kwargs[:gas_limit]
    else
      Tx.estimate_intrinsic_gas(contract.bin)
    end
  fun = contract.functions.select { |func| func.name == function }[0]
  params = {
    value: kwargs[:tx_value] || 0,
    gas_limit: gas_limit,
    chain_id: chain_id,
    to: kwargs[:address] || contract.address,
    data: call_payload(fun, args),
  }
  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
end

def transact_and_wait(contract, function, *args, **kwargs)

Returns:
  • (Object, Boolean) - returns the result of the transaction (hash and execution status).

Raises:
  • (Client::ContractExecutionError) - if the execution fails.
def transact_and_wait(contract, function, *args, **kwargs)
  begin
    hash = wait_for_tx(transact(contract, function, *args, **kwargs))
    return hash, tx_succeeded?(hash)
  rescue IOError => e
    raise ContractExecutionError, e
  end
end

def transfer(destination, amount, **kwargs)

Returns:
  • (String) - the local transaction hash.

Parameters:
  • **nonce (Integer) -- optional specific nonce for transaction.
  • **legacy (Boolean) -- enables legacy transactions (pre-EIP-1559).
  • **sender_key (Eth::Key) -- the sender private key.
  • amount (Integer) -- the transfer amount in Wei.
  • destination (Eth::Address) -- the destination address.
  • amount (Integer) -- the transfer amount in Wei.
  • destination (Eth::Address) -- the destination address.

Overloads:
  • transfer(destination, amount, **kwargs)
  • transfer(destination, amount)
def transfer(destination, amount, **kwargs)
  params = {
    value: amount,
    to: destination,
    gas_limit: Tx::DEFAULT_GAS_LIMIT,
    chain_id: chain_id,
  }
  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
end

def transfer_and_wait(destination, amount, **kwargs)

Returns:
  • (String) - the transaction hash once it is mined.
def transfer_and_wait(destination, amount, **kwargs)
  wait_for_tx(transfer(destination, amount, **kwargs))
end

def transfer_erc20(erc20_contract, destination, amount, **kwargs)

Returns:
  • (Object) - returns the result of the transaction.

Parameters:
  • **tx_value (Integer) -- optional transaction value field filling.
  • **nonce (Integer) -- optional specific nonce for transaction.
  • **gas_limit (Integer) -- optional gas limit override for the transfer.
  • **legacy (Boolean) -- enables legacy transactions (pre-EIP-1559).
  • **sender_key (Eth::Key) -- the sender private key.
  • amount (Integer) -- the transfer amount (mind the `decimals()`).
  • destination (Eth::Address) -- the destination address.
  • erc20_contract (Eth::Contract) -- the ERC20 contract to write to.
  • amount (Integer) -- the transfer amount (mind the `decimals()`).
  • destination (Eth::Address) -- the destination address.
  • erc20_contract (Eth::Contract) -- the ERC20 contract to write to.

Overloads:
  • transfer_erc20(erc20_contract, destination, amount, **kwargs)
  • transfer_erc20(erc20_contract, destination, amount)
def transfer_erc20(erc20_contract, destination, amount, **kwargs)
  destination = destination.to_s if destination.instance_of? Eth::Address
  transact(erc20_contract, "transfer", destination, amount, **kwargs)
end

def transfer_erc20_and_wait(erc20_contract, destination, amount, **kwargs)

Returns:
  • (Object) - returns the result of the transaction.
def transfer_erc20_and_wait(erc20_contract, destination, amount, **kwargs)
  transact_and_wait(erc20_contract, "transfer", destination, amount, **kwargs)
end

def tx_mined?(hash)

Returns:
  • (Boolean) - true if included in a block.

Parameters:
  • hash (String) -- the transaction hash.
def tx_mined?(hash)
  mined_tx = eth_get_transaction_by_hash hash
  !mined_tx.nil? && !mined_tx["result"].nil? && !mined_tx["result"]["blockNumber"].nil?
end

def tx_succeeded?(hash)

Returns:
  • (Boolean) - true if status is success.

Parameters:
  • hash (String) -- the transaction hash.
def tx_succeeded?(hash)
  tx_receipt = eth_get_transaction_receipt(hash)
  !tx_receipt.nil? && !tx_receipt["result"].nil? && tx_receipt["result"]["status"] == "0x1"
end

def wait_for_tx(hash)

Raises:
  • (Timeout::Error) - if it's not mined within 5 minutes.

Returns:
  • (String) - the transaction hash once the transaction is mined.

Parameters:
  • hash (String) -- the transaction hash.
def wait_for_tx(hash)
  start_time = Time.now
  timeout = 300
  retry_rate = 0.1
  loop do
    raise Timeout::Error if ((Time.now - start_time) > timeout)
    return hash if tx_mined? hash
    sleep retry_rate
  end
end