eth 0.5.14 → 0.5.16

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.
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  # -*- encoding : ascii-8bit -*-
16
+ require "bigdecimal"
16
17
 
17
18
  # Provides the {Eth} module.
18
19
  module Eth
@@ -33,55 +34,18 @@ module Eth
33
34
  def type(type, arg)
34
35
  if %w(string bytes).include? type.base_type and type.sub_type.empty? and type.dimensions.empty?
35
36
  raise EncodingError, "Argument must be a String" unless arg.instance_of? String
37
+ arg = handle_hex_string arg, type
36
38
 
37
39
  # encodes strings and bytes
38
40
  size = type Type.size_type, arg.size
39
41
  padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
40
42
  "#{size}#{arg}#{padding}"
41
- elsif type.base_type == "tuple" && type.dimensions.size == 1 && type.dimensions[0] != 0
42
- result = ""
43
- result += struct_offsets(type.nested_sub, arg)
44
- result += arg.map { |x| type(type.nested_sub, x) }.join
45
- result
46
- elsif type.dynamic? && arg.is_a?(Array)
47
-
48
- # encodes dynamic-sized arrays
49
- head, tail = "", ""
50
- head += type(Type.size_type, arg.size)
51
- nested_sub = type.nested_sub
52
-
53
- # calculate offsets
54
- if %w(string bytes).include?(type.base_type) && type.sub_type.empty?
55
- offset = 0
56
- arg.size.times do |i|
57
- if i == 0
58
- offset = arg.size * 32
59
- else
60
- number_of_words = ((arg[i - 1].size + 32 - 1) / 32).floor
61
- total_bytes_length = number_of_words * 32
62
- offset += total_bytes_length + 32
63
- end
64
-
65
- head += type(Type.size_type, offset)
66
- end
67
- elsif nested_sub.base_type == "tuple" && nested_sub.dynamic?
68
- head += struct_offsets(nested_sub, arg)
69
- end
70
-
71
- arg.size.times do |i|
72
- head += type nested_sub, arg[i]
73
- end
74
- "#{head}#{tail}"
43
+ elsif type.base_type == "tuple" && type.dimensions.empty?
44
+ tuple arg, type
45
+ elsif !type.dimensions.empty?
46
+ encode_array type, arg
75
47
  else
76
- if type.dimensions.empty?
77
-
78
- # encode a primitive type
79
- primitive_type type, arg
80
- else
81
-
82
- # encode static-size arrays
83
- arg.map { |x| type(type.nested_sub, x) }.join
84
- end
48
+ primitive_type type, arg
85
49
  end
86
50
  end
87
51
 
@@ -122,6 +86,7 @@ module Eth
122
86
 
123
87
  # Properly encodes unsigned integers.
124
88
  def uint(arg, type)
89
+ arg = coerce_number arg
125
90
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
126
91
  raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::UINT_MAX or arg < Constant::UINT_MIN
127
92
  real_size = type.sub_type.to_i
@@ -132,6 +97,7 @@ module Eth
132
97
 
133
98
  # Properly encodes signed integers.
134
99
  def int(arg, type)
100
+ arg = coerce_number arg
135
101
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
136
102
  raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::INT_MAX or arg < Constant::INT_MIN
137
103
  real_size = type.sub_type.to_i
@@ -148,6 +114,7 @@ module Eth
148
114
 
149
115
  # Properly encodes unsigned fixed-point numbers.
150
116
  def ufixed(arg, type)
117
+ arg = coerce_number arg
151
118
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
152
119
  high, low = type.sub_type.split("x").map(&:to_i)
153
120
  raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
@@ -156,6 +123,7 @@ module Eth
156
123
 
157
124
  # Properly encodes signed fixed-point numbers.
158
125
  def fixed(arg, type)
126
+ arg = coerce_number arg
159
127
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
160
128
  high, low = type.sub_type.split("x").map(&:to_i)
161
129
  raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
@@ -185,8 +153,11 @@ module Eth
185
153
 
186
154
  # Properly encodes tuples.
187
155
  def tuple(arg, type)
188
- raise EncodingError, "Expecting Hash: #{arg}" unless arg.instance_of? Hash
156
+ unless arg.is_a?(Hash) || arg.is_a?(Array)
157
+ raise EncodingError, "Expecting Hash or Array: #{arg}"
158
+ end
189
159
  raise EncodingError, "Expecting #{type.components.size} elements: #{arg}" unless arg.size == type.components.size
