diff --git a/lib/eth/client.rb b/lib/eth/client.rb index 541a7b3e..2f24c8e1 100644 --- a/lib/eth/client.rb +++ b/lib/eth/client.rb @@ -54,6 +54,7 @@ class ContractExecutionError < StandardError; end def self.create(host) return Client::Ipc.new host if host.end_with? ".ipc" return Client::Http.new host if host.start_with? "http" + return Client::Ws.new host if host.start_with? "ws" raise ArgumentError, "Unable to detect client type!" end @@ -480,13 +481,17 @@ def send_command(command, args) params: marshal(args), id: next_id, } - output = JSON.parse(send_request(payload.to_json)) + payload = payload.to_json + output = send_request(payload) + output = JSON.parse(output) + pp output["error"] raise IOError, output["error"]["message"] unless output["error"].nil? output end # Increments the request id. def next_id + @id ||= 0 @id += 1 end @@ -523,3 +528,4 @@ def marshal(params) # Load the client/* libraries require "eth/client/http" require "eth/client/ipc" +require "eth/client/ws" diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb new file mode 100644 index 00000000..e102bc8e --- /dev/null +++ b/lib/eth/client/ws.rb @@ -0,0 +1,64 @@ +# Copyright (c) 2016-2025 The Ruby-Eth Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# ws://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Provides the {Eth} module. +require "websocket-client-simple" + +module Eth + class Client::Ws < Client + # The host of the HTTP endpoint. + attr_reader :host + + # The port of the HTTP endpoint. + attr_reader :port + + # The full URI of the HTTP endpoint, including path. + attr_reader :uri + + # Attribute indicator for SSL. + attr_reader :ssl + + def initialize(host) + super + uri = URI.parse(host) + raise ArgumentError, "Unable to parse the WS-URI!" unless ["ws", "wss"].include? uri.scheme + @host = uri.host + @port = uri.port + @ssl = uri.scheme == "wss" + if uri.query + @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}?#{uri.query}") + else + @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}") + end + end + + def send_request(payload) + ws = WebSocket::Client::Simple.connect @uri + ws.on :message do |msg| + puts "#{@uri}: #{msg}" + # id = JSON.parse(JSON.generate(msg.to_s))["id"] + # pp id + # @response = msg.to_s + end + ws.on :error do |event| + raise "Error connecting to #{@uri}: #{event}" + end + ws.on :close do + puts "#{@uri}: connection closed" + end + ws.send(payload) + # return JSON.generate({ "jsonrpc": "2.0", "id": 1, "result": "0x539" }) + end + end +end diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 03d2cd51..5a2c3d0f 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -6,10 +6,12 @@ # to provide both http and ipc to pass these tests. let(:geth_ipc_path) { "/tmp/geth.ipc" } let(:geth_http_path) { "http://127.0.0.1:8545" } + let(:geth_ws_path) { "ws://127.0.0.1:8546" } let(:geth_http_authed_path) { "http://username:password@127.0.0.1:8545" } let(:geth_http_query_path) { "http://127.0.0.1:8545?foo=bar&asdf=qwer" } subject(:geth_ipc) { Client.create geth_ipc_path } subject(:geth_http) { Client.create geth_http_path } + subject(:geth_ws) { Client.create geth_ws_path } subject(:geth_http_authed) { Client.create geth_http_authed_path } subject(:geth_http_query) { Client.create geth_http_query_path } @@ -45,6 +47,15 @@ expect(geth_http_query.uri.query).to eq "foo=bar&asdf=qwer" end + it "creates an ws cleint" do + expect(geth_ws).to be + expect(geth_ws).to be_instance_of Client::Ws + expect(geth_ws.host).to eq "127.0.0.1" + expect(geth_ws.port).to eq 8546 + expect(geth_ws.uri.to_s).to eq geth_ws_path + expect(geth_ws.ssl).to be_falsy + end + it "connects to an drpc api" do expect(drpc_mainnet).to be expect(drpc_mainnet).to be_instance_of Client::Http @@ -68,7 +79,7 @@ expect(geth_http_authed.ssl).to be_falsy end - it "functions as geth development client" do + it "functions as geth development client (ipc)" do expect(geth_ipc.id).to eq 0 expect(geth_ipc.chain_id).to eq Chain::PRIVATE_GETH expect(geth_ipc.default_account).to be_instance_of Address @@ -76,6 +87,14 @@ expect(geth_ipc.max_fee_per_gas).to eq Tx::DEFAULT_GAS_PRICE end + it "functions as geth development client (ws)" do + expect(geth_ws.id).to eq 0 + expect(geth_ws.chain_id).to eq Chain::PRIVATE_GETH + # expect(geth_ws.default_account).to be_instance_of Address + expect(geth_ws.max_priority_fee_per_gas).to eq Tx::DEFAULT_PRIORITY_FEE + expect(geth_ws.max_fee_per_gas).to eq Tx::DEFAULT_GAS_PRICE + end + it "http can query basic methods" do # the default account is prefunded; this test fails if you manually drain the account to zero