eth 0.5.15 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58842e5f3990be268758249080b56b70c1f26f8299ef27e7549db6cc2f675e51
4
- data.tar.gz: 388a599dde8f50f34d46287e028e4dfe0c855815baa10de4a8c258b549429286
3
+ metadata.gz: ad70051a0aa71d2f7ed5a4acea236b677c9d1f4eb3467f8194de19a8de9f8714
4
+ data.tar.gz: 5558228d301116bd2be49ac184c01f3c2d1cb0e725deb9ab3f070021927b63cf
5
5
  SHA512:
6
- metadata.gz: 3d4e42a99aebc7a47673b0706ce8476e08beca249ecae812f10c76e4f8767088b6a185c232abf9cc977a6680b08ac7b38de9d211cfc741f3bb2aa5fce9a4730c
7
- data.tar.gz: cb29ed0c90b56eeae5ce519a436fada62e47690c1ca447e230d0fa2f3915fe97cd3346506c6b67e9c93a88d333197f0735529dd3c66c764d1895e76b1aa82b57
6
+ metadata.gz: acf9b6267efa46befed740d7d00c5938ed8d384b3297c57953f71c2ae5ebac31bd279a44694dd3675f77648f66e381dccacb70f533896600b3fd1bdc15a04a3b
7
+ data.tar.gz: dbb137aa5449234ba86f64951740d0f985693f37d1da329a70b7efdfb44b17a8bb2511a74560ac07a8d116aba100894e584c55fc3cc367c5c91e48fed456593a
@@ -24,15 +24,15 @@ jobs:
24
24
  - ruby
25
25
  steps:
26
26
  - name: "Checkout repository"
27
- uses: actions/checkout@v4
27
+ uses: actions/checkout@v5
28
28
  - name: "Initialize CodeQL"
29
- uses: github/codeql-action/init@v3
29
+ uses: github/codeql-action/init@v4
30
30
  with:
31
31
  languages: "${{ matrix.language }}"
32
32
  - name: Autobuild
33
- uses: github/codeql-action/autobuild@v3
33
+ uses: github/codeql-action/autobuild@v4
34
34
  - name: "Perform CodeQL Analysis"
35
- uses: github/codeql-action/analyze@v3
35
+ uses: github/codeql-action/analyze@v4
36
36
  - uses: ruby/setup-ruby@v1
37
37
  with:
38
38
  ruby-version: '3.4'
@@ -10,7 +10,7 @@ jobs:
10
10
  docs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v4
13
+ - uses: actions/checkout@v5
14
14
  - uses: ruby/setup-ruby@v1
15
15
  with:
16
16
  ruby-version: '3.4'
@@ -21,33 +21,51 @@ jobs:
21
21
  os: [ubuntu-latest, macos-latest]
22
22
  ruby: ['3.3', '3.4']
23
23
  steps:
24
- - uses: actions/checkout@v4
25
- - uses: ruby/setup-ruby@v1
26
- with:
27
- ruby-version: ${{ matrix.ruby }}
28
- bundler-cache: false
24
+ - uses: actions/checkout@v5
29
25
  - name: MacOs Dependencies
26
+ if: matrix.os == 'macos-latest'
30
27
  run: |
28
+ brew update
31
29
  brew tap ethereum/ethereum
32
- brew install --verbose pkg-config automake autogen geth solidity
33
- if: startsWith(matrix.os, 'macOS')
30
+ brew install --verbose autoconf automake libtool pkg-config autogen geth solidity
34
31
  - name: Ubuntu Dependencies
32
+ if: matrix.os == 'ubuntu-latest'
35
33
  run: |
36
34
  sudo add-apt-repository -y ppa:ethereum/ethereum
37
35
  sudo apt-get update
38
- sudo apt-get install geth solc
39
- if: startsWith(matrix.os, 'Ubuntu')
36
+ sudo apt-get install -y autoconf automake libtool pkg-config geth solc
37
+ - uses: ruby/setup-ruby@v1
38
+ with:
39
+ ruby-version: ${{ matrix.ruby }}
40
+ bundler-cache: true
40
41
  - name: Run Geth
41
42
  run: |
42
- geth --dev --http --ipcpath /tmp/geth.ipc &
43
- disown &
43
+ geth --dev \
44
+ --http \
45
+ --ws \
46
+ --ipcpath /tmp/geth.ipc \
47
+ >/tmp/geth.log 2>&1 &
48
+ echo $! > /tmp/geth.pid
49
+ sleep 10
44
50
  - name: Gem Dependencies
45
51
  run: |
46
- git submodule update --init
47
- bundle install
52
+ git submodule update --init --recursive
48
53
  - name: Run Tests
49
54
  run: |
50
55
  bundle exec rspec
56
+ - name: Stop Geth
57
+ if: always()
58
+ run: |
59
+ if [ -f /tmp/geth.pid ]; then
60
+ kill "$(cat /tmp/geth.pid)" 2>/dev/null || true
61
+ fi
62
+ - name: Geth Logs (on failure)
63
+ if: failure()
64
+ run: |
65
+ if [ -f /tmp/geth.log ]; then
66
+ echo "===== geth log ====="
67
+ tail -n 200 /tmp/geth.log
68
+ fi
51
69
  - name: Upload coverage to Codecov
