etheruby 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0c566325afa370e6f3d4dc52964b205b26a53da7
4
- data.tar.gz: 10cd9eac53229fe3a293fe0cefc879abf9d2cc3f
3
+ metadata.gz: 62e7a1ef2415572fb35e1617898576247fabeccb
4
+ data.tar.gz: 9f6747ff369f23fe11f3a67c0b815b6a53bbfd72
5
5
  SHA512:
6
- metadata.gz: 6dc714bdcc04e5778b508d809e5e29fb230c4a1640f3e377c55d123a12d9929eb879f6d73175274816b7491a0943c2e96a0b60216ce240b2630e3038ff5553e8
7
- data.tar.gz: c3cae2decb6fb2c2f8e79c3d27ed2dcbf3cf2c2ece666382445892cba8f226a4946e1f598a4f2803dd358b0158f35fab32c9cf24b3c1270a83c2ab8a97ea708b
6
+ metadata.gz: b5deae709916778cfd2e39217f22edb24929da0a57054cd5020a0882e8bf222d3b1928d90140a21ce99f9aaeb0eb103f4172031f1d9b857f05aee62ea39e9648
7
+ data.tar.gz: f725746f91570b71d37d89b62c241945688603fedaf17f0fb56db178511eb73206edd72d9d45fba75b88ebdb16458765e242166222d81765b7c03649e99b9051
@@ -1,11 +1,11 @@
1
1
  require 'bigdecimal'
2
2
  require_relative 'type_matchers'
3
+ require_relative 'treat_variable'
4
+ require_relative 'encoders/int'
3
5
 
4
6
  module Etheruby
5
7
 
6
- class IncorrectTypeError < StandardError; end
7
8
  class ArgumentsCountError < StandardError; end
8
- class InvalidFormatForDataError < StandardError; end
9
9
 
10
10
  class ArgumentsGenerator
11
11
 
@@ -16,127 +16,23 @@ module Etheruby
16
16
  @args = args
17
17
  end
18
18
 
19
- def treat_variable(param, arg)
20
- if match = TypeMatchers.is_sized_type(param)
21
- # Parameter is a sized type, e.g. uint256, byte32 ...
22
- send("#{match[1]}_encode".to_sym, match[2].to_i, arg)
23
-
24
- elsif match = TypeMatchers.is_dualsized_type(param)
25
- # Parameter is a dual sized array type, e.g. fixed16x16
26
- send("#{match[1]}_encode".to_sym, match[2].to_i, match[3].to_i, arg)
27
-
28
- elsif match = TypeMatchers.is_static_array_type(param)
29
- # Parameter is a staticly sized array type, e.g. uint256[24]
30
- static_array_encode(match[1], match[2].to_i, arg)
31
-
32
- elsif match = TypeMatchers.is_dynamic_array_type(param)
33
- # Parameter is a dynamicaly sized array type, e.g. uint256[]
34
- dynamic_array_encode(match[1], arg)
35
-
36
- else
37
- # Parameter is a single-word type : string, bytes, address etc...
38
- send("#{param}_encode".to_sym, arg)
39
-
40
- end
41
- end
42
-
43
19
  def to_s