160
+ arg = arg.transform_keys(&:to_s) if arg.is_a?(Hash) # because component_type.name is String
190
161
 
191
162
  static_size = 0
192
163
  type.components.each_with_index do |component, i|
@@ -209,28 +180,84 @@ module Eth
209
180
  dynamic_values << dynamic_value
210
181
  dynamic_offset += dynamic_value.size
211
182
  else
212
- offsets_and_static_values << type(component_type, arg.is_a?(Array) ? arg[i] : arg[component_type.name])
183
+ offsets_and_static_values << type(component_type, arg.is_a?(Array) ? arg[i] : arg.fetch(component_type.name))
213
184
  end
214
185
  end
215
186
 
216
187
  offsets_and_static_values.join + dynamic_values.join
217
188
  end
218
189
 
219
- # Properly encode struct offsets.
220
- def struct_offsets(type, arg)
221
- result = ""
222
- offset = arg.size
223
- tails_encoding = arg.map { |a| type(type, a) }
224
- arg.size.times do |i|
225
- if i == 0
226
- offset *= 32
227
- else
228
- offset += tails_encoding[i - 1].size
190
+ def coerce_number(arg)
191
+ return arg if arg.is_a? Numeric
192
+ return arg.to_i(0) if arg.is_a?(String) && arg.match?(/^-?(0x)?[0-9a-fA-F]+$/)
193
+ return BigDecimal(arg) if arg.is_a?(String) && arg.match?(/^-?\d+(\.\d+)?$/)
194
+ arg
195
+ end
196
+
197
+ # Encodes array values of any dimensionality.
198
+ #
199
+ # @param type [Eth::Abi::Type] the type describing the array.
200
+ # @param values [Array] the Ruby values to encode.
201
+ # @return [String] ABI encoded array payload.
202
+ # @raise [EncodingError] if the value cardinality does not match static dimensions.
203
+ def encode_array(type, values)
204
+ raise EncodingError, "Expecting Array value" unless values.is_a?(Array)
205
+
206
+ required_length = type.dimensions.last
207
+ if required_length != 0 && values.size != required_length
208
+ raise EncodingError, "Expecting #{required_length} elements: #{values.size} provided"
209
+ end
210
+
211
+ nested_sub = type.nested_sub
212
+
213
+ if required_length.zero?
214
+ encode_dynamic_array(nested_sub, values)
215
+ else
216
+ encode_static_array(nested_sub, values)
217
+ end
218
+ end
219
+
220
+ # Encodes dynamic-sized arrays, including nested tuples.
221
+ #
222
+ # @param nested_sub [Eth::Abi::Type] the element type.
223
+ # @param values [Array] elements to encode.
224
+ # @return [String] ABI encoded dynamic array payload.
225
+ def encode_dynamic_array(nested_sub, values)
226
+ head = type(Type.size_type, values.size)
227
+ element_heads, element_tails = encode_array_elements(nested_sub, values)
228
+ head + element_heads + element_tails
229
+ end
230
+
231
+ # Encodes static-sized arrays, including nested tuples.
232
+ #
233
+ # @param nested_sub [Eth::Abi::Type] the element type.
234
+ # @param values [Array] elements to encode.
235
+ # @return [String] ABI encoded static array payload.
236
+ def encode_static_array(nested_sub, values)
237
+ element_heads, element_tails = encode_array_elements(nested_sub, values)
238
+ element_heads + element_tails
239
+ end
240
+
241
+ # Encodes the head/tail portions for array elements.
242
+ #
243
+ # @param nested_sub [Eth::Abi::Type] the element type.
244
+ # @param values [Array] elements to encode.
245
+ # @return [Array<String, String>] head/tail encoded segments.
246
+ def encode_array_elements(nested_sub, values)
247
+ if nested_sub.dynamic?
248
+ head = ""
249
+ tail = ""
250
+ offset = values.size * 32
251
+ values.each do |value|
252
+ encoded = type(nested_sub, value)
253
+ head += type(Type.size_type, offset)
254
+ tail += encoded
255
+ offset += encoded.size
229
256
  end
230
- offset_string = type(Type.size_type, offset)
231
- result += offset_string
257
+ [head, tail]
258
+ else
259
+ [values.map { |value| type(nested_sub, value) }.join, ""]
232
260
  end
233
- result
234
261
  end
235
262
 
236
263
  # Properly encodes hash-strings.
