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 +4 -4
 - data/.github/workflows/codeql.yml +4 -4
 - data/.github/workflows/docs.yml +1 -1
 - data/.github/workflows/spec.yml +31 -13
 - data/CHANGELOG.md +25 -5
 - data/README.md +4 -4
 - data/eth.gemspec +9 -0
 - 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 +7 -4
 - 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 +46 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: ad70051a0aa71d2f7ed5a4acea236b677c9d1f4eb3467f8194de19a8de9f8714
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 5558228d301116bd2be49ac184c01f3c2d1cb0e725deb9ab3f070021927b63cf
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 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@ 
     | 
| 
      
 27 
     | 
    
         
            +
                    uses: actions/checkout@v5
         
     | 
| 
       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
    
    
    
        data/.github/workflows/spec.yml
    CHANGED
    
    | 
         @@ -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@ 
     | 
| 
       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  
     | 
| 
       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/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
         
     | 
    
        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.
         
     | 
| 
         @@ -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  
     | 
| 
       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"
         
     | 
    
        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.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. 
     | 
| 
      
 260 
     | 
    
         
            +
            rubygems_version: 3.6.9
         
     | 
| 
       217 
261 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       218 
262 
     | 
    
         
             
            summary: Ruby Ethereum library.
         
     | 
| 
       219 
263 
     | 
    
         
             
            test_files: []
         
     |