eth 0.5.13 → 0.5.15

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql.yml +1 -1
  3. data/.github/workflows/docs.yml +2 -2
  4. data/.github/workflows/spec.yml +1 -3
  5. data/CHANGELOG.md +33 -0
  6. data/CODE_OF_CONDUCT.md +3 -5
  7. data/Gemfile +3 -3
  8. data/LICENSE.txt +1 -1
  9. data/README.md +6 -6
  10. data/SECURITY.md +2 -2
  11. data/eth.gemspec +4 -1
  12. data/lib/eth/abi/decoder.rb +18 -7
  13. data/lib/eth/abi/encoder.rb +14 -26
  14. data/lib/eth/abi/event.rb +5 -1
  15. data/lib/eth/abi/function.rb +124 -0
  16. data/lib/eth/abi/packed/encoder.rb +196 -0
  17. data/lib/eth/abi/type.rb +77 -16
  18. data/lib/eth/abi.rb +29 -2
  19. data/lib/eth/address.rb +3 -1
  20. data/lib/eth/api.rb +1 -1
  21. data/lib/eth/chain.rb +9 -1
  22. data/lib/eth/client/http.rb +7 -3
  23. data/lib/eth/client/ipc.rb +1 -1
  24. data/lib/eth/client.rb +38 -37
  25. data/lib/eth/constant.rb +1 -1
  26. data/lib/eth/contract/error.rb +62 -0
  27. data/lib/eth/contract/event.rb +69 -16
  28. data/lib/eth/contract/function.rb +22 -1
  29. data/lib/eth/contract/function_input.rb +1 -1
  30. data/lib/eth/contract/function_output.rb +12 -4
  31. data/lib/eth/contract/initializer.rb +1 -1
  32. data/lib/eth/contract.rb +56 -5
  33. data/lib/eth/eip712.rb +49 -13
  34. data/lib/eth/ens/coin_type.rb +1 -1
  35. data/lib/eth/ens/resolver.rb +1 -1
  36. data/lib/eth/ens.rb +1 -1
  37. data/lib/eth/key/decrypter.rb +1 -1
  38. data/lib/eth/key/encrypter.rb +1 -1
  39. data/lib/eth/key.rb +2 -2
  40. data/lib/eth/rlp/decoder.rb +1 -1
  41. data/lib/eth/rlp/encoder.rb +1 -1
  42. data/lib/eth/rlp/sedes/big_endian_int.rb +1 -1
  43. data/lib/eth/rlp/sedes/binary.rb +1 -1
  44. data/lib/eth/rlp/sedes/list.rb +1 -1
  45. data/lib/eth/rlp/sedes.rb +1 -1
  46. data/lib/eth/rlp.rb +1 -1
  47. data/lib/eth/signature.rb +1 -1
  48. data/lib/eth/solidity.rb +1 -1
  49. data/lib/eth/tx/eip1559.rb +33 -8
  50. data/lib/eth/tx/eip2930.rb +32 -7
  51. data/lib/eth/tx/eip4844.rb +389 -0
  52. data/lib/eth/tx/eip7702.rb +520 -0
  53. data/lib/eth/tx/legacy.rb +31 -7
  54. data/lib/eth/tx.rb +88 -1
  55. data/lib/eth/unit.rb +1 -1
  56. data/lib/eth/util.rb +20 -8
  57. data/lib/eth/version.rb +2 -2
  58. data/lib/eth.rb +1 -1
  59. metadata +26 -16
