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, HttpAuth, Ipc

Defined Under Namespace

Classes: Http, HttpAuth, Ipc

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.



62
63
64
65
66
67
# File 'lib/eth/client.rb', line 62

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

Instance Attribute Details

#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 (coinbase) of the connected client.

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

Returns:



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

def 
  @default_account
end

#gas_limitObject

The default gas limit for the transaction, defaults to Tx::DEFAULT_GAS_LIMIT.



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

def gas_limit
  @gas_limit
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, ...

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, Tx::DEFAULT_GAS_PRICE, and Tx::DEFAULT_GAS_LIMIT. Use #max_priority_fee_per_gas, #max_fee_per_gas, and #gas_limit 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.



53
54
55
56
57
58
# File 'lib/eth/client.rb', line 53

def self.create(host)
  return Client::Ipc.new host if host.end_with? ".ipc"
  return Client::HttpAuth.new host if Regexp.new(":.*@.*:", Regexp::IGNORECASE).match host
  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).

    • **gas_limit (Integer)

      optional gas limit override for deploying the contract.

Returns:

  • (Object)

    returns the result of the call.

Raises:

  • (ArgumentError)


259
260
261
262
263
264
265
266
267
268
# File 'lib/eth/client.rb', line 259

def call(contract, function, *args, **kwargs)
  func = contract.functions.select { |func| func.name == function }[0]
  raise ArgumentError, "this function does not exist!" if func.nil?
  output = call_raw(contract, func, *args, **kwargs)
  if output&.length == 1
    return output[0]
  else
    return output
  end
end

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

Deploys a contract. Uses eth_coinbase 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.

Returns:

  • (String)

    the transaction hash.

Raises:

  • (ArgumentError)

    in case the contract does not have any source.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/eth/client.rb', line 194

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,
  }
  if kwargs[: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 kwargs[:sender_key].nil?
    # Uses the provided key as sender and signer
    params.merge!({
      from: kwargs[:sender_key].address,
      nonce: get_nonce(kwargs[:sender_key].address),
    })
    tx = Eth::Tx.new(params)
    tx.sign kwargs[:sender_key]
    return eth_send_raw_transaction(tx.hex)["result"]
  else
    # Uses the default account as sender and external signer
    params.merge!({
      from: ,
      nonce: get_nonce(),
    })
    return eth_send_transaction(params)["result"]
  end
end

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

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

See #deploy for params and overloads.

Returns:

  • (String)

    the contract address once it's mined.



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

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.



90
91
92
# File 'lib/eth/client.rb', line 90

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.



98
99
100
# File 'lib/eth/client.rb', line 98

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

#is_mined_tx?(hash) ⇒ Boolean

Checkes wether a transaction is mined or not.

Parameters:

  • hash (String)

    the transaction hash.

Returns:

  • (Boolean)

    true if included in a block.



376
377
378
379
# File 'lib/eth/client.rb', line 376

def is_mined_tx?(hash)
  mined_tx = eth_get_transaction_by_hash hash
  !mined_tx.nil? && !mined_tx["result"].nil? && !mined_tx["result"]["blockNumber"].nil?
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.



355
356
357
358
359
360
361
362
# File 'lib/eth/client.rb', line 355

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.is_hex? hash
  signature = Util.hex_to_bin signature if Util.is_hex? signature
  magic = Util.hex_to_bin magic if Util.is_hex? magic
  result = call(contract, "isValidSignature", hash, signature)
  return result === magic
end

#reset_idInteger

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

Returns:

  • (Integer)

    0



368
369
370
# File 'lib/eth/client.rb', line 368

def reset_id
  @id = 0
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 deploying the contract.

Returns:

  • (Object)

    returns the result of the transaction.



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/eth/client.rb', line 292

def transact(contract, function, *args, **kwargs)
  gas_limit = if kwargs[:gas_limit]
      kwargs[:gas_limit]
    else
      Tx.estimate_intrinsic_gas(contract.bin) + Tx::CREATE_GAS
    end
  fun = contract.functions.select { |func| func.name == function }[0]
  params = {
    value: 0,
    gas_limit: gas_limit,
    chain_id: chain_id,
    to: kwargs[:address] || contract.address,
    data: call_payload(fun, args),
  }
  if kwargs[: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 kwargs[:sender_key].nil?
    # use the provided key as sender and signer
    params.merge!({
      from: kwargs[:sender_key].address,
      nonce: get_nonce(kwargs[:sender_key].address),
    })
    tx = Eth::Tx.new(params)
    tx.sign kwargs[:sender_key]
    return eth_send_raw_transaction(tx.hex)["result"]
  else
    # use the default account as sender and external signer
    params.merge!({
      from: ,
      nonce: get_nonce(),
    })
    return eth_send_transaction(params)["result"]
  end
end

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

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)

    returns the result of the transaction.



341
342
343
# File 'lib/eth/client.rb', line 341

def transact_and_wait(contract, function, *args, **kwargs)
  wait_for_tx(transact(contract, function, *args, **kwargs))
end

#transfer(destination, amount, sender_key = nil, legacy = false) ⇒ String

Simply transfer Ether to an account without any call data or access lists attached. Uses eth_coinbase 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.

Parameters:

  • destination (Eth::Address)

    the destination address.

  • amount (Integer)

    the transfer amount in Wei.

  • sender_key (Eth::Key) (defaults to: nil)

    the sender private key.

  • legacy (Boolean) (defaults to: false)

    enables legacy transactions (pre-EIP-1559).

Returns:

  • (String)

    the local transaction hash.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/eth/client.rb', line 125

def transfer(destination, amount, sender_key = nil, legacy = false)
  params = {
    value: amount,
    to: destination,
    gas_limit: gas_limit,
    chain_id: chain_id,
  }
  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 sender_key.nil?

    # use the provided key as sender and signer
    params.merge!({
      from: sender_key.address,
      nonce: get_nonce(sender_key.address),
    })
    tx = Eth::Tx.new(params)
    tx.sign sender_key
    return eth_send_raw_transaction(tx.hex)["result"]
  else

    # use the default account as sender and external signer
    params.merge!({
      from: ,
      nonce: get_nonce(),
    })
    return eth_send_transaction(params)["result"]
  end
end

#transfer_and_wait(destination, amount, sender_key = nil, legacy = false) ⇒ String

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

See #transfer for params and overloads.

Returns:

  • (String)

    the transaction hash once it is mined.



109
110
111
# File 'lib/eth/client.rb', line 109

def transfer_and_wait(destination, amount, sender_key = nil, legacy = false)
  wait_for_tx(transfer(destination, amount, sender_key, legacy))
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.



386
387
388
389
390
391
392
393
394
395
# File 'lib/eth/client.rb', line 386

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 is_mined_tx? hash
    sleep retry_rate
  end
end