etherlite 0.1.0

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/Guardfile +10 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +41 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/etherlite.gemspec +31 -0
  14. data/lib/etherlite.rb +66 -0
  15. data/lib/etherlite/abi.rb +22 -0
  16. data/lib/etherlite/account.rb +67 -0
  17. data/lib/etherlite/address.rb +21 -0
  18. data/lib/etherlite/api/address.rb +21 -0
  19. data/lib/etherlite/api/node.rb +36 -0
  20. data/lib/etherlite/client.rb +11 -0
  21. data/lib/etherlite/commands/abi/load_contract.rb +66 -0
  22. data/lib/etherlite/commands/abi/load_function.rb +28 -0
  23. data/lib/etherlite/commands/abi/load_type.rb +55 -0
  24. data/lib/etherlite/commands/base.rb +0 -0
  25. data/lib/etherlite/commands/contract/event_base/decode_log_inputs.rb +21 -0
  26. data/lib/etherlite/commands/contract/function/encode_arguments.rb +32 -0
  27. data/lib/etherlite/commands/utils/validate_address.rb +25 -0
  28. data/lib/etherlite/configuration.rb +33 -0
  29. data/lib/etherlite/connection.rb +39 -0
  30. data/lib/etherlite/contract/base.rb +58 -0
  31. data/lib/etherlite/contract/event_base.rb +36 -0
  32. data/lib/etherlite/contract/event_input.rb +19 -0
  33. data/lib/etherlite/contract/function.rb +43 -0
  34. data/lib/etherlite/railtie.rb +29 -0
  35. data/lib/etherlite/railties/configuration_extensions.rb +9 -0
  36. data/lib/etherlite/railties/utils.rb +13 -0
  37. data/lib/etherlite/types/address.rb +20 -0
  38. data/lib/etherlite/types/array_base.rb +36 -0
  39. data/lib/etherlite/types/array_dynamic.rb +13 -0
  40. data/lib/etherlite/types/array_fixed.rb +23 -0
  41. data/lib/etherlite/types/base.rb +27 -0
  42. data/lib/etherlite/types/bool.rb +26 -0
  43. data/lib/etherlite/types/byte_string.rb +17 -0
  44. data/lib/etherlite/types/bytes.rb +19 -0
  45. data/lib/etherlite/types/fixed.rb +38 -0
  46. data/lib/etherlite/types/integer.rb +38 -0
  47. data/lib/etherlite/types/string.rb +10 -0
  48. data/lib/etherlite/utils.rb +63 -0
  49. data/lib/etherlite/version.rb +3 -0
  50. data/lib/generators/etherlite/init_generator.rb +10 -0
  51. data/lib/generators/etherlite/templates/etherlite.yml +11 -0
  52. metadata +206 -0