@@ -0,0 +1,196 @@
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
+ # Encapsulates the module for non-standard packed encoding used in Solidity.
24
+ module Packed
25
+
26
+ # Provides a utility module to assist encoding ABIs.
27
+ module Encoder
28
+ extend self
29
+
30
+ # Encodes a specific value, either static or dynamic in non-standard
31
+ # packed encoding mode.
32
+ #
33
+ # @param type [Eth::Abi::Type] type to be encoded.
34
+ # @param arg [String|Number] value to be encoded.
35
+ # @return [String] the packed encoded type.
36
+ # @raise [EncodingError] if value does not match type.
37
+ # @raise [ArgumentError] if encoding fails for type.
38
+ def type(type, arg)
39
+ case type
40
+ when /^uint(\d+)$/
41
+ uint(arg, $1.to_i / 8)
42
+ when /^int(\d+)$/
43
+ int(arg, $1.to_i / 8)
44
+ when "bool"
45
+ bool(arg)
46
+ when /^ureal(\d+)x(\d+)$/, /^ufixed(\d+)x(\d+)$/
47
+ ufixed(arg, $1.to_i / 8, $2.to_i)
48
+ when /^real(\d+)x(\d+)$/, /^fixed(\d+)x(\d+)$/
49
+ fixed(arg, $1.to_i / 8, $2.to_i)
50
+ when "string"
51
+ string(arg)
52
+ when /^bytes(\d+)$/
53
+ bytes(arg, $1.to_i)
54
+ when "bytes"
55
+ string(arg)
56
+ when /^tuple\((.+)\)$/
57
+ tuple($1.split(","), arg)
58
+ when /^hash(\d+)$/
59
+ hash(arg, $1.to_i / 8)
60
+ when "address"
61
+ address(arg)
62
+ when /^(.+)\[\]$/
63
+ array($1, arg)
64
+ when /^(.+)\[(\d+)\]$/
65
+ fixed_array($1, arg, $2.to_i)
66
+ else
67
+ raise EncodingError, "Unhandled type: #{type}"
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Properly encodes signed integers.
74
+ def uint(value, byte_size)
75
+ raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
76
+ raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::UINT_MAX or value < Constant::UINT_MIN
77
+ i = value.to_i
78
+ Util.zpad_int i, byte_size
79
+ end
80
+
81
+ # Properly encodes signed integers.
82
+ def int(value, byte_size)
83
+ raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
84
+ raise ValueOutOfBounds, "Number out of range: #{value}" if value > Constant::INT_MAX or value < Constant::INT_MIN
85
+ real_size = byte_size * 8
86
+ i = value.to_i % 2 ** real_size
87
+ Util.zpad_int i, byte_size
88
+ end
89
+
90
+ # Properly encodes booleans.
91
+ def bool(value)
92
+ raise EncodingError, "Argument is not bool: #{value}" unless value.instance_of? TrueClass or value.instance_of? FalseClass
93
+ (value ? "\x01" : "\x00").b
94
+ end
95
+
96
+ # Properly encodes unsigned fixed-point numbers.
97
+ def ufixed(value, byte_size, decimals)
98
+ raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
99
+ raise ValueOutOfBounds, value unless value >= 0 and value < 2 ** decimals
100
+ scaled_value = (value * (10 ** decimals)).to_i
101
+ uint(scaled_value, byte_size)
102
+ end
103
+
104
+ # Properly encodes signed fixed-point numbers.
105
+ def fixed(value, byte_size, decimals)
106
+ raise ArgumentError, "Don't know how to handle this input." unless value.is_a? Numeric
107
+ raise ValueOutOfBounds, value unless value >= -2 ** (decimals - 1) and value < 2 ** (decimals - 1)
108
+ scaled_value = (value * (10 ** decimals)).to_i
109
+ int(scaled_value, byte_size)
110
+ end
111
+
112
+ # Properly encodes byte(-string)s.
113
+ def bytes(value, length)
114
+ raise EncodingError, "Expecting String: #{value}" unless value.instance_of? String
115
+ value = handle_hex_string value, length
116
+ raise ArgumentError, "Value must be a string of length #{length}" unless value.is_a?(String) && value.bytesize == length
117
+ value.b
118
+ end
119
+
120
+ # Properly encodes (byte-)strings.
121
+ def string(value)
122
+ raise ArgumentError, "Value must be a string" unless value.is_a?(String)
123
+ value.b
124
+ end
125
+
126
+ # Properly encodes tuples.
127
+ def tuple(types, values)
128
+ Abi.solidity_packed(types, values)
129
+ end
130
+
131
+ # Properly encodes hash-strings.
132
+ def hash(value, byte_size)
133
+ raise EncodingError, "Argument too long: #{value}" unless byte_size > 0 and byte_size <= 32
134
+ hash_bytes = handle_hex_string value, byte_size
135
+ hash_bytes.b
136
+ end
137
+
138
+ # Properly encodes addresses.
139
+ def address(value)
140
+ if value.is_a? Address
141
+
142
+ # from checksummed address with 0x prefix
143
+ Util.zpad_hex value.to_s[2..-1], 20
144
+ elsif value.is_a? Integer
145
+
146
+ # address from integer
147
+ Util.zpad_int value, 20
148
+ elsif value.size == 20
149
+
150
+ # address from encoded address
151
+ Util.zpad value, 20
152
+ elsif value.size == 40
153
+
154
+ # address from hexadecimal address
155
+ Util.zpad_hex value, 20
156
+ elsif value.size == 42 and value[0, 2] == "0x"
157
+
158
+ # address from hexadecimal address with 0x prefix
159
+ Util.zpad_hex value[2..-1], 20
160
+ else
161
+ raise EncodingError, "Could not parse address: #{value}"
162
+ end
163
+ end
164
+
165
+ # Properly encodes dynamic-sized arrays.
166
+ def array(type, values)
167
+ values.map { |value| type(type, value) }.join.b
168
+ end
169
+
170
+ # Properly encodes fixed-size arrays.
171
+ def fixed_array(type, values, size)
172
+ raise ArgumentError, "Array size does not match" unless values.size == size
173
+ array(type, values)
174
+ end
175
+
176
+ # The ABI encoder needs to be able to determine between a hex `"123"`
177
+ # and a binary `"123"` string.
178
+ def handle_hex_string(val, len)
179
+ if Util.prefixed? val or
180
+ (len === val.size / 2 and Util.hex? val)
181
+
182
+ # There is no way telling whether a string is hex or binary with certainty
183
+ # in Ruby. Therefore, we assume a `0x` prefix to indicate a hex string.
184
+ # Additionally, if the string size is exactly the double of the expected
185
+ # binary size, we can assume a hex value.
186
+ Util.hex_to_bin val
187
+ else
188
+
189
+ # Everything else will be assumed binary or raw string.
190
+ val.b
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
data/lib/eth/abi/type.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -80,10 +80,24 @@ 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) && type.bytesize <= 256
84
85
 