44
- raise ArgumentsCountError.new unless params.count == args.count
45
- (0..params.count-1).map { |i| treat_variable(params[i], args[i]) }.join
46
- end
47
-
48
- ##
49
- # int<X> encoding
50
- def int_encode(size, arg)
51
- if arg >= 0
52
- arg.to_s(16).rjust(size / 4, '0')
53
- else
54
- mask = (1 << size) - 1
55
- (arg & mask).to_s(16)
56
- end
57
- end
58
-
59
- ##
60
- # uint<X> encoding
61
- def uint_encode(size, arg)
62
- raise InvalidFormatForDataError.new("unsigned integer #{arg} < 0") if arg < 0
63
- int_encode(size, arg)
64
- end
65
-
66
- ##
67
- # ufixed<X> encoding
68
- def ufixed_encode(size_i, size_d, arg)
69
- raise InvalidFormatForDataError.new("unsigned fixed #{arg} < 0") if arg < 0
70
- fixed_encode(size_i, size_d, arg)
71
- end
72
-
73
- ##
74
- # fixed<X> encoding
75
- def fixed_encode(size_i, size_d, arg)
76
- raise InvalidFormatForDataError.new("Please use BigDecimal !") unless arg.is_a? BigDecimal
77
- if arg >= 0
78
- int_part, dec_part = arg.to_i, arg - arg.to_i
79
- else
80
- int_part, dec_part = arg.to_i, (arg + arg.to_i.abs).abs
20
+ raise ArgumentsCountError.new("Bad number of arguments") unless args.count == params.count
21
+ head = ''
22
+ tail = ''
23
+ current_tail_position = (params.count*32)
24
+ (0..params.count-1).each do |i|
25
+ param, arg = params[i], args[i]
26
+ if Etheruby.is_static_type? param
27
+ head += Etheruby.treat_variable(param, arg, :encode).to_s
28
+ else
29
+ head += Etheruby::Encoders::Uint.new(current_tail_position).encode
30
+ content = Etheruby.treat_variable(param, arg, :encode).to_s
31
+ current_tail_position += content.length/2
32
+ tail += content
33
+ end
81
34
  end
82
- int_encode(size_i, int_part) + \
83
- decimal_representation(size_d, dec_part)
84
- end
85
-
86
- ##
87
- # Represent the decimal part in hexadimal according to the precision
88
- def decimal_representation(precision, value)
89
- (0..precision-1).map {
90
- int_part, value = (value*2).to_i, (value*2) - (value*2).to_i
91
- int_part
92
- }.each_slice(8).map { |slice|
93
- slice.join.to_i(2).to_s(16).rjust(2,'0')
94
- }.join
95
- end
96
-
97
- ##
98
- # Encode a static array
99
- def static_array_encode(type, size, arg)
100
- raise InvalidFormatForDataError.new(
101
- "Array have #{arg.count} items for #{size} sized variable"
102
- ) unless arg.count == size
103
- arg.map { |item| treat_variable(type, item) }.join
104
- end
105
-
106
- ##
107
- # Creates a dynamic array
108
- def dynamic_array_encode(type, arg)
109
- uint_encode(256, arg.count) + static_array_encode(type, arg.count, arg)
110
- end
111
-
112
- ##
113
- # byte<X> encodeing
114
- def byte_encode(size, arg)
115
- arg.map{ |b| b.to_s(16).rjust(2,'0') }.join.rjust(size,'0')
116
- end
117
-
118
- ##
119
- # address<X> encoding
120
- def address_encode(arg)
121
- uint_encode(160, arg)
122
- end
123
-
124
- ##
125
- # string<x> encoding (as bytes)
126
- def string_encode(arg)
127
- bytes_encode(arg.codepoints)
128
- end
129
-
130
- ##
131
- # bytes (dynamic size) encoding
132
- def bytes_encode(arg)
133
- uint_encode(256, arg.count) + arg.map{ |b| b.to_s(16).rjust(2,'0') }.join
134
- end
135
-
136
- ##
137
- # boolean encoding (as uint8)
138
- def bool_encode(arg)
139
- uint_encode(8, arg ? 1 : 0)
35
+ return head + tail
140
36
  end
141
37
 
142
38
  end
