eth 0.5.15 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/codeql.yml +4 -4
- data/.github/workflows/docs.yml +2 -2
- data/.github/workflows/spec.yml +32 -14
- data/CHANGELOG.md +25 -5
- data/Gemfile +1 -1
- data/README.md +5 -5
- data/eth.gemspec +10 -1
- data/lib/eth/abi/decoder.rb +82 -38
- data/lib/eth/abi/encoder.rb +85 -47
- data/lib/eth/abi/type.rb +6 -3
- data/lib/eth/bls.rb +68 -0
- data/lib/eth/client/http.rb +5 -8
- data/lib/eth/client/ws.rb +323 -0
- data/lib/eth/client.rb +14 -8
- data/lib/eth/tx/eip4844.rb +13 -1
- data/lib/eth/tx.rb +4 -1
- data/lib/eth/version.rb +1 -1
- data/lib/eth.rb +1 -0
- metadata +47 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98f8ce464d7df37a4eba624469d53362687773991ccabf623e9d91d78e450479
|
|
4
|
+
data.tar.gz: ce0601d35ca795205565e541c97115f396c5e7553a9aa6a76421167bf6bc4e69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb9c363ec65417ce041a3a847f3d573a2128724fabb1b80483453c3097c1f726b123d78fb546b15a691ab83e64e2409ef7ef8d40461348c3948014ad52159991
|
|
7
|
+
data.tar.gz: 869b4229c3861eb7be055d0b5559e039269e267ad79203d9f4dd2923d8a08e863af9afce2cb6e973e74130db2fd6a4a04c084a0bc7460b167ab50b3813fa1592
|
|
@@ -24,15 +24,15 @@ jobs:
|
|
|
24
24
|
- ruby
|
|
25
25
|
steps:
|
|
26
26
|
- name: "Checkout repository"
|
|
27
|
-
uses: actions/checkout@
|
|
27
|
+
uses: actions/checkout@v6
|
|
28
28
|
- name: "Initialize CodeQL"
|
|
29
|
-
uses: github/codeql-action/init@
|
|
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@
|
|
33
|
+
uses: github/codeql-action/autobuild@v4
|
|
34
34
|
- name: "Perform CodeQL Analysis"
|
|
35
|
-
uses: github/codeql-action/analyze@
|
|
35
|
+
uses: github/codeql-action/analyze@v4
|
|
36
36
|
- uses: ruby/setup-ruby@v1
|
|
37
37
|
with:
|
|
38
38
|
ruby-version: '3.4'
|
data/.github/workflows/docs.yml
CHANGED
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
docs:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
14
|
- uses: ruby/setup-ruby@v1
|
|
15
15
|
with:
|
|
16
16
|
ruby-version: '3.4'
|
|
@@ -20,7 +20,7 @@ jobs:
|
|
|
20
20
|
gem install yard
|
|
21
21
|
yard doc
|
|
22
22
|
- name: Deploy GH Pages
|
|
23
|
-
uses: JamesIves/github-pages-deploy-action@v4.7.
|
|
23
|
+
uses: JamesIves/github-pages-deploy-action@v4.7.6
|
|
24
24
|
with:
|
|
25
25
|
branch: gh-pages
|
|
26
26
|
folder: doc/
|
data/.github/workflows/spec.yml
CHANGED
|
@@ -19,35 +19,53 @@ jobs:
|
|
|
19
19
|
fail-fast: false
|
|
20
20
|
matrix:
|
|
21
21
|
os: [ubuntu-latest, macos-latest]
|
|
22
|
-
ruby: ['3.
|
|
22
|
+
ruby: ['3.4', '4.0']
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
25
|
-
- uses: ruby/setup-ruby@v1
|
|
26
|
-
with:
|
|
27
|
-
ruby-version: ${{ matrix.ruby }}
|
|
28
|
-
bundler-cache: false
|
|
24
|
+
- uses: actions/checkout@v6
|
|
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
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
###
|
|
11
|
-
*
|
|
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/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
[](https://github.com/q9f/eth.rb/pulse)
|
|
15
15
|
[](https://q9f.github.io/eth.rb)
|
|
16
16
|
[](https://github.com/q9f/eth.rb/wiki)
|
|
17
|
-
[](LICENSE)
|
|
17
|
+
[](LICENSE.txt)
|
|
18
18
|
[](https://github.com/q9f/eth.rb/issues)
|
|
19
19
|
|
|
20
20
|
A straightforward library to build, sign, and broadcast Ethereum transactions. It allows the separation of key and node management. Sign transactions and handle keys anywhere you can run Ruby and broadcast transactions through any local or remote node. Sign messages and recover signatures for authentication.
|
|
@@ -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
|
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
spec.test_files = spec.files.grep %r{^(test|spec|features)/}
|
|
33
33
|
|
|
34
34
|
spec.platform = Gem::Platform::RUBY
|
|
35
|
-
spec.required_ruby_version = ">= 3.0", "<
|
|
35
|
+
spec.required_ruby_version = ">= 3.0", "< 5.0"
|
|
36
36
|
|
|
37
37
|
# bigdecimal for big decimals ;)
|
|
38
38
|
spec.add_dependency "bigdecimal", "~> 3.1"
|
|
@@ -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
|
data/lib/eth/abi/decoder.rb
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
101
|
+
l = type.dimensions.last
|
|
94
102
|
nested_sub = type.nested_sub
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
data/lib/eth/abi/encoder.rb
CHANGED
|
@@ -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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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?
|
|
84
|
+
raise ParseError, "Invalid type format" unless type.is_a? String
|
|
85
85
|
|
|
86
|
-
if type.start_with?("tuple(")
|
|
87
|
-
|
|
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
|
data/lib/eth/client/http.rb
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
@@ -40,35 +40,40 @@ 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
|
|
43
|
+
# Raised when an RPC call returns an error. Carries the error code and the optional
|
|
44
44
|
# hex-encoded error data to support custom error decoding.
|
|
45
45
|
class RpcError < IOError
|
|
46
46
|
attr_reader :data
|
|
47
|
+
attr_reader :code
|
|
47
48
|
|
|
48
49
|
# Constructor for the {RpcError} class.
|
|
49
50
|
#
|
|
50
51
|
# @param message [String] the error message returned by the RPC.
|
|
51
52
|
# @param data [String] optional hex encoded error data.
|
|
52
|
-
|
|
53
|
+
# @param code [String] optional error code returned by the RPC.
|
|
54
|
+
def initialize(message, data = nil, code = nil)
|
|
53
55
|
super(message)
|
|
54
56
|
@data = data
|
|
57
|
+
@code = code
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
# Creates a new RPC-Client, either by providing an HTTP/S host
|
|
59
|
-
# an IPC path. Supports basic authentication with username and password.
|
|
61
|
+
# Creates a new RPC-Client, either by providing an HTTP/S host, WS/S host,
|
|
62
|
+
# or an IPC path. Supports basic authentication with username and password.
|
|
60
63
|
#
|
|
61
|
-
# **Note**, this sets the
|
|
64
|
+
# **Note**, this sets the following gas defaults: {Tx::DEFAULT_PRIORITY_FEE}
|
|
62
65
|
# and {Tx::DEFAULT_GAS_PRICE. Use {#max_priority_fee_per_gas} and
|
|
63
66
|
# {#max_fee_per_gas} to set custom values prior to submitting transactions.
|
|
64
67
|
#
|
|
65
|
-
# @param host [String] either an HTTP/S host or an IPC path.
|
|
68
|
+
# @param host [String] either an HTTP/S host, WS/S host, or an IPC path.
|
|
66
69
|
# @return [Eth::Client::Ipc] an IPC client.
|
|
67
70
|
# @return [Eth::Client::Http] an HTTP client.
|
|
71
|
+
# @return [Eth::Client::Ws] a WebSocket client.
|
|
68
72
|
# @raise [ArgumentError] in case it cannot determine the client type.
|
|
69
73
|
def self.create(host)
|
|
70
74
|
return Client::Ipc.new host if host.end_with? ".ipc"
|
|
71
75
|
return Client::Http.new host if host.start_with? "http"
|
|
76
|
+
return Client::Ws.new host if host.start_with? "ws"
|
|
72
77
|
raise ArgumentError, "Unable to detect client type!"
|
|
73
78
|
end
|
|
74
79
|
|
|
@@ -481,7 +486,7 @@ module Eth
|
|
|
481
486
|
}
|
|
482
487
|
output = JSON.parse(send_request(payload.to_json))
|
|
483
488
|
if (err = output["error"])
|
|
484
|
-
raise RpcError.new(err["message"], err["data"])
|
|
489
|
+
raise RpcError.new(err["message"], err["data"], err["code"])
|
|
485
490
|
end
|
|
486
491
|
output
|
|
487
492
|
end
|
|
@@ -524,3 +529,4 @@ end
|
|
|
524
529
|
# Load the client/* libraries
|
|
525
530
|
require "eth/client/http"
|
|
526
531
|
require "eth/client/ipc"
|
|
532
|
+
require "eth/client/ws"
|
data/lib/eth/tx/eip4844.rb
CHANGED
|
@@ -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
data/lib/eth.rb
CHANGED
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.
|
|
4
|
+
version: 0.5.17
|
|
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
|
|
@@ -206,14 +250,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
206
250
|
version: '3.0'
|
|
207
251
|
- - "<"
|
|
208
252
|
- !ruby/object:Gem::Version
|
|
209
|
-
version: '
|
|
253
|
+
version: '5.0'
|
|
210
254
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
211
255
|
requirements:
|
|
212
256
|
- - ">="
|
|
213
257
|
- !ruby/object:Gem::Version
|
|
214
258
|
version: '0'
|
|
215
259
|
requirements: []
|
|
216
|
-
rubygems_version: 3.6.
|
|
260
|
+
rubygems_version: 3.6.9
|
|
217
261
|
specification_version: 4
|
|
218
262
|
summary: Ruby Ethereum library.
|
|
219
263
|
test_files: []
|