85
- # type dimension can only be numeric
86
- dims = dimension.scan(/\[[0-9]*\]/)
86
+ if type.start_with?("tuple(")
87
+ inner, rest = extract_tuple(type)
88
+ inner_types = split_tuple_types(inner)
89
+ inner_types.each { |t| Type.parse(t) }
90
+ base_type = "tuple"
91
+ sub_type = ""
92
+ dimension = rest
93
+ else
94
+ match = /\A([a-z]+)([0-9]*x?[0-9]*)((?:\[\d+\]|\[\])*)\z/.match(type)
95
+ raise ParseError, "Invalid type format" unless match
96
+ _, base_type, sub_type, dimension = match.to_a
97
+ end
98
+
99
+ # type dimension can only be numeric or empty for dynamic arrays
100
+ dims = dimension.scan(/\[\d+\]|\[\]/)
87
101
  raise ParseError, "Unknown characters found in array declaration" if dims.join != dimension
88
102
 
89
103
  # enforce base types
@@ -93,8 +107,8 @@ module Eth
93
107
  sub_type = sub_type.to_s
94
108
  @base_type = base_type
95
109
  @sub_type = sub_type
96
- @dimensions = dims.map { |x| x[1...-1].to_i }
97
- @components = components.map { |component| Abi::Type.parse(component["type"], component.dig("components"), component.dig("name")) } unless components.nil?
110
+ @dimensions = dims.map { |x| x == "[]" ? 0 : x[1...-1].to_i }
111
+ @components = components.map { |component| Abi::Type.parse(component["type"], component.dig("components"), component.dig("name")) } if components&.any?
98
112
  @name = component_name
99
113
  end
100
114
 
@@ -123,10 +137,10 @@ module Eth
123
137
  if dimensions.empty?