@@ -0,0 +1,17 @@
1
+ require_relative 'base'
2
+ require_relative 'int'
3
+
4
+ module Etheruby::Encoders
5
+
6
+ class Address < Base
7
+ def encode
8
+ Uint.new(data).encode
9
+ end
10
+
11
+ def decode
12
+ v, s = Uint.new(data).decode
13
+ return "0x#{v.to_s(16)}", s
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'base'
2
+ require_relative 'int'
3
+ require_relative '../treat_variable'
4
+
5
+ module Etheruby::Encoders
6
+
7
+ class StaticArray < Base
8
+
9
+ attr_reader :type, :size
10
+
11
+ def initialize(_type, _size, _data)
12
+ super(_data)
13
+ @type = _type
14
+ @size = _size
15
+ end
16
+
17
+ def to_s
18
+ encode
19
+ end
20
+
21
+ def encode
22
+ if Etheruby::is_static_type? type
23
+ data.map{ |d| Etheruby::treat_variable(type, d, :encode) }.join
24
+ else
25
+ head_x = ''
26
+ enc_x = ''
27
+ c_pos = size * 32
28
+ data.map { |d|
29
+ treated = Etheruby::treat_variable(type, d, :encode)
30
+ {size: treated.length/2, value: treated}
31
+ }.each { |item|
32
+ head_x += Uint.new(c_pos).encode
33
+ c_pos += item[:size]
34
+ enc_x += item[:value]
35
+ }
36
+ head_x + enc_x
37
+ end
38
+ end
39
+
40
+ def decode
41
+ if Etheruby::is_static_type?(type)
42
+ sub_decode(0, data)
43
+ else
44
+ sub_decode(size*32, data[size*2*32..data.length])
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def sub_decode(total_taken, real_data)
51
+ values = []
52
+ loop do
53
+ v, s = Etheruby::treat_variable(type, real_data, :decode)
54
+ v, s = v.decode if v.is_a? StaticArray or v.is_a? DynamicArray
55
+ values << v
56
+ total_taken += s
57
+ break if values.count == size
58
+ real_data = real_data[s*2..real_data.length]
59
+ end
60
+ return values, total_taken
61
+ end
62
+
63
+ end
64
+
65
+ class DynamicArray < Base
66
+
67
+ attr_reader :type
68
+
69
+ def initialize(_type, _data)
70
+ super(_data)
71
+ @type = _type
72
+ end
73
+
74
+ def to_s
75
+ encode
76
+ end
77
+
78
+ def encode
79
+ Uint.new(data.count).encode + \
80
+ StaticArray.new(type, data.count, data).encode
81
+ end
82
+
83
+ def decode
84
+ size, taken = Uint.new(data[0..63]).decode
85
+ v, s = StaticArray.new(type, size, data[64..data.length]).decode
86
+ return v, s+taken
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,28 @@
1
+ module Etheruby
2
+
3
+ module Encoders
4
+
5
+ class IncorrectTypeError < StandardError; end
6
+ class InvalidFormatForDataError < StandardError; end
7
+
8
+ class Base
9
+ attr_reader :data
10
+
11
+ def initialize(_data)
12
+ @data = _data
13
+ end
14
+
15
+ protected
16
+
17
+ def determinate_closest_padding(size)
18
+ if size % 32 == 0
19
+ size
20
+ else
21
+ 32*(size/32+1)
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'base'
2
+ require_relative 'int'
3
+
4
+ module Etheruby::Encoders
5
+
6
+ class Bool < Base
7
+ def encode
8
+ Uint.new(data ? 1 : 0).encode
9
+ end
10
+
11
+ def decode
12
+ Uint.new(data).decode[0] == 1
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'base'
2
+ require_relative 'int'
3
+
4
+ module Etheruby::Encoders
5
+
6
+ class Byte < Base
7
+
8
+ attr_reader :size
9
+
10
+ def initialize(_size,_data)
11
+ super(_data)
12
+ @size = _size
13
+ end
14
+
15
+ def encode
16
+ data.map{ |b|
17
+ b.to_s(16).rjust(2,'0')
18
+ }.join.ljust(determinate_closest_padding(size)*2, '0')
19
+ end
20
+
21
+ def decode
22
+ return data[0..(size*2)-1].split('').each_slice(2).map{ |b| b.join.to_i(16) },
23
+ determinate_closest_padding(size)
24
+ end
25
+
26
+ end
27
+
28
+ class Bytes < Base
29
+
30
+ def encode
31
+ Uint.new(data.length).encode + Byte.new(data.length, data).encode
32
+ end
33
+
34
+ def decode
35
+ size = Uint.new(data[0..63]).decode[0]
36
+ return Byte.new(size,data[64..data.length]).decode[0], determinate_closest_padding(size+32)
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'base'
2
+ require_relative 'int'
3
+
4
+ module Etheruby::Encoders
5
+
6
+ class FixedBase < Base
7
+ attr_reader :size_i, :size_d
8
+
9
+ def initialize(_size_i, _size_d, _data)
10
+ super(_data)
11
+ @size_i = _size_i
12
+ @size_d = _size_d
13
+ end
14
+
15
+ protected
16
+
17
+ def encode_decimal_representation(value)
18
+ (0..@size_d-1).map {
19
+ int_part, value = (value*2).to_i, (value*2) - (value*2).to_i
20
+ int_part
21
+ }.each_slice(8).map { |slice|
22
+ slice.join.to_i(2).to_s(16).rjust(2,'0')
23
+ }.join
24
+ end
25
+
26
+ def decode_decimal_representation(part)
27
+ value = 0.0
28
+ exp = -1
29
+ bin_rpr = part.to_i(16).to_s(2).rjust(@size_d,'0')
30
+ bin_rpr.split("").each do |bit|
31
+ value += 2**exp if bit == '1'
32
+ exp -= 1
33
+ end
34
+ value
35
+ end
36
+ end
37
+
38
+ class Fixed < FixedBase
39
+
40
+ def encode
41
+ if data >= 0
42
+ int_part, dec_part = data.to_i, data - data.to_i
43
+ else
44
+ int_part, dec_part = data.to_i, (data + data.to_i.abs).abs
45
+ end
46
+ Int.new(int_part).encode(size_i/4) + \
47
+ encode_decimal_representation(dec_part).ljust((256-size_i)/4,'0')
48
+ end
49
+
50
+ def decode
51
+ @data = data[0..63]
52
+ int_part = Int.new(data[0..(size_i/4)-1]).decode[0]
53
+ dec_part = decode_decimal_representation(data[(size_i / 4)..data.length])
54
+ if int_part >= 0
55
+ return (dec_part + int_part), 32
56
+ else
57
+ return -(dec_part + int_part.abs), 32
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ class Ufixed < FixedBase
64
+
65
+ def encode
66
+ raise InvalidFormatForDataError.new("Unsigned fixed #{data} < 0") if data < 0
67
+ Fixed.new(size_i, size_d, data).encode
68
+ end
69
+
70
+ def decode
71
+ @data = data[0..63]
72
+ return Uint.new(data[0..(size_i/4)-1]).decode[0] +
73
+ decode_decimal_representation(data[(size_i / 4)..data.length]), 32
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'base'
2
+
3
+ module Etheruby::Encoders
4
+
5
+ class Int < Base
6
+
7
+ def encode(pad_to=64)
8
+ if data >= 0
9
+ data.to_s(16).rjust(pad_to, '0')
10
+ else
11
+ mask = (1 << pad_to*4) - 1
12
+ (data & mask).to_s(16).rjust(pad_to, 'f')
13
+ end
14
+ end
15
+
16
+ def decode
17
+ in_int = data[0..63].to_i(16)
18
+ in_bin = in_int.to_s(2).rjust(64, '0')
19
+ if in_bin[0] == '1'
20
+ return -(in_bin.split("").map{ |i| i == '1' ? '0' : '1' }.join.to_i(2) + 1), 32
21
+ else
22
+ return in_int, 32
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ class Uint < Base
29
+
30
+ def encode(pad_to=64)
31
+ raise InvalidFormatForDataError.new("Unsigned integer #{data} < 0") if data < 0
32
+ Int.new(data).encode(pad_to)
33
+ end
34
+
35
+ def decode
36
+ return data[0..63].to_i(16), 32
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'base'
2
+ require_relative 'bytes'
3
+
4
+ module Etheruby::Encoders
5
+
6
+ class String < Base
7
+ def encode
8
+ Bytes.new(data.codepoints).encode
9
+ end
10
+
11
+ def decode
12
+ v, s = Bytes.new(data).decode
13
+ return v.pack('U*'), s
14
+ end
15
+ end
16
+
17
+ end
@@ -4,107 +4,23 @@ module Etheruby
4
4
 
