eth 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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 +53 -0
 - data/CODE_OF_CONDUCT.md +3 -5
 - data/Gemfile +3 -3
 - data/README.md +8 -6
 - data/SECURITY.md +2 -2
 - data/eth.gemspec +10 -1
 - data/lib/eth/abi/decoder.rb +94 -39
 - data/lib/eth/abi/encoder.rb +85 -58
 - data/lib/eth/abi/function.rb +124 -0
 - data/lib/eth/abi/type.rb +69 -5
 - data/lib/eth/abi.rb +2 -0
 - data/lib/eth/bls.rb +68 -0
 - data/lib/eth/chain.rb +3 -0
 - data/lib/eth/client/http.rb +5 -8
 - data/lib/eth/client/ws.rb +323 -0
 - data/lib/eth/client.rb +44 -40
 - data/lib/eth/contract/error.rb +62 -0
 - data/lib/eth/contract/function.rb +21 -0
 - data/lib/eth/contract/function_output.rb +11 -3
 - data/lib/eth/contract.rb +55 -4
 - data/lib/eth/eip712.rb +48 -13
 - data/lib/eth/key.rb +1 -1
 - data/lib/eth/tx/eip1559.rb +32 -7
 - data/lib/eth/tx/eip2930.rb +31 -6
 - data/lib/eth/tx/eip4844.rb +401 -0
 - data/lib/eth/tx/eip7702.rb +34 -9
 - data/lib/eth/tx/legacy.rb +30 -6
 - data/lib/eth/tx.rb +45 -4
 - data/lib/eth/util.rb +19 -7
 - data/lib/eth/version.rb +1 -1
 - data/lib/eth.rb +1 -0
 - metadata +53 -15
 
| 
         @@ -0,0 +1,323 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Copyright (c) 2016-2025 The Ruby-Eth Contributors
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Licensed under the Apache License, Version 2.0 (the "License");
         
     | 
| 
      
 4 
     | 
    
         
            +
            # you may not use this file except in compliance with the License.
         
     | 
| 
      
 5 
     | 
    
         
            +
            # You may obtain a copy of the License at
         
     | 
| 
      
 6 
     | 
    
         
            +
            #
         
     | 
| 
      
 7 
     | 
    
         
            +
            #     http://www.apache.org/licenses/LICENSE-2.0
         
     | 
| 
      
 8 
     | 
    
         
            +
            #
         
     | 
| 
      
 9 
     | 
    
         
            +
            # Unless required by applicable law or agreed to in writing, software
         
     | 
| 
      
 10 
     | 
    
         
            +
            # distributed under the License is distributed on an "AS IS" BASIS,
         
     | 
| 
      
 11 
     | 
    
         
            +
            # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         
     | 
| 
      
 12 
     | 
    
         
            +
            # See the License for the specific language governing permissions and
         
     | 
| 
      
 13 
     | 
    
         
            +
            # limitations under the License.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            require "socket"
         
     | 
| 
      
 16 
     | 
    
         
            +
            require "openssl"
         
     | 
| 
      
 17 
     | 
    
         
            +
            require "uri"
         
     | 
| 
      
 18 
     | 
    
         
            +
            require "base64"
         
     | 
| 
      
 19 
     | 
    
         
            +
            require "securerandom"
         
     | 
| 
      
 20 
     | 
    
         
            +
            require "digest/sha1"
         
     | 
| 
      
 21 
     | 
    
         
            +
            require "thread"
         
     | 
| 
      
 22 
     | 
    
         
            +
            require "ipaddr"
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            # Provides the {Eth} module.
         
     | 
| 
      
 25 
     | 
    
         
            +
            module Eth
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              # Provides a WS/S-RPC client with automatic reconnection support.
         
     | 
| 
      
 28 
     | 
    
         
            +
              class Client::Ws < Client
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                # The host of the WebSocket endpoint.
         
     | 
| 
      
 31 
     | 
    
         
            +
                attr_reader :host
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                # The port of the WebSocket endpoint.
         
     | 
| 
      
 34 
     | 
    
         
            +
                attr_reader :port
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                # The full URI of the WebSocket endpoint, including path.
         
     | 
| 
      
 37 
     | 
    
         
            +
                attr_reader :uri
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # Attribute indicator for SSL.
         
     | 
| 
      
 40 
     | 
    
         
            +
                attr_reader :ssl
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                # Constructor for the WebSocket client. Should not be used; use
         
     | 
| 
      
 43 
     | 
    
         
            +
                # {Client.create} instead.
         
     | 
| 
      
 44 
     | 
    
         
            +
                #
         
     | 
| 
      
 45 
     | 
    
         
            +
                # @param host [String] a URI pointing to a WebSocket RPC-API.
         
     | 
| 
      
 46 
     | 
    
         
            +
                def initialize(host)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  super
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @uri = URI.parse(host)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  raise ArgumentError, "Unable to parse the WebSocket-URI!" unless %w[ws wss].include?(@uri.scheme)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @host = @uri.host
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @port = @uri.port
         
     | 
| 
      
 52 
     | 
    
         
            +
                  @ssl = @uri.scheme == "wss"
         
     | 
| 
      
 53 
     | 
    
         
            +
                  @path = build_path(@uri)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  @mutex = Mutex.new
         
     | 
| 
      
 55 
     | 
    
         
            +
                  @socket = nil
         
     | 
| 
      
 56 
     | 
    
         
            +
                  @fragments = nil
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                # Sends an RPC request to the connected WebSocket endpoint.
         
     | 
| 
      
 60 
     | 
    
         
            +
                #
         
     | 
| 
      
 61 
     | 
    
         
            +
                # @param payload [Hash] the RPC request parameters.
         
     | 
| 
      
 62 
     | 
    
         
            +
                # @return [String] a JSON-encoded response.
         
     | 
| 
      
 63 
     | 
    
         
            +
                def send_request(payload)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  attempts = 0
         
     | 
| 
      
 65 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 66 
     | 
    
         
            +
                    attempts += 1
         
     | 
| 
      
 67 
     | 
    
         
            +
                    @mutex.synchronize do
         
     | 
| 
      
 68 
     | 
    
         
            +
                      ensure_socket
         
     | 
| 
      
 69 
     | 
    
         
            +
                      write_frame(@socket, payload)
         
     | 
| 
      
 70 
     | 
    
         
            +
                      return read_message(@socket)
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
                  rescue IOError, SystemCallError => e
         
     | 
| 
      
 73 
     | 
    
         
            +
                    @mutex.synchronize { close_socket }
         
     | 
| 
      
 74 
     | 
    
         
            +
                    retry if attempts < 2
         
     | 
| 
      
 75 
     | 
    
         
            +
                    raise e
         
     | 
| 
      
 76 
     | 
    
         
            +
                  end
         
     | 
| 
      
 77 
     | 
    
         
            +
                end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                # Closes the underlying WebSocket connection.
         
     | 
| 
      
 80 
     | 
    
         
            +
                #
         
     | 
| 
      
 81 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 82 
     | 
    
         
            +
                def close
         
     | 
| 
      
 83 
     | 
    
         
            +
                  @mutex.synchronize { close_socket }
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                private
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                def ensure_socket
         
     | 