@@ -0,0 +1,124 @@
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -*- encoding : ascii-8bit -*-
16
+
17
+ # Provides the {Eth} module.
18
+ module Eth
19
+
20
+ # Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI).
21
+ module Abi
22
+
23
+ # Provides a module to decode transaction input data.
24
+ module Function
25
+ extend self
26
+
27
+ # Build function signature string from ABI interface.
28
+ #
29
+ # @param interface [Hash] ABI function interface.
30
+ # @return [String] interface signature string.
31
+ def signature(interface)
32
+ name = interface.fetch("name")
33
+ inputs = interface.fetch("inputs", [])
34
+ types = inputs.map { |i| type(i) }
35
+ "#{name}(#{types.join(",")})"
36
+ end
37
+
38
+ # Compute selector for ABI function interface.
39
+ #
40
+ # @param interface [Hash] ABI function interface.
41
+ # @return [String] a hex-string selector.
42
+ def selector(interface)
43
+ sig = signature(interface)
44
+ Util.prefix_hex(Util.bin_to_hex(Util.keccak256(sig))[0, 8])
45
+ end
46
+
47
+ # Gets the input type for functions.
48
+ #
49
+ # @param input [Hash] function input.
50
+ # @return [String] input type.
51
+ def type(input)
52
+ if input["type"] == "tuple"
53
+ "(#{input["components"].map { |c| type(c) }.join(",")})"
54
+ elsif input["type"] == "enum"
55
+ "uint8"
56
+ else
57
+ input["type"]
58
+ end
59
+ end
60
+
61
+ # A decoded function call.
62
+ class CallDescription
63
+ # The function ABI interface used to decode the call.
64
+ attr_accessor :function_interface
65
+
66
+ # The positional arguments of the call.
67
+ attr_accessor :args
68
+
69
+ # The named arguments of the call.
70
+ attr_accessor :kwargs
71
+
72
+ # The function selector.
73
+ attr_accessor :selector
74
+
75
+ # Creates a description object for a decoded function call.
76
+ #
77
+ # @param function_interface [Hash] function ABI type.
78
+ # @param selector [String] function selector hex-string.
79
+ # @param args [Array] decoded positional arguments.
80
+ # @param kwargs [Hash] decoded keyword arguments.
81
+ def initialize(function_interface, selector, args, kwargs)
82
+ @function_interface = function_interface
83
+ @selector = selector
84
+ @args = args
85
+ @kwargs = kwargs
86
+ end
87
+
88
+ # The function name. (e.g. transfer)
89
+ def name
90
+ @name ||= function_interface.fetch("name")
91
+ end
92
+
93
+ # The function signature. (e.g. transfer(address,uint256))
94
+ def signature
95
+ @signature ||= Function.signature(function_interface)
96
+ end
97
+ end
98
+
99
+ # Decodes a transaction input with a set of ABI interfaces.
100
+ #
101
+ # @param interfaces [Array] function ABI types.
102
+ # @param data [String] transaction input data.
103
+ # @return [CallDescription, nil] a CallDescription object or nil if selector unknown.
104
+ def decode(interfaces, data)
105
+ data = Util.remove_hex_prefix(data)
106
+ selector = Util.prefix_hex(data[0, 8])
107
+ payload = Util.prefix_hex(data[8..] || "")
108
+
109
+ selector_to_interfaces = Hash[interfaces.map { |i| [selector(i), i] }]
110
+ if (interface = selector_to_interfaces[selector])
111
+ inputs = interface.fetch("inputs", [])
112
+ types = inputs.map { |i| type(i) }
113
+ args = Abi.decode(types, payload)
114
+ kwargs = {}
115
+ inputs.each_with_index do |input, i|
116
+ name = input.fetch("name", "")
117
+ kwargs[name.to_sym] = args[i] unless name.empty?
118
+ end
119
+ CallDescription.new(interface, selector, args, kwargs)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
data/lib/eth/abi/type.rb CHANGED
@@ -80,10 +80,27 @@ module Eth
80
80
  return
81
81
  end
82
82
 