5
5
  class ResponseParser
6
6
 
7
+ attr_reader :returns, :response
8
+
7
9
  def initialize(_returns, _response)
8
10
  @returns = _returns
9
- @response = _response
10
- end
11
-
12
- def treat_variable(param)
13
- if match = TypeMatchers.is_sized_type(param)
14
- # Parameter is a sized type, e.g. uint256, byte32 ...
15
- send("#{match[1]}_decode".to_sym, match[2].to_i)
16
-
17
- elsif match = TypeMatchers.is_dualsized_type(param)
18
- # Parameter is a dual sized array type, e.g. fixed16x16
19
- send("#{match[1]}_decode".to_sym, match[2].to_i, match[3].to_i)
20
-
21
- elsif match = TypeMatchers.is_static_array_type(param)
22
- # Parameter is a staticly sized array type, e.g. uint256[24]
23
- static_array_decode(match[1], match[2].to_i)
24
-
25
- elsif match = TypeMatchers.is_dynamic_array_type(param)
26
- # Parameter is a dynamicaly sized array type, e.g. uint256[]
27
- dynamic_array_decode(match[1])
28
-
29
- else
30
- # Parameter is a single-word type : string, bytes, address etc...
31
- send("#{param}_decode".to_sym)
32
-
33
- end
11
+ @response = _response[2.._response.length]
34
12
  end