| 
      
 89 
     | 
    
         
            +
                  return if @socket && !@socket.closed?
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  socket = open_socket
         
     | 
| 
      
 92 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 93 
     | 
    
         
            +
                    perform_handshake(socket)
         
     | 
| 
      
 94 
     | 
    
         
            +
                    @socket = socket
         
     | 
| 
      
 95 
     | 
    
         
            +
                    @fragments = nil
         
     | 
| 
      
 96 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 97 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 98 
     | 
    
         
            +
                      socket.close unless socket.closed?
         
     | 
| 
      
 99 
     | 
    
         
            +
                    rescue IOError, SystemCallError
         
     | 
| 
      
 100 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 101 
     | 
    
         
            +
                    end
         
     | 
| 
      
 102 
     | 
    
         
            +
                    @socket = nil
         
     | 
| 
      
 103 
     | 
    
         
            +
                    raise
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
                end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                # Establishes the TCP socket for the RPC connection and upgrades it to TLS
         
     | 
| 
      
 108 
     | 
    
         
            +
                # when a secure endpoint is requested. TLS sessions enforce peer
         
     | 
| 
      
 109 
     | 
    
         
            +
                # verification, load the default system trust store, and enable hostname
         
     | 
| 
      
 110 
     | 
    
         
            +
                # verification when the current OpenSSL bindings support it.
         
     | 
| 
      
 111 
     | 
    
         
            +
                #
         
     | 
| 
      
 112 
     | 
    
         
            +
                # @return [TCPSocket, OpenSSL::SSL::SSLSocket] the established socket.
         
     | 
| 
      
 113 
     | 
    
         
            +
                # @raise [IOError, SystemCallError, OpenSSL::SSL::SSLError] if the socket
         
     | 
| 
      
 114 
     | 
    
         
            +
                #   cannot be opened or the TLS handshake fails.
         
     | 
| 
      
 115 
     | 
    
         
            +
                def open_socket
         
     | 
| 
      
 116 
     | 
    
         
            +
                  tcp = TCPSocket.new(@host, @port)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  return tcp unless @ssl
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                  context = OpenSSL::SSL::SSLContext.new
         
     | 
| 
      
 120 
     | 
    
         
            +
                  params = { verify_mode: OpenSSL::SSL::VERIFY_PEER }
         
     | 
| 
      
 121 
     | 
    
         
            +
                  params[:verify_hostname] = true if context.respond_to?(:verify_hostname=)
         
     | 
| 
      
 122 
     | 
    
         
            +
                  context.set_params(params)
         
     | 
| 
      
 123 
     | 
    
         
            +
                  context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                  ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp, context)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  ssl_socket.hostname = @host
         
     | 
| 
      
 127 
     | 
    
         
            +
                  ssl_socket.sync_close = true
         
     | 
| 
      
 128 
     | 
    
         
            +
                  ssl_socket.connect
         
     | 
| 
      
 129 
     | 
    
         
            +
                  ssl_socket
         
     | 
| 
      
 130 
     | 
    
         
            +
                end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                def perform_handshake(socket)
         
     | 
| 
      
 133 
     | 
    
         
            +
                  key = Base64.strict_encode64(SecureRandom.random_bytes(16))
         
     | 
| 
      
 134 
     | 
    
         
            +
                  request = build_handshake_request(key)
         
     | 
| 
      
 135 
     | 
    
         
            +
                  socket.write(request)
         
     | 
| 
      
 136 
     | 
    
         
            +
                  response = read_handshake_response(socket)
         
     | 
| 
      
 137 
     | 
    
         
            +
                  verify_handshake!(response, key)
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                def build_handshake_request(key)
         
     | 
| 
      
 141 
     | 
    
         
            +
                  origin = build_origin_header
         
     | 
| 
      
 142 
     | 
    
         
            +
                  host_header = build_host_header
         
     | 
| 
      
 143 
     | 
    
         
            +
                  "GET #{@path} HTTP/1.1\r\n" \
         
     | 
| 
      
 144 
     | 
    
         
            +
                  "Host: #{host_header}\r\n" \
         
     | 
| 
      
 145 
     | 
    
         
            +
                  "Upgrade: websocket\r\n" \
         
     | 
| 
      
 146 
     | 
    
         
            +
                  "Connection: Upgrade\r\n" \
         
     | 
| 
      
 147 
     | 
    
         
            +
                  "Sec-WebSocket-Version: 13\r\n" \
         
     | 
| 
      
 148 
     | 
    
         
            +
                  "Sec-WebSocket-Key: #{key}\r\n" \
         
     | 
| 
      
 149 
     | 
    
         
            +
                  "Origin: #{origin}\r\n\r\n"
         
     | 
| 
      
 150 
     | 
    
         
            +
                end
         
     | 
| 
      
 151 
     | 
    
         
            +
             
     | 
| 
      
 152 
     | 
    
         
            +
                def read_handshake_response(socket)
         
     | 
| 
      
 153 
     | 
    
         
            +
                  response = +""
         
     | 
| 
      
 154 
     | 
    
         
            +
                  until response.end_with?("\r\n\r\n")
         
     | 
| 
      
 155 
     | 
    
         
            +
                    chunk = socket.readpartial(1024)
         
     | 
| 
      
 156 
     | 
    
         
            +
                    raise IOError, "Incomplete WebSocket handshake" if chunk.nil?
         
     | 
| 
      
 157 
     | 
    
         
            +
                    response << chunk
         
     | 
| 
      
 158 
     | 
    
         
            +
                  end
         
     | 
| 
      
 159 
     | 
    
         
            +
                  response
         
     | 
| 
      
 160 
     | 
    
         
            +
                rescue EOFError
         
     | 
| 
      
 161 
     | 
    
         
            +
                  raise IOError, "Incomplete WebSocket handshake"
         
     | 
| 
      
 162 
     | 
    
         
            +
                end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                def verify_handshake!(response, key)
         
     | 
| 
      
 165 
     | 
    
         
            +
                  status_line = response.lines.first&.strip
         
     | 
| 
      
 166 
     | 
    
         
            +
                  unless status_line&.start_with?("HTTP/1.1 101")
         
     | 
| 
      
 167 
     | 
    
         
            +
                    raise IOError, "WebSocket handshake failed (status: #{status_line || "unknown"})"
         
     | 
| 
      
 168 
     | 
    
         
            +
                  end
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                  accept = response[/Sec-WebSocket-Accept:\s*(.+)\r/i, 1]&.strip
         
     | 
