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)
-
(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)
-
(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)
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)
- 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)
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
-
(Integer)
- the chain ID.
def chain_id @chain_id ||= eth_chain_id["result"].to_i 16 end
def default_account
-
(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)
-
(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)
-
(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)
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)
-
(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)
-
(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(_)
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")
-
(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?
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)
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
def next_id @id += 1 end
def reset_id
-
(Integer)
- 0
def reset_id @id = 0 end
def resolve_ens(ens_name, registry = Ens::DEFAULT_ADDRESS, coin_type = Ens::CoinType::ETHEREUM)
-
(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)
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)
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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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