124
138
  if !(["string", "bytes", "tuple"].include?(base_type) and sub_type.empty?)
125
139
  s = 32
126
- elsif base_type == "tuple" && components.none?(&:dynamic?)
140
+ elsif base_type == "tuple" and components&.none?(&:dynamic?)
127
141
  s = components.sum(&:size)
128
142
  end
129
- elsif dimensions.last != 0 && !nested_sub.dynamic?
143
+ elsif dimensions.last != 0 and !nested_sub.dynamic?
130
144
  s = dimensions.last * nested_sub.size
131
145
  end
132
146
  @size ||= s
@@ -153,7 +167,7 @@ module Eth
153
167
  if base_type == "tuple"
154
168
  "(" + components.map(&:to_s).join(",") + ")" + (dimensions.size > 0 ? dimensions.map { |x| "[#{x == 0 ? "" : x}]" }.join : "")
155
169
  elsif dimensions.empty?
156
- if %w[string bytes].include?(base_type) && sub_type.empty?
170
+ if %w[string bytes].include?(base_type) and sub_type.empty?
157
171
  base_type
158
172
  else
159
173
  "#{base_type}#{sub_type}"
@@ -175,7 +189,7 @@ module Eth
175
189
  when "bytes"
176
190
 
177
191
  # bytes can be no longer than 32 bytes
178
- raise ParseError, "Maximum 32 bytes for fixed-length string or bytes" unless sub_type.empty? || sub_type.to_i <= 32
192
+ raise ParseError, "Maximum 32 bytes for fixed-length string or bytes" unless sub_type.empty? or (sub_type.to_i <= 32 and sub_type.to_i > 0)
179
193
  when "tuple"
180
194
 
181
195
  # tuples can not have any suffix
@@ -187,16 +201,16 @@ module Eth
187
201
 
188
202
  # integer size must be valid
189
203
  size = sub_type.to_i
190
- raise ParseError, "Integer size out of bounds" unless size >= 8 && size <= 256
204
+ raise ParseError, "Integer size out of bounds" unless size >= 8 and size <= 256
191
205
  raise ParseError, "Integer size must be multiple of 8" unless size % 8 == 0
192
206
  when "ureal", "real", "fixed", "ufixed"
193
207
 
194
208
  # floats must have valid dimensional suffix
195
- raise ParseError, "Real type must have suffix of form <high>x<low>, e.g. 128x128" unless sub_type =~ /\A[0-9]+x[0-9]+\z/
196
- high, low = sub_type.split("x").map(&:to_i)
197
- total = high + low
198
- raise ParseError, "Real size out of bounds (max 32 bytes)" unless total >= 8 && total <= 256
199
- raise ParseError, "Real high/low sizes must be multiples of 8" unless high % 8 == 0 && low % 8 == 0
209
+ raise ParseError, "Real type must have suffix of form <size>x<decimals>, e.g. 128x128" unless sub_type =~ /\A[0-9]+x[0-9]+\z/
210
+ size, decimals = sub_type.split("x").map(&:to_i)
211
+ total = size + decimals
212
+ raise ParseError, "Real size out of bounds (max 32 bytes)" unless total >= 8 and total <= 256
213
+ raise ParseError, "Real size must be multiples of 8" unless size % 8 == 0
200
214
  when "hash"
201
215
 
202
216
  # hashs must have numerical suffix
@@ -215,6 +229,53 @@ module Eth
215
229
  raise ParseError, "Unknown base type"
216
230
  end
217
231
  end