83
- _, base_type, sub_type, dimension = /([a-z]*)([0-9]*x?[0-9]*)((\[[0-9]*\])*)/.match(type).to_a
83
+ # ensure the type string is reasonable before attempting to parse
84
+ raise ParseError, "Invalid type format" unless type.is_a? String
85
+
86
+ if type.start_with?("tuple(") || type.start_with?("(")
87
+ tuple_str = type.start_with?("tuple(") ? type : "tuple#{type}"
88
+ inner, rest = extract_tuple(tuple_str)
89
+ inner_types = split_tuple_types(inner)
90
+ inner_types.each { |t| Type.parse(t) }
91
+ base_type = "tuple"
92
+ sub_type = ""
93
+ dimension = rest
94
+ components ||= inner_types.map { |t| { "type" => t } }
95
+ else
96
+ match = /\A([a-z]+)([0-9]*x?[0-9]*)((?:\[\d+\]|\[\])*)\z/.match(type)
97
+ raise ParseError, "Invalid type format" unless match
98
+ _, base_type, sub_type, dimension = match.to_a
99
+ sub_type = "256" if %w[uint int].include?(base_type) && sub_type.empty?
100
+ end
84
101
 
85
- # type dimension can only be numeric
86
- dims = dimension.scan(/\[[0-9]*\]/)
102
+ # type dimension can only be numeric or empty for dynamic arrays
103
+ dims = dimension.scan(/\[\d+\]|\[\]/)
87
104
  raise ParseError, "Unknown characters found in array declaration" if dims.join != dimension
88
105
 
89
106
  # enforce base types
@@ -93,7 +110,7 @@ module Eth
93
110
  sub_type = sub_type.to_s
94
111
  @base_type = base_type
95
112
  @sub_type = sub_type
96
- @dimensions = dims.map { |x| x[1...-1].to_i }
113
+ @dimensions = dims.map { |x| x == "[]" ? 0 : x[1...-1].to_i }
97
114
  @components = components.map { |component| Abi::Type.parse(component["type"], component.dig("components"), component.dig("name")) } if components&.any?
98
115
  @name = component_name
99
116
  end
@@ -123,7 +140,7 @@ module Eth
123
140
  if dimensions.empty?
124
141
  if !(["string", "bytes", "tuple"].include?(base_type) and sub_type.empty?)
125
142
  s = 32
126
- elsif base_type == "tuple" and components.none?(&:dynamic?)
143
+ elsif base_type == "tuple" and components&.none?(&:dynamic?)
127
144
  s = components.sum(&:size)
128
145
  end
129
146
  elsif dimensions.last != 0 and !nested_sub.dynamic?
@@ -215,6 +232,53 @@ module Eth
215
232
  raise ParseError, "Unknown base type"
216
233
  end
217
234
  end
235
+
236
+ # Extracts the inner type list and trailing dimensions from an inline tuple definition.
237
+ def extract_tuple(type)
238
+ idx = 6 # skip "tuple("
239
+ depth = 1
240
+ while idx < type.length && depth > 0
241
+ case type[idx]
242
+ when "("
243
+ depth += 1
244
+ when ")"
245
+ depth -= 1
246
+ end
247
+ idx += 1
248
+ end
249
+ raise ParseError, "Invalid tuple format" unless depth.zero?
250
+ inner = type[6...(idx - 1)]
251
+ rest = type[idx..] || ""
252
+ [inner, rest]
253
+ end
254
+
255
+ # Splits a tuple component list into individual type strings, handling nested tuples.
256
+ def split_tuple_types(str)
257
+ types = []
258
+ depth = 0
259
+ current = ""
260
+ str.each_char do |ch|
261
+ case ch
262
+ when "("
263
+ depth += 1
264
+ current << ch
265
+ when ")"
266
+ depth -= 1
267
+ current << ch
268
+ when ","
269
+ if depth.zero?
270
+ types << current
271
+ current = ""
272
+ else
273
+ current << ch
274
+ end
275
+ else
276
+ current << ch
277
+ end
278
+ end
279
+ types << current unless current.empty?
280
+ types
281
+ end
218
282
  end
219
283
  end
220
284
  end
data/lib/eth/abi.rb CHANGED
@@ -44,6 +44,7 @@ module Eth
44
44
  return solidity_packed(types, args) if packed
45
45
  types = [types] unless types.instance_of? Array
46
46
  args = [args] unless args.instance_of? Array
47
+ raise ArgumentError, "Types and values must be the same length" if types.length != args.length
47
48
 
48
49
  # parse all types
49
50
  parsed_types = types.map { |t| Type === t ? t : Type.parse(t) }
@@ -151,4 +152,5 @@ require "eth/abi/packed/encoder"
151
152
  require "eth/abi/decoder"
152
153
  require "eth/abi/encoder"
153
154
  require "eth/abi/event"
155
+ require "eth/abi/function"
154
156
  require "eth/abi/type"
