From 790dae033ab46057bd14c2f7e8c40ff81d2404de Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Thu, 9 Mar 2023 00:33:14 +0900 Subject: [PATCH 01/22] Enabling connections with websocket client --- eth.gemspec | 3 ++ lib/eth/client.rb | 3 ++ lib/eth/client/ws.rb | 77 +++++++++++++++++++++++++++++++++++++++++ spec/eth/client_spec.rb | 13 +++++++ 4 files changed, 96 insertions(+) create mode 100644 lib/eth/client/ws.rb diff --git a/eth.gemspec b/eth.gemspec index b928f826..5915ea57 100644 --- a/eth.gemspec +++ b/eth.gemspec @@ -48,4 +48,7 @@ Gem::Specification.new do |spec| # scrypt for encrypted key derivation spec.add_dependency "scrypt", "~> 3.0" + + # websocket for websocket client + spec.add_dependency "websocket-client-simple", "~> 0.6.0" end diff --git a/lib/eth/client.rb b/lib/eth/client.rb index 10b40095..31b73a3d 100644 --- a/lib/eth/client.rb +++ b/lib/eth/client.rb @@ -51,10 +51,12 @@ class ContractExecutionError < StandardError; end # @param host [String] either an HTTP/S host or an IPC path. # @return [Eth::Client::Ipc] an IPC client. # @return [Eth::Client::Http] an HTTP client. + # @return [Eth::Client::Ws] an WebSocket client. # @raise [ArgumentError] in case it cannot determine the client type. 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 @@ -525,3 +527,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..fce2e4b2 --- /dev/null +++ b/lib/eth/client/ws.rb @@ -0,0 +1,77 @@ +# Copyright (c) 2016-2022 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 +# +# http://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. + +require 'websocket-client-simple' + +# Provides the {Eth} module. +module Eth + + # Provides an WebSocket client. + 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 + + # Constructor for the WebSocket Client. Should not be used; use + # {Client.create} intead. + # + # @param host [String] an URI pointing to an HTTP RPC-API. + def initialize(host) + super + uri = URI.parse(host) + raise ArgumentError, "Unable to parse the HTTP-URI!" unless ["ws", "wss"].include? uri.scheme + @host = uri.host + @port = uri.port + @ssl = uri.scheme == "wss" + @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}") + end + + # Sends an RPC request to the connected WebSocket client. + # + # @param payload [Hash] the RPC request parameters. + # @return [String] a JSON-encoded response. + def send(payload) + ws = WebSocket::Handshake::Client.new(url: @uri) + ws.on :message do |msg| + puts ">> #{msg.data}" + end + + ws.on :open do + puts "-- websocket open (#{ws.url})" + end + + ws.on :close do |e| + puts "-- websocket close (#{e.inspect})" + exit 1 + end + + ws.on :error do |e| + puts "-- error (#{e.inspect})" + end + + loop do + ws.send STDIN.gets.strip + end + end + end +end diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index cb8e95ca..78cdc6dc 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -7,6 +7,7 @@ let(:geth_ipc_path) { "/tmp/geth.ipc" } let(:geth_http_path) { "http://127.0.0.1:8545" } let(:geth_http_authed_path) { "http://username:password@127.0.0.1:8545" } + let(:geth_dev_ws_path) { "ws://127.0.0.1:8546" } subject(:geth_ipc) { Client.create geth_ipc_path } subject(:geth_http) { Client.create geth_http_path } subject(:geth_http_authed) { Client.create geth_http_authed_path } @@ -14,6 +15,9 @@ # it expects an $INFURA_TOKEN in environment let(:infura_api) { "https://mainnet.infura.io/v3/#{ENV["INFURA_TOKEN"]}" } subject(:infura_mainnet) { Client.create infura_api } + subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } + subject(:geth_dev_http) { Client.create geth_dev_http_path } + subject(:geth_dev_ws) { Client.create geth_dev_ws_path } describe ".create .initialize" do it "creates an ipc client" do @@ -31,6 +35,15 @@ expect(geth_http.ssl).to be_falsy end + it "creates an ws client" do + expect(geth_dev_ws).to be + expect(geth_dev_ws).to be_instance_of Client::Ws + expect(geth_dev_ws.host).to eq "127.0.0.1" + expect(geth_dev_ws.port).to eq 8546 + expect(geth_dev_ws.uri.to_s).to eq geth_dev_ws_path + expect(geth_dev_ws.ssl).to be_falsy + end + it "connects to an infura api" do expect(infura_mainnet).to be expect(infura_mainnet).to be_instance_of Client::Http From d6098a4878051b5c79da89f562db13fb95310fd9 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sun, 30 Apr 2023 15:30:18 +0900 Subject: [PATCH 02/22] Create websocket client --- lib/eth/client/ws.rb | 43 ++++++++++++++--------------------------- spec/eth/client_spec.rb | 3 --- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index fce2e4b2..eeb37cb5 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -23,27 +23,14 @@ 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 - # Constructor for the WebSocket Client. Should not be used; use # {Client.create} intead. # # @param host [String] an URI pointing to an HTTP RPC-API. def initialize(host) super - uri = URI.parse(host) - raise ArgumentError, "Unable to parse the HTTP-URI!" unless ["ws", "wss"].include? uri.scheme - @host = uri.host - @port = uri.port - @ssl = uri.scheme == "wss" - @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}") + @host = host + setup_websocket end # Sends an RPC request to the connected WebSocket client. @@ -51,27 +38,27 @@ def initialize(host) # @param payload [Hash] the RPC request parameters. # @return [String] a JSON-encoded response. def send(payload) - ws = WebSocket::Handshake::Client.new(url: @uri) - ws.on :message do |msg| + @ws.send(payload.to_json) + end + + private + + def setup_websocket + @ws = WebSocket::Client::Simple.connect @host + + @ws.on :message do |msg| puts ">> #{msg.data}" end - - ws.on :open do - puts "-- websocket open (#{ws.url})" + @ws.on :open do + puts "-- websocket open (#{@host})" end - - ws.on :close do |e| + @ws.on :close do |e| puts "-- websocket close (#{e.inspect})" exit 1 end - - ws.on :error do |e| + @ws.on :error do |e| puts "-- error (#{e.inspect})" end - - loop do - ws.send STDIN.gets.strip - end end end end diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 78cdc6dc..2ad33a65 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -39,9 +39,6 @@ expect(geth_dev_ws).to be expect(geth_dev_ws).to be_instance_of Client::Ws expect(geth_dev_ws.host).to eq "127.0.0.1" - expect(geth_dev_ws.port).to eq 8546 - expect(geth_dev_ws.uri.to_s).to eq geth_dev_ws_path - expect(geth_dev_ws.ssl).to be_falsy end it "connects to an infura api" do From 85e4422b21e4431ce514c7559196ba43e72acec4 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Tue, 2 May 2023 01:49:00 +0900 Subject: [PATCH 03/22] 2023 --- lib/eth/client/ws.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index eeb37cb5..2c2fdb18 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2016-2022 The Ruby-Eth Contributors +# Copyright (c) 2016-2023 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. From 66c05e4cbe50949c03144f50ded2d7f13796f764 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Tue, 2 May 2023 02:47:38 +0900 Subject: [PATCH 04/22] add spec --- spec/eth/client_spec.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 2ad33a65..e277c7ff 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -38,7 +38,7 @@ it "creates an ws client" do expect(geth_dev_ws).to be expect(geth_dev_ws).to be_instance_of Client::Ws - expect(geth_dev_ws.host).to eq "127.0.0.1" + expect(geth_dev_ws.host).to eq geth_dev_ws_path end it "connects to an infura api" do @@ -435,4 +435,23 @@ expect(geth_ipc.is_valid_signature(contract, hashed, signature)).to be true end end + + describe ".send" do + it 'sends an RPC request and receives a response' do + payload = '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' + geth_dev_ws.send(payload) + response = nil + + geth_dev_ws.instance_variable_get(:@ws).on :message do |msg| + response = JSON.parse(msg.data) + expect(response).not_to be_nil + expect(response['id']).to eq(payload[:id]) + expect(response['jsonrpc']).to eq(payload[:jsonrpc]) + expect(response['result']).not_to be_nil + block_number = response['result'].to_i(16) + binding.pry + expect(block_number).to be > 0 + end + end + end end From 2a47fb59bdbeb9ac2b65efcf8ecb2347403d43b0 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Tue, 2 May 2023 09:11:25 +0900 Subject: [PATCH 05/22] remove debug code --- spec/eth/client_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index e277c7ff..b4a4a126 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -449,7 +449,6 @@ expect(response['jsonrpc']).to eq(payload[:jsonrpc]) expect(response['result']).not_to be_nil block_number = response['result'].to_i(16) - binding.pry expect(block_number).to be > 0 end end From b10cc6623a0c6ccf6ec72b1c2f21d2c7fc16f322 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Wed, 3 May 2023 22:48:26 +0900 Subject: [PATCH 06/22] Suppress standard output --- spec/eth/client_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index b4a4a126..524ba599 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -17,7 +17,10 @@ subject(:infura_mainnet) { Client.create infura_api } subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } subject(:geth_dev_http) { Client.create geth_dev_http_path } - subject(:geth_dev_ws) { Client.create geth_dev_ws_path } + subject(:geth_dev_ws) { + $stdout = StringIO.new + Client.create geth_dev_ws_path + } describe ".create .initialize" do it "creates an ipc client" do From 43751be92e54151cdacbbc35f576e45eb4a8cc39 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Wed, 3 May 2023 23:11:16 +0900 Subject: [PATCH 07/22] Format files --- lib/eth/client/ws.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index 2c2fdb18..f954d57a 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'websocket-client-simple' +require "websocket-client-simple" # Provides the {Eth} module. module Eth From 20ffb2876debb9a474adbc5ea49c3efb87eda227 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 5 May 2023 02:23:20 +0900 Subject: [PATCH 08/22] WebSocket Server --- .github/workflows/spec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index c2db7b1d..e0e355d3 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -39,7 +39,7 @@ jobs: if: startsWith(matrix.os, 'Ubuntu') - name: Run Geth run: | - geth --dev --http --ipcpath /tmp/geth.ipc & + geth --dev --http --ws --ipcpath /tmp/geth.ipc & disown & - name: Gem Dependencies run: | From 86c22460fb5b28633641ae27200811a47d080d5c Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 5 May 2023 02:30:22 +0900 Subject: [PATCH 09/22] run rufo --- spec/eth/client_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 524ba599..7bfd724c 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -440,7 +440,7 @@ end describe ".send" do - it 'sends an RPC request and receives a response' do + it "sends an RPC request and receives a response" do payload = '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' geth_dev_ws.send(payload) response = nil @@ -448,10 +448,10 @@ geth_dev_ws.instance_variable_get(:@ws).on :message do |msg| response = JSON.parse(msg.data) expect(response).not_to be_nil - expect(response['id']).to eq(payload[:id]) - expect(response['jsonrpc']).to eq(payload[:jsonrpc]) - expect(response['result']).not_to be_nil - block_number = response['result'].to_i(16) + expect(response["id"]).to eq(payload[:id]) + expect(response["jsonrpc"]).to eq(payload[:jsonrpc]) + expect(response["result"]).not_to be_nil + block_number = response["result"].to_i(16) expect(block_number).to be > 0 end end From deacccca27c568339b7272d3567f2b0af87c7870 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 5 May 2023 10:15:53 +0900 Subject: [PATCH 10/22] update comments --- CONTRIBUTING.md | 2 +- README.md | 2 +- bin/setup | 2 +- spec/eth/client_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 124315b5..040e1aa4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ Run `git submodule update --init --recursive` to fetch it. If your tests are failing make sure you pulled the ethereum/tests submodule and run a local geth node in background with -`geth --dev --http --ipcpath /tmp/geth.ipc` as we are running some tests +`geth --dev --http --ws --ipcpath /tmp/geth.ipc` as we are running some tests against a local live node. Other static test data is available in `fixtures/` diff --git a/README.md b/README.md index 982e9104..b5737019 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ The goal is to have 100% API documentation available. The test suite expects working local HTTP and IPC endpoints with a prefunded developer account, e.g.: ```shell -geth --dev --http --ipcpath /tmp/geth.ipc & +geth --dev --http --ws --ipcpath /tmp/geth.ipc & ``` It also expects an `$INFURA_TOKEN` in environment to test some ENS queries on mainnet. diff --git a/bin/setup b/bin/setup index f1c2f9b2..51542ca8 100755 --- a/bin/setup +++ b/bin/setup @@ -6,4 +6,4 @@ rufo . yard doc rspec -echo "Tests fail? Run \`geth --dev --http --ipcpath /tmp/geth.ipc\` in background and try again." +echo "Tests fail? Run \`geth --dev --http --ws --ipcpath /tmp/geth.ipc\` in background and try again." diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 7bfd724c..c36af101 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -2,7 +2,7 @@ describe Client do - # run `geth --dev --http --ipcpath /tmp/geth.ipc` + # run `geth --dev --http --ws --ipcpath /tmp/geth.ipc` # 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" } From 28ed6e0e9bc85001ed0cd5fe93afdef57d04172e Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 5 May 2023 10:17:04 +0900 Subject: [PATCH 11/22] pending: run .send tests with mock or mock server --- spec/eth/client_spec.rb | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index c36af101..3ee26a16 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -18,7 +18,6 @@ subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } subject(:geth_dev_http) { Client.create geth_dev_http_path } subject(:geth_dev_ws) { - $stdout = StringIO.new Client.create geth_dev_ws_path } @@ -440,20 +439,6 @@ end describe ".send" do - it "sends an RPC request and receives a response" do - payload = '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' - geth_dev_ws.send(payload) - response = nil - - geth_dev_ws.instance_variable_get(:@ws).on :message do |msg| - response = JSON.parse(msg.data) - expect(response).not_to be_nil - expect(response["id"]).to eq(payload[:id]) - expect(response["jsonrpc"]).to eq(payload[:jsonrpc]) - expect(response["result"]).not_to be_nil - block_number = response["result"].to_i(16) - expect(block_number).to be > 0 - end - end + pending("using the mock websocket server") end end From afb0dc7e593f010f869332d263173d0ae4134f6d Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Mon, 15 May 2023 04:38:12 +0900 Subject: [PATCH 12/22] add spec --- lib/eth/client/ws.rb | 4 +++- spec/eth/client_spec.rb | 51 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index f954d57a..978adbc8 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -36,7 +36,7 @@ def initialize(host) # Sends an RPC request to the connected WebSocket client. # # @param payload [Hash] the RPC request parameters. - # @return [String] a JSON-encoded response. + # @return [Integer] Number of bytes sent by this method. def send(payload) @ws.send(payload.to_json) end @@ -49,9 +49,11 @@ def setup_websocket @ws.on :message do |msg| puts ">> #{msg.data}" end + @ws.on :open do puts "-- websocket open (#{@host})" end + @ws.on :close do |e| puts "-- websocket close (#{e.inspect})" exit 1 diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 3ee26a16..7eb71524 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -17,9 +17,7 @@ subject(:infura_mainnet) { Client.create infura_api } subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } subject(:geth_dev_http) { Client.create geth_dev_http_path } - subject(:geth_dev_ws) { - Client.create geth_dev_ws_path - } + subject(:geth_dev_ws) { Client.create geth_dev_ws_path } describe ".create .initialize" do it "creates an ipc client" do @@ -438,7 +436,50 @@ end end - describe ".send" do - pending("using the mock websocket server") + describe '.send' do + let(:captured_stdout) { StringIO.new } + + # Replace $stdout to capture standard output + before(:each) do + @orig_stdout = $stdout + $stdout = captured_stdout + end + + after(:each) do + $stdout = @orig_stdout + end + + it 'should set up the WebSocket connection' do + expect(geth_dev_ws.instance_variable_get('@ws')).to be_instance_of(WebSocket::Client::Simple::Client) + end + + it 'should send a message to the WebSocket server and receive a response' do + payload = { + id: 1, + jsonrpc: '2.0', + method: 'eth_subscribe', + params: ["newHeads"], + } + received_data = nil + + geth_dev_ws.instance_variable_get('@ws').on :message do |msg| + received_data = JSON.parse(msg.data) + end + + sleep 0.001 + geth_dev_ws.send(payload) + sleep 0.001 + + expect(received_data['id']).to eq(payload[:id]) + expect(received_data['jsonrpc']).to eq(payload[:jsonrpc]) + expect(received_data['result']).to start_with('0x') + + contract = Eth::Contract.from_file(file: "spec/fixtures/contracts/dummy.sol") + geth_http.deploy_and_wait(contract) + + expect(received_data['method']).to eq("eth_subscription") + expect(received_data['params']["subscription"]).to start_with('0x') + expect(received_data['params']["result"]["parentHash"]).to start_with('0x') + end end end From f7fca05b69b7505e1a0965ece237ef53849b7fdc Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Mon, 15 May 2023 04:46:34 +0900 Subject: [PATCH 13/22] rufo --- spec/eth/client_spec.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 7eb71524..4ba3214e 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -436,7 +436,7 @@ end end - describe '.send' do + describe ".send" do let(:captured_stdout) { StringIO.new } # Replace $stdout to capture standard output @@ -449,20 +449,20 @@ $stdout = @orig_stdout end - it 'should set up the WebSocket connection' do - expect(geth_dev_ws.instance_variable_get('@ws')).to be_instance_of(WebSocket::Client::Simple::Client) + it "should set up the WebSocket connection" do + expect(geth_dev_ws.instance_variable_get("@ws")).to be_instance_of(WebSocket::Client::Simple::Client) end - it 'should send a message to the WebSocket server and receive a response' do + it "should send a message to the WebSocket server and receive a response" do payload = { id: 1, - jsonrpc: '2.0', - method: 'eth_subscribe', + jsonrpc: "2.0", + method: "eth_subscribe", params: ["newHeads"], } received_data = nil - geth_dev_ws.instance_variable_get('@ws').on :message do |msg| + geth_dev_ws.instance_variable_get("@ws").on :message do |msg| received_data = JSON.parse(msg.data) end @@ -470,16 +470,16 @@ geth_dev_ws.send(payload) sleep 0.001 - expect(received_data['id']).to eq(payload[:id]) - expect(received_data['jsonrpc']).to eq(payload[:jsonrpc]) - expect(received_data['result']).to start_with('0x') + expect(received_data["id"]).to eq(payload[:id]) + expect(received_data["jsonrpc"]).to eq(payload[:jsonrpc]) + expect(received_data["result"]).to start_with("0x") contract = Eth::Contract.from_file(file: "spec/fixtures/contracts/dummy.sol") geth_http.deploy_and_wait(contract) - expect(received_data['method']).to eq("eth_subscription") - expect(received_data['params']["subscription"]).to start_with('0x') - expect(received_data['params']["result"]["parentHash"]).to start_with('0x') + expect(received_data["method"]).to eq("eth_subscription") + expect(received_data["params"]["subscription"]).to start_with("0x") + expect(received_data["params"]["result"]["parentHash"]).to start_with("0x") end end end From 5ddd28bde8c25879d2eb7117473a8fe9da10788c Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sat, 20 May 2023 14:03:20 +0900 Subject: [PATCH 14/22] Update lib/eth/client/ws.rb Co-authored-by: Afri <58883403+q9f@users.noreply.github.com> --- lib/eth/client/ws.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index 978adbc8..ae9791bd 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -20,7 +20,7 @@ module Eth # Provides an WebSocket client. class Client::Ws < Client - # The host of the HTTP endpoint. + # The host of the WebSocket endpoint. attr_reader :host # Constructor for the WebSocket Client. Should not be used; use From c7b6c966ce0a7001ad53408923506794a90081c1 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sat, 20 May 2023 15:19:54 +0900 Subject: [PATCH 15/22] Update lib/eth/client/ws.rb Co-authored-by: Afri <58883403+q9f@users.noreply.github.com> --- lib/eth/client/ws.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index ae9791bd..2e3d6062 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -29,7 +29,12 @@ class Client::Ws < Client # @param host [String] an URI pointing to an HTTP RPC-API. def initialize(host) super - @host = host + uri = URI.parse(host) + raise ArgumentError, "Unable to parse the WebSocket-URI!" unless ["ws", "wss"].include? uri.scheme + @host = uri.host + @port = uri.port + @ssl = uri.scheme == "wss" + @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}") setup_websocket end From 52656a9c2dd25f42a6676356a3f3634434058449 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sat, 20 May 2023 16:47:16 +0900 Subject: [PATCH 16/22] fix Client::Ws --- lib/eth/client/ws.rb | 11 ++++++++++- spec/eth/client_spec.rb | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index 2e3d6062..da8ae1ad 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -23,6 +23,15 @@ class Client::Ws < Client # The host of the WebSocket 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 + # Constructor for the WebSocket Client. Should not be used; use # {Client.create} intead. # @@ -49,7 +58,7 @@ def send(payload) private def setup_websocket - @ws = WebSocket::Client::Simple.connect @host + @ws = WebSocket::Client::Simple.connect @uri.to_s @ws.on :message do |msg| puts ">> #{msg.data}" diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 4ba3214e..0f91fd00 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -38,7 +38,10 @@ it "creates an ws client" do expect(geth_dev_ws).to be expect(geth_dev_ws).to be_instance_of Client::Ws - expect(geth_dev_ws.host).to eq geth_dev_ws_path + expect(geth_dev_ws.host).to eq "127.0.0.1" + expect(geth_dev_ws.port).to eq 8546 + expect(geth_dev_ws.uri.to_s).to eq geth_dev_ws_path + expect(geth_dev_ws.ssl).to be_falsy end it "connects to an infura api" do From fcaf5db86e632d919dca0e2047a4bf237abae749 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sat, 20 May 2023 16:50:55 +0900 Subject: [PATCH 17/22] send -> send_request --- lib/eth/client/ws.rb | 2 +- spec/eth/client_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index da8ae1ad..b5b493f2 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -51,7 +51,7 @@ def initialize(host) # # @param payload [Hash] the RPC request parameters. # @return [Integer] Number of bytes sent by this method. - def send(payload) + def send_request(payload) @ws.send(payload.to_json) end diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 0f91fd00..c940ed63 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -470,7 +470,7 @@ end sleep 0.001 - geth_dev_ws.send(payload) + geth_dev_ws.send_request(payload) sleep 0.001 expect(received_data["id"]).to eq(payload[:id]) From d25baa31ea563d4460b4e7fe2f92c7e81f992cfe Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sat, 20 May 2023 17:14:04 +0900 Subject: [PATCH 18/22] use logger --- lib/eth/client/ws.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index b5b493f2..3080035c 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -13,6 +13,7 @@ # limitations under the License. require "websocket-client-simple" +require "logger" # Provides the {Eth} module. module Eth @@ -58,22 +59,25 @@ def send_request(payload) private def setup_websocket + logger = Logger.new(STDOUT) + logger.level = Logger::Severity::WARN + @ws = WebSocket::Client::Simple.connect @uri.to_s @ws.on :message do |msg| - puts ">> #{msg.data}" + msg.data end @ws.on :open do - puts "-- websocket open (#{@host})" + logger.info "websocket open (#{@host})" end @ws.on :close do |e| - puts "-- websocket close (#{e.inspect})" - exit 1 + logger.info "websocket close (#{e.inspect})" end + @ws.on :error do |e| - puts "-- error (#{e.inspect})" + logger.error "websocket error (#{e.inspect})" end end end From d6f7dd473664166d88bed47a6b24ceda3a22a1f8 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sun, 21 May 2023 22:55:56 +0900 Subject: [PATCH 19/22] run without using sleep --- lib/eth/client/ws.rb | 4 ++++ spec/eth/client_spec.rb | 26 ++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index 3080035c..72c02dd9 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -56,6 +56,10 @@ def send_request(payload) @ws.send(payload.to_json) end + def open? + @ws.open? + end + private def setup_websocket diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index c940ed63..23d1c15d 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -440,18 +440,6 @@ end describe ".send" do - let(:captured_stdout) { StringIO.new } - - # Replace $stdout to capture standard output - before(:each) do - @orig_stdout = $stdout - $stdout = captured_stdout - end - - after(:each) do - $stdout = @orig_stdout - end - it "should set up the WebSocket connection" do expect(geth_dev_ws.instance_variable_get("@ws")).to be_instance_of(WebSocket::Client::Simple::Client) end @@ -469,9 +457,19 @@ received_data = JSON.parse(msg.data) end - sleep 0.001 + # Wait for the connection to be established + start_time = Time.now + loop do + break if geth_dev_ws.instance_variable_get("@ws").open? || (Time.now - start_time > 3) + end + geth_dev_ws.send_request(payload) - sleep 0.001 + + # Wait for the response to be received + start_time = Time.now + loop do + break if received_data || (Time.now - start_time > 3) + end expect(received_data["id"]).to eq(payload[:id]) expect(received_data["jsonrpc"]).to eq(payload[:jsonrpc]) From 94f71d333dd52be65c32214685ba580a3717b2e7 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Sun, 21 May 2023 23:01:04 +0900 Subject: [PATCH 20/22] add comments --- lib/eth/client/ws.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index 72c02dd9..c9d80b66 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -56,6 +56,9 @@ def send_request(payload) @ws.send(payload.to_json) end + # Checks if the WebSocket connection is open. + # + # @return [Boolean] true if the connection is open, false otherwise. def open? @ws.open? end From 1567a376b5e629649ebb975798d322c6d1dbcdb6 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Tue, 23 May 2023 14:12:11 +0900 Subject: [PATCH 21/22] enabling Logger to be configured from the outside --- lib/eth/client.rb | 4 ++-- lib/eth/client/ws.rb | 11 ++++------- spec/eth/client_spec.rb | 3 ++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/eth/client.rb b/lib/eth/client.rb index 42f6e4a6..e6bb3e9f 100644 --- a/lib/eth/client.rb +++ b/lib/eth/client.rb @@ -53,10 +53,10 @@ class ContractExecutionError < StandardError; end # @return [Eth::Client::Http] an HTTP client. # @return [Eth::Client::Ws] an WebSocket client. # @raise [ArgumentError] in case it cannot determine the client type. - def self.create(host) + def self.create(host, options = {}) 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" + return Client::Ws.new(host, options) if host.start_with? "ws" raise ArgumentError, "Unable to detect client type!" end diff --git a/lib/eth/client/ws.rb b/lib/eth/client/ws.rb index c9d80b66..284f8b90 100644 --- a/lib/eth/client/ws.rb +++ b/lib/eth/client/ws.rb @@ -37,15 +37,15 @@ class Client::Ws < Client # {Client.create} intead. # # @param host [String] an URI pointing to an HTTP RPC-API. - def initialize(host) - super + def initialize(host, options = {}) + super(host) uri = URI.parse(host) raise ArgumentError, "Unable to parse the WebSocket-URI!" unless ["ws", "wss"].include? uri.scheme @host = uri.host @port = uri.port @ssl = uri.scheme == "wss" @uri = URI("#{uri.scheme}://#{@host}:#{@port}#{uri.path}") - setup_websocket + setup_websocket(options[:logger]) end # Sends an RPC request to the connected WebSocket client. @@ -65,10 +65,7 @@ def open? private - def setup_websocket - logger = Logger.new(STDOUT) - logger.level = Logger::Severity::WARN - + def setup_websocket(logger) @ws = WebSocket::Client::Simple.connect @uri.to_s @ws.on :message do |msg| diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 23d1c15d..263d9487 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -15,9 +15,10 @@ # it expects an $INFURA_TOKEN in environment let(:infura_api) { "https://mainnet.infura.io/v3/#{ENV["INFURA_TOKEN"]}" } subject(:infura_mainnet) { Client.create infura_api } + let(:logger) { Logger.new(STDOUT, level: Logger::WARN) } subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } subject(:geth_dev_http) { Client.create geth_dev_http_path } - subject(:geth_dev_ws) { Client.create geth_dev_ws_path } + subject(:geth_dev_ws) { Client.create(geth_dev_ws_path, { logger: logger }) } describe ".create .initialize" do it "creates an ipc client" do From e88b2f998167d31ee2503183cbebae25ebc92a68 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Wed, 24 May 2023 09:43:38 +0900 Subject: [PATCH 22/22] add websocket open and closed spec --- spec/eth/client_spec.rb | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/spec/eth/client_spec.rb b/spec/eth/client_spec.rb index 263d9487..b97779e6 100644 --- a/spec/eth/client_spec.rb +++ b/spec/eth/client_spec.rb @@ -15,7 +15,7 @@ # it expects an $INFURA_TOKEN in environment let(:infura_api) { "https://mainnet.infura.io/v3/#{ENV["INFURA_TOKEN"]}" } subject(:infura_mainnet) { Client.create infura_api } - let(:logger) { Logger.new(STDOUT, level: Logger::WARN) } + let(:logger) { Logger.new(STDOUT, level: Logger::FATAL) } subject(:geth_dev_ipc) { Client.create geth_dev_ipc_path } subject(:geth_dev_http) { Client.create geth_dev_http_path } subject(:geth_dev_ws) { Client.create(geth_dev_ws_path, { logger: logger }) } @@ -484,4 +484,31 @@ expect(received_data["params"]["result"]["parentHash"]).to start_with("0x") end end + + describe ".open?" do + it "checks if the WebSocket connection is open" do + expect(geth_dev_ws.open?).to be false + + start_time = Time.now + loop do + break if geth_dev_ws.instance_variable_get("@ws").open? || (Time.now - start_time > 3) + end + + expect(geth_dev_ws.open?).to be true + end + end + + describe ".close?" do + it "checks if the WebSocket connection is close" do + start_time = Time.now + loop do + break if geth_dev_ws.instance_variable_get("@ws").open? || (Time.now - start_time > 3) + end + + ws = geth_dev_ws.instance_variable_get("@ws") + expect(ws.closed?).to be false + ws.close + expect(ws.closed?).to be true + end + end end