Class: Eth::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/eth/client.rb

Overview

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

Direct Known Subclasses

Http, Ipc

Defined Under Namespace

Classes: ContractExecutionError, Http, Ipc, RpcError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(_) ⇒ Client

Constructor for the Eth::Client super-class. Should not be used; use create intead.



77
78
79
80
81
# File 'lib/eth/client.rb', line 77

def initialize(_)
  @id = 0
  @max_priority_fee_per_gas = Tx::DEFAULT_PRIORITY_FEE
  @max_fee_per_gas = Tx::DEFAULT_GAS_PRICE
end

Instance Attribute Details

#block_numberObject

The block number used for archive calls.



38
39
40
# File 'lib/eth/client.rb', line 38

def block_number
  @block_number
end

#chain_idInteger (readonly)

Gets the chain ID of the connected network.

Returns:

  • (Integer)

    the chain ID.



26
27
28
# File 'lib/eth/client.rb', line 26

def chain_id
  @chain_id
end

#default_accountEth::Address

Gets the default account (first account) of the connected client.

Note, that many remote providers (e.g., Infura) do not provide any accounts.

Returns:

Raises:

  • (ArgumentError)


29
30
31
# File 'lib/eth/client.rb', line 29

def 
  @default_account
end

#idObject (readonly)

The client’s RPC-request ID starting at 0.



23
24
25
# File 'lib/eth/client.rb', line 23

def id
  @id
end

#max_fee_per_gasObject

The default transaction max fee per gas in Wei, defaults to Tx::DEFAULT_GAS_PRICE.



35
36
37
# File 'lib/eth/client.rb', line 35

def max_fee_per_gas
  @max_fee_per_gas
end

#max_priority_fee_per_gasObject

The default transaction max priority fee per gas in Wei, defaults to Tx::DEFAULT_PRIORITY_FEE.



32
33
34
# File 'lib/eth/client.rb', line 32

def max_priority_fee_per_gas
  @max_priority_fee_per_gas
end

Class Method Details

.create(host) ⇒ Eth::Client::Ipc, Eth::Client::Http

Creates a new RPC-Client, either by providing an HTTP/S host or an IPC path. Supports basic authentication with username and password.