| 
      
 171 
     | 
    
         
            +
                  expected = Base64.strict_encode64(Digest::SHA1.digest("#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
         
     | 
| 
      
 172 
     | 
    
         
            +
                  raise IOError, "WebSocket handshake failed (missing accept header)" unless accept
         
     | 
| 
      
 173 
     | 
    
         
            +
                  raise IOError, "WebSocket handshake failed (invalid accept header)" unless accept == expected
         
     | 
| 
      
 174 
     | 
    
         
            +
                end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                def write_frame(socket, payload, opcode = 0x1)
         
     | 
| 
      
 177 
     | 
    
         
            +
                  frame_payload = payload.is_a?(String) ? payload.dup : payload.to_s
         
     | 
| 
      
 178 
     | 
    
         
            +
                  mask_key = SecureRandom.random_bytes(4)
         
     | 
| 
      
 179 
     | 
    
         
            +
                  header = [0x80 | opcode]
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                  length = frame_payload.bytesize
         
     | 
| 
      
 182 
     | 
    
         
            +
                  if length <= 125
         
     | 
| 
      
 183 
     | 
    
         
            +
                    header << (0x80 | length)
         
     | 
| 
      
 184 
     | 
    
         
            +
                  elsif length <= 0xFFFF
         
     | 
| 
      
 185 
     | 
    
         
            +
                    header << (0x80 | 126)
         
     | 
| 
      
 186 
     | 
    
         
            +
                    header.concat([length].pack("n").bytes)
         
     | 
| 
      
 187 
     | 
    
         
            +
                  else
         
     | 
| 
      
 188 
     | 
    
         
            +
                    header << (0x80 | 127)
         
     | 
| 
      
 189 
     | 
    
         
            +
                    header.concat([length].pack("Q>").bytes)
         
     | 
| 
      
 190 
     | 
    
         
            +
                  end
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                  masked_payload = apply_mask(frame_payload, mask_key)
         
     | 
| 
      
 193 
     | 
    
         
            +
                  socket.write(header.pack("C*") + mask_key + masked_payload)
         
     | 
| 
      
 194 
     | 
    
         
            +
                end
         
     | 
| 
      
 195 
     | 
    
         
            +
             
     | 
| 
      
 196 
     | 
    
         
            +
                def read_message(socket)
         
     | 
| 
      
 197 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 198 
     | 
    
         
            +
                    frame = read_frame(socket)
         
     | 
| 
      
 199 
     | 
    
         
            +
                    return frame if frame
         
     | 
| 
      
 200 
     | 
    
         
            +
                  end
         
     | 
| 
      
 201 
     | 
    
         
            +
                end
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                def read_frame(socket)
         
     | 
| 
      
 204 
     | 
    
         
            +
                  header = read_bytes(socket, 2)
         
     | 
| 
      
 205 
     | 
    
         
            +
                  byte1, byte2 = header.bytes
         
     | 
| 
      
 206 
     | 
    
         
            +
                  opcode = byte1 & 0x0F
         
     | 
| 
      
 207 
     | 
    
         
            +
                  masked = (byte2 & 0x80) == 0x80
         
     | 
| 
      
 208 
     | 
    
         
            +
                  length = byte2 & 0x7F
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
                  length = read_bytes(socket, 2).unpack1("n") if length == 126
         
     | 
| 
      
 211 
     | 
    
         
            +
                  length = read_bytes(socket, 8).unpack1("Q>") if length == 127
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
                  mask_key = masked ? read_bytes(socket, 4).bytes : nil
         
     | 
| 
      
 214 
     | 
    
         
            +
                  payload = read_bytes(socket, length)
         
     | 
| 
      
 215 
     | 
    
         
            +
                  payload_bytes = payload.bytes
         
     | 
| 
      
 216 
     | 
    
         
            +
                  if mask_key
         
     | 
| 
      
 217 
     | 
    
         
            +
                    payload_bytes.map!.with_index { |byte, index| byte ^ mask_key[index % 4] }
         
     | 
| 
      
 218 
     | 
    
         
            +
                  end
         
     | 
| 
      
 219 
     | 
    
         
            +
                  data = payload_bytes.pack("C*")
         
     | 
| 
      
 220 
     | 
    
         
            +
             
     | 
| 
      
 221 
     | 
    
         
            +
                  case opcode
         
     | 
| 
      
 222 
     | 
    
         
            +
                  when 0x0
         
     | 
| 
      
 223 
     | 
    
         
            +
                    (@fragments ||= +"") << data
         
     | 
| 
      
 224 
     | 
    
         
            +
                    if (byte1 & 0x80) == 0x80
         
     | 
| 
      
 225 
     | 
    
         
            +
                      message = @fragments.dup
         
     | 
| 
      
 226 
     | 
    
         
            +
                      @fragments = nil
         
     | 
| 
      
 227 
     | 
    
         
            +
                      message
         
     | 
| 
      
 228 
     | 
    
         
            +
                    else
         
     | 
| 
      
 229 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 230 
     | 
    
         
            +
                    end
         
     | 
| 
      
 231 
     | 
    
         
            +
                  when 0x1, 0x2
         
     | 
| 
      
 232 
     | 
    
         
            +
                    if (byte1 & 0x80) == 0x80
         
     | 
| 
      
 233 
     | 
    
         
            +
                      data
         
     | 
| 
      
 234 
     | 
    
         
            +
                    else
         
     | 
| 
      
 235 
     | 
    
         
            +
                      @fragments = data
         
     | 
| 
      
 236 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 237 
     | 
    
         
            +
                    end
         
     | 
| 
      
 238 
     | 
    
         
            +
                  when 0x8
         
     | 
| 
      
 239 
     | 
    
         
            +
                    close_socket
         
     | 
| 
      
 240 
     | 
    
         
            +
                    raise IOError, "WebSocket closed"
         
     | 
| 
      
 241 
     | 
    
         
            +
                  when 0x9
         
     | 
| 
      
 242 
     | 
    
         
            +
                    write_frame(socket, data, 0xA)
         
     | 
| 
      
 243 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 244 
     | 
    
         
            +
                  when 0xA
         
     | 
| 
      
 245 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 246 
     | 
    
         
            +
                  else
         
     | 
| 
      
 247 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 248 
     | 
    
         
            +
                  end
         
     | 
| 
      
 249 
     | 
    
         
            +
                end
         
     | 
| 
      
 250 
     | 
    
         
            +
             
     | 
| 
      
 251 
     | 
    
         
            +
                def read_bytes(socket, length)
         
     | 
| 
      
 252 
     | 
    
         
            +
                  data = +""
         
     | 
| 
      
 253 
     | 
    
         
            +
                  while data.bytesize < length
         
     | 
| 
      
 254 
     | 
    
         
            +
                    chunk = socket.read(length - data.bytesize)
         
     | 
| 
      
 255 
     | 
    
         
            +
                    raise IOError, "Unexpected end of WebSocket stream" if chunk.nil? || chunk.empty?
         
     | 
| 
      
 256 
     | 
    
         
            +
                    data << chunk
         
     | 
| 
      
 257 
     | 
    
         
            +
                  end
         
     | 
| 
      
 258 
     | 
    
         
            +
                  data
         
     | 
| 
      
 259 
     | 
    
         
            +
                end
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                def apply_mask(payload, mask_key)
         
     | 
| 
      
 262 
     | 
    
         
            +
                  mask_bytes = mask_key.bytes
         
     | 
| 
      
 263 
     | 
    
         
            +
                  payload.bytes.map.with_index { |byte, index| byte ^ mask_bytes[index % 4] }.pack("C*")
         
     | 
| 
      
 264 
     | 
    
         
            +
                end
         
     | 
| 
      
 265 
     | 
    
         
            +
             
     | 
| 
      
 266 
     | 
    
         
            +
                def build_origin_header
         
     | 
| 
      
 267 
     | 
    
         
            +
                  scheme = @ssl ? "https" : "http"
         
     | 
| 
      
 268 
     | 
    
         
            +
                  host = format_origin_host(@uri.host)
         
     | 
| 
      
 269 
     | 
    
         
            +
                  default_port = @ssl ? 443 : 80
         
     | 
| 
      
 270 
     | 
    
         
            +
                  port = @uri.port
         
     | 
| 
      
 271 
     | 
    
         
            +
                  port_suffix = port == default_port ? "" : ":#{port}"
         
     | 
| 
      
 272 
     | 
    
         
            +
                  "#{scheme}://#{host}#{port_suffix}"
         
     | 
| 
      
 273 
     | 
    
         
            +
                end
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                def build_host_header
         
     | 
| 
      
 276 
     | 
    
         
            +
                  "#{format_host(@uri.host)}:#{@uri.port}"
         
     | 
| 
      
 277 
     | 
    
         
            +
                end
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
                def format_origin_host(host)
         
     | 
| 
      
 280 
     | 
    
         
            +
                  return "localhost" if loopback_host?(host)
         
     | 
| 
      
 281 
     | 
    
         
            +
                  format_host(host)
         
     | 
| 
      
 282 
     | 
    
         
            +
                end
         
     | 
| 
      
 283 
     | 
    
         
            +
             
     | 
| 
      
 284 
     | 
    
         
            +
                def format_host(host)
         
     | 
| 
      
 285 
     | 
    
         
            +
                  return host unless host&.include?(":")
         
     | 
| 
      
 286 
     | 
    
         
            +
                  host.start_with?("[") ? host : "[#{host}]"
         
     | 
| 
      
 287 
     | 
    
         
            +
                end
         
     | 
| 
      
 288 
     | 
    
         
            +
             
     | 
| 
      
 289 
     | 
    
         
            +
                def loopback_host?(host)
         
     | 
| 
      
 290 
     | 
    
         
            +
                  return false if host.nil?
         
     | 
| 
      
 291 
     | 
    
         
            +
                  return true if host == "localhost"
         
     | 
| 
      
 292 
     | 
    
         
            +
                  IPAddr.new(host).loopback?
         
     | 
| 
      
 293 
     | 
    
         
            +
                rescue IPAddr::InvalidAddressError
         
     | 
| 
      
 294 
     | 
    
         
            +
                  false
         
     | 
| 
      
 295 
     | 
    
         
            +
                end
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
                def close_socket
         
     | 
| 
      
 298 
     | 
    
         
            +
                  return unless @socket
         
     | 
| 
      
 299 
     | 
    
         
            +
             
     | 
| 
      
 300 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 301 
     | 
    
         
            +
                    write_frame(@socket, [1000].pack("n"), 0x8)
         
     | 
| 
      
 302 
     | 
    
         
            +
                  rescue IOError, SystemCallError
         
     | 
| 
      
 303 
     | 
    
         
            +
                    # ignore errors while closing
         
     | 
| 
      
 304 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 305 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 306 
     | 
    
         
            +
                      @socket.close unless @socket.closed?
         
     | 
| 
      
 307 
     | 
    
         
            +
                    rescue IOError, SystemCallError
         
     | 
| 
      
 308 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 309 
     | 
    
         
            +
                    end
         
     | 
| 
      
 310 
     | 
    
         
            +
                    @socket = nil
         
     | 
| 
      
 311 
     | 
    
         
            +
                    @fragments = nil
         
     | 
| 
      
 312 
     | 
    
         
            +
                  end
         
     | 
| 
      
 313 
     | 
    
         
            +
                end
         
     | 
| 
      
 314 
     | 
    
         
            +
             
     | 
| 
      
 315 
     | 
    
         
            +
                def build_path(uri)
         
     | 
| 
      
 316 
     | 
    
         
            +
                  path = uri.path
         
     | 
| 
      
 317 
     | 
    
         
            +
                  path = "/" if path.nil? || path.empty?
         
     | 
| 
      
 318 
     | 
    
         
            +
                  query = uri.query
         
     | 
| 
      
 319 
     | 
    
         
            +
                  path += "?#{query}" if query
         
     | 
| 
      
 320 
     | 
    
         
            +
                  path
         
     | 
| 
      
 321 
     | 
    
         
            +
                end
         
     | 
| 
      
 322 
     | 
    
         
            +
              end
         
     | 
| 
      
 323 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/eth/client.rb
    CHANGED
    
    | 
         @@ -16,7 +16,7 @@ 
     | 
|
| 
       16 
16 
     | 
    
         
             
            module Eth
         
     | 
| 
       17 
17 
     | 
    
         | 
| 
       18 
18 
     | 
    
         
             
              # Provides the {Eth::Client} super-class to connect to Ethereum
         
     | 
| 
       19 
     | 
    
         
            -
              # network's RPC-API endpoints (IPC or  
     | 
| 
      
 19 
     | 
    
         
            +
              # network's RPC-API endpoints (IPC, HTTP/S, or WS/S).
         
     | 
| 
       20 
20 
     | 
    
         
             
              class Client
         
     | 
| 
       21 
21 
     | 
    
         | 
| 
       22 
22 
     | 
    
         
             
                # The client's RPC-request ID starting at 0.
         
     | 
| 
         @@ -40,20 +40,37 @@ module Eth 
     | 
|
| 
       40 
40 
     | 
    
         
             
                # A custom error type if a contract interaction fails.
         
     | 
| 
       41 
41 
     | 
    
         
             
                class ContractExecutionError < StandardError; end
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
                #  
     | 
| 
       44 
     | 
    
         
            -
                #  
     | 
| 
      
 43 
     | 
    
         
            +
                # Raised when an RPC call returns an error. Carries the optional
         
     | 
| 
      
 44 
     | 
    
         
            +
                # hex-encoded error data to support custom error decoding.
         
     | 
| 
      
 45 
     | 
    
         
            +
                class RpcError < IOError
         
     | 
| 
      
 46 
     | 
    
         
            +
                  attr_reader :data
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  # Constructor for the {RpcError} class.
         
     | 
| 
      
 49 
     | 
    
         
            +
                  #
         
     | 
| 
      
 50 
     | 
    
         
            +
                  # @param message [String] the error message returned by the RPC.
         
     | 
| 
      
 51 
     | 
    
         
            +
                  # @param data [String] optional hex encoded error data.
         
     | 
| 
      
 52 
     | 
    
         
            +
                  def initialize(message, data = nil)
         
     | 
| 
      
 53 
     | 
    
         
            +
                    super(message)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    @data = data
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 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.
         
     | 
| 
       45 
60 
     | 
    
         
             
                #
         
     | 
| 
       46 
61 
     | 
    
         
             
                # **Note**, this sets the folling gas defaults: {Tx::DEFAULT_PRIORITY_FEE}
         
     | 
| 
       47 
62 
     | 
    
         
             
                # and {Tx::DEFAULT_GAS_PRICE. Use {#max_priority_fee_per_gas} and
         
     | 
| 
       48 
63 
     | 
    
         
             
                # {#max_fee_per_gas} to set custom values prior to submitting transactions.
         
     | 
| 
       49 
64 
     | 
    
         
             
                #
         
     | 
| 
       50 
     | 
    
         
            -
                # @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.
         
     | 
| 
       51 
66 
     | 
    
         
             
                # @return [Eth::Client::Ipc] an IPC client.
         
     | 
| 
       52 
67 
     | 
    
         
             
                # @return [Eth::Client::Http] an HTTP client.
         
     | 
| 
      
 68 
     | 
    
         
            +
                # @return [Eth::Client::Ws] a WebSocket client.
         
     | 
| 
       53 
69 
     | 
    
         
             
                # @raise [ArgumentError] in case it cannot determine the client type.
         
     | 
| 
       54 
70 
     | 
    
         
             
                def self.create(host)
         
     | 
| 
       55 
71 
     | 
    
         
             
                  return Client::Ipc.new host if host.end_with? ".ipc"
         
     | 
| 
       56 
72 
     | 
    
         
             
                  return Client::Http.new host if host.start_with? "http"
         
     | 
| 
      
 73 
     | 
    
         
            +
                  return Client::Ws.new host if host.start_with? "ws"
         
     | 
| 
       57 
74 
     | 
    
         
             
                  raise ArgumentError, "Unable to detect client type!"
         
     | 
| 
       58 
75 
     | 
    
         
             
                end
         
     | 
| 
       59 
76 
     | 
    
         | 
| 
         @@ -251,21 +268,28 @@ module Eth 
     | 
|
| 
       251 
268 
     | 
    
         
             
                #   @param **sender_key [Eth::Key] the sender private key.
         
     | 
| 
       252 
269 
     | 
    
         
             
                #   @param **legacy [Boolean] enables legacy transactions (pre-EIP-1559).
         
     | 
| 
       253 
270 
     | 
    
         
             
                # @return [Object] returns the result of the call.
         
     | 
| 
      
 271 
     | 
    
         
            +
                # @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
         
     | 
| 
       254 
272 
     | 
    
         
             
                def call(contract, function, *args, **kwargs)
         
     | 
| 
       255 
     | 
    
         
            -
                   
     | 
| 
       256 
     | 
    
         
            -
                   
     | 
| 
       257 
     | 
    
         
            -
             
     | 
| 
       258 
     | 
    
         
            -
             
     | 
| 
       259 
     | 
    
         
            -
             
     | 
| 
       260 
     | 
    
         
            -
             
     | 
| 
       261 
     | 
    
         
            -
             
     | 
| 
       262 
     | 
    
         
            -
             
     | 
| 
       263 
     | 
    
         
            -
             
     | 
| 
      
 273 
     | 
    
         
            +
                  function = contract.function(function, args: args.size)
         
     | 
| 
      
 274 
     | 
    
         
            +
                  output = function.decode_call_result(
         
     | 
| 
      
 275 
     | 
    
         
            +
                    eth_call(
         
     | 
| 
      
 276 
     | 
    
         
            +
                      {
         
     | 
| 
      
 277 
     | 
    
         
            +
                        data: function.encode_call(*args),
         
     | 
| 
      
 278 
     | 
    
         
            +
                        to: kwargs[:address] || contract.address,
         
     | 
| 
      
 279 
     | 
    
         
            +
                        from: kwargs[:from],
         
     | 
| 
      
 280 
     | 
    
         
            +
                        gas: kwargs[:gas],
         
     | 
| 
      
 281 
     | 
    
         
            +
                        gasPrice: kwargs[:gas_price],
         
     | 
| 
      
 282 
     | 
    
         
            +
                        value: kwargs[:value],
         
     | 
| 
      
 283 
     | 
    
         
            +
                      }.compact
         
     | 
| 
      
 284 
     | 
    
         
            +
                    )["result"]
         
     | 
| 
      
 285 
     | 
    
         
            +
                  )
         
     | 
| 
       264 
286 
     | 
    
         
             
                  if output&.length == 1
         
     | 
| 
       265 
287 
     | 
    
         
             
                    output[0]
         
     | 
| 
       266 
288 
     | 
    
         
             
                  else
         
     | 
| 
       267 
289 
     | 
    
         
             
                    output
         
     | 
| 
       268 
290 
     | 
    
         
             
                  end
         
     | 
| 
      
 291 
     | 
    
         
            +
                rescue RpcError => e
         
     | 
| 
      
 292 
     | 
    
         
            +
                  raise ContractExecutionError, contract.decode_error(e)
         
     | 
| 
       269 
293 
     | 
    
         
             
                end
         
     | 
| 
       270 
294 
     | 
    
         | 
| 
       271 
295 
     | 
    
         
             
                # Executes a contract function with a transaction (transactional
         
     | 
| 
         @@ -298,13 +322,12 @@ module Eth 
     | 
|
| 
       298 
322 
     | 
    
         
             
                    else
         
     | 
| 
       299 
323 
     | 
    
         
             
                      Tx.estimate_intrinsic_gas(contract.bin)
         
     | 
| 
       300 
324 
     | 
    
         
             
                    end
         
     | 
| 
       301 
     | 
    
         
            -
                  fun = contract.functions.select { |func| func.name == function }[0]
         
     | 
| 
       302 
325 
     | 
    
         
             
                  params = {
         
     | 
| 
       303 
326 
     | 
    
         
             
                    value: kwargs[:tx_value] || 0,
         
     | 
| 
       304 
327 
     | 
    
         
             
                    gas_limit: gas_limit,
         
     | 
| 
       305 
328 
     | 
    
         
             
                    chain_id: chain_id,
         
     | 
| 
       306 
329 
     | 
    
         
             
                    to: kwargs[:address] || contract.address,
         
     | 
| 
       307 
     | 
    
         
            -
                    data:  
     | 
| 
      
 330 
     | 
    
         
            +
                    data: contract.function(function, args: args.size).encode_call(*args),
         
     | 
| 
       308 
331 
     | 
    
         
             
                  }
         
     | 
| 
       309 
332 
     | 
    
         
             
                  send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
         
     | 
| 
       310 
333 
     | 
    
         
             
                end
         
     | 
| 
         @@ -320,8 +343,8 @@ module Eth 
     | 
|
| 
       320 
343 
     | 
    
         
             
                  begin
         
     | 
| 
       321 
344 
     | 
    
         
             
                    hash = wait_for_tx(transact(contract, function, *args, **kwargs))
         
     | 
| 
       322 
345 
     | 
    
         
             
                    return hash, tx_succeeded?(hash)
         
     | 
| 
       323 
     | 
    
         
            -
                  rescue  
     | 
| 
       324 
     | 
    
         
            -
                    raise ContractExecutionError, e
         
     | 
| 
      
 346 
     | 
    
         
            +
                  rescue RpcError => e
         
     | 
| 
      
 347 
     | 
    
         
            +
                    raise ContractExecutionError, contract.decode_error(e)
         
     | 
| 
       325 
348 
     | 
    
         
             
                  end
         
     | 
| 
       326 
349 
     | 
    
         
             
                end
         
     | 
| 
       327 
350 
     | 
    
         | 
| 
         @@ -442,28 +465,6 @@ module Eth 
     | 
|
| 
       442 
465 
     | 
    
         
             
                  end
         
     | 
| 
       443 
466 
     | 
    
         
             
                end
         
     | 
| 
       444 
467 
     | 
    
         | 
| 
       445 
     | 
    
         
            -
                # Non-transactional function call called from call().
         
     | 
| 
       446 
     | 
    
         
            -
                # @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
         
     | 
| 
       447 
     | 
    
         
            -
                def call_raw(contract, func, *args, **kwargs)
         
     | 
| 
       448 
     | 
    
         
            -
                  params = {
         
     | 
| 
       449 
     | 
    
         
            -
                    data: call_payload(func, args),
         
     | 
| 
       450 
     | 
    
         
            -
                    to: kwargs[:address] || contract.address,
         
     | 
| 
       451 
     | 
    
         
            -
                    from: kwargs[:from],
         
     | 
| 
       452 
     | 
    
         
            -
                  }.compact
         
     | 
| 
       453 
     | 
    
         
            -
             
     | 
| 
       454 
     | 
    
         
            -
                  raw_result = eth_call(params)["result"]
         
     | 
| 
       455 
     | 
    
         
            -
                  types = func.outputs.map { |i| i.type }
         
     | 
| 
       456 
     | 
    
         
            -
                  return nil if raw_result == "0x"
         
     | 
| 
       457 
     | 
    
         
            -
                  Eth::Abi.decode(types, raw_result)
         
     | 
| 
       458 
     | 
    
         
            -
                end
         
     | 
| 
       459 
     | 
    
         
            -
             
     | 
| 
       460 
     | 
    
         
            -
                # Encodes function call payloads.
         
     | 
| 
       461 
     | 
    
         
            -
                def call_payload(fun, args)
         
     | 
| 
       462 
     | 
    
         
            -
                  types = fun.inputs.map(&:parsed_type)
         
     | 
| 
       463 
     | 
    
         
            -
                  encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
         
     | 
| 
       464 
     | 
    
         
            -
                  Util.prefix_hex(fun.signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
         
     | 
| 
       465 
     | 
    
         
            -
                end
         
     | 
| 
       466 
     | 
    
         
            -
             
     | 
| 
       467 
468 
     | 
    
         
             
                # Encodes constructor params
         
     | 
| 
       468 
469 
     | 
    
         
             
                def encode_constructor_params(contract, args)
         
     | 
| 
       469 
470 
     | 
    
         
             
                  types = contract.constructor_inputs.map { |input| input.type }
         
     | 
| 
         @@ -481,7 +482,9 @@ module Eth 
     | 
|
| 
       481 
482 
     | 
    
         
             
                    id: next_id,
         
     | 
| 
       482 
483 
     | 
    
         
             
                  }
         
     | 
| 
       483 
484 
     | 
    
         
             
                  output = JSON.parse(send_request(payload.to_json))
         
     | 
| 
       484 
     | 
    
         
            -
                   
     | 
| 
      
 485 
     | 
    
         
            +
                  if (err = output["error"])
         
     | 
| 
      
 486 
     | 
    
         
            +
                    raise RpcError.new(err["message"], err["data"])
         
     | 
| 
      
 487 
     | 
    
         
            +
                  end
         
     | 
| 
       485 
488 
     | 
    
         
             
                  output
         
     | 
| 
       486 
489 
     | 
    
         
             
                end
         
     | 
| 
       487 
490 
     | 
    
         | 
| 
         @@ -523,3 +526,4 @@ end 
     | 
|
| 
       523 
526 
     | 
    
         
             
            # Load the client/* libraries
         
     | 
| 
       524 
527 
     | 
    
         
             
            require "eth/client/http"
         
     | 
| 
       525 
528 
     | 
    
         
             
            require "eth/client/ipc"
         
     | 
| 
      
 529 
     | 
    
         
            +
            require "eth/client/ws"
         
     | 
| 
         @@ -0,0 +1,62 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Copyright (c) 2016-2025 The Ruby-Eth Contributors
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Licensed under the Apache License, Version 2.0 (the "License");
         
     | 
| 
      
 4 
     | 
    
         
            +
            # you may not use this file except in compliance with the License.
         
     | 
| 
      
 5 
     | 
    
         
            +
            # You may obtain a copy of the License at
         
     | 
| 
      
 6 
     | 
    
         
            +
            #
         
     | 
| 
      
 7 
     | 
    
         
            +
            #     http://www.apache.org/licenses/LICENSE-2.0
         
     | 
| 
      
 8 
     | 
    
         
            +
            #
         
     | 
| 
      
 9 
     | 
    
         
            +
            # Unless required by applicable law or agreed to in writing, software
         
     | 
| 
      
 10 
     | 
    
         
            +
            # distributed under the License is distributed on an "AS IS" BASIS,
         
     | 
| 
      
 11 
     | 
    
         
            +
            # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         
     | 
| 
      
 12 
     | 
    
         
            +
            # See the License for the specific language governing permissions and
         
     | 
| 
      
 13 
     | 
    
         
            +
            # limitations under the License.
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            # -*- encoding : ascii-8bit -*-
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            # Provides the {Eth} module.
         
     | 
| 
      
 18 
     | 
    
         
            +
            module Eth
         
     | 
| 
      
 19 
     | 
    
         
            +
              # Provide classes for contract custom errors.
         
     | 
| 
      
 20 
     | 
    
         
            +
              class Contract::Error
         
     | 
| 
      
 21 
     | 
    
         
            +
                attr_accessor :name, :inputs, :signature, :error_string
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                # Constructor of the {Eth::Contract::Error} class.
         
     | 
| 
      
 24 
     | 
    
         
            +
                #
         
     | 
| 
      
 25 
     | 
    
         
            +
                # @param data [Hash] contract abi data for the error.
         
     | 
| 
      
 26 
     | 
    
         
            +
                def initialize(data)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @name = data["name"]
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @inputs = data.fetch("inputs", []).map do |input|
         
     | 
| 
      
 29 
     | 
    
         
            +
                    Eth::Contract::FunctionInput.new(input)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @error_string = self.class.calc_signature(@name, @inputs)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @signature = self.class.encoded_error_signature(@error_string)
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                # Creates error strings.
         
     | 
| 
      
 36 
     | 
    
         
            +
                #
         
     | 
| 
      
 37 
     | 
    
         
            +
                # @param name [String] error name.
         
     | 
| 
      
 38 
     | 
    
         
            +
                # @param inputs [Array<Eth::Contract::FunctionInput>] error input class list.
         
     | 
| 
      
 39 
     | 
    
         
            +
                # @return [String] error string.
         
     | 
| 
      
 40 
     | 
    
         
            +
                def self.calc_signature(name, inputs)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  "#{name}(#{inputs.map { |x| x.parsed_type.to_s }.join(",")})"
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                # Encodes an error signature.
         
     | 
| 
      
 45 
     | 
    
         
            +
                #
         
     | 
| 
      
 46 
     | 
    
         
            +
                # @param signature [String] error signature.
         
     | 
| 
      
 47 
     | 
    
         
            +
                # @return [String] encoded error signature string.
         
     | 
| 
      
 48 
     | 
    
         
            +
                def self.encoded_error_signature(signature)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  Util.prefix_hex(Util.bin_to_hex(Util.keccak256(signature)[0..3]))
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                # Decodes a revert error payload.
         
     | 
| 
      
 53 
     | 
    
         
            +
                #
         
     | 
| 
      
 54 
     | 
    
         
            +
                # @param data [String] the hex-encoded revert data including selector.
         
     | 
| 
      
 55 
     | 
    
         
            +
                # @return [Array] decoded error arguments.
         
     | 
| 
      
 56 
     | 
    
         
            +
                def decode(data)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  types = inputs.map(&:type)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  payload = "0x" + data[10..]
         
     | 
| 
      
 59 
     | 
    
         
            +
                  Eth::Abi.decode(types, payload)
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
              end
         
     | 
| 
      
 62 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -53,5 +53,26 @@ module Eth 
     | 
|
| 
       53 
53 
     | 
    
         
             
                def self.encoded_function_signature(signature)
         
     | 
| 
       54 
54 
     | 
    
         
             
                  Util.bin_to_hex Util.keccak256(signature)[0..3]
         
     | 
| 
       55 
55 
     | 
    
         
             
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                # Encodes a function call arguments
         
     | 
| 
      
 58 
     | 
    
         
            +
                #
         
     | 
| 
      
 59 
     | 
    
         
            +
                # @param args [Array] function arguments
         
     | 
| 
      
 60 
     | 
    
         
            +
                # @return [String] encoded function call data
         
     | 
| 
      
 61 
     | 
    
         
            +
                def encode_call(*args)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  types = inputs.map(&:parsed_type)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
         
     | 
| 
      
 64 
     | 
    
         
            +
                  Util.prefix_hex(signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                # Decodes a function call result
         
     | 
| 
      
 68 
     | 
    
         
            +
                #
         
     | 
| 
      
 69 
     | 
    
         
            +
                # @param data [String] eth_call result in hex format
         
     | 
| 
      
 70 
     | 
    
         
            +
                # @return [Array]
         
     | 
| 
      
 71 
     | 
    
         
            +
                def decode_call_result(data)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  return nil if data == "0x"
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  types = outputs.map(&:parsed_type)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  Eth::Abi.decode(types, data)
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
       56 
77 
     | 
    
         
             
              end
         
     | 
| 
       57 
78 
     | 
    
         
             
            end
         
     | 
| 
         @@ -19,19 +19,27 @@ module Eth 
     | 
|
| 
       19 
19 
     | 
    
         | 
| 
       20 
20 
     | 
    
         
             
              # Provide classes for contract function output.
         
     | 
| 
       21 
21 
     | 
    
         
             
              class Contract::FunctionOutput
         
     | 
| 
       22 
     | 
    
         
            -
                attr_accessor :type, :name
         
     | 
| 
      
 22 
     | 
    
         
            +
                attr_accessor :type, :raw_type, :name
         
     | 
| 
       23 
23 
     | 
    
         | 
| 
       24 
24 
     | 
    
         
             
                # Constructor of the {Eth::Contract::FunctionOutput} class.
         
     | 
| 
       25 
25 
     | 
    
         
             
                #
         
     | 
| 
       26 
26 
     | 
    
         
             
                # @param data [Hash] contract abi data.
         
     | 
| 
       27 
27 
     | 
    
         
             
                def initialize(data)
         
     | 
| 
       28 
     | 
    
         
            -
                  @ 
     | 
| 
      
 28 
     | 
    
         
            +
                  @raw_type = data["type"]
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @type = Eth::Abi::Type.parse(data["type"], data["components"])
         
     | 
| 
       29 
30 
     | 
    
         
             
                  @name = data["name"]
         
     | 
| 
       30 
31 
     | 
    
         
             
                end
         
     | 
| 
       31 
32 
     | 
    
         | 
| 
       32 
33 
     | 
    
         
             
                # Returns complete types with subtypes, e.g., `uint256`.
         
     | 
| 
       33 
34 
     | 
    
         
             
                def type
         
     | 
| 
       34 
     | 
    
         
            -
                  @type.base_type + 
     | 
| 
      
 35 
     | 
    
         
            +
                  @type.base_type +
         
     | 
| 
      
 36 
     | 
    
         
            +
                    @type.sub_type +
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @type.dimensions.map { |dimension| "[#{dimension > 0 ? dimension : ""}]" }.join("")
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                # Returns parsed types.
         
     | 
| 
      
 41 
     | 
    
         
            +
                def parsed_type
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @type
         
     | 
| 
       35 
43 
     | 
    
         
             
                end
         
     | 
| 
       36 
44 
     | 
    
         
             
              end
         
     | 
| 
       37 
45 
     | 
    
         
             
            end
         
     | 
    
        data/lib/eth/contract.rb
    CHANGED
    
    | 
         @@ -25,7 +25,7 @@ module Eth 
     | 
|
| 
       25 
25 
     | 
    
         
             
                attr_accessor :key
         
     | 
| 
       26 
26 
     | 
    
         
             
                attr_accessor :gas_limit, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :nonce
         
     | 
| 
       27 
27 
     | 
    
         
             
                attr_accessor :bin, :name, :abi, :class_object
         
     | 
| 
       28 
     | 
    
         
            -
                attr_accessor :events, :functions, :constructor_inputs
         
     | 
| 
      
 28 
     | 
    
         
            +
                attr_accessor :events, :functions, :constructor_inputs, :errors
         
     | 
| 
       29 
29 
     | 
    
         | 
| 
       30 
30 
     | 
    
         
             
                # Constructor of the {Eth::Contract} class.
         
     | 
| 
       31 
31 
     | 
    
         
             
                #
         
     | 
| 
         @@ -44,7 +44,7 @@ module Eth 
     | 
|
| 
       44 
44 
     | 
    
         
             
                  @name = _name
         
     | 
| 
       45 
45 
     | 
    
         
             
                  @bin = bin
         
     | 
| 
       46 
46 
     | 
    
         
             
                  @abi = abi
         
     | 
| 
       47 
     | 
    
         
            -
                  @constructor_inputs, @functions, @events = parse_abi(abi)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  @constructor_inputs, @functions, @events, @errors = parse_abi(abi)
         
     | 
| 
       48 
48 
     | 
    
         
             
                end
         
     | 
| 
       49 
49 
     | 
    
         | 
| 
       50 
50 
     | 
    
         
             
                # Creates a contract wrapper from a Solidity file.
         
     | 
| 
         @@ -106,6 +106,52 @@ module Eth 
     | 
|
| 
       106 
106 
     | 
    
         
             
                  end
         
     | 
| 
       107 
107 
     | 
    
         
             
                end
         
     | 
| 
       108 
108 
     | 
    
         | 
| 
      
 109 
     | 
    
         
            +
                # Finds a function by name.
         
     | 
| 
      
 110 
     | 
    
         
            +
                #
         
     | 
| 
      
 111 
     | 
    
         
            +
                # @param name [String] function name.
         
     | 
| 
      
 112 
     | 
    
         
            +
                # @param args [Integer, nil] number of arguments of a function.
         
     | 
| 
      
 113 
     | 
    
         
            +
                # @return [Eth::Contract::Function] function object.
         
     | 
| 
      
 114 
     | 
    
         
            +
                # @raise [ArgumentError] if function not found.
         
     | 
| 
      
 115 
     | 
    
         
            +
                def function(name, args: nil)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  functions.find do |f|
         
     | 
| 
      
 117 
     | 
    
         
            +
                    f.name == name && (args.nil? || args == f.inputs.size)
         
     | 
| 
      
 118 
     | 
    
         
            +
                  end || raise(ArgumentError, "this function does not exist!")
         
     | 
| 
      
 119 
     | 
    
         
            +
                end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                # Finds an error by name.
         
     | 
| 
      
 122 
     | 
    
         
            +
                #
         
     | 
| 
      
 123 
     | 
    
         
            +
                # @param name [String] error name.
         
     | 
| 
      
 124 
     | 
    
         
            +
                # @param args [Integer, nil] number of arguments of an error.
         
     | 
| 
      
 125 
     | 
    
         
            +
                # @return [Eth::Contract::Error] error object.
         
     | 
| 
      
 126 
     | 
    
         
            +
                # @raise [ArgumentError] if error not found.
         
     | 
| 
      
 127 
     | 
    
         
            +
                def error(name, args: nil)
         
     | 
| 
      
 128 
     | 
    
         
            +
                  errors.find do |e|
         
     | 
| 
      
 129 
     | 
    
         
            +
                    e.name == name && (args.nil? || args == e.inputs.size)
         
     | 
| 
      
 130 
     | 
    
         
            +
                  end || raise(ArgumentError, "this error does not exist!")
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                # Decodes a custom error returned by an RPC error using the contract ABI.
         
     | 
| 
      
 134 
     | 
    
         
            +
                #
         
     | 
| 
      
 135 
     | 
    
         
            +
                # @param rpc_error [RpcError] the RPC error containing revert data.
         
     | 
| 
      
 136 
     | 
    
         
            +
                # @return [String] a human readable error message.
         
     | 
| 
      
 137 
     | 
    
         
            +
                def decode_error(rpc_error)
         
     | 
| 
      
 138 
     | 
    
         
            +
                  data = rpc_error.data
         
     | 
| 
      
 139 
     | 
    
         
            +
                  return rpc_error.message if data.nil? || errors.nil?
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                  signature = data[0, 10]
         
     | 
| 
      
 142 
     | 
    
         
            +
                  if (err = errors.find { |e| e.signature == signature })
         
     | 
| 
      
 143 
     | 
    
         
            +
                    values = err.decode(data)
         
     | 
| 
      
 144 
     | 
    
         
            +
                    args = values&.map { |v| v.is_a?(String) ? v : v.inspect }&.join(",")
         
     | 
| 
      
 145 
     | 
    
         
            +
                    args ||= ""
         
     | 
| 
      
 146 
     | 
    
         
            +
                    "execution reverted: #{err.name}(#{args})"
         
     | 
| 
      
 147 
     | 
    
         
            +
                  elsif signature == "0x08c379a0"
         
     | 
| 
      
 148 
     | 
    
         
            +
                    reason = Abi.decode(["string"], "0x" + data[10..])&.first
         
     | 
| 
      
 149 
     | 
    
         
            +
                    "execution reverted: #{reason}"
         
     | 
| 
      
 150 
     | 
    
         
            +
                  else
         
     | 
| 
      
 151 
     | 
    
         
            +
                    rpc_error.message
         
     | 
| 
      
 152 
     | 
    
         
            +
                  end
         
     | 
| 
      
 153 
     | 
    
         
            +
                end
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
       109 
155 
     | 
    
         
             
                # Create meta classes for smart contracts.
         
     | 
| 
       110 
156 
     | 
    
         
             
                def build
         
     | 
| 
       111 
157 
     | 
    
         
             
                  class_name = @name
         
     | 
| 
         @@ -116,9 +162,12 @@ module Eth 
     | 
|
| 
       116 
162 
     | 
    
         
             
                    def_delegators :parent, :name, :abi, :bin
         
     | 
| 
       117 
163 
     | 
    
         
             
                    def_delegators :parent, :gas_limit, :gas_price, :gas_limit=, :gas_price=, :nonce, :nonce=
         
     | 
| 
       118 
164 
     | 
    
         
             
                    def_delegators :parent, :max_fee_per_gas, :max_fee_per_gas=, :max_priority_fee_per_gas, :max_priority_fee_per_gas=
         
     | 
| 
       119 
     | 
    
         
            -
                    def_delegators :parent, :events
         
     | 
| 
      
 165 
     | 
    
         
            +
                    def_delegators :parent, :events, :errors
         
     | 
| 
       120 
166 
     | 
    
         
             
                    def_delegators :parent, :address, :address=
         
     | 
| 
       121 
167 
     | 
    
         
             
                    def_delegator :parent, :functions
         
     | 
| 
      
 168 
     | 
    
         
            +
                    def_delegator :parent, :function
         
     | 
| 
      
 169 
     | 
    
         
            +
                    def_delegator :parent, :error
         
     | 
| 
      
 170 
     | 
    
         
            +
                    def_delegator :parent, :decode_error
         
     | 
| 
       122 
171 
     | 
    
         
             
                    def_delegator :parent, :constructor_inputs
         
     | 
| 
       123 
172 
     | 
    
         
             
                    define_method :parent do
         
     | 
| 
       124 
173 
     | 
    
         
             
                      parent
         
     | 
| 
         @@ -140,7 +189,8 @@ module Eth 
     | 
|
| 
       140 
189 
     | 
    
         
             
                  end
         
     | 
| 
       141 
190 
     | 
    
         
             
                  functions = abi.select { |x| x["type"] == "function" }.map { |fun| Eth::Contract::Function.new(fun) }
         
     | 
| 
       142 
191 
     | 
    
         
             
                  events = abi.select { |x| x["type"] == "event" }.map { |evt| Eth::Contract::Event.new(evt) }
         
     | 
| 
       143 
     | 
    
         
            -
                  [ 
     | 
| 
      
 192 
     | 
    
         
            +
                  errors = abi.select { |x| x["type"] == "error" }.map { |err| Eth::Contract::Error.new(err) }
         
     | 
| 
      
 193 
     | 
    
         
            +
                  [constructor_inputs, functions, events, errors]
         
     | 
| 
       144 
194 
     | 
    
         
             
                end
         
     | 
| 
       145 
195 
     | 
    
         
             
              end
         
     | 
| 
       146 
196 
     | 
    
         
             
            end
         
     | 
| 
         @@ -151,3 +201,4 @@ require "eth/contract/function" 
     | 
|
| 
       151 
201 
     | 
    
         
             
            require "eth/contract/function_input"
         
     | 
| 
       152 
202 
     | 
    
         
             
            require "eth/contract/function_output"
         
     | 
| 
       153 
203 
     | 
    
         
             
            require "eth/contract/initializer"
         
     | 
| 
      
 204 
     | 
    
         
            +
            require "eth/contract/error"
         
     |