52
70
  uses: codecov/codecov-action@v5
53
71
  with:
data/CHANGELOG.md CHANGED
@@ -3,12 +3,32 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  ## [0.5.15]
5
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)
6
+ * Implement EIP712 array encoding [#361](https://github.com/q9f/eth.rb/pull/361)
7
+ * Support nested dynamic arrays in ABI [#356](https://github.com/q9f/eth.rb/pull/356)
8
+ * Allow signing transactions with external signatures [#349](https://github.com/q9f/eth.rb/pull/349)
9
+ * Feat: add eip-4844 transactions [#345](https://github.com/q9f/eth.rb/pull/345)
10
+ * Support Solidity custom errors per ERC-6093 [#344](https://github.com/q9f/eth.rb/pull/344)
11
+ * Allow to use chains with id > 4294967295 [#337](https://github.com/q9f/eth.rb/pull/337)
9
12
 
10
- ### Fixed
11
- * Eth/abi: handle tuple type without components [#335](https://github.com/q9f/eth.rb/issues/335)
13
+ ### Changed
14
+ * Harden ABI type parsing [#358](https://github.com/q9f/eth.rb/pull/358)
15
+ * Ensure ABI decoder rejects ZST offsets [#359](https://github.com/q9f/eth.rb/pull/359)
16
+ * Test: decode eip4844 blobs [#360](https://github.com/q9f/eth.rb/pull/360)
17
+ * Abi: decode transaction input [#354](https://github.com/q9f/eth.rb/pull/354)
18
+ * Fix tuple output decoding for contract calls [#353](https://github.com/q9f/eth.rb/pull/353)
19
+ * Add comprehensive Tx module tests [#352](https://github.com/q9f/eth.rb/pull/352)
20
+ * Move error decoding to contract module [#350](https://github.com/q9f/eth.rb/pull/350)
21
+ * Enforce minimal RLP integer decoding [#351](https://github.com/q9f/eth.rb/pull/351)
22
+ * Fix tuple size calculation without components [#348](https://github.com/q9f/eth.rb/pull/348)
23
+ * Docs: update readme [#347](https://github.com/q9f/eth.rb/pull/347)
24
+ * Chore: update development dependencies [#346](https://github.com/q9f/eth.rb/pull/346)
25
+ * Handle hex string inputs in big-endian conversion [#343](https://github.com/q9f/eth.rb/pull/343)
26
+ * Handle uppercase hex prefixes [#339](https://github.com/q9f/eth.rb/pull/339)
27
+ * Add methods to encode function call and decode its result [#334](https://github.com/q9f/eth.rb/pull/334)
28
+ * Docs(util): fix hex? return type [#342](https://github.com/q9f/eth.rb/pull/342)
29
+ * Handle hex input consistently in int_to_big_endian [#341](https://github.com/q9f/eth.rb/pull/341)
30
+ * Fix receiver option spelling [#340](https://github.com/q9f/eth.rb/pull/340)
31
+ * Chore: bump version to 0.5.15 [#333](https://github.com/q9f/eth.rb/pull/333)
12
32
 
13
33
  ## [0.5.14]
14
34
  ### Added
data/README.md CHANGED
@@ -32,12 +32,12 @@ What you get:
32
32
  - [x] EIP-2028 Call-data intrinsic gas cost estimates (plus access lists)
33
33
  - [x] EIP-2718 Ethereum Transaction Envelopes (and types)
34
34
  - [x] EIP-2930 Ethereum Type-1 Transactions (with access lists)
35
- - [x] EIP-4844 Ethereum Type-3 Transactions (with shard blobs)
35
+ - [x] EIP-4844 Ethereum Type-3 Transactions (with shard blobs, up to 9 blobs per block)
36
36
  - [x] EIP-7702 Ethereum Type-4 Transactions (with authorization lists)
37
37
  - [x] ABI-Encoder and Decoder (including type parser)
38
38
  - [x] Packed ABI-Encoder for Solidity smart contracts
39
39
  - [x] RLP-Encoder and Decoder (including sedes)
40
- - [x] RPC-Client (IPC/HTTP) for Execution-Layer APIs
40
+ - [x] RPC-Client (IPC/HTTP/WS) for Execution-Layer APIs
41
41
  - [x] Solidity bindings (compile contracts from Ruby)
42
42
  - [x] Full smart-contract support (deploy, transact, and call)
43
43
  - [x] ERC-6093 custom Solidity errors
@@ -76,10 +76,10 @@ yard doc
76
76
  The goal is to have 100% API documentation available.
77
77
 
78
78
  ## Testing
79
- The test suite expects working local HTTP and IPC endpoints with a prefunded developer account, e.g.:
79
+ The test suite expects working local HTTP, WS, and IPC endpoints with a prefunded developer account, e.g.:
80
80
 
81
81
  ```shell
82
- geth --dev --http --ipcpath /tmp/geth.ipc &
82
+ geth --dev --http --ws --ipcpath /tmp/geth.ipc &
83
83
  ```
84
84
 
85
85
  To run tests, simply use `rspec`. Note, that the Ethereum test fixtures are also required.
data/eth.gemspec CHANGED
@@ -52,6 +52,15 @@ Gem::Specification.new do |spec|
52
52
  # openssl for encrypted key derivation
53
53
  spec.add_dependency "openssl", "~> 3.3"
54
54
 
55
+ # base64 is required explicitly in Ruby >= 3.4
56
+ spec.add_dependency "base64", "~> 0.1"
57
+
55
58
  # scrypt for encrypted key derivation
56
59
  spec.add_dependency "scrypt", "~> 3.0"
60
+
61
+ # bls12-381 for BLS signatures and pairings
62
+ spec.add_dependency "bls12-381", "~> 0.3"
63
+
64
+ # httpx for HTTP/2 and persistent connections
65
+ spec.add_dependency "httpx", "~> 1.6"
57
66
  end
@@ -31,48 +31,51 @@ module Eth
31
31
  # @return [String] the decoded data for the type.
32
32
  # @raise [DecodingError] if decoding fails for type.
33
33
  def type(type, arg)
34
- if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
35
- # Case: decoding a string/bytes
36
- if type.dimensions.empty?
37
- l = Util.deserialize_big_endian_to_int arg[0, 32]
38
- data = arg[32..-1]
39
- raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)
40
-
41
- # decoded strings and bytes
42
- data[0, l]
43
- # Case: decoding array of string/bytes
44
- else
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
34
+ if %w(string bytes).include?(type.base_type) and type.sub_type.empty? and type.dimensions.empty?
35
+ l = Util.deserialize_big_endian_to_int arg[0, 32]
36
+ data = arg[32..-1]
37
+ raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)
47
38
 
48
- # Decode each element of the array
49
- (1..l).map do |i|
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
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
54
- type(Type.parse(type.base_type), arg[pointer + 32, Util.ceil32(data_l) + 32])
55
- end
56
- end
57
- elsif type.base_type == "tuple"
39
+ # decoded strings and bytes
40
+ data[0, l]
41
+ elsif type.base_type == "tuple" && type.dimensions.empty?
58
42
  offset = 0
59
- data = {}
43
+ result = []
60
44
  raise DecodingError, "Cannot decode tuples without known components" if type.components.nil?
61
- type.components.each do |c|
62
- if c.dynamic?
63
- pointer = Util.deserialize_big_endian_to_int arg[offset, 32] # Pointer to the size of the array's element
64
- data_len = Util.deserialize_big_endian_to_int arg[pointer, 32] # length of the element
65
45
 
66
- data[c.name] = type(c, arg[pointer, Util.ceil32(data_len) + 32])
46
+ head_offset = 0
47
+ dynamic_offsets = []
48
+ type.components.each_with_index do |component, index|
49
+ if component.dynamic?
50
+ raise DecodingError, "Offset out of bounds" if head_offset + 32 > arg.size
51
+ pointer = Util.deserialize_big_endian_to_int arg[head_offset, 32]
52
+ dynamic_offsets << [index, pointer]
53
+ head_offset += 32
54
+ else
55
+ size = component.size
56
+ raise DecodingError, "Offset out of bounds" if head_offset + size > arg.size
57
+ head_offset += size
58
+ end
59
+ end
60
+
61
+ dynamic_ranges = tuple_dynamic_ranges(dynamic_offsets, arg.size, head_offset)
62
+
63
+ type.components.each_with_index do |component, index|
64
+ if component.dynamic?
65
+ pointer, next_offset = dynamic_ranges[index]
66
+ raise DecodingError, "Offset out of bounds" if pointer > arg.size || next_offset > arg.size
67
+ raise DecodingError, "Offset out of bounds" if next_offset < pointer
68
+ result << type(component, arg[pointer, next_offset - pointer])
67
69
  offset += 32
68
70
  else
69
- size = c.size
70
- data[c.name] = type(c, arg[offset, size])
71
+ size = component.size
72
+ raise DecodingError, "Offset out of bounds" if offset + size > arg.size
73
+ result << type(component, arg[offset, size])
71
74
  offset += size
72
75
  end
73
76
  end
74
- data
75
- elsif type.dynamic?
77
+ result
78
+ elsif type.dynamic? && !type.dimensions.empty? && type.dimensions.last == 0
76
79
  l = Util.deserialize_big_endian_to_int arg[0, 32]
77
80
  nested_sub = type.nested_sub
78
81
 
@@ -83,18 +86,36 @@ module Eth
83
86
  raise DecodingError, "Offset out of bounds" if off < 32 * l || off > arg.size - 64
84
87
  off
85
88
  end
86
- offsets.map { |off| type(nested_sub, arg[32 + off..]) }
89
+ offsets.each_with_index.map do |off, index|
90
+ start = 32 + off
91
+ stop = index + 1 < offsets.length ? 32 + offsets[index + 1] : arg.size
92
+ raise DecodingError, "Offset out of bounds" if stop > arg.size || stop < start
93
+ type(nested_sub, arg[start, stop - start])
94
+ end
87
95
  else
88
96
  raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + nested_sub.size * l
89
97
  # decoded dynamic-sized arrays with static sub-types
90
98
  (0...l).map { |i| type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
91
99
  end
92
100
  elsif !type.dimensions.empty?
93
- l = type.dimensions.first
101
+ l = type.dimensions.last
94
102
  nested_sub = type.nested_sub
95
103
 
96
- # decoded static-size arrays
97
- (0...l).map { |i| type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
104
+ if nested_sub.dynamic?
105
+ raise DecodingError, "Wrong data size for static array" unless arg.size >= 32 * l
106
+ offsets = (0...l).map do |i|
107
+ off = Util.deserialize_big_endian_to_int arg[32 * i, 32]
108
+ raise DecodingError, "Offset out of bounds" if off < 32 * l || off > arg.size - 32
109
+ off
110
+ end
111
+ offsets.each_with_index.map do |off, i|
112
+ size = (i + 1 < offsets.length ? offsets[i + 1] : arg.size) - off
113
+ type(nested_sub, arg[off, size])
114
+ end
115
+ else
116
+ # decoded static-size arrays with static sub-types
117
+ (0...l).map { |i| type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
118
+ end
98
119
  else
99
120
 
100
121
  # decoded primitive types
@@ -119,7 +140,9 @@ module Eth
119
140
  size = Util.deserialize_big_endian_to_int data[0, 32]
120
141
 
121
142
  # decoded dynamic-sized array
122
- data[32..-1][0, size]
143
+ decoded = data[32..-1][0, size]
144
+ decoded.force_encoding(Encoding::UTF_8)
145
+ decoded
123
146
  else
124
147
 
125
148
  # decoded static-sized array
@@ -159,6 +182,27 @@ module Eth
159
182
  raise DecodingError, "Unknown primitive type: #{type.base_type}"
160
183
  end
161
184
  end
185
+
186
+ private
187
+
188
+ # Computes the byte ranges for dynamic tuple components.
189
+ #
190
+ # @param offsets [Array<Array(Integer, Integer)>] list of tuples containing the component index and pointer.
191
+ # @param total_size [Integer] total number of bytes available for the tuple.
192
+ # @param head_size [Integer] size in bytes of the tuple head.
193
+ # @return [Hash{Integer=>Array(Integer, Integer)}] mapping component index to a [start, stop) range.
194
+ # @raise [DecodingError] if the encoded offsets overlap or leave the tuple head.
195
+ def tuple_dynamic_ranges(offsets, total_size, head_size)
196
+ ranges = {}
197
+ sorted = offsets.sort_by { |(_, pointer)| pointer }
198
+ sorted.each_with_index do |(index, pointer), idx|
199
+ raise DecodingError, "Offset out of bounds" if pointer < head_size || pointer > total_size
200
+ next_pointer = idx + 1 < sorted.length ? sorted[idx + 1][1] : total_size
201
+ raise DecodingError, "Offset out of bounds" if next_pointer < pointer || next_pointer > total_size
202
+ ranges[index] = [pointer, next_pointer]
203
+ end
204
+ ranges
205
+ end
162
206
  end
163
207
  end
164
208
  end
@@ -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,44 +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 = type(Type.size_type, arg.size)
50
- nested_sub = type.nested_sub
51
-
52
- if nested_sub.dynamic?
53
- tails = arg.map { |a| type(nested_sub, a) }
54
- offset = arg.size * 32
55
- tails.each do |t|
56
- head += type(Type.size_type, offset)
57
- offset += t.size
58
- end
59
- head + tails.join
60
- else
61
- arg.each { |a| head += type(nested_sub, a) }
62
- head
63
- end
43
+ elsif type.base_type == "tuple" && type.dimensions.empty?
44
+ tuple arg, type
45
+ elsif !type.dimensions.empty?
46
+ encode_array type, arg
64
47
  else
65
- if type.dimensions.empty?
66
-
67
- # encode a primitive type
68
- primitive_type type, arg
69
- else
70
-
71
- # encode static-size arrays
72
- arg.map { |x| type(type.nested_sub, x) }.join
73
- end
48
+ primitive_type type, arg
74
49
  end
75
50
  end
76
51
 
@@ -111,6 +86,7 @@ module Eth
111
86
 
112
87
  # Properly encodes unsigned integers.
113
88
  def uint(arg, type)
89
+ arg = coerce_number arg
114
90
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
115
91
  raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::UINT_MAX or arg < Constant::UINT_MIN
116
92
  real_size = type.sub_type.to_i
@@ -121,6 +97,7 @@ module Eth
121
97
 
122
98
  # Properly encodes signed integers.
123
99
  def int(arg, type)
100
+ arg = coerce_number arg
124
101
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
125
102
  raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Constant::INT_MAX or arg < Constant::INT_MIN
126
103
  real_size = type.sub_type.to_i
@@ -137,6 +114,7 @@ module Eth
137
114
 
138
115
  # Properly encodes unsigned fixed-point numbers.
139
116
  def ufixed(arg, type)
117
+ arg = coerce_number arg
140
118
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
141
119
  high, low = type.sub_type.split("x").map(&:to_i)
142
120
  raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
@@ -145,6 +123,7 @@ module Eth
145
123
 
146
124
  # Properly encodes signed fixed-point numbers.
147
125
  def fixed(arg, type)
126
+ arg = coerce_number arg
148
127
  raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
149
128
  high, low = type.sub_type.split("x").map(&:to_i)
150
129
  raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
@@ -174,8 +153,11 @@ module Eth
174
153
 
175
154
  # Properly encodes tuples.
176
155
  def tuple(arg, type)
177
- 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
178
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
179
161
 
180
162
  static_size = 0
181
163
  type.components.each_with_index do |component, i|
@@ -198,28 +180,84 @@ module Eth
198
180
  dynamic_values << dynamic_value
199
181
  dynamic_offset += dynamic_value.size
200
182
  else
201
- 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))
202
184
  end
203
185
  end
204
186
 
205
187
  offsets_and_static_values.join + dynamic_values.join
206
188
  end
207
189
 
208
- # Properly encode struct offsets.
209
- def struct_offsets(type, arg)
210
- result = ""
211
- offset = arg.size
212
- tails_encoding = arg.map { |a| type(type, a) }
213
- arg.size.times do |i|
214
- if i == 0
215
- offset *= 32
216
- else
217
- 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
218
256
  end
219
- offset_string = type(Type.size_type, offset)
220
- result += offset_string
257
+ [head, tail]
258
+ else
259
+ [values.map { |value| type(nested_sub, value) }.join, ""]
221
260
  end
222
- result
223
261
  end
224
262
 
225
263
  # Properly encodes hash-strings.
data/lib/eth/abi/type.rb CHANGED
@@ -81,19 +81,22 @@ module Eth
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
+ raise ParseError, "Invalid type format" unless type.is_a? String
85
85
 
86
- if type.start_with?("tuple(")
87
- inner, rest = extract_tuple(type)
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)
88
89
  inner_types = split_tuple_types(inner)
89
90
  inner_types.each { |t| Type.parse(t) }
90
91
  base_type = "tuple"
91
92
  sub_type = ""
92
93
  dimension = rest
94
+ components ||= inner_types.map { |t| { "type" => t } }
93
95
  else
94
96
  match = /\A([a-z]+)([0-9]*x?[0-9]*)((?:\[\d+\]|\[\])*)\z/.match(type)
95
97
  raise ParseError, "Invalid type format" unless match
96
98
  _, base_type, sub_type, dimension = match.to_a
99
+ sub_type = "256" if %w[uint int].include?(base_type) && sub_type.empty?
97
100
  end
98
101
 
99
102
  # type dimension can only be numeric or empty for dynamic arrays
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
@@ -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
 
@@ -0,0 +1,323 @@
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
+ require "socket"
16
+ require "openssl"
17
+ require "uri"
18
+ require "base64"
19
+ require "securerandom"
20
+ require "digest/sha1"
21
+ require "thread"
22
+ require "ipaddr"
23
+
24
+ # Provides the {Eth} module.
25
+ module Eth
26
+
27
+ # Provides a WS/S-RPC client with automatic reconnection support.
28
+ class Client::Ws < Client
29
+
30
+ # The host of the WebSocket endpoint.
31
+ attr_reader :host
32
+
33
+ # The port of the WebSocket endpoint.
34
+ attr_reader :port
35
+
36
+ # The full URI of the WebSocket endpoint, including path.
37
+ attr_reader :uri
38
+
39
+ # Attribute indicator for SSL.
40
+ attr_reader :ssl
41
+
42
+ # Constructor for the WebSocket client. Should not be used; use
43
+ # {Client.create} instead.
44
+ #
45
+ # @param host [String] a URI pointing to a WebSocket RPC-API.
46
+ def initialize(host)
47
+ super
48
+ @uri = URI.parse(host)
49
+ raise ArgumentError, "Unable to parse the WebSocket-URI!" unless %w[ws wss].include?(@uri.scheme)
50
+ @host = @uri.host
51
+ @port = @uri.port
52
+ @ssl = @uri.scheme == "wss"
53
+ @path = build_path(@uri)
54
+ @mutex = Mutex.new
55
+ @socket = nil
56
+ @fragments = nil
57
+ end
58
+
59
+ # Sends an RPC request to the connected WebSocket endpoint.
60
+ #
61
+ # @param payload [Hash] the RPC request parameters.
62
+ # @return [String] a JSON-encoded response.
63
+ def send_request(payload)
64
+ attempts = 0
65
+ begin
66
+ attempts += 1
67
+ @mutex.synchronize do
68
+ ensure_socket
69
+ write_frame(@socket, payload)
70
+ return read_message(@socket)
71
+ end
72
+ rescue IOError, SystemCallError => e
73
+ @mutex.synchronize { close_socket }
74
+ retry if attempts < 2
75
+ raise e
76
+ end
77
+ end
78
+
79
+ # Closes the underlying WebSocket connection.
80
+ #
81
+ # @return [void]
82
+ def close
83
+ @mutex.synchronize { close_socket }
84
+ end
85
+
86
+ private
87
+
88
+ def ensure_socket
89
+ return if @socket && !@socket.closed?
90
+
91
+ socket = open_socket
92
+ begin
93
+ perform_handshake(socket)
94
+ @socket = socket
95
+ @fragments = nil
96
+ rescue StandardError
97
+ begin
98
+ socket.close unless socket.closed?
99
+ rescue IOError, SystemCallError
100
+ nil
101
+ end
102
+ @socket = nil
103
+ raise
104
+ end
105
+ end
106
+
107
+ # Establishes the TCP socket for the RPC connection and upgrades it to TLS
108
+ # when a secure endpoint is requested. TLS sessions enforce peer
109
+ # verification, load the default system trust store, and enable hostname
110
+ # verification when the current OpenSSL bindings support it.
111
+ #
112
+ # @return [TCPSocket, OpenSSL::SSL::SSLSocket] the established socket.
113
+ # @raise [IOError, SystemCallError, OpenSSL::SSL::SSLError] if the socket
114
+ # cannot be opened or the TLS handshake fails.
115
+ def open_socket
116
+ tcp = TCPSocket.new(@host, @port)
117
+ return tcp unless @ssl
118
+
119
+ context = OpenSSL::SSL::SSLContext.new
120
+ params = { verify_mode: OpenSSL::SSL::VERIFY_PEER }
121
+ params[:verify_hostname] = true if context.respond_to?(:verify_hostname=)
122
+ context.set_params(params)
123
+ context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
124
+
125
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp, context)
126
+ ssl_socket.hostname = @host
127
+ ssl_socket.sync_close = true
128
+ ssl_socket.connect
129
+ ssl_socket
130
+ end
131
+
132
+ def perform_handshake(socket)
133
+ key = Base64.strict_encode64(SecureRandom.random_bytes(16))
134
+ request = build_handshake_request(key)
135
+ socket.write(request)
136
+ response = read_handshake_response(socket)
137
+ verify_handshake!(response, key)
138
+ end
139
+
140
+ def build_handshake_request(key)
141
+ origin = build_origin_header
142
+ host_header = build_host_header
143
+ "GET #{@path} HTTP/1.1\r\n" \
144
+ "Host: #{host_header}\r\n" \
145
+ "Upgrade: websocket\r\n" \
146
+ "Connection: Upgrade\r\n" \
147
+ "Sec-WebSocket-Version: 13\r\n" \
148
+ "Sec-WebSocket-Key: #{key}\r\n" \
149
+ "Origin: #{origin}\r\n\r\n"
150
+ end
151
+
152
+ def read_handshake_response(socket)
153
+ response = +""
154
+ until response.end_with?("\r\n\r\n")
155
+ chunk = socket.readpartial(1024)
156
+ raise IOError, "Incomplete WebSocket handshake" if chunk.nil?
157
+ response << chunk
158
+ end
159
+ response
160
+ rescue EOFError
161
+ raise IOError, "Incomplete WebSocket handshake"
162
+ end
163
+
164
+ def verify_handshake!(response, key)
165
+ status_line = response.lines.first&.strip
166
+ unless status_line&.start_with?("HTTP/1.1 101")
167
+ raise IOError, "WebSocket handshake failed (status: #{status_line || "unknown"})"
168
+ end
169
+
170
+ accept = response[/Sec-WebSocket-Accept:\s*(.+)\r/i, 1]&.strip
171
+ expected = Base64.strict_encode64(Digest::SHA1.digest("#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
172
+ raise IOError, "WebSocket handshake failed (missing accept header)" unless accept
173
+ raise IOError, "WebSocket handshake failed (invalid accept header)" unless accept == expected
174
+ end
175
+
176
+ def write_frame(socket, payload, opcode = 0x1)
177
+ frame_payload = payload.is_a?(String) ? payload.dup : payload.to_s
178
+ mask_key = SecureRandom.random_bytes(4)
179
+ header = [0x80 | opcode]
180
+
181
+ length = frame_payload.bytesize
182
+ if length <= 125
183
+ header << (0x80 | length)
184
+ elsif length <= 0xFFFF
185
+ header << (0x80 | 126)
186
+ header.concat([length].pack("n").bytes)
187
+ else
188
+ header << (0x80 | 127)
189
+ header.concat([length].pack("Q>").bytes)
190
+ end
191
+
192
+ masked_payload = apply_mask(frame_payload, mask_key)
193
+ socket.write(header.pack("C*") + mask_key + masked_payload)
194
+ end
195
+
196
+ def read_message(socket)
197
+ loop do
198
+ frame = read_frame(socket)
199
+ return frame if frame
200
+ end
201
+ end
202
+
203
+ def read_frame(socket)
204
+ header = read_bytes(socket, 2)
205
+ byte1, byte2 = header.bytes
206
+ opcode = byte1 & 0x0F
207
+ masked = (byte2 & 0x80) == 0x80
208
+ length = byte2 & 0x7F
209
+
210
+ length = read_bytes(socket, 2).unpack1("n") if length == 126
211
+ length = read_bytes(socket, 8).unpack1("Q>") if length == 127
212
+
213
+ mask_key = masked ? read_bytes(socket, 4).bytes : nil
214
+ payload = read_bytes(socket, length)
215
+ payload_bytes = payload.bytes
216
+ if mask_key
217
+ payload_bytes.map!.with_index { |byte, index| byte ^ mask_key[index % 4] }
218
+ end
219
+ data = payload_bytes.pack("C*")
220
+
221
+ case opcode
222
+ when 0x0
223
+ (@fragments ||= +"") << data
224
+ if (byte1 & 0x80) == 0x80
225
+ message = @fragments.dup
226
+ @fragments = nil
227
+ message
228
+ else
229
+ nil
230
+ end
231
+ when 0x1, 0x2
232
+ if (byte1 & 0x80) == 0x80
233
+ data
234
+ else
235
+ @fragments = data
236
+ nil
237
+ end
238
+ when 0x8
239
+ close_socket
240
+ raise IOError, "WebSocket closed"
241
+ when 0x9
242
+ write_frame(socket, data, 0xA)
243
+ nil
244
+ when 0xA
245
+ nil
246
+ else
247
+ nil
248
+ end
249
+ end
250
+
251
+ def read_bytes(socket, length)
252
+ data = +""
253
+ while data.bytesize < length
254
+ chunk = socket.read(length - data.bytesize)
255
+ raise IOError, "Unexpected end of WebSocket stream" if chunk.nil? || chunk.empty?
256
+ data << chunk
257
+ end
258
+ data
259
+ end
260
+
261
+ def apply_mask(payload, mask_key)
262
+ mask_bytes = mask_key.bytes
263
+ payload.bytes.map.with_index { |byte, index| byte ^ mask_bytes[index % 4] }.pack("C*")
264
+ end
265
+
266
+ def build_origin_header
267
+ scheme = @ssl ? "https" : "http"
268
+ host = format_origin_host(@uri.host)
269
+ default_port = @ssl ? 443 : 80
270
+ port = @uri.port
271
+ port_suffix = port == default_port ? "" : ":#{port}"
272
+ "#{scheme}://#{host}#{port_suffix}"
273
+ end
274
+
275
+ def build_host_header
276
+ "#{format_host(@uri.host)}:#{@uri.port}"
277
+ end
278
+
279
+ def format_origin_host(host)
280
+ return "localhost" if loopback_host?(host)
281
+ format_host(host)
282
+ end
283
+
284
+ def format_host(host)
285
+ return host unless host&.include?(":")
286
+ host.start_with?("[") ? host : "[#{host}]"
287
+ end
288
+
289
+ def loopback_host?(host)
290
+ return false if host.nil?
291
+ return true if host == "localhost"
292
+ IPAddr.new(host).loopback?
293
+ rescue IPAddr::InvalidAddressError
294
+ false
295
+ end
296
+
297
+ def close_socket
298
+ return unless @socket
299
+
300
+ begin
301
+ write_frame(@socket, [1000].pack("n"), 0x8)
302
+ rescue IOError, SystemCallError
303
+ # ignore errors while closing
304
+ ensure
305
+ begin
306
+ @socket.close unless @socket.closed?
307
+ rescue IOError, SystemCallError
308
+ nil
309
+ end
310
+ @socket = nil
311
+ @fragments = nil
312
+ end
313
+ end
314
+
315
+ def build_path(uri)
316
+ path = uri.path
317
+ path = "/" if path.nil? || path.empty?
318
+ query = uri.query
319
+ path += "?#{query}" if query
320
+ path
321
+ end
322
+ end
323
+ end
data/lib/eth/client.rb CHANGED
@@ -16,7 +16,7 @@
16
16
  module Eth
17
17
 
18
18
  # Provides the {Eth::Client} super-class to connect to Ethereum
19
- # network's RPC-API endpoints (IPC or HTTP).
19
+ # network's RPC-API endpoints (IPC, HTTP/S, or WS/S).
20
20
  class Client
21
21
 
22
22
  # The client's RPC-request ID starting at 0.
@@ -55,20 +55,22 @@ module Eth
55
55
  end
56
56
  end
57
57
 
58
- # Creates a new RPC-Client, either by providing an HTTP/S host or
59
- # an IPC path. Supports basic authentication with username and password.
58
+ # Creates a new RPC-Client, either by providing an HTTP/S host, WS/S host,
59
+ # or an IPC path. Supports basic authentication with username and password.
60
60
  #
61
61
  # **Note**, this sets the folling gas defaults: {Tx::DEFAULT_PRIORITY_FEE}
62
62
  # and {Tx::DEFAULT_GAS_PRICE. Use {#max_priority_fee_per_gas} and
63
63
  # {#max_fee_per_gas} to set custom values prior to submitting transactions.
64
64
  #
65
- # @param host [String] either an HTTP/S host or an IPC path.
65
+ # @param host [String] either an HTTP/S host, WS/S host, or an IPC path.
66
66
  # @return [Eth::Client::Ipc] an IPC client.
67
67
  # @return [Eth::Client::Http] an HTTP client.
68
+ # @return [Eth::Client::Ws] a WebSocket client.
68
69
  # @raise [ArgumentError] in case it cannot determine the client type.
69
70
  def self.create(host)
70
71
  return Client::Ipc.new host if host.end_with? ".ipc"
71
72
  return Client::Http.new host if host.start_with? "http"
73
+ return Client::Ws.new host if host.start_with? "ws"
72
74
  raise ArgumentError, "Unable to detect client type!"
73
75
  end
74
76
 
@@ -524,3 +526,4 @@ end
524
526
  # Load the client/* libraries
525
527
  require "eth/client/http"
526
528
  require "eth/client/ipc"
529
+ require "eth/client/ws"
@@ -23,6 +23,18 @@ module Eth
23
23
  # Ref: https://eips.ethereum.org/EIPS/eip-4844
24
24
  class Eip4844
25
25
 
26
+ # The blob gas consumed by a single blob.
27
+ GAS_PER_BLOB = (2 ** 17).freeze
28
+
29
+ # The target blob gas per block as per EIP-7691.
30
+ TARGET_BLOB_GAS_PER_BLOCK = 786_432.freeze
31
+
32
+ # The maximum blob gas allowed in a block as per EIP-7691.
33
+ MAX_BLOB_GAS_PER_BLOCK = 1_179_648.freeze
34
+
35
+ # The maximum number of blobs permitted in a single block.
36
+ MAX_BLOBS_PER_BLOCK = (MAX_BLOB_GAS_PER_BLOCK / GAS_PER_BLOB).freeze
37
+
26
38
  # The EIP-155 Chain ID.
27
39
  # Ref: https://eips.ethereum.org/EIPS/eip-155
28
40
  attr_reader :chain_id
@@ -89,7 +101,7 @@ module Eth
89
101
  # @option params [String] :data the transaction data payload.
90
102
  # @option params [Array] :access_list an optional access list.
91
103
  # @option params [Integer] :max_fee_per_blob_gas the max blob fee per gas.
92
- # @option params [Array] :blob_versioned_hashes the blob versioned hashes.
104
+ # @option params [Array] :blob_versioned_hashes the blob versioned hashes (max 9).
93
105
  # @raise [ParameterError] if gas limit is too low.
94
106
  def initialize(params)
95
107
  fields = { recovery_id: nil, r: 0, s: 0 }.merge params
data/lib/eth/tx.rb CHANGED
@@ -286,7 +286,7 @@ module Eth
286
286
  #
287
287
  # @param fields [Hash] the transaction fields.
288
288
  # @return [Hash] the validated transaction fields.
289
- # @raise [ParameterError] if max blob fee or blob hashes are invalid.
289
+ # @raise [ParameterError] if max blob fee or blob hashes are invalid or exceed limits.
290
290
  def validate_eip4844_params(fields)
291
291
  if fields[:max_fee_per_blob_gas].nil? or fields[:max_fee_per_blob_gas] < 0
292
292
  raise ParameterError, "Invalid max blob fee #{fields[:max_fee_per_blob_gas]}!"
@@ -294,6 +294,9 @@ module Eth
294
294
  if fields[:blob_versioned_hashes].nil? or !fields[:blob_versioned_hashes].is_a? Array or fields[:blob_versioned_hashes].empty?
295
295
  raise ParameterError, "Invalid blob versioned hashes #{fields[:blob_versioned_hashes]}!"
296
296
  end
297
+ if fields[:blob_versioned_hashes].length > Eip4844::MAX_BLOBS_PER_BLOCK
298
+ raise ParameterError, "Too many blob versioned hashes #{fields[:blob_versioned_hashes].length}!"
299
+ end
297
300
  if fields[:to].nil? or fields[:to].empty?
298
301
  raise ParameterError, "Invalid destination address #{fields[:to]}!"
299
302
  end
data/lib/eth/version.rb CHANGED
@@ -22,7 +22,7 @@ module Eth
22
22
  MINOR = 5.freeze
23
23
 
24
24
  # Defines the patch version of the {Eth} module.
25
- PATCH = 15.freeze
25
+ PATCH = 16.freeze
26
26
 
27
27
  # Defines the version string of the {Eth} module.
28
28
  VERSION = [MAJOR, MINOR, PATCH].join(".").freeze
data/lib/eth.rb CHANGED
@@ -32,5 +32,6 @@ require "eth/solidity"
32
32
  require "eth/tx"
33
33
  require "eth/unit"
34
34
  require "eth/util"
35
+ require "eth/bls"
35
36
  require "eth/ens"
36
37
  require "eth/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.15
4
+ version: 0.5.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Ellis
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: base64
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.1'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: scrypt
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,34 @@ dependencies:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
124
  version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bls12-381
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.3'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.3'
139
+ - !ruby/object:Gem::Dependency
140
+ name: httpx
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.6'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.6'
111
153
  description: Library to handle Ethereum accounts, messages, and transactions.
112
154
  email:
113
155
  - email@steveell.is
@@ -149,10 +191,12 @@ files:
149
191
  - lib/eth/abi/type.rb
150
192
  - lib/eth/address.rb
151
193
  - lib/eth/api.rb
194
+ - lib/eth/bls.rb
152
195
  - lib/eth/chain.rb
153
196
  - lib/eth/client.rb
154
197
  - lib/eth/client/http.rb
155
198
  - lib/eth/client/ipc.rb
199
+ - lib/eth/client/ws.rb
156
200
  - lib/eth/constant.rb
157
201
  - lib/eth/contract.rb
158
202
  - lib/eth/contract/error.rb
@@ -213,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
257
  - !ruby/object:Gem::Version
214
258
  version: '0'
215
259
  requirements: []
216
- rubygems_version: 3.6.7
260
+ rubygems_version: 3.6.9
217
261
  specification_version: 4
218
262
  summary: Ruby Ethereum library.
219
263
  test_files: []