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.
- checksums.yaml +4 -4
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +2 -2
- data/.github/workflows/spec.yml +1 -3
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +3 -5
- data/Gemfile +3 -3
- data/LICENSE.txt +1 -1
- data/README.md +6 -6
- data/SECURITY.md +2 -2
- data/eth.gemspec +4 -1
- data/lib/eth/abi/decoder.rb +18 -7
- data/lib/eth/abi/encoder.rb +14 -26
- data/lib/eth/abi/event.rb +5 -1
- data/lib/eth/abi/function.rb +124 -0
- data/lib/eth/abi/packed/encoder.rb +196 -0
- data/lib/eth/abi/type.rb +77 -16
- data/lib/eth/abi.rb +29 -2
- data/lib/eth/address.rb +3 -1
- data/lib/eth/api.rb +1 -1
- data/lib/eth/chain.rb +9 -1
- data/lib/eth/client/http.rb +7 -3
- data/lib/eth/client/ipc.rb +1 -1
- data/lib/eth/client.rb +38 -37
- data/lib/eth/constant.rb +1 -1
- data/lib/eth/contract/error.rb +62 -0
- data/lib/eth/contract/event.rb +69 -16
- data/lib/eth/contract/function.rb +22 -1
- data/lib/eth/contract/function_input.rb +1 -1
- data/lib/eth/contract/function_output.rb +12 -4
- data/lib/eth/contract/initializer.rb +1 -1
- data/lib/eth/contract.rb +56 -5
- data/lib/eth/eip712.rb +49 -13
- data/lib/eth/ens/coin_type.rb +1 -1
- data/lib/eth/ens/resolver.rb +1 -1
- data/lib/eth/ens.rb +1 -1
- data/lib/eth/key/decrypter.rb +1 -1
- data/lib/eth/key/encrypter.rb +1 -1
- data/lib/eth/key.rb +2 -2
- data/lib/eth/rlp/decoder.rb +1 -1
- data/lib/eth/rlp/encoder.rb +1 -1
- data/lib/eth/rlp/sedes/big_endian_int.rb +1 -1
- data/lib/eth/rlp/sedes/binary.rb +1 -1
- data/lib/eth/rlp/sedes/list.rb +1 -1
- data/lib/eth/rlp/sedes.rb +1 -1
- data/lib/eth/rlp.rb +1 -1
- data/lib/eth/signature.rb +1 -1
- data/lib/eth/solidity.rb +1 -1
- data/lib/eth/tx/eip1559.rb +33 -8
- data/lib/eth/tx/eip2930.rb +32 -7
- data/lib/eth/tx/eip4844.rb +389 -0
- data/lib/eth/tx/eip7702.rb +520 -0
- data/lib/eth/tx/legacy.rb +31 -7
- data/lib/eth/tx.rb +88 -1
- data/lib/eth/unit.rb +1 -1
- data/lib/eth/util.rb +20 -8
- data/lib/eth/version.rb +2 -2
- data/lib/eth.rb +1 -1
- 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-
|
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
|
-
|
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
|
-
|
86
|
-
|
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")) }
|
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"
|
140
|
+
elsif base_type == "tuple" and components&.none?(&:dynamic?)
|
127
141
|
s = components.sum(&:size)
|
128
142
|
end
|
129
|
-
elsif dimensions.last != 0
|
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)
|
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?
|
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
|
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 <
|
196
|
-
|
197
|
-
total =
|
198
|
-
raise ParseError, "Real size out of bounds (max 32 bytes)" unless total >= 8
|
199
|
-
raise ParseError, "Real
|
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-
|
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-
|
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
data/lib/eth/chain.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2016-
|
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
|
|
data/lib/eth/client/http.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2016-
|
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
|
-
|
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 =
|
58
|
+
@uri = uri
|
55
59
|
end
|
56
60
|
end
|
57
61
|
|
data/lib/eth/client/ipc.rb
CHANGED
data/lib/eth/client.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2016-
|
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
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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:
|
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
|
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
|
-
|
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
@@ -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
|