etherlite 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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