eth 0.5.14 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f56a3acb3981d3087e8125660e5670b8ba8bf6a5255e2742e439498327cd82
4
- data.tar.gz: c54057d9b851ad567e846e390949927afe1986387eaba95fc8facb0aa3fef20d
3
+ metadata.gz: 58842e5f3990be268758249080b56b70c1f26f8299ef27e7549db6cc2f675e51
4
+ data.tar.gz: 388a599dde8f50f34d46287e028e4dfe0c855815baa10de4a8c258b549429286
5
5
  SHA512:
6
- metadata.gz: ee0dacd29dde60d8368c382a8d0fdf444895a66f2795689961112f34e752fced4a350e26b255d5bc4b1872e7c423c5780cc35487bd2b349f984ac0f75facbfd8
7
- data.tar.gz: ca7fbe6c973aceb5dcc56031dbfa03903392ba9ad2ff0aa90d29c4c6311cc899961954e606c9c400df523305815eb89b26bc1fed2c0d65fcb042b59f0b63064f
6
+ metadata.gz: 3d4e42a99aebc7a47673b0706ce8476e08beca249ecae812f10c76e4f8767088b6a185c232abf9cc977a6680b08ac7b38de9d211cfc741f3bb2aa5fce9a4730c
7
+ data.tar.gz: cb29ed0c90b56eeae5ce519a436fada62e47690c1ca447e230d0fa2f3915fe97cd3346506c6b67e9c93a88d333197f0735529dd3c66c764d1895e76b1aa82b57
data/CHANGELOG.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## [0.5.15]
5
+ ### Added
6
+ * Eth/tx: add support for EIP-4844 transactions [#345](https://github.com/q9f/eth.rb/pull/345)
7
+ * Eth/contract: support solidity custom errors as per ERC-6093 [#344](https://github.com/q9f/eth.rb/pull/344)
8
+ * Eth/abi: decode transaction input [#354](https://github.com/q9f/eth.rb/pull/354)
9
+
10
+ ### Fixed
11
+ * Eth/abi: handle tuple type without components [#335](https://github.com/q9f/eth.rb/issues/335)
12
+
13
+ ## [0.5.14]
14
+ ### Added
15
+ * Add ability to decode event parameters using ABI reference. [#328](https://github.com/q9f/eth.rb/pull/328)
16
+ * Add support for EIP-7702 transactions [#320](https://github.com/q9f/eth.rb/pull/320)
17
+ * Eth/abi: implement packed encoder [#310](https://github.com/q9f/eth.rb/pull/310)
18
+
19
+ ### Changed
20
+ * Chore: run rufo, add docs [#332](https://github.com/q9f/eth.rb/pull/332)
21
+ * Spec/client: fix nonce too low error handling in spec [#331](https://github.com/q9f/eth.rb/pull/331)
22
+ * Move the tests that are failing due to a geth upgrade to pending [#330](https://github.com/q9f/eth.rb/pull/330)
23
+ * Spec/client: don't require any rpc api token for tests [#326](https://github.com/q9f/eth.rb/pull/326)
24
+ * Build(deps): bump JamesIves/github-pages-deploy-action from 4.7.2 to 4.7.3 [#327](https://github.com/q9f/eth.rb/pull/327)
25
+ * Eth/eip712: prepare tests for packed encoding [#216](https://github.com/q9f/eth.rb/pull/216)
26
+ * Spec/solidity: mute system call output [#319](https://github.com/q9f/eth.rb/pull/319)
27
+ * Updated nesting of describe blocks in the EIP-1559 spec. [#318](https://github.com/q9f/eth.rb/pull/318)
28
+ * Update README.md [#317](https://github.com/q9f/eth.rb/pull/317)
29
+ * Docs: update README.md [#314](https://github.com/q9f/eth.rb/pull/314)
30
+ * Gem: update copyright headers [#312](https://github.com/q9f/eth.rb/pull/312)
31
+ * Build(deps): bump JamesIves/github-pages-deploy-action from 4.7.1 to 4.7.2 [#309](https://github.com/q9f/eth.rb/pull/309)
32
+ * Spec: switch from infura to drpc [#308](https://github.com/q9f/eth.rb/pull/308)
33
+ * Ci: update ruby version [#307](https://github.com/q9f/eth.rb/pull/307)
34
+ * Gem: bump version to 0.5.14 [#305](https://github.com/q9f/eth.rb/pull/305)
35
+ * Docs: update changelog [#304](https://github.com/q9f/eth.rb/pull/304)
36
+
4
37
  ## [0.5.13]
5
38
  ### Changed
6
39
  * Eth/api: update to latest available go-ethereum apis [#301](https://github.com/q9f/eth.rb/pull/301)
data/CODE_OF_CONDUCT.md CHANGED
@@ -115,8 +115,6 @@ community.
115
115
 
116
116
  ## Attribution
117
117
 
118
- This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119
- version 2.1, available at
120
- [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121
- Community Impact Guidelines were inspired by
122
- [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
118
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
119
+ version 2.1, available at [v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
120
+ Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder.
data/Gemfile CHANGED
@@ -3,14 +3,14 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  group :test, :development do
6
- gem "bundler", "~> 2.4"
6
+ gem "bundler", "~> 2.5"
7
7
  gem "pry", "~> 0.15"
8
8
  gem "rake", "~> 13.2"
9
- gem "rdoc", "~> 6.9"
9
+ gem "rdoc", "~> 6.13"
10
10
  gem "rspec", "~> 3.13"
11
11
  gem "rufo", "~> 0.18"
12
12
  gem "simplecov", "~> 0.22"
13
- gem "simplecov-cobertura", "~> 2.1"
13
+ gem "simplecov-cobertura", "~> 3.0"
14
14
  gem "yard", "~> 0.9"
15
15
  end
16
16
 
data/README.md CHANGED
@@ -10,9 +10,7 @@
10
10
  [![GitHub release (latest by date)](https://img.shields.io/github/v/release/q9f/eth.rb)](https://github.com/q9f/eth.rb/releases)
11
11
  [![Gem](https://img.shields.io/gem/v/eth)](https://rubygems.org/gems/eth)
12
12
  [![Gem](https://img.shields.io/gem/dt/eth)](https://rubygems.org/gems/eth)
13
- [![Visitors](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fq9f%2Feth.rb&count_bg=%2379C83D&title_bg=%23555555&icon=rubygems.svg&icon_color=%23FF0000&title=visitors&edge_flat=false)](https://hits.seeyoufarm.com)
14
13
  [![codecov](https://codecov.io/gh/q9f/eth.rb/branch/main/graph/badge.svg?token=IK7USBPBZY)](https://codecov.io/gh/q9f/eth.rb)
15
- [![Maintainability](https://api.codeclimate.com/v1/badges/469e6f66425198ad7614/maintainability)](https://codeclimate.com/github/q9f/eth.rb/maintainability)
16
14
  [![Top Language](https://img.shields.io/github/languages/top/q9f/eth.rb?color=red)](https://github.com/q9f/eth.rb/pulse)
17
15
  [![Yard Doc API](https://img.shields.io/badge/documentation-API-blue)](https://q9f.github.io/eth.rb)
18
16
  [![Usage Wiki](https://img.shields.io/badge/usage-WIKI-blue)](https://github.com/q9f/eth.rb/wiki)
@@ -34,11 +32,15 @@ What you get:
34
32
  - [x] EIP-2028 Call-data intrinsic gas cost estimates (plus access lists)
35
33
  - [x] EIP-2718 Ethereum Transaction Envelopes (and types)
36
34
  - [x] EIP-2930 Ethereum Type-1 Transactions (with access lists)
35
+ - [x] EIP-4844 Ethereum Type-3 Transactions (with shard blobs)
36
+ - [x] EIP-7702 Ethereum Type-4 Transactions (with authorization lists)
37
37
  - [x] ABI-Encoder and Decoder (including type parser)
38
+ - [x] Packed ABI-Encoder for Solidity smart contracts
38
39
  - [x] RLP-Encoder and Decoder (including sedes)
39
40
  - [x] RPC-Client (IPC/HTTP) for Execution-Layer APIs
40
41
  - [x] Solidity bindings (compile contracts from Ruby)
41
42
  - [x] Full smart-contract support (deploy, transact, and call)
43
+ - [x] ERC-6093 custom Solidity errors
42
44
 
43
45
  ## Installation
44
46
  Add this line to your application's Gemfile:
@@ -88,7 +90,7 @@ bundle install
88
90
  rspec
89
91
  ```
90
92
 
91
- The goal is to have 100% specification coverage for all code inside this gem.
93
+ The goal is to have 100% unit-test coverage for all code inside this gem.
92
94
 
93
95
  ## Contributing
94
96
  Pull requests are welcome! To contribute, please consider the following:
data/SECURITY.md CHANGED
@@ -10,8 +10,8 @@ later.
10
10
 
11
11
  | Gem | Version | Supported |
12
12
  | -------------- | ------- | ------------------ |
13
- | `eth` | 0.5.x | :white_check_mark: |
14
- | `eth` | < 0.5 | :x: |
13
+ | `eth` | >= 0.5 | :white_check_mark: |
14
+ | `eth` | < 0.5 | :x: |
15
15
  | `ethereum` | _any_ | :x: |
16
16
  | `ethereum-abi` | _any_ | :x: |
17
17
  | `rlp` | _any_ | :x: |
data/eth.gemspec CHANGED
@@ -50,7 +50,7 @@ Gem::Specification.new do |spec|
50
50
  spec.add_dependency "rbsecp256k1", "~> 6.0"
51
51
 
52
52
  # openssl for encrypted key derivation
53
- spec.add_dependency "openssl", ">= 2.2", "< 4.0"
53
+ spec.add_dependency "openssl", "~> 3.3"
54
54
 
55
55
  # scrypt for encrypted key derivation
56
56
  spec.add_dependency "scrypt", "~> 3.0"
@@ -43,11 +43,14 @@ module Eth
43
43
  # Case: decoding array of string/bytes
44
44
  else
45
45
  l = Util.deserialize_big_endian_to_int arg[0, 32]
46
+ raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + 32 * l
46
47
 
47
48
  # Decode each element of the array
48
49
  (1..l).map do |i|
49
50
  pointer = Util.deserialize_big_endian_to_int arg[i * 32, 32] # Pointer to the size of the array's element
51
+ raise DecodingError, "Offset out of bounds" if pointer < 32 * l || pointer > arg.size - 64
50
52
  data_l = Util.deserialize_big_endian_to_int arg[32 + pointer, 32] # length of the element
53
+ raise DecodingError, "Offset out of bounds" if pointer + 32 + Util.ceil32(data_l) > arg.size
51
54
  type(Type.parse(type.base_type), arg[pointer + 32, Util.ceil32(data_l) + 32])
52
55
  end
53
56
  end
@@ -73,11 +76,19 @@ module Eth
73
76
  l = Util.deserialize_big_endian_to_int arg[0, 32]
74
77
  nested_sub = type.nested_sub
75
78
 
76
- # ref https://github.com/ethereum/tests/issues/691
77
- raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.dynamic?
78
-
79
- # decoded dynamic-sized arrays
80
- (0...l).map { |i| type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
79
+ if nested_sub.dynamic?
80
+ raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + 32 * l
81
+ offsets = (0...l).map do |i|
82
+ off = Util.deserialize_big_endian_to_int arg[32 + 32 * i, 32]
83
+ raise DecodingError, "Offset out of bounds" if off < 32 * l || off > arg.size - 64
84
+ off
85
+ end
86
+ offsets.map { |off| type(nested_sub, arg[32 + off..]) }
87
+ else
88
+ raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + nested_sub.size * l
89
+ # decoded dynamic-sized arrays with static sub-types
90
+ (0...l).map { |i| type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
91
+ end
81
92
  elsif !type.dimensions.empty?
82
93
  l = type.dimensions.first
83
94
  nested_sub = type.nested_sub
@@ -46,32 +46,21 @@ module Eth
46
46
  elsif type.dynamic? && arg.is_a?(Array)
47
47
 
48
48
  # encodes dynamic-sized arrays
49
- head, tail = "", ""
50
- head += type(Type.size_type, arg.size)
49
+ head = type(Type.size_type, arg.size)
51
50
  nested_sub = type.nested_sub
52
51
 
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
-
52
+ if nested_sub.dynamic?
53
+ tails = arg.map { |a| type(nested_sub, a) }
54
+ offset = arg.size * 32
55
+ tails.each do |t|
65
56
  head += type(Type.size_type, offset)
57
+ offset += t.size
66
58
  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]
59
+ head + tails.join
60
+ else
61
+ arg.each { |a| head += type(nested_sub, a) }
62
+ head
73
63
  end
74
- "#{head}#{tail}"
75
64
  else
76
65
  if type.dimensions.empty?
77
66
 
@@ -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,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
85
+
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
84
98
 
85
- # type dimension can only be numeric
86
- dims = dimension.scan(/\[[0-9]*\]/)
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,7 +107,7 @@ 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 }
110
+ @dimensions = dims.map { |x| x == "[]" ? 0 : x[1...-1].to_i }
97
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
@@ -123,7 +137,7 @@ 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" and components.none?(&:dynamic?)
140
+ elsif base_type == "tuple" and components&.none?(&:dynamic?)
127
141
  s = components.sum(&:size)
128
142
  end
129
143
  elsif dimensions.last != 0 and !nested_sub.dynamic?
@@ -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
@@ -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/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
 
data/lib/eth/client.rb CHANGED
@@ -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
 
@@ -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