etherlite 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -7
  3. data/etherlite.gemspec +2 -0
  4. data/lib/etherlite.rb +12 -7
  5. data/lib/etherlite/abi.rb +44 -4
  6. data/lib/etherlite/account/anonymous.rb +9 -0
  7. data/lib/etherlite/account/base.rb +56 -0
  8. data/lib/etherlite/account/local.rb +40 -0
  9. data/lib/etherlite/account/private_key.rb +51 -0
  10. data/lib/etherlite/api/node.rb +15 -12
  11. data/lib/etherlite/api/rpc.rb +55 -0
  12. data/lib/etherlite/commands/abi/load_contract.rb +24 -10
  13. data/lib/etherlite/commands/abi/load_function.rb +5 -5
  14. data/lib/etherlite/commands/abi/load_type.rb +1 -1
  15. data/lib/etherlite/commands/contract/event_base/decode_log_inputs.rb +30 -10
  16. data/lib/etherlite/configuration.rb +2 -1
  17. data/lib/etherlite/connection.rb +8 -4
  18. data/lib/etherlite/contract/base.rb +53 -1
  19. data/lib/etherlite/contract/function.rb +18 -14
  20. data/lib/etherlite/errors.rb +27 -0
  21. data/lib/etherlite/nonce_manager.rb +13 -14
  22. data/lib/etherlite/support/array.rb +43 -0
  23. data/lib/etherlite/transaction.rb +33 -1
  24. data/lib/etherlite/types/array_dynamic.rb +19 -5
  25. data/lib/etherlite/types/array_fixed.rb +19 -10
  26. data/lib/etherlite/types/{bool.rb → boolean.rb} +1 -1
  27. data/lib/etherlite/types/byte_string.rb +6 -0
  28. data/lib/etherlite/types/string.rb +6 -1
  29. data/lib/etherlite/utils.rb +4 -2
  30. data/lib/etherlite/version.rb +1 -1
  31. data/lib/generators/etherlite/templates/etherlite.yml +1 -0
  32. metadata +38 -8
  33. data/lib/etherlite/account.rb +0 -85
  34. data/lib/etherlite/commands/base.rb +0 -0
  35. data/lib/etherlite/commands/contract/function/encode_arguments.rb +0 -32
  36. data/lib/etherlite/pk_account.rb +0 -82
  37. data/lib/etherlite/types/array_base.rb +0 -36
@@ -0,0 +1,55 @@
1
+ module Etherlite
2
+ module Api
3
+ module Rpc
4
+ def eth_block_number
5
+ Etherlite::Utils.hex_to_uint ipc_call(:eth_blockNumber)
6
+ end
7
+
8
+ def eth_gas_price
9
+ Etherlite::Utils.hex_to_uint ipc_call(:eth_gasPrice)
10
+ end
11
+
12
+ def eth_get_transaction_receipt(_tx_hash)
13
+ ipc_call(:eth_getTransactionReceipt, _tx_hash)
14
+ end
15
+
16
+ def eth_get_transaction_count(_address, _block = 'latest')
17
+ Etherlite::Utils.hex_to_uint ipc_call(:eth_getTransactionCount, _address, _block)
18
+ end
19
+
20
+ def eth_call(_params, _block = 'latest')
21
+ ipc_call(:eth_call, _params, _block)
22
+ end
23
+
24
+ def eth_send_raw_transaction(_hex_data)
25
+ ipc_call(:eth_sendRawTransaction, _hex_data)
26
+ end
27
+
28
+ def eth_send_transaction(_params)
29
+ ipc_call(:eth_sendTransaction, _params)
30
+ end
31
+
32
+ def personal_send_transaction(_params, _passphrase)
33
+ ipc_call(:personal_sendTransaction, _params, _passphrase)
34
+ end
35
+
36
+ # TestRPC support
37
+
38
+ def evm_snapshot
39
+ ipc_call(:evm_snapshot)
40
+ end
41
+
42
+ def evm_revert(_snapshot_id)
43
+ ipc_call(:evm_revert, _snapshot_id)
44
+ end
45
+
46
+ def evm_increase_time(_seconds)
47
+ Etherlite::Utils.hex_to_uint ipc_call(:evm_increase_time, _seconds)
48
+ end
49
+
50
+ def evm_mine
51
+ ipc_call(:evm_mine)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,8 +1,10 @@
1
1
  module Etherlite::Abi