232
+
233
+ # Extracts the inner type list and trailing dimensions from an inline tuple definition.
234
+ def extract_tuple(type)
235
+ idx = 6 # skip "tuple("
236
+ depth = 1
237
+ while idx < type.length && depth > 0
238
+ case type[idx]
239
+ when "("
240
+ depth += 1
241
+ when ")"
242
+ depth -= 1
243
+ end
244
+ idx += 1
245
+ end
246
+ raise ParseError, "Invalid tuple format" unless depth.zero?
247
+ inner = type[6...(idx - 1)]
248
+ rest = type[idx..] || ""
249
+ [inner, rest]
250
+ end
251
+
252
+ # Splits a tuple component list into individual type strings, handling nested tuples.
253
+ def split_tuple_types(str)
254
+ types = []
255
+ depth = 0
256
+ current = ""
257
+ str.each_char do |ch|
258
+ case ch
259
+ when "("
260
+ depth += 1
261
+ current << ch
262
+ when ")"
263
+ depth -= 1
264
+ current << ch
265
+ when ","
266
+ if depth.zero?
267
+ types << current
268
+ current = ""
269
+ else
270
+ current << ch
271
+ end
272
+ else
273
+ current << ch
274
+ end
275
+ end
276
+ types << current unless current.empty?
277
+ types
278
+ end
218
279
  end
219
280
  end
220
281
  end
data/lib/eth/abi.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -38,8 +38,13 @@ module Eth
38
38
  #
39
39
  # @param types [Array] types to be ABI-encoded.
40
40
  # @param args [Array] values to be ABI-encoded.
41
+ # @param packed [Bool] set true to return packed encoding (default: `false`).
41
42
  # @return [String] the encoded ABI data.
42
- def encode(types, args)
43
+ def encode(types, args, packed = false)
44
+ return solidity_packed(types, args) if packed
45
+ types = [types] unless types.instance_of? Array
46
+ args = [args] unless args.instance_of? Array
47
+ raise ArgumentError, "Types and values must be the same length" if types.length != args.length
43
48
 
44
49
  # parse all types
45
50
  parsed_types = types.map { |t| Type === t ? t : Type.parse(t) }
@@ -64,6 +69,26 @@ module Eth
64
69
  "#{head}#{tail}"
65
70
  end
66
71
 
72
+ # Encodes Application Binary Interface (ABI) data in non-standard packed mode.
73
+ # It accepts multiple arguments and encodes using the head/tail mechanism.
74
+ #
75
+ # @param types [Array] types to be ABI-encoded.
76
+ # @param args [Array] values to be ABI-encoded.
77
+ # @return [String] the encoded packed ABI data.
78
+ # @raise [ArgumentError] if types and args are of different size.
79
+ def solidity_packed(types, args)
80
+ raise ArgumentError, "Types and values must be the same length" if types.length != args.length
81
+
82
+ # We do not use the type system for packed encoding but want to call the parser once
83
+ # to enforce the type validation.
84
+ _ = types.map { |t| Type === t ? t : Type.parse(t) }
85
+
86
+ packed = types.zip(args).map do |type, arg|
87
+ Abi::Packed::Encoder.type(type, arg)
88
+ end.join
89
+ packed.force_encoding(Encoding::ASCII_8BIT)
90
+ end
91
+
67
92
  # Decodes Application Binary Interface (ABI) data. It accepts multiple
68
93
  # arguments and decodes using the head/tail mechanism.
69
94
  #
@@ -123,7 +148,9 @@ module Eth
123
148
  end
124
149
  end
125
150
 
151
+ require "eth/abi/packed/encoder"
126
152
  require "eth/abi/decoder"
127
153
  require "eth/abi/encoder"
128
154
  require "eth/abi/event"
155
+ require "eth/abi/function"
129
156
  require "eth/abi/type"
data/lib/eth/address.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -17,6 +17,8 @@ module Eth
17
17
 
18
18
  # The {Eth::Address} class to handle checksummed Ethereum addresses.
19
19
  class Address
20
+
21
+ # The literal zero address 0x0.
20
22
  ZERO = "0x0000000000000000000000000000000000000000"
21
23
 
22
24
  # Provides a special checksum error if EIP-55 is violated.
data/lib/eth/api.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
data/lib/eth/chain.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -149,13 +149,21 @@ module Eth
149
149
  # Chain ID for Arbitrum Goerli testnet.
150
150
  GOERLI_ARBITRUM = 421613.freeze
151
151
 
152
+ # Chain ID for Hoodi testnet.
153
+ HOODI = 560048.freeze
154
+
152
155
  # Chain ID for Sepolia testnet.