@@ -0,0 +1,11 @@
1
+ module Etherlite
2
+ class Client
3
+ include Api::Node
4
+
5
+ attr_reader :connection
6
+
7
+ def initialize
8
+ @connection = Connection.new
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,66 @@
1
+ module Etherlite::Abi
2
+ class LoadContract < PowerTypes::Command.new(:json)
3
+ def perform
4
+ klass = Class.new(Etherlite::Contract::Base)
5
+
6
+ abi_definitions.each do |definition|
7
+ case definition['type']
8
+ when 'function'
9
+ define_function klass, definition
10
+ when 'event'
11
+ define_event klass, definition
12
+ end
13
+ end
14
+
15
+ klass.functions.freeze
16
+ klass.events.freeze
17
+ klass
18
+ end
19
+
20
+ private
21
+
22
+ def abi_definitions
23
+ @json['abi'] || []
24
+ end
25
+
26
+ 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
+ )
34
+
35
+ _class.functions << function
36
+ _class.class_eval do
37
+ define_method(_definition['name'].underscore) do |*params|
38
+ account = (params.last.is_a?(Hash) && params.last[:as]) || default_account
39
+
40
+ raise ArgumentError, 'must provide a source account' if account.nil?
41
+ account.call address, function, *params
42
+ end
43
+ end
44
+ end
45
+
46
+ def define_event(_class, _definition)
47
+ event_inputs = _definition['inputs'].map do |input|
48
+ Etherlite::Contract::EventInput.new(
49
+ input['name'], LoadType.for(signature: input['type']), input['indexed']
50
+ )
51
+ end
52
+
53
+ event_class = Class.new(Etherlite::Contract::EventBase) do
54
+ event_inputs.each do |input|
55
+ define_method(input.name) { attributes[input.name] }
56
+ end
57
+ end
58
+
59
+ event_class.instance_variable_set(:@original_name, _definition['name'])
60
+ event_class.instance_variable_set(:@inputs, event_inputs)
61
+
62
+ _class.events << event_class
63
+ _class.const_set(_definition['name'], event_class)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,28 @@
1
+ module Etherlite::Abi
2
+ class LoadFunction < PowerTypes::Command.new(:signature)
3
+ MATCHER = /^(payable|onchain|\w[\w\d]*) (\w[\w\d]*)\((.*?)\)$/
4
+
5
+ def perform
6
+ parts = MATCHER.match @signature
7
+ raise ArgumentError, 'invalid method signature' if parts.nil?
8
+
9
+ args = parts[3].split(',').map { |a| LoadType.for(signature: a.strip) }
10
+
11
+ case parts[1]
12
+ when 'payable'
13
+ build parts[2], args, payable: true
14
+ when 'onchain'
15
+ build parts[2], args
16
+ else
17
+ return_type = parts[1] == 'void' ? nil : LoadType.for(signature: parts[1])
18
+ build parts[2], args, constant: true, returns: return_type
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build(*_args)
25
+ Etherlite::Contract::Function.new *_args
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ module Etherlite::Abi
2
+ class LoadType < PowerTypes::Command.new(:signature)
3
+ MATCHER = /^(?:(?:(?<ty>uint|int|bytes)(?<b1>\d+)|(?<ty>uint|int|bytes)(?<b1>\d+)|(?<ty>fixed|ufixed)(?<b1>\d+)x(?<b2>\d+)|(?<ty>address|bool|uint|int|fixed|ufixed))(?:\[(?<dim>\d*)\]|)|(?<ty>bytes|string))$/
4
+
5
+ def perform
6
+ parts = MATCHER.match @signature
7
+ raise ArgumentError, "Invalid argument type #{@signature}" if parts.nil?
8
+
9
+ type = build_base_type parts
10
+ build_array_type type, parts
11
+ end
12
+
13
+ private
14
+
15
+ def build_base_type(_parts) # rubocop:disable Metrics/CyclomaticComplexity
16
+ case _parts[:ty]
17
+ when 'uint' then Etherlite::Types::Integer.new(false, b1_256(_parts))
18
+ when 'int' then Etherlite::Types::Integer.new(true, b1_256(_parts))
19
+ when 'ufixed' then Etherlite::Types::Fixed.new(false, b1_128(_parts), b2_128(_parts))
20
+ when 'fixed' then Etherlite::Types::Fixed.new(true, b1_128(_parts), b2_128(_parts))
21
+ when 'string' then Etherlite::Types::String.new
22
+ when 'address' then Etherlite::Types::Address.new
23
+ when 'bool' then Etherlite::Types::Bool.new
24
+ when 'bytes'
25
+ if _parts[:b1].present?
26
+ Etherlite::Types::Bytes.new(_parts[:b1].to_i)
27
+ else
28
+ Etherlite::Types::ByteString.new
29
+ end
30
+ end
31
+ end
32
+
33
+ def build_array_type(_base_type, _parts)
34
+ return _base_type if _parts[:dim].nil?
35
+
36
+ if _parts[:dim].empty?
37
+ Etherlite::Types::ArrayDynamic.new _base_type
38
+ else
39
+ Etherlite::Types::ArrayFixed.new _base_type, _parts[:dim].to_i
40
+ end
41
+ end
42
+
43
+ def b1_256(_parts)
44
+ _parts[:b1].nil? ? 256 : _parts[:b1].to_i
45
+ end
46
+
47
+ def b1_128(_parts)
48
+ _parts[:b1].nil? ? 128 : _parts[:b1].to_i
49
+ end
50
+
51
+ def b2_128(_parts)
52
+ _parts[:b2].nil? ? 128 : _parts[:b2].to_i
53
+ end
54
+ end
55
+ end
File without changes
@@ -0,0 +1,21 @@
1
+ class Etherlite::Contract::EventBase
2
+ class DecodeLogInputs < PowerTypes::Command.new(:connection, :inputs, :json)
3
+ def perform
4
+ indexed = []
5
+ non_indexed = []
6
+ attributes = {}
7
+
8
+ @inputs.each { |i| (i.indexed? ? indexed : non_indexed) << i }
9
+
10
+ @json['data'][2..-1].scan(/.{64}/).each_with_index do |data, i|
11
+ attributes[non_indexed[i].name] = non_indexed[i].type.decode(@connection, data)
12
+ end
13
+
14
+ @json['topics'][1..-1].each_with_index do |topic, i|
15
+ attributes[indexed[i].name] = indexed[i].type.decode(@connection, topic[2..-1])
16
+ end
17
+
18
+ attributes
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ class Etherlite::Contract::Function
2
+ class EncodeArguments < PowerTypes::Command.new(:subtypes, :values)
3
+ def perform # rubocop:disable Metrics/MethodLength
4
+ if @values.length != @subtypes.count
5
+ raise ArgumentError, "Expected #{@subtypes.count} arguments, got #{@values.length} "
6
+ end
7
+
8
+ head = []
9
+ tail = []
10
+ tail_offset = calculate_head_offset
11
+
12
+ @subtypes.each_with_index do |type, i|
13
+ content = type.encode @values[i]
14
+ if type.dynamic?
15
+ head << Etherlite::Utils.uint_to_hex(tail_offset)
16
+ tail << content
17
+ tail_offset += content.length / 2 # hex string, 2 characters per byte
18
+ else
19
+ head << content
20
+ end
21
+ end
22
+
23
+ head.join + tail.join
24
+ end
25
+
26
+ private
27
+
28
+ def calculate_head_offset
29
+ @subtypes.inject(0) { |r, type| r + (type.dynamic? ? 32 : type.size) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module Etherlite::Utils
2
+ class ValidateAddress < PowerTypes::Command.new(:address)
3
+ MATCHER = /^(0x)?[0-9a-fA-F]{40}$/
4
+
5
+ def perform
6
+ return false unless MATCHER === @address
7
+ return false if /[A-F]/ === @address && !valid_checksum?
8
+ true
9
+ end
10
+
11
+ private
12
+
13
+ def valid_checksum?
14
+ trimmed_address = @address.gsub(/^0x/, '')
15
+ address_hash = Etherlite::Utils.sha3 trimmed_address.downcase
16
+
17
+ trimmed_address.chars.each_with_index do |c, i|
18
+ hash_byte = address_hash[i].to_i(16)
19
+ return false if (hash_byte > 7 && c.upcase != c) || (hash_byte <= 7 && c.downcase != c)
20
+ end
21
+
22
+ true
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module Etherlite
2
+ class Configuration
3
+ DEFAULTS = {
4
+ url: 'http://127.0.0.1:8545',
5
+ logger: nil # set by method
6
+ }
7
+
8
+ attr_accessor :url, :logger
9
+
10
+ def initialize
11
+ assign_attributes DEFAULTS
12
+ end
13
+
14
+ def reset
15
+ assign_attributes DEFAULTS
16
+ end
17
+
18
+ def assign_attributes(_options)
19
+ _options.each { |k, v| public_send("#{k}=", v) }
20
+ self
21
+ end
22
+
23
+ def logger
24
+ @logger || default_logger
25
+ end
26
+
27
+ private
28
+
29
+ def default_logger
30
+ @default_logger ||= Logger.new(STDOUT)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ module Etherlite
2
+ class Connection
3
+ def initialize(_uri)
4
+ @uri = _uri
5
+ end
6
+
7
+ def ipc_call(_method, *_params)
8
+ id = new_unique_id
9
+ payload = { jsonrpc: "2.0", method: _method, params: _params, id: id }
10
+
11
+ # TODO: support ipc
12
+ Net::HTTP.start(@uri.hostname, @uri.port) do |http|
13
+ return handle_response http.post(
14
+ @uri.path.empty? ? '/' : @uri.path,
15
+ payload.to_json,
16
+ "Content-Type" => "application/json"
17
+ ), id
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def new_unique_id
24
+ (Time.now.to_f * 1000.0).to_i
25
+ end
26
+
27
+ def handle_response(_response, _id)
28
+ case _response
29
+ when Net::HTTPSuccess
30
+ # puts _response.body
31
+ json_body = JSON.parse _response.body
32
+ # TODO: check id
33
+ json_body['result']
34
+ else
35
+ raise _response
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ module Etherlite::Contract
2
+ class Base
3
+ include Etherlite::Api::Address
4
+
5
+ def self.functions
6
+ @functions ||= []
7
+ end
8
+
9
+ def self.events
10
+ @events ||= []
11
+ end
12
+
13
+ def self.at(_address, client: nil, as: nil)
14
+ client ||= ::Etherlite # use default client if no client is provided
15
+
16
+ new(
17
+ client.connection,
18
+ Etherlite::Utils.normalize_address_param(_address),
19
+ as || client.first_account
20
+ )
21
+ end
22
+
23
+ attr_reader :connection
24
+
25
+ def initialize(_connection, _normalized_address, _default_account)
26
+ @connection = _connection
27
+ @normalized_address = _normalized_address
28
+ @default_account = _default_account
29
+ end
30
+
31
+ def get_logs(events: nil, from_block: :earliest, to_block: :latest)
32
+ params = {
33
+ address: '0x' + @normalized_address,
34
+ fromBlock: Etherlite::Utils.encode_block_param(from_block),
35
+ toBlock: Etherlite::Utils.encode_block_param(to_block)
36
+ }
37
+
38
+ params[:topics] = [events.map { |e| event_topic e }] unless events.nil?
39
+
40
+ event_map = Hash[(events || self.class.events).map { |e| [event_topic(e), e] }]
41
+
42
+ logs = @connection.ipc_call(:eth_getLogs, params)
43
+ logs.map do |log|
44
+ event = event_map[log["topics"].first]
45
+ # TODO: support anonymous events!
46
+ event.decode(@connection, log) unless event.nil?
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :default_account, :normalized_address
53
+
54
+ def event_topic(_event)
55
+ '0x' + Etherlite::Utils.sha3(_event.signature)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
1
+ require 'etherlite/commands/contract/event_base/decode_log_inputs'
2
+
3
+ module Etherlite::Contract
4
+ class EventBase
5
+ def self.inputs
6
+ @inputs
7
+ end
8
+
9
+ def self.original_name
10
+ @original_name
11
+ end
12
+
13
+ def self.signature
14
+ @signature ||= begin
15
+ input_sig = @inputs.map { |i| i.type.signature }
16
+ "#{@original_name}(#{input_sig.join(',')})"
17
+ end
18
+ end
19
+
20
+ def self.decode(_connection, _json)
21
+ new(
22
+ _json['blockNumber'].nil? ? nil : Etherlite::Utils.hex_to_uint(_json['blockNumber']),
23
+ _json['transactionHash'],
24
+ DecodeLogInputs.for(connection: _connection, inputs: inputs, json: _json)
25
+ )
26
+ end
27
+
28
+ attr_reader :block_number, :tx_hash, :attributes
29
+
30
+ def initialize(_block_number, _tx_hash, _attributes)
31
+ @block_number = _block_number
32
+ @tx_hash = _tx_hash
33
+ @attributes = _attributes
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ module Etherlite::Contract
2
+ class EventInput
3
+ attr_reader :original_name, :type
4
+
5
+ def initialize(_original_name, _type, _indexed)
6
+ @original_name = _original_name
7
+ @type = _type
8
+ @indexed = _indexed
9
+ end
10
+
11
+ def name
12
+ @name ||= @original_name.underscore
13
+ end
14
+
15
+ def indexed?
16
+ @indexed
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ require 'etherlite/commands/contract/function/encode_arguments'
2
+
3
+ module Etherlite::Contract
4
+ class Function
5
+ attr_reader :name, :args
6
+
7
+ def initialize(_name, _args, returns: nil, payable: false, constant: false)
8
+ @name = _name
9
+ @args = _args
10
+
11
+ @returns = returns
12
+ @payable = payable
13
+ @constant = constant
14
+ end
15
+
16
+ def constant?
17
+ @constant
18
+ end
19
+
20
+ def payable?
21
+ @payable
22
+ end
23
+
24
+ def signature
25
+ @signature ||= begin
26
+ arg_signatures = @args.map &:signature
27
+ "#{@name}(#{arg_signatures.join(',')})"
28
+ end
29
+ end
30
+
31
+ def encode(_values)
32
+ signature_hash = Etherlite::Utils.sha3 signature
33
+ encoded_args = EncodeArguments.for subtypes: @args, values: _values
34
+
35
+ '0x' + signature_hash[0..7] + encoded_args
36
+ end
37
+
38
+ def decode(_connection, _values)
39
+ # TODO: decode return values
40
+ _values
41
+ end
42
+ end
43
+ end