2
- class LoadContract < PowerTypes::Command.new(:json)
2
+ class LoadContract < PowerTypes::Command.new(:artifact)
3
3
  def perform
4
4
  klass = Class.new(Etherlite::Contract::Base)
5
5
 
6
+ define_class_methods klass
7
+
6
8
  abi_definitions.each do |definition|
7
9
  case definition['type']
8
10
  when 'function'
@@ -20,21 +22,15 @@ module Etherlite::Abi
20
22
  private
21
23
 
22
24
  def abi_definitions
23
- @json['abi'] || []
25
+ @artifact['abi'] || []
24
26
  end
25
27
 
26
28
  def define_function(_class, _definition)
27
- function_args = _definition['inputs'].map { |input| LoadType.for signature: input['type'] }
28
- function = Etherlite::Contract::Function.new(
29
- _definition['name'],
30
- function_args,
31
- payable: _definition['payable'],
32
- constant: _definition['constant']
33
- )
29
+ function = build_function_from_definition _definition
34
30
 
35
31
  _class.functions << function
36
32
  _class.class_eval do
37
- define_method(_definition['name'].underscore) do |*params|
33
+ define_method(function.name.underscore) do |*params|
38
34
  account = (params.last.is_a?(Hash) && params.last[:as]) || default_account
39
35
 
40
36
  raise ArgumentError, 'must provide a source account' if account.nil?
@@ -62,5 +58,23 @@ module Etherlite::Abi
62
58
  _class.events << event_class
63
59
  _class.const_set(_definition['name'], event_class)
64
60
  end
61
+
62
+ def define_class_methods(_class)
63
+ unlinked_binary = @artifact['unlinked_binary'] || ''
64
+
65
+ _class.class_eval do
66
+ define_singleton_method('unlinked_binary') { unlinked_binary }
67
+ end
68
+ end
69
+
70
+ def build_function_from_definition(_definition)
71
+ Etherlite::Contract::Function.new(
72
+ _definition['name'],
73
+ _definition['inputs'].map { |input| LoadType.for signature: input['type'] },
74
+ _definition['outputs'].map { |input| LoadType.for signature: input['type'] },
75
+ _definition['payable'],
76
+ _definition['constant']
77
+ )
78
+ end
65
79
  end
66
80
  end
@@ -6,16 +6,16 @@ module Etherlite::Abi
6
6
  parts = MATCHER.match @signature
7
7
  raise ArgumentError, 'invalid method signature' if parts.nil?
8
8
 
9
- args = parts[3].split(',').map { |a| LoadType.for(signature: a.strip) }
9
+ inputs = parts[3].split(',').map { |a| LoadType.for(signature: a.strip) }
10
10
 
11
11
  case parts[1]
12
12
  when 'payable'
13
- build parts[2], args, payable: true
13
+ build parts[2], inputs, [], true, false
14
14
  when 'onchain'
15
- build parts[2], args
15
+ build parts[2], inputs, [], false, false
16
16
  else
17
- return_type = parts[1] == 'void' ? nil : LoadType.for(signature: parts[1])
18
- build parts[2], args, constant: true, returns: return_type
17
+ ouputs = parts[1] == 'void' ? [] : [LoadType.for(signature: parts[1])]
18
+ build parts[2], inputs, ouputs, false, true
19
19
  end
20
20
  end
21
21
 
@@ -20,7 +20,7 @@ module Etherlite::Abi
20
20
  when 'fixed' then Etherlite::Types::Fixed.new(true, b1_128(_parts), b2_128(_parts))
21
21
  when 'string' then Etherlite::Types::String.new
22
22
  when 'address' then Etherlite::Types::Address.new