data/lib/eth/bls.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bls"
4
+
5
+ module Eth
6
+ # Helper methods for interacting with BLS12-381 points and signatures
7
+ module Bls
8
+ module_function
9
+
10
+ # Decode a compressed G1 public key from hex.
11
+ # @param [String] hex a compressed G1 point
12
+ # @return [BLS::PointG1]
13
+ def decode_public_key(hex)
14
+ BLS::PointG1.from_hex Util.remove_hex_prefix(hex)
15
+ end
16
+
17
+ # Encode a G1 public key to compressed hex.
18
+ # @param [BLS::PointG1] point
19
+ # @return [String] hex string prefixed with 0x
20
+ def encode_public_key(point)
21
+ Util.prefix_hex point.to_hex(compressed: true)
22
+ end
23
+
24
+ # Decode a compressed G2 signature from hex.
25
+ # @param [String] hex a compressed G2 point
26
+ # @return [BLS::PointG2]
27
+ def decode_signature(hex)
28
+ BLS::PointG2.from_hex Util.remove_hex_prefix(hex)
29
+ end
30
+
31
+ # Encode a G2 signature to compressed hex.
32
+ # @param [BLS::PointG2] point
33
+ # @return [String] hex string prefixed with 0x
34
+ def encode_signature(point)
35
+ Util.prefix_hex point.to_hex(compressed: true)
36
+ end
37
+
38
+ # Derive a compressed public key from a private key.
39
+ # @param [String] priv_hex private key as hex
40
+ # @return [String] compressed G1 public key (hex)
41
+ def get_public_key(priv_hex)
42
+ key = BLS.get_public_key Util.remove_hex_prefix(priv_hex)
43
+ encode_public_key key
44
+ end
45
+
46
+ # Sign a message digest with the given private key.
47
+ # @param [String] message message digest (hex)
48
+ # @param [String] priv_hex private key as hex
49
+ # @return [String] compressed G2 signature (hex)
50
+ def sign(message, priv_hex)
51
+ sig = BLS.sign Util.remove_hex_prefix(message),
52
+ Util.remove_hex_prefix(priv_hex)
53
+ encode_signature sig
54
+ end
55
+
56
+ # Verify a BLS signature using pairings. This mirrors the behaviour of
57
+ # the BLS12-381 pairing precompile.
58
+ # @param [String] message message digest (hex)
59
+ # @param [String] signature_hex compressed G2 signature (hex)
60
+ # @param [String] pubkey_hex compressed G1 public key (hex)
61
+ # @return [Boolean] verification result
62
+ def verify(message, signature_hex, pubkey_hex)
63
+ signature = decode_signature(signature_hex)
64
+ pubkey = decode_public_key(pubkey_hex)
65
+ BLS.verify(signature, Util.remove_hex_prefix(message), pubkey)
66
+ end
67
+ end
68
+ end
data/lib/eth/chain.rb CHANGED
@@ -161,6 +161,9 @@ module Eth
161
161
  # Chain ID for Holesovice testnet.
162
162
  HOLESKY = HOLESOVICE
163
163
 
164
+ # Chain ID for Basecamp testnet.
165
+ BASECAMP = 123420001114.freeze
166
+
164
167
  # Chain ID for the geth private network preset.
165
168
  PRIVATE_GETH = 1337.freeze
166
169
 
@@ -12,7 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- require "net/http"
15
+ require "uri"
16
+ require "httpx"
16
17
 
17
18
  # Provides the {Eth} module.
18
19
  module Eth
@@ -57,6 +58,7 @@ module Eth
57
58
  else
58
59
  @uri = uri
59
60
  end
61
+ @client = HTTPX.plugin(:persistent).with(headers: { "Content-Type" => "application/json" })
60
62
  end
61
63
 
62
64
  # Sends an RPC request to the connected HTTP client.
@@ -64,13 +66,8 @@ module Eth
64
66
  # @param payload [Hash] the RPC request parameters.
65
67
  # @return [String] a JSON-encoded response.
66
68
  def send_request(payload)
67
- http = Net::HTTP.new(@host, @port)
68
- http.use_ssl = @ssl
69
- header = { "Content-Type" => "application/json" }
70
- request = Net::HTTP::Post.new(@uri, header)
71
- request.body = payload
72
- response = http.request(request)
73
- response.body
69
+ response = @client.post(@uri, body: payload)
70
+ response.body.to_s
74
71
  end
75
72
  end
76
73