-
Notifications
You must be signed in to change notification settings - Fork 93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enabling connections with websocket client #222
base: main
Are you sure you want to change the base?
Changes from 25 commits
790dae0
d6098a4
85e4422
66c05e4
2a47fb5
b10cc66
43751be
b9d49bc
20ffb28
86c2246
deacccc
28ed6e0
afb0dc7
f7fca05
5ddd28b
c7b6c96
52656a9
fcaf5db
d25baa3
d6f7dd4
94f71d3
1567a37
e88b2f9
e87f507
efadc4a
fb0563e
4c8d760
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# 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. | ||
# 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" | ||
require "logger" | ||
|
||
# Provides the {Eth} module. | ||
module Eth | ||
|
||
# Provides an WebSocket client. | ||
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. | ||
# | ||
# @param host [String] an URI pointing to an HTTP RPC-API. | ||
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(options[:logger]) | ||
end | ||
|
||
# Sends an RPC request to the connected WebSocket client. | ||
# | ||
# @param payload [Hash] the RPC request parameters. | ||
# @return [Integer] Number of bytes sent by this method. | ||
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 | ||
|
||
private | ||
|
||
def setup_websocket(logger) | ||
@ws = WebSocket::Client::Simple.connect @uri.to_s | ||
|
||
@ws.on :message do |msg| | ||
msg.data | ||
end | ||
|
||
@ws.on :open do | ||
logger.info "websocket open (#{@host})" | ||
end | ||
|
||
@ws.on :close do |e| | ||
logger.info "websocket close (#{e.inspect})" | ||
end | ||
|
||
@ws.on :error do |e| | ||
logger.error "websocket error (#{e.inspect})" | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -2,18 +2,23 @@ | |||||
|
||||||
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" } | ||||||
let(:geth_http_authed_path) { "http://username:[email protected]: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 } | ||||||
|
||||||
# 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::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 }) } | ||||||
|
||||||
describe ".create .initialize" do | ||||||
it "creates an ipc client" do | ||||||
|
@@ -31,6 +36,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 | ||||||
|
@@ -424,4 +438,76 @@ | |||||
expect(geth_ipc.is_valid_signature(contract, hashed, signature)).to be true | ||||||
end | ||||||
end | ||||||
|
||||||
describe ".send" do | ||||||
it "should set up the WebSocket connection" do | ||||||
expect(geth_dev_ws.instance_variable_get("@ws")).to be_instance_of(WebSocket::Client::Simple::Client) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we expose |
||||||
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 | ||||||
|
||||||
# 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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while this is fine for a test, it would be good if it supports all APIs out of the box (instead of using |
||||||
|
||||||
# Wait for the response to be received | ||||||
start_time = Time.now | ||||||
loop do | ||||||
break if received_data || (Time.now - start_time > 3) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be good to add a TIMEOUT to the client, so we don't have to deal with this externally |
||||||
end | ||||||
|
||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
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 | ||||||
|
||||||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where do these two lines come from?