23
- when 'bool' then Etherlite::Types::Bool.new
23
+ when 'bool' then Etherlite::Types::Boolean.new
24
24
  when 'bytes'
25
25
  if _parts[:b1].present?
26
26
  Etherlite::Types::Bytes.new(_parts[:b1].to_i)
@@ -1,21 +1,41 @@
1
1
  class Etherlite::Contract::EventBase
2
2
  class DecodeLogInputs < PowerTypes::Command.new(:connection, :inputs, :json)
3
3
  def perform
4
- indexed = []
5
- non_indexed = []
6
- attributes = {}
4
+ store_by_name non_indexed_inputs, non_indexed_values
5
+ store_by_name indexed_inputs, indexed_values
6
+ attributes
7
+ end
7
8
 
8
- @inputs.each { |i| (i.indexed? ? indexed : non_indexed) << i }
9
+ private
9
10
 
10
- @json['data'][2..-1].scan(/.{64}/).each_with_index do |data, i|
11
- attributes[non_indexed[i].original_name] = non_indexed[i].type.decode(@connection, data)
12
- end
11
+ def attributes
12
+ @attributes ||= {}
13
+ end
14
+
15
+ def non_indexed_inputs
16
+ @non_indexed_inputs ||= @inputs.select { |i| !i.indexed? }
17
+ end
13
18
 
14
- @json['topics'][1..-1].each_with_index do |topic, i|
15
- attributes[indexed[i].original_name] = indexed[i].type.decode(@connection, topic[2..-1])
19
+ def non_indexed_values
20
+ Etherlite::Support::Array.decode(
21
+ @connection, non_indexed_inputs.map(&:type), @json['data'][2..-1]
22
+ )
23
+ end
24
+
25
+ def indexed_inputs
26
+ @indexed_inputs ||= @inputs.select(&:indexed?)
27
+ end
28
+
29
+ def indexed_values
30
+ topics = @json['topics'][1..-1]
31
+ indexed_inputs.each_with_index.map do |input, i|
32
+ # dynamic indexed inputs cannot be retrieved (only the hash is stored)
33
+ input.type.dynamic? ? topics[i] : input.type.decode(@connection, topics[i])
16
34
  end
35
+ end
17
36
 
18
- attributes
37
+ def store_by_name(_inputs, _values)
38
+ _inputs.each_with_index { |input, i| attributes[input.original_name] = _values[i] }
19
39
  end
20
40
  end
21
41
  end
@@ -2,10 +2,11 @@ module Etherlite
2
2
  class Configuration
3
3
  DEFAULTS = {
4
4
  url: 'http://127.0.0.1:8545',
5
+ chain_id: nil, # any chain
5
6
  logger: nil # set by method
6
7
  }
7
8
 
8
- attr_accessor :url, :logger
9
+ attr_accessor :url, :chain_id, :logger
9
10
 
10
11
  def initialize
11
12
  assign_attributes DEFAULTS
@@ -1,7 +1,12 @@
1
1
  module Etherlite
2
2
  class Connection
3
- def initialize(_uri)
3
+ include Api::Rpc
4
+
5
+ attr_reader :chain_id
6
+
7
+ def initialize(_uri, _chain_id = nil)
4
8
  @uri = _uri
9
+ @chain_id = _chain_id
5
10
  end
6
11
 
7
12
  def ipc_call(_method, *_params)
@@ -31,12 +36,11 @@ module Etherlite
31
36
  def handle_response(_response, _id)
32
37
  case _response
33
38
  when Net::HTTPSuccess
34
- # puts _response.body
35
39
  json_body = JSON.parse _response.body
36
- # TODO: check id
40
+ raise NodeError.new json_body['error'] if json_body['error']
37
41
  json_body['result']
38
42
  else
39
- raise _response
43
+ raise RPCError.new _response.code, _response.body
40
44
  end
41
45
  end
42
46
  end
@@ -1,15 +1,62 @@
1
1
  module Etherlite::Contract
2
+ ##
3
+ # The base class for etherlite contract classes
4
+ #
2
5
  class Base
3
6
  include Etherlite::Api::Address
4
7
 
