eth 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f56a3acb3981d3087e8125660e5670b8ba8bf6a5255e2742e439498327cd82
4
- data.tar.gz: c54057d9b851ad567e846e390949927afe1986387eaba95fc8facb0aa3fef20d
3
+ metadata.gz: ad70051a0aa71d2f7ed5a4acea236b677c9d1f4eb3467f8194de19a8de9f8714
4
+ data.tar.gz: 5558228d301116bd2be49ac184c01f3c2d1cb0e725deb9ab3f070021927b63cf
5
5
  SHA512:
6
- metadata.gz: ee0dacd29dde60d8368c382a8d0fdf444895a66f2795689961112f34e752fced4a350e26b255d5bc4b1872e7c423c5780cc35487bd2b349f984ac0f75facbfd8
7
- data.tar.gz: ca7fbe6c973aceb5dcc56031dbfa03903392ba9ad2ff0aa90d29c4c6311cc899961954e606c9c400df523305815eb89b26bc1fed2c0d65fcb042b59f0b63064f
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
@@ -1,6 +1,59 @@
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
+ * 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)
12
+
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)
32
+
33
+ ## [0.5.14]
34
+ ### Added
35
+ * Add ability to decode event parameters using ABI reference. [#328](https://github.com/q9f/eth.rb/pull/328)
36
+ * Add support for EIP-7702 transactions [#320](https://github.com/q9f/eth.rb/pull/320)
37
+ * Eth/abi: implement packed encoder [#310](https://github.com/q9f/eth.rb/pull/310)
38
+
39
+ ### Changed
40
+ * Chore: run rufo, add docs [#332](https://github.com/q9f/eth.rb/pull/332)
41
+ * Spec/client: fix nonce too low error handling in spec [#331](https://github.com/q9f/eth.rb/pull/331)
42
+ * Move the tests that are failing due to a geth upgrade to pending [#330](https://github.com/q9f/eth.rb/pull/330)
43
+ * Spec/client: don't require any rpc api token for tests [#326](https://github.com/q9f/eth.rb/pull/326)
44
+ * 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)
45
+ * Eth/eip712: prepare tests for packed encoding [#216](https://github.com/q9f/eth.rb/pull/216)
46
+ * Spec/solidity: mute system call output [#319](https://github.com/q9f/eth.rb/pull/319)
47
+ * Updated nesting of describe blocks in the EIP-1559 spec. [#318](https://github.com/q9f/eth.rb/pull/318)
48
+ * Update README.md [#317](https://github.com/q9f/eth.rb/pull/317)
49
+ * Docs: update README.md [#314](https://github.com/q9f/eth.rb/pull/314)
50
+ * Gem: update copyright headers [#312](https://github.com/q9f/eth.rb/pull/312)
51
+ * 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)
52
+ * Spec: switch from infura to drpc [#308](https://github.com/q9f/eth.rb/pull/308)
53
+ * Ci: update ruby version [#307](https://github.com/q9f/eth.rb/pull/307)
54
+ * Gem: bump version to 0.5.14 [#305](https://github.com/q9f/eth.rb/pull/305)
55
+ * Docs: update changelog [#304](https://github.com/q9f/eth.rb/pull/304)
56
+
4
57
  ## [0.5.13]
5
58
  ### Changed
6
59
  * 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, up to 9 blobs per block)
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
- - [x] RPC-Client (IPC/HTTP) for Execution-Layer APIs
40
+ - [x] RPC-Client (IPC/HTTP/WS) 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:
@@ -74,10 +76,10 @@ yard doc
74
76
  The goal is to have 100% API documentation available.
75
77
 
76
78
  ## Testing
77
- 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.:
78
80
 
79
81
  ```shell
80
- geth --dev --http --ipcpath /tmp/geth.ipc &
82
+ geth --dev --http --ws --ipcpath /tmp/geth.ipc &
81
83
  ```
82
84
 
83
85
  To run tests, simply use `rspec`. Note, that the Ethereum test fixtures are also required.
@@ -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,8 +50,17 @@ 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
+
55
+ # base64 is required explicitly in Ruby >= 3.4
56
+ spec.add_dependency "base64", "~> 0.1"
54
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,59 +31,91 @@ 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]
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)
46
38
 
47
- # Decode each element of the array
48
- (1..l).map do |i|
49
- pointer = Util.deserialize_big_endian_to_int arg[i * 32, 32] # Pointer to the size of the array's element
50
- data_l = Util.deserialize_big_endian_to_int arg[32 + pointer, 32] # length of the element
51
- type(Type.parse(type.base_type), arg[pointer + 32, Util.ceil32(data_l) + 32])
52
- end
53
- end
54
- elsif type.base_type == "tuple"
39
+ # decoded strings and bytes
40
+ data[0, l]
41
+ elsif type.base_type == "tuple" && type.dimensions.empty?
55
42
  offset = 0
56
- data = {}
43
+ result = []
57
44
  raise DecodingError, "Cannot decode tuples without known components" if type.components.nil?
58
- type.components.each do |c|
59
- if c.dynamic?
60
- pointer = Util.deserialize_big_endian_to_int arg[offset, 32] # Pointer to the size of the array's element
61
- data_len = Util.deserialize_big_endian_to_int arg[pointer, 32] # length of the element
62
45
 
63
- 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])
64
69
  offset += 32
65
70
  else
66
- size = c.size
67
- 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])
68
74
  offset += size
69
75
  end
70
76
  end
71
- data
72
- elsif type.dynamic?
77
+ result
78
+ elsif type.dynamic? && !type.dimensions.empty? && type.dimensions.last == 0
73
79
  l = Util.deserialize_big_endian_to_int arg[0, 32]
74
80
  nested_sub = type.nested_sub
75
81
 
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]) }
82
+ if nested_sub.dynamic?
83
+ raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + 32 * l
84
+ offsets = (0...l).map do |i|
85
+ off = Util.deserialize_big_endian_to_int arg[32 + 32 * i, 32]
86
+ raise DecodingError, "Offset out of bounds" if off < 32 * l || off > arg.size - 64
87
+ off
88
+ end
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
95
+ else
96
+ raise DecodingError, "Wrong data size for dynamic array" unless arg.size >= 32 + nested_sub.size * l
97
+ # decoded dynamic-sized arrays with static sub-types
98
+ (0...l).map { |i| type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
99
+ end
81
100
  elsif !type.dimensions.empty?
82
- l = type.dimensions.first
101
+ l = type.dimensions.last
83
102
  nested_sub = type.nested_sub
84
103
 
85
- # decoded static-size arrays
86
- (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
87
119
  else
88
120
 
89
121
  # decoded primitive types
@@ -108,7 +140,9 @@ module Eth
108
140
  size = Util.deserialize_big_endian_to_int data[0, 32]
109
141
 
110
142
  # decoded dynamic-sized array
111
- data[32..-1][0, size]
143
+ decoded = data[32..-1][0, size]
144
+ decoded.force_encoding(Encoding::UTF_8)
145
+ decoded
112
146
  else
113
147
 
114
148
  # decoded static-sized array
@@ -148,6 +182,27 @@ module Eth
148
182
  raise DecodingError, "Unknown primitive type: #{type.base_type}"
149
183
  end
150
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
151
206
  end
152
207
  end
153
208
  end