153
156
  SEPOLIA = 11155111.freeze
154
157
 
155
158
  # Chain ID for Holesovice testnet.
156
159
  HOLESOVICE = 11166111.freeze
160
+
161
+ # Chain ID for Holesovice testnet.
157
162
  HOLESKY = HOLESOVICE
158
163
 
164
+ # Chain ID for Basecamp testnet.
165
+ BASECAMP = 123420001114.freeze
166
+
159
167
  # Chain ID for the geth private network preset.
160
168
  PRIVATE_GETH = 1337.freeze
161
169
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -49,9 +49,13 @@ module Eth
49
49
  if !(uri.user.nil? && uri.password.nil?)
50
50
  @user = uri.user
51
51
  @password = uri.password
52
- @uri = URI("#{uri.scheme}://#{uri.user}:#{uri.password}@#{@host}:#{@port}#{uri.path}")
52
+ if uri.query
53
+ @uri = URI("#{uri.scheme}://#{uri.user}:#{uri.password}@#{@host}:#{@port}#{uri.path}?#{uri.query}")
54
+ else
55
+ @uri = URI("#{uri.scheme}://#{uri.user}:#{uri.password}@#{@host}:#{@port}#{uri.path}")
56
+ end
53
57
  else
54
- @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}")
58
+ @uri = uri
55
59
  end
56
60
  end
57
61
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
data/lib/eth/client.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -40,6 +40,21 @@ module Eth
40
40
  # A custom error type if a contract interaction fails.
41
41
  class ContractExecutionError < StandardError; end
42
42
 
43
+ # Raised when an RPC call returns an error. Carries the optional
44
+ # hex-encoded error data to support custom error decoding.
45
+ class RpcError < IOError
46
+ attr_reader :data
47
+
48
+ # Constructor for the {RpcError} class.
49
+ #
50
+ # @param message [String] the error message returned by the RPC.
51
+ # @param data [String] optional hex encoded error data.
52
+ def initialize(message, data = nil)
53
+ super(message)
54
+ @data = data
55
+ end
56
+ end
57
+
43
58
  # Creates a new RPC-Client, either by providing an HTTP/S host or
44
59
  # an IPC path. Supports basic authentication with username and password.
45
60
  #
@@ -251,21 +266,28 @@ module Eth
251
266
  # @param **sender_key [Eth::Key] the sender private key.
252
267
  # @param **legacy [Boolean] enables legacy transactions (pre-EIP-1559).
253
268
  # @return [Object] returns the result of the call.
269
+ # @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
254
270
  def call(contract, function, *args, **kwargs)
255
- func = contract.functions.select { |func| func.name == function }
256
- raise ArgumentError, "this function does not exist!" if func.nil? || func.size === 0
257
- selected_func = func.first
258
- func.each do |f|
259
- if f.inputs.size === args.size
260
- selected_func = f
261
- end
262
- end
263
- output = call_raw(contract, selected_func, *args, **kwargs)
271
+ function = contract.function(function, args: args.size)
272
+ output = function.decode_call_result(
273
+ eth_call(
274
+ {
275
+ data: function.encode_call(*args),
276
+ to: kwargs[:address] || contract.address,
277
+ from: kwargs[:from],
278
+ gas: kwargs[:gas],
279
+ gasPrice: kwargs[:gas_price],
280
+ value: kwargs[:value],
281
+ }.compact
282
+ )["result"]
283
+ )
264
284
  if output&.length == 1
265
285
  output[0]
266
286
  else
267
287
  output
268
288
  end
289
+ rescue RpcError => e
290
+ raise ContractExecutionError, contract.decode_error(e)
269
291
  end
270
292
 
271
293
  # Executes a contract function with a transaction (transactional
@@ -298,13 +320,12 @@ module Eth
298
320
  else
299
321
  Tx.estimate_intrinsic_gas(contract.bin)
300
322
  end