8
+ # The contract's registered functions
5
9
  def self.functions
6
10
  @functions ||= []
7
11
  end
8
12
 
13
+ # The contract's registered events
9
14
  def self.events
10
15
  @events ||= []
11
16
  end
12
17
 
18
+ # The contract's compiled bytecode in binary format (stored as hex)
19
+ def self.binary
20
+ @binary ||= begin
21
+ if /__[^_]+_+/ === unlinked_binary
22
+ raise UnlinkedContractError, 'compiled contract contains unresolved library references'
23
+ end
24
+
25
+ unlinked_binary
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Deploys the contract and waits for the creation transaction to be mined.
31
+ #
32
+ # This method can be given a source account or client. If an account is given, the it will be
33
+ # used to send the creation transaction. If instead a client is given, the client
34
+ # `default_account` will be used to send the transaction. If no account nor client is given,
35
+ # then the default_account from the default client will be used (if configured).
36
+ #
37
+ # @param as (Object) The source account
38
+ # @param client (Object) The source client (no effect if :as is given)
39
+ # @param timeout (Integer) The transaction mining timeout in seconds, defaults to 120 seconds.
40
+ #
41
+ # This method also takes any parameters the underlying account.send_transaction accepts, like
42
+ # :gas, :gas_price, etc.
43
+ #
44
+ # @return [Object] instance of the contract class pointing the newly deployed contract address.
45
+ #
46
+ def self.deploy(_options = {})
47
+ as = _options[:as] || _options[:client].try(:default_account) || Etherlite.default_account
48
+
49
+ tx = as.send_transaction({ data: binary }.merge(_options))
50
+ if tx.wait_for_block(timeout: _options.fetch(:timeout, 120))
51
+ at tx.contract_address, as: as
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Creates a new instance of the contract class that points to a given address.
57
+ #
58
+ #
59
+ #
13
60
  def self.at(_address, client: nil, as: nil)
14
61
  _address = Etherlite::Utils.normalize_address_param _address
15
62
 
@@ -17,7 +64,7 @@ module Etherlite::Contract
17
64
  new(as.connection, _address, as)
18
65
  else
19
66
  client ||= ::Etherlite
20
- new(client.connection, _address, client.first_account)
67
+ new(client.connection, _address, client.default_account)
21
68
  end
22
69
  end
23
70
 
@@ -29,6 +76,11 @@ module Etherlite::Contract
29
76
  @default_account = _default_account
30
77
  end
31
78
 
79
+ ##
80
+ # Searches for event logs emitted by this contract
81
+ #
82
+ #
83
+ #
32
84
  def get_logs(events: nil, from_block: :earliest, to_block: :latest)