35
13
 
36
14
  def parse
37
- if @returns.count == 1
38
- treat_variable(@returns[0])
15
+ if returns.count == 1
16
+ Etheruby::treat_variable(returns[0], response, :decode)
39
17
  else
40
- @returns.map { |type| treat_variable(type) }
18
+ returns.each do |type|
19
+
20
+ end
41
21
  end
42
22
  end
43
23
 
44
- # Each decode method will receive in parameters the response string remaining
45
- # to parse. It will extract the type of the response considering it as the
46
- # first thing in the string and returns the string without it. Doing this,
47
- # method calls will be chainable easily.
48
-
49
- ##
50
- # int<X> decoding
51
- def int_decode(size)
52
- end
53
-
54
- ##
55
- # uint<X> decoding
56
- def uint_decode(size)
57
- v, @response = @response[0..(size/4)].to_i(16),
58
- @response[(size/4)..@response.length]
59
- v
60
- end
61
-
62
- ##
63
- # ufixed<X> decoding
64
- def ufixed_decode(size_i, size_d)
65
- end
66
-
67
- ##
68
- # fixed<X> decoding
69
- def fixed_decode(size_i, size_d)
70
- end
71
-
72
- ##
73
- # Decodes a static array
74
- def static_array_decode(type, size)
75
- end
76
-
77
- ##
78
- # Decodes a dynamic array
79
- def dynamic_array_decode(type)
80
- end
81
-
82
- ##
83
- # byte<X> decoding
84
- def byte_decode(size)
85
- end
86
-
87
- ##
88
- # address<X> decoding
89
- def address_decode
90
- end
91
-
92
- ##
93
- # string<x> decoding (as bytes)
94
- def string_decode
95
- end
96
-
97
- ##
98
- # bytes (dynamic size) decoding
99
- def bytes_decode
100
- end
101
-
102
- ##
103
- # boolean decoding (as uint8)
104
- def bool_decode
105
- uint_decode(8) == 1
106
- end
107
-
108
24
  end
109
25
 
110
26
  end