301
- fun = contract.functions.select { |func| func.name == function }[0]
302
323
  params = {
303
324
  value: kwargs[:tx_value] || 0,
304
325
  gas_limit: gas_limit,
305
326
  chain_id: chain_id,
306
327
  to: kwargs[:address] || contract.address,
307
- data: call_payload(fun, args),
328
+ data: contract.function(function, args: args.size).encode_call(*args),
308
329
  }
309
330
  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
310
331
  end
@@ -320,8 +341,8 @@ module Eth
320
341
  begin
321
342
  hash = wait_for_tx(transact(contract, function, *args, **kwargs))
322
343
  return hash, tx_succeeded?(hash)
323
- rescue IOError => e
324
- raise ContractExecutionError, e
344
+ rescue RpcError => e
345
+ raise ContractExecutionError, contract.decode_error(e)
325
346
  end
326
347
  end
327
348
 
@@ -442,28 +463,6 @@ module Eth
442
463
  end
443
464
  end
444
465
 
445
- # Non-transactional function call called from call().
446
- # @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
447
- def call_raw(contract, func, *args, **kwargs)
448
- params = {
449
- data: call_payload(func, args),
450
- to: kwargs[:address] || contract.address,
451
- from: kwargs[:from],
452
- }.compact
453
-
454
- raw_result = eth_call(params)["result"]
455
- types = func.outputs.map { |i| i.type }
456
- return nil if raw_result == "0x"
457
- Eth::Abi.decode(types, raw_result)
458
- end
459
-
460
- # Encodes function call payloads.
461
- def call_payload(fun, args)
462
- types = fun.inputs.map(&:parsed_type)
463
- encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
464
- Util.prefix_hex(fun.signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
465
- end
466
-
467
466
  # Encodes constructor params
468
467
  def encode_constructor_params(contract, args)
469
468
  types = contract.constructor_inputs.map { |input| input.type }
@@ -481,7 +480,9 @@ module Eth
481
480
  id: next_id,
482
481
  }
483
482
  output = JSON.parse(send_request(payload.to_json))
484
- raise IOError, output["error"]["message"] unless output["error"].nil?
483
+ if (err = output["error"])
484
+ raise RpcError.new(err["message"], err["data"])
485
+ end
485
486
  output
486
487
  end
487
488
 
data/lib/eth/constant.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2023 The Ruby-Eth Contributors
1
+ # Copyright (c) 2016-2025 The Ruby-Eth Contributors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -0,0 +1,62 @@
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
+ # Provide classes for contract custom errors.
20
+ class Contract::Error
21
+ attr_accessor :name, :inputs, :signature, :error_string
22
+
23
+ # Constructor of the {Eth::Contract::Error} class.
24
+ #
25
+ # @param data [Hash] contract abi data for the error.
26
+ def initialize(data)
27
+ @name = data["name"]
28
+ @inputs = data.fetch("inputs", []).map do |input|
29
+ Eth::Contract::FunctionInput.new(input)
30
+ end
31
+ @error_string = self.class.calc_signature(@name, @inputs)
32
+ @signature = self.class.encoded_error_signature(@error_string)
33
+ end
34
+
35
+ # Creates error strings.
36
+ #
37
+ # @param name [String] error name.
38
+ # @param inputs [Array<Eth::Contract::FunctionInput>] error input class list.
39
+ # @return [String] error string.
40
+ def self.calc_signature(name, inputs)
41
+ "#{name}(#{inputs.map { |x| x.parsed_type.to_s }.join(",")})"
42
+ end
43
+
44
+ # Encodes an error signature.
45
+ #
46
+ # @param signature [String] error signature.
47
+ # @return [String] encoded error signature string.
48
+ def self.encoded_error_signature(signature)
49
+ Util.prefix_hex(Util.bin_to_hex(Util.keccak256(signature)[0..3]))
50
+ end
51
+
52
+ # Decodes a revert error payload.
53
+ #
54
+ # @param data [String] the hex-encoded revert data including selector.
55
+ # @return [Array] decoded error arguments.
56
+ def decode(data)
57
+ types = inputs.map(&:type)
58
+ payload = "0x" + data[10..]
59
+ Eth::Abi.decode(types, payload)
60
+ end
61
+ end
62
+ end