33
85
  params = {
34
86
  address: json_encoded_address,
@@ -1,16 +1,14 @@
1
- require 'etherlite/commands/contract/function/encode_arguments'
2
-
3
1
  module Etherlite::Contract
4
2
  class Function
5
- attr_reader :name, :args
3
+ attr_reader :name, :inputs, :outputs
6
4
 
7
- def initialize(_name, _args, returns: nil, payable: false, constant: false)
5
+ def initialize(_name, _inputs, _outputs, _payable, _constant)
8
6
  @name = _name
9
- @args = _args
7
+ @inputs = _inputs
8
+ @outputs = _outputs
10
9
 
11
- @returns = returns
12
- @payable = payable
13
- @constant = constant
10
+ @payable = _payable
11
+ @constant = _constant
14
12
  end
15
13
 
16
14
  def constant?
@@ -23,21 +21,27 @@ module Etherlite::Contract
23
21
 
24
22
  def signature
25
23
  @signature ||= begin
26
- arg_signatures = @args.map &:signature
24
+ arg_signatures = @inputs.map &:signature
27
25
  "#{@name}(#{arg_signatures.join(',')})"
28
26
  end
29
27
  end
30
28
 
31
29
  def encode(_values)
30
+ if _values.length != @inputs.count
31
+ raise ArgumentError, "Expected #{@inputs.count} arguments, got #{_values.length} "
32
+ end
33
+
32
34
  signature_hash = Etherlite::Utils.sha3 signature
33
- encoded_args = EncodeArguments.for subtypes: @args, values: _values
35
+ encoded_inputs = Etherlite::Support::Array.encode(@inputs, _values)
34
36
 
35
- '0x' + signature_hash[0..7] + encoded_args
37
+ '0x' + signature_hash[0..7] + encoded_inputs
36
38
  end
37
39
 
38
- def decode(_connection, _values)
39
- # TODO: decode return values
40
- _values
40
+ def decode(_connection, _data)
41
+ return nil if @outputs.empty?
42
+
43
+ result = Etherlite::Support::Array.decode(_connection, @outputs, _data[2..-1])
44
+ result.length > 1 ? result : result[0]
41
45
  end
42
46
  end
43
47
  end
@@ -0,0 +1,27 @@
1
+ module Etherlite
2
+ class Error < StandardError; end
3
+
4
+ class RPCError < Error
5
+ attr_reader :http_status, :http_body
6
+
7
+ def initialize(_status, _body)
8
+ super("Request returned status code #{_status}")
9
+ @http_status = _status
10
+ @http_body = _body
11
+ end
12
+ end
13
+
14
+ class NodeError < Error
15
+ attr_reader :rpc_error_code, :rpc_error_message
16
+
17
+ def initialize(_error)
18
+ super("#{_error['code']}: #{_error['message']}")
19
+ @rpc_error_code = _error['code']
20
+ @rpc_error_message = _error['message']
21
+ end
22
+ end
23
+
24
+ class NotSupportedError < Error; end
25
+
26
+ class UnlinkedContractError < Error; end
27
+ end
@@ -3,30 +3,31 @@ module Etherlite
3
3
  @@nonce_cache = {}
4
4
  @@nonce_mutex = Mutex.new
5
5
 
6
+ def self.clear_cache
7
+ @@nonce_cache = {}
8
+ end
9
+
6
10
  def initialize(_connection)
7
11
  @connection = _connection
8
12
  end
9
13
 
10
- def last_nonce_for(_address)
11
- _address = Utils.encode_address_param _address
12
- last_nonce = @@nonce_cache[_address]
13
- last_nonce = last_observed_nonce_for(_address) if last_nonce.nil?
14
+ def last_nonce_for(_normalized_address)
15
+ last_nonce = @@nonce_cache[_normalized_address]
16
+ last_nonce = last_observed_nonce_for(_normalized_address) if last_nonce.nil?
14
17
  last_nonce
15
18
  end
16
19
 
17
- def with_next_nonce_for(_address)
18
- _address = Utils.encode_address_param _address
19
-
20
+ def with_next_nonce_for(_normalized_address)
20
21
  @@nonce_mutex.synchronize do
21
- next_nonce = last_nonce_for(_address) + 1
22
+ next_nonce = last_nonce_for(_normalized_address) + 1
22
23
 
23
24
  begin
24
25
  result = yield next_nonce
25
- @@nonce_cache[_address] = next_nonce
26
+ @@nonce_cache[_normalized_address] = next_nonce
26
27
  return result
27
28
  rescue
28
29
  # if yield fails, cant be sure about transaction status so must rely again on observing.
29
- @@nonce_cache.delete _address
30
+ @@nonce_cache.delete _normalized_address
30
31
  raise
31
32
  end
32
33
  end
@@ -34,12 +35,10 @@ module Etherlite
34
35
 
35
36
  private
36
37
 
37
- def last_observed_nonce_for(_address)
38
+ def last_observed_nonce_for(_normalized_address)
38
39
  # TODO: support using tx_pool API to improve this:
39
40
  # http://qnimate.com/calculating-nonce-for-raw-transactions-in-geth/
40
- Etherlite::Utils.hex_to_uint(
41
- @connection.ipc_call(:eth_getTransactionCount, _address, 'pending')
42
- ) - 1
41
+ @connection.eth_get_transaction_count('0x' + _normalized_address, 'pending') - 1
43
42
  end
44
43
  end
45
44
  end