@@ -0,0 +1,52 @@
1
+ require_relative 'encoders/address'
2
+ require_relative 'encoders/arrays'
3
+ require_relative 'encoders/bool'
4
+ require_relative 'encoders/bytes'
5
+ require_relative 'encoders/fixed'
6
+ require_relative 'encoders/int'
7
+ require_relative 'encoders/string'
8
+ require_relative 'type_matchers'
9
+
10
+ module Etheruby
11
+
12
+ def is_static_type?(param)
13
+ !(%w(string bytes).include?(param.to_s) || TypeMatchers.is_dynamic_array_type(param))
14
+ end
15
+
16
+ def is_array?(param)
17
+ TypeMatchers.is_static_array_type(param) || TypeMatchers.is_dynamic_array_type(param)
18
+ end
19
+
20
+ def treat_variable(param, arg, direction)
21
+ if match = TypeMatchers.is_sized_type(param)
22
+ # Parameter is a sized type, e.g. uint256, byte32 ...
23
+ klass = Etheruby::Encoders.const_get(match[1].capitalize)
24
+ return case klass
25
+ when Encoders::Bytes
26
+ Byte.new(match[2].to_i, arg)
27
+ else
28
+ klass.new(arg)
29
+ end.send(direction)
30
+
31
+ elsif match = TypeMatchers.is_dualsized_type(param)
32
+ # Parameter is a dual sized array type, e.g. fixed16x16, ufixed128x128
33
+ return Etheruby::Encoders.const_get(match[1].capitalize).
34
+ new(match[2].to_i, match[3].to_i, arg).send(direction)
35
+
36
+ elsif match = TypeMatchers.is_static_array_type(param)
37
+ # Parameter is a staticly sized array type, e.g. uint256[24]
38
+ Etheruby::Encoders::StaticArray.new(match[1], match[2].to_i, arg)
39
+
40
+ elsif match = TypeMatchers.is_dynamic_array_type(param)
41
+ # Parameter is a dynamicaly sized array type, e.g. uint256[]
42
+ Etheruby::Encoders::DynamicArray.new(match[1], arg).send(direction)
43
+
44
+ else
45
+ # Parameter is a single-word type : string, bytes, address etc...
46
+ Etheruby::Encoders.const_get(param.capitalize).new(arg).send(direction)
47
+
48
+ end
49
+ end
50
+
51
+ module_function :treat_variable, :is_static_type?, :is_array?
52
+ end
@@ -7,7 +7,7 @@ module Etheruby
7
7
  end
8
8
 
9
9
  def is_dualsized_type(param)
10
- param.to_s.match /^(.+)(\d+)x(\d+)$/
10
+ param.to_s.match /^([a-z]+)(\d+)x(\d+)$/
11
11
  end
12
12
 
13
13
  def is_static_array_type(param)
data/lib/etheruby.rb CHANGED
@@ -47,7 +47,7 @@ module Etheruby
47
47
  composed_body[kw] = method_info[kw] if method_info.has_key? kw
48
48
  }
49
49
  @@logger.debug("Calling #{method_info[:name]} with parameters #{composed_body.inspect}")
50
- response = Client.eth.call composed_body, "latest"
50
+ response = call_api composed_body
51
51
  if response.has_key? 'error'
52
52
  @@logger.error("Failed contract execution #{response['error']['message']}")
53
53
  else
@@ -60,6 +60,10 @@ module Etheruby
60
60
  end
61
61
  end
62
62
 
63
+ def self.call_api(composed_body)
64
+ Client.eth.call composed_body, "latest"
65
+ end
66
+
63
67
  def self.address
64
68
  "0x#{@@address.to_s(16)}"
65
69
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etheruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jérémy SEBAN
@@ -91,7 +91,16 @@ files:
91
91
  - lib/etheruby/arguments_generator.rb
92
92
  - lib/etheruby/client.rb
93
93
  - lib/etheruby/contract_method_dsl.rb
94
+ - lib/etheruby/encoders/address.rb
95
+ - lib/etheruby/encoders/arrays.rb
96
+ - lib/etheruby/encoders/base.rb
97
+ - lib/etheruby/encoders/bool.rb
98
+ - lib/etheruby/encoders/bytes.rb
99
+ - lib/etheruby/encoders/fixed.rb
100
+ - lib/etheruby/encoders/int.rb
101
+ - lib/etheruby/encoders/string.rb
94
102
  - lib/etheruby/response_parser.rb
103
+ - lib/etheruby/treat_variable.rb
95
104
  - lib/etheruby/type_matchers.rb
96
105
  homepage: https://github.com/MechanicalSloth/etheruby
97
106
  licenses: