etherlite 0.1.0

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