Note, this sets the folling gas defaults: Tx::DEFAULT_PRIORITY_FEE and Use {#max_priority_fee_per_gas and #max_fee_per_gas to set custom values prior to submitting transactions.

Parameters:

  • host (String)

    either an HTTP/S host or an IPC path.

Returns:

Raises:

  • (ArgumentError)

    in case it cannot determine the client type.



69
70
71
72
73
# File 'lib/eth/client.rb', line 69

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

Instance Method Details

#call(contract, function) ⇒ Object #call(contract, function, *args) ⇒ Object #call(contract, function, *args, **kwargs) ⇒ Object

Calls a contract function without executing it (non-transactional contract read).

Overloads:

  • #call(contract, function) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to call.

    • function (String)

      method name to be called.

  • #call(contract, function, *args) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to call.

    • function (String)

      method name to be called.

    • *args

      optional function arguments.

  • #call(contract, function, *args, **kwargs) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to call.

    • function (String)

      method name to be called.

    • *args

      optional function arguments.

    • **sender_key (Eth::Key)

      the sender private key.

    • **legacy (Boolean)

      enables legacy transactions (pre-EIP-1559).

Returns:

  • (Object)

    returns the result of the call.

See Also:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/eth/client.rb', line 270

def call(contract, function, *args, **kwargs)
  function = contract.function(function, args: args.size)
  output = function.decode_call_result(
    eth_call(
      {
        data: function.encode_call(*args),
        to: kwargs[:address] || contract.address,
        from: kwargs[:from],
        gas: kwargs[:gas],
        gasPrice: kwargs[:gas_price],
        value: kwargs[:value],
      }.compact
    )["result"]
  )
  if output&.length == 1
    output[0]
  else
    output
  end
rescue RpcError => e
  raise ContractExecutionError, contract.decode_error(e)
end

#deploy(contract) ⇒ String #deploy(contract, *args) ⇒ String #deploy(contract, *args, **kwargs) ⇒ String

Deploys a contract. Uses eth_accounts or external signer if no sender key is provided.

Note, that many remote providers (e.g., Infura) do not provide any accounts. Provide a sender_key: if you experience issues.

Overloads:

  • #deploy(contract) ⇒ String

    Parameters:

  • #deploy(contract, *args) ⇒ String

    Parameters:

    • contract (Eth::Contract)

      the contracts to deploy.

    • *args (optional)

      variable constructor parameter list.

  • #deploy(contract, *args, **kwargs) ⇒ String

    Parameters:

    • contract (Eth::Contract)

      the contracts to deploy.

    • *args (optional)

      variable constructor parameter list.

    • **sender_key (Eth::Key)

      the sender private key.

    • **legacy (Boolean)

      enables legacy transactions (pre-EIP-1559).

    • **gas_limit (Integer)

      optional gas limit override for deploying the contract.

    • **nonce (Integer)

      optional specific nonce for transaction.

Returns:

  • (String)

    the transaction hash.

Raises:

  • (ArgumentError)

    in case the contract does not have any source.



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/eth/client.rb', line 231

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

#deploy_and_wait(contract, *args, **kwargs) ⇒ String

Deploys a contract and waits for it to be mined. Uses eth_accounts or external signer if no sender key is provided.

See #deploy for params and overloads.

Returns:

  • (String)

    the contract address once it’s mined.



205
206
207
208
209
# File 'lib/eth/client.rb', line 205

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

#get_balance(address) ⇒ Integer

Gets the balance for an address.

Parameters:

  • address (Eth::Address)

    the address to get the balance for.

Returns:

  • (Integer)

    the balance in Wei.



105
106
107
# File 'lib/eth/client.rb', line 105

def get_balance(address)
  eth_get_balance(address)["result"].to_i 16
end

#get_nonce(address) ⇒ Integer

Gets the next nonce for an address used to draft new transactions.

Parameters:

  • address (Eth::Address)

    the address to get the nonce for.

Returns:

  • (Integer)

    the next nonce to be used.



113
114
115
# File 'lib/eth/client.rb', line 113

def get_nonce(address)
  eth_get_transaction_count(address, "pending")["result"].to_i 16
end

#is_valid_signature(contract, hash, signature, magic = "1626ba7e") ⇒ Boolean

Provides an interface to call isValidSignature as per EIP-1271 on a given smart contract to verify the given hash and signature matching the magic value.

Parameters:

  • contract (Eth::Contract)

    a deployed contract implementing EIP-1271.

  • hash (String)

    the message hash to be checked against the signature.

  • signature (String)

    the signature to be recovered by the contract.

  • magic (String) (defaults to: "1626ba7e")

    the expected magic value (defaults to 1626ba7e).

Returns:

  • (Boolean)

    true if magic matches and signature is valid.

Raises:

  • (ArgumentError)

    in case the contract cannot be called yet.



359
360
361
362
363
364
365
366
# File 'lib/eth/client.rb', line 359

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

#reset_idInteger

Gives control over resetting the RPC request ID back to zero. Usually not needed.

Returns:

  • (Integer)

    0



372
373
374
# File 'lib/eth/client.rb', line 372

def reset_id
  @id = 0
end

#resolve_ens(ens_name, registry = Ens::DEFAULT_ADDRESS, coin_type = Ens::CoinType::ETHEREUM) ⇒ Eth::Address

Resolves an ENS name to an Ethereum address on the connected chain.

Parameters:

  • ens_name (String)

    The ENS name, e.g., fancy.eth.

  • registry (String) (defaults to: Ens::DEFAULT_ADDRESS)

    the address for the ENS registry.

  • coin_type (Integer) (defaults to: Ens::CoinType::ETHEREUM)

    the coin type as per EIP-2304.

Returns:

  • (Eth::Address)

    the Ethereum address resolved from the ENS record.



123
124
125
126
# File 'lib/eth/client.rb', line 123

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

#transact(contract, function) ⇒ Object #transact(contract, function, *args) ⇒ Object #transact(contract, function, *args, **kwargs) ⇒ Object

Executes a contract function with a transaction (transactional contract read/write).

Note, that many remote providers (e.g., Infura) do not provide any accounts. Provide a sender_key: if you experience issues.

Overloads:

  • #transact(contract, function) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to write to.

    • function (String)

      method name to be executed.

  • #transact(contract, function, *args) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to write to.

    • function (String)

      method name to be executed.

    • *args

      optional function arguments.

  • #transact(contract, function, *args, **kwargs) ⇒ Object

    Parameters:

    • contract (Eth::Contract)

      the subject contract to write to.

    • function_name (String)

      method name to be executed.

    • *args

      optional function arguments.

    • **sender_key (Eth::Key)

      the sender private key.

    • **legacy (Boolean)

      enables legacy transactions (pre-EIP-1559).

    • **address (Eth::Address)

      contract address.

    • **gas_limit (Integer)

      optional gas limit override for transacting with the contract.

    • **nonce (Integer)

      optional specific nonce for transaction.

    • **tx_value (Integer)

      optional transaction value field filling.

Returns:

  • (Object)

    returns the result of the transaction.



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/eth/client.rb', line 317

def transact(contract, function, *args, **kwargs)
  gas_limit = if kwargs[:gas_limit]
      kwargs[:gas_limit]
    else
      Tx.estimate_intrinsic_gas(contract.bin)
    end
  params = {
    value: kwargs[:tx_value] || 0,
    gas_limit: gas_limit,
    chain_id: chain_id,
    to: kwargs[:address] || contract.address,
    data: contract.function(function, args: args.size).encode_call(*args),
  }
  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
end

#transact_and_wait(contract, function, *args, **kwargs) ⇒ Object, Boolean

Executes a contract function with a transaction and waits for it to be mined (transactional contract read/write).

See #transact for params and overloads.

Returns:

  • (Object, Boolean)

    returns the result of the transaction (hash and execution status).

Raises:



340
341
342
343
344
345
346
347
# File 'lib/eth/client.rb', line 340

def transact_and_wait(contract, function, *args, **kwargs)
  begin
    hash = wait_for_tx(transact(contract, function, *args, **kwargs))
    return hash, tx_succeeded?(hash)
  rescue RpcError => e
    raise ContractExecutionError, contract.decode_error(e)
  end
end

#transfer(destination, amount) ⇒ String #transfer(destination, amount, **kwargs) ⇒ String

Simply transfer Ether to an account without any call data or access lists attached. Uses eth_accounts and external signer if no sender key is provided.

Note, that many remote providers (e.g., Infura) do not provide any accounts. Provide a sender_key: if you experience issues.

Overloads:

  • #transfer(destination, amount) ⇒ String

    Parameters:

    • destination (Eth::Address)

      the destination address.

    • amount (Integer)

      the transfer amount in Wei.

  • #transfer(destination, amount, **kwargs) ⇒ String

    Parameters:

    • destination (Eth::Address)

      the destination address.

    • amount (Integer)

      the transfer amount in Wei.

    • **sender_key (Eth::Key)

      the sender private key.

    • **legacy (Boolean)

      enables legacy transactions (pre-EIP-1559).

    • **nonce (Integer)

      optional specific nonce for transaction.

Returns:

  • (String)

    the local transaction hash.



156
157
158
159
160
161
162
163
164
# File 'lib/eth/client.rb', line 156

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

#transfer_and_wait(destination, amount, **kwargs) ⇒ String

Simply transfer Ether to an account and waits for it to be mined. Uses eth_accounts and external signer if no sender key is provided.

See #transfer for params and overloads.

Returns:

  • (String)

    the transaction hash once it is mined.



135
136
137
# File 'lib/eth/client.rb', line 135

def transfer_and_wait(destination, amount, **kwargs)
  wait_for_tx(transfer(destination, amount, **kwargs))
end

#transfer_erc20(erc20_contract, destination, amount) ⇒ Object #transfer_erc20(erc20_contract, destination, amount, **kwargs) ⇒ Object

Transfers a token that implements the ERC20 transfer() interface.

Note, that many remote providers (e.g., Infura) do not provide any accounts. Provide a sender_key: if you experience issues.

Overloads:

  • #transfer_erc20(erc20_contract, destination, amount) ⇒ Object

    Parameters:

    • erc20_contract (Eth::Contract)

      the ERC20 contract to write to.

    • destination (Eth::Address)

      the destination address.

    • amount (Integer)

      the transfer amount (mind the decimals()).

  • #transfer_erc20(erc20_contract, destination, amount, **kwargs) ⇒ Object

    Parameters:

    • erc20_contract (Eth::Contract)

      the ERC20 contract to write to.

    • destination (Eth::Address)

      the destination address.

    • amount (Integer)

      the transfer amount (mind the decimals()).

    • **sender_key (Eth::Key)

      the sender private key.

    • **legacy (Boolean)

      enables legacy transactions (pre-EIP-1559).

    • **gas_limit (Integer)

      optional gas limit override for the transfer.

    • **nonce (Integer)

      optional specific nonce for transaction.

    • **tx_value (Integer)

      optional transaction value field filling.

Returns:

  • (Object)

    returns the result of the transaction.



194
195
196
197
# File 'lib/eth/client.rb', line 194

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

#transfer_erc20_and_wait(erc20_contract, destination, amount, **kwargs) ⇒ Object

Transfers a token that implements the ERC20 transfer() interface.

See #transfer_erc20 for params and overloads.

Returns:

  • (Object)

    returns the result of the transaction.



171
172
173
# File 'lib/eth/client.rb', line 171

def transfer_erc20_and_wait(erc20_contract, destination, amount, **kwargs)
  transact_and_wait(erc20_contract, "transfer", destination, amount, **kwargs)
end

#tx_mined?(hash) ⇒ Boolean

Checks whether a transaction is mined or not.

Parameters:

  • hash (String)

    the transaction hash.

Returns:

  • (Boolean)

    true if included in a block.



380
381
382
383
# File 'lib/eth/client.rb', line 380

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

#tx_succeeded?(hash) ⇒ Boolean

Checks whether a contract transaction succeeded or not.

Parameters:

  • hash (String)

    the transaction hash.

Returns:

  • (Boolean)

    true if status is success.



389
390
391
392
# File 'lib/eth/client.rb', line 389

def tx_succeeded?(hash)
  tx_receipt = eth_get_transaction_receipt(hash)
  !tx_receipt.nil? && !tx_receipt["result"].nil? && tx_receipt["result"]["status"] == "0x1"
end

#wait_for_tx(hash) ⇒ String

Waits for an transaction to be mined by the connected chain.

Parameters:

  • hash (String)

    the transaction hash.

Returns:

  • (String)

    the transaction hash once the transaction is mined.

Raises:

  • (Timeout::Error)

    if it’s not mined within 5 minutes.



399
400
401
402
403
404
405
406
407
408
# File 'lib/eth/client.rb', line 399

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