solana_rpc_ruby 1.0.1 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,4 +1,9 @@
1
1
  ![specs](https://github.com/Block-Logic/solana-rpc-ruby/actions/workflows/specs.yml/badge.svg)
2
+ ![Maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg)
3
+ ![Last Commit](https://img.shields.io/github/last-commit/Block-Logic/solana-rpc-ruby)
4
+ ![Tag](https://img.shields.io/github/v/tag/Block-Logic/solana-rpc-ruby)
5
+ ![Stars](https://img.shields.io/github/stars/Block-Logic/solana-rpc-ruby.svg)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
7
  # solana_rpc_ruby
3
8
  A Solana RPC Client for Ruby. This gem provides a wrapper methods for Solana RPC JSON API https://docs.solana.com/developing/clients/jsonrpc-api.
4
9
 
@@ -6,7 +11,7 @@ A Solana RPC Client for Ruby. This gem provides a wrapper methods for Solana RPC
6
11
 
7
12
  ### Requirements
8
13
 
9
- This gem requires Ruby 2.6+ and h Rails 6.0+. It MIGHT work with lower versions, but was not tested againt them.
14
+ This gem requires Ruby 2.6+ and h Rails 6.0+. It MIGHT work with lower versions, but was not tested with them.
10
15
  Add the following line to your Gemfile:
11
16
 
12
17
  ```ruby
@@ -35,20 +40,162 @@ end
35
40
  You can customize it to your needs.
36
41
 
37
42
  ### Usage examples
43
+
44
+ #### JSON RPC API
38
45
  ```ruby
39
46
  # If you set default cluster you don't need to pass it every time.
40
47
  method_wrapper = SolanaRpcRuby::MethodsWrapper.new(
41
- cluster: 'https://api.testnet.solana.com', # optional, if not passed, default cluster from config will be used
42
- id: 123 # optional, if not passed, default random number from range 1 to 99_999 will be used
48
+ # optional, if not passed, default cluster from config will be used
49
+ cluster: 'https://api.testnet.solana.com',
50
+
51
+ # optional, if not passed, default random number
52
+ # from range 1 to 99_999 will be used
53
+ id: 123
43
54
  )
44
55
 
45
56
  response = method_wrapper.get_account_info(account_pubkey)
46
57
  puts response
47
58
 
48
- # You can check cluster and that are used.
59
+ # You can check cluster and id that are used.
49
60
  method_wrapper.cluster
50
61
  method_wrapper.id
51
62
  ```
63
+ #### Subscription Websocket (BETA)
64
+ ```ruby
65
+ ws_method_wrapper = SolanaRpcRuby::WebsocketsMethodsWrapper.new(
66
+ # optional, if not passed, default ws_cluster from config will be used
67
+ cluster: 'ws://api.testnet.solana.com',
68
+
69
+ # optional, if not passed, default random number
70
+ # from range 1 to 99_999 will be used
71
+ id: 123
72
+ )
73
+
74
+ # You should see stream of messages in your console.
75
+ ws_method_wrapper.root_subscribe
76
+
77
+ # You can pass a block to do something with websocket's messages, ie:
78
+ block = Proc.new do |message|
79
+ json = JSON.parse(message)
80
+ puts json['params']
81
+ end
82
+
83
+ ws_method_wrapper.root_subscribe(&block)
84
+
85
+ # You can check cluster and id that are used.
86
+ ws_method_wrapper.cluster
87
+ ws_method_wrapper.id
88
+ ```
89
+
90
+ #### Websockets usage in Rails
91
+ You can easily plug-in websockets connection to your rails app by using ActionCable.
92
+ Here is an example for development environment.
93
+ More explanation on Action Cable here: https://www.pluralsight.com/guides/updating-a-rails-app's-wall-feed-in-real-time-with-actioncable
94
+
95
+ 0. Make sure that you have action_cable and solana_rpc_ruby gems installed properly. Also install redis unless you have it.
96
+
97
+ 1. Mount action_cable in `routes.rb`.
98
+ ```ruby
99
+ Rails.application.routes.draw do
100
+ mount ActionCable.server => '/cable'
101
+ ...
102
+ end
103
+ ```
104
+
105
+ 2. Update `config/environments/development.rb`.
106
+ ```ruby
107
+ config.action_cable.url = "ws://localhost:3000/cable"
108
+ config.action_cable.allowed_request_origins = [/http:\/\/*/, /https:\/\/*/]
109
+ ```
110
+
111
+ 3. Update adapter in `cable.yml`.
112
+ ```ruby
113
+ development:
114
+ adapter: redis
115
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
116
+ ```
117
+
118
+ 4. Create a channel.
119
+ ```ruby
120
+ rails g channel wall
121
+ ```
122
+
123
+ 5. Your `wall_channel.rb` should look like this:
124
+ ```ruby
125
+ class WallChannel < ApplicationCable::Channel
126
+ def subscribed
127
+ stream_from "wall_channel"
128
+ end
129
+
130
+ def unsubscribed
131
+ # Any cleanup needed when channel is unsubscribed
132
+ end
133
+ end
134
+ ```
135
+
136
+ 6. Your `wall_channel.js` should look like this (json keys are configured for `root_subscription` method response):
137
+ ```js
138
+ import consumer from "./consumer"
139
+
140
+ consumer.subscriptions.create("WallChannel", {
141
+ connected() {
142
+ console.log("Connected to WallChannel");
143
+ // Called when the subscription is ready for use on the server
144
+ },
145
+
146
+ disconnected() {
147
+ // Called when the subscription has been terminated by the server
148
+ },
149
+
150
+ received(data) {
151
+ let wall = document.getElementById('wall');
152
+
153
+ wall.innerHTML += "<p>Result: "+ data['message']['result'] + "</p>";
154
+ // Called when there's incoming data on the websocket for this channel
155
+ }
156
+ });
157
+
158
+
159
+ ```
160
+
161
+ 7. Create placeholder somewhere in your view for messages.
162
+ ```html
163
+ <div id='wall' style='overflow-y: scroll; height:400px;''>
164
+ <h1>Solana subscription messages</h1>
165
+ </div>
166
+ ```
167
+
168
+ 8. Create a script with a block to run websockets (`script/websockets_solana.rb`).
169
+ ```ruby
170
+ require_relative '../config/environment'
171
+
172
+ ws_method_wrapper = SolanaRpcRuby::WebsocketsMethodsWrapper.new
173
+
174
+ # Example of block that can be passed to the method to manipulate the data.
175
+ block = Proc.new do |message|
176
+ json = JSON.parse(message)
177
+
178
+ ActionCable.server.broadcast(
179
+ "wall_channel",
180
+ {
181
+ message: json['params']
182
+ }
183
+ )
184
+ end
185
+
186
+ ws_method_wrapper.root_subscribe(&block)
187
+ ```
188
+ 9. Run `rails s`, open webpage where you put your placeholder.
189
+ 10. Open `http://localhost:3000/address_with_websockets_view`.
190
+ 11. Run `rails r script/websockets_solana.rb` in another terminal window.
191
+ 12. You should see incoming websocket messages on your webpage.
192
+ ### Demo scripts
193
+ Gem is coming with demo scripts that you can run and test API and Websockets.
194
+
195
+ 1. Clone the repo
196
+ 2. Set the gemset
197
+ 3. Run `ruby demo.rb` or `ruby demo_ws_METHOD.rb` to see example output.
198
+ 4. Check the gem or Solana JSON RPC API docs to get more information about method usage and modify demo scripts loosely.
52
199
 
53
200
  All info about methods you can find in the docs on: https://www.rubydoc.info/github/Block-Logic/solana-rpc-ruby/main/SolanaRpcRuby
54
201
 
@@ -5,6 +5,8 @@ SolanaRpcRuby.config do |c|
5
5
  #
6
6
  # You can use this setting or pass cluster directly, check the docs.
7
7
  # c.cluster = 'https://api.testnet.solana.com'
8
+ # c.ws_cluster = 'ws://api.testnet.solana.com'
9
+
8
10
 
9
11
  # This one is mandatory.
10
12
  c.json_rpc_version = '2.0'
@@ -4,6 +4,9 @@ module SolanaRpcRuby
4
4
  # ApiClient class serves as a client for solana JSON RPC API.
5
5
  # @see https://docs.solana.com/developing/clients/jsonrpc-api
6
6
  class ApiClient
7
+ OPEN_TIMEOUT = 120
8
+ READ_TIMEOUT = 120
9
+
7
10
  # Determines which cluster will be used to send requests.
8
11
  # @return [String]
9
12
  attr_accessor :cluster
@@ -30,27 +33,35 @@ module SolanaRpcRuby
30
33
  #
31
34
  # @return [Object] Net::HTTPOK
32
35
  def call_api(body:, http_method:, params: {})
33
- uri = URI(@cluster)
34
- rpc_response = Net::HTTP.public_send(
35
- http_method,
36
- uri,
37
- body,
38
- default_headers,
39
- )
36
+ uri = URI.parse(@cluster)
37
+
38
+ request = Net::HTTP::Post.new(uri, default_headers)
39
+ request.body = body
40
40
 
41
- rpc_response
41
+ Net::HTTP.start(
42
+ uri.host,
43
+ uri.port,
44
+ use_ssl: true,
45
+ open_timeout: OPEN_TIMEOUT,
46
+ read_timeout: READ_TIMEOUT
47
+ ) do |http|
48
+ http.request(request)
49
+ end
42
50
 
43
51
  rescue Timeout::Error,
44
52
  Net::HTTPError,
45
53
  Net::HTTPNotFound,
46
54
  Net::HTTPClientException,
55
+ Net::HTTPServerException,
47
56
  Net::HTTPFatalError,
48
- Net::ReadTimeout => e
49
- fail ApiError.new(message: e.message)
50
- rescue StandardError => e
57
+ Net::ReadTimeout,
58
+ Errno::ECONNREFUSED,
59
+ SocketError => e
51
60
 
61
+ fail ApiError.new(error_class: e.class, message: e.message)
62
+ rescue StandardError => e
52
63
  message = "#{e.class} #{e.message}\n Backtrace: \n #{e.backtrace}"
53
- fail ApiError.new(message: message)
64
+ fail ApiError.new(error_class: e.class, message: e.message)
54
65
  end
55
66
 
56
67
  private
@@ -17,11 +17,22 @@ module SolanaRpcRuby
17
17
  # @param message [String]
18
18
  #
19
19
  # @return [SolanaRpcRuby::ApiError]
20
- def initialize(code: nil, message:)
20
+ def initialize(message:, error_class: nil, code: nil)
21
21
  @code = code
22
22
  @message = message.to_s
23
+ @error_class = error_class
23
24
 
24
- super message
25
+ additional_info
26
+
27
+ super @message
28
+ end
29
+
30
+ private
31
+ def additional_info
32
+ wrong_url_errors = [Errno::ECONNREFUSED, SocketError]
33
+ if wrong_url_errors.include?(@error_class)
34
+ @message += '. Check if the RPC url you provided is correct.'
35
+ end
25
36
  end
26
37
  end
27
38
  end
@@ -12,5 +12,18 @@ module SolanaRpcRuby
12
12
 
13
13
  object.nil? || object.empty?
14
14
  end
15
+
16
+ # Creates method name to match names required by Solana RPC JSON.
17
+ #
18
+ # @param method [String]
19
+ #
20
+ # @return [String]
21
+ def create_method_name(method)
22
+ return '' unless method && (method.is_a?(String) || method.is_a?(Symbol))
23
+
24
+ method.to_s.split('_').map.with_index do |string, i|
25
+ i == 0 ? string : string.capitalize
26
+ end.join
27
+ end
15
28
  end
16
29
  end
@@ -1321,13 +1321,5 @@ module SolanaRpcRuby
1321
1321
  return response
1322
1322
  end
1323
1323
  end
1324
-
1325
- def create_method_name(method)
1326
- return '' unless method
1327
-
1328
- method.to_s.split('_').map.with_index do |string, i|
1329
- i == 0 ? string : string.capitalize
1330
- end.join
1331
- end
1332
1324
  end
1333
1325
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolanaRpcRuby
4
- VERSION = '1.0.1'
4
+ VERSION = '1.1.3'
5
5
  end
@@ -0,0 +1,111 @@
1
+ require 'net/http'
2
+ require 'faye/websocket'
3
+ module SolanaRpcRuby
4
+ ##
5
+ # WebsocketClient class serves as a websocket client for solana JSON RPC API.
6
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api
7
+ class WebsocketClient
8
+ include RequestBody
9
+
10
+ KEEPALIVE_TIME = 60
11
+ SLEEP_TIME = 10
12
+ RETRIES_LIMIT = 3
13
+
14
+ # Determines which cluster will be used to send requests.
15
+ # @return [String]
16
+ attr_accessor :cluster
17
+
18
+ # Api client used to connect with API.
19
+ # @return [Object]
20
+ attr_accessor :client
21
+
22
+ # Initialize object with cluster address where requests will be sent.
23
+ #
24
+ # @param websocket_client [Object]
25
+ # @param cluster [String]
26
+ def initialize(websocket_client: Faye::WebSocket, cluster: nil)
27
+ @client = websocket_client
28
+ @cluster = cluster || SolanaRpcRuby.ws_cluster
29
+ @retries = 0
30
+
31
+ message = 'Websocket cluster is missing. Please provide default cluster in config or pass it to the client directly.'
32
+ raise ArgumentError, message unless @cluster
33
+ end
34
+
35
+ # Connects with cluster's websocket.
36
+ #
37
+ # @param body [String]
38
+ # @param &block [Proc]
39
+ #
40
+ # @return [String] # messages from websocket
41
+ def connect(body, &block)
42
+ EM.run {
43
+ # ping option sends some data to the server periodically,
44
+ # which prevents the connection to go idle.
45
+ ws ||= Faye::WebSocket::Client.new(@cluster, nil)
46
+ EM::PeriodicTimer.new(KEEPALIVE_TIME) do
47
+ while !ws.ping
48
+ @retries += 1
49
+
50
+ unless @retries <= 3
51
+ puts '3 ping retries failed, close connection.'
52
+ ws.close
53
+ break
54
+ end
55
+
56
+ puts 'Ping failed, sleep for 10 seconds...'
57
+ sleep SLEEP_TIME
58
+ end
59
+ end
60
+
61
+ ws.on :open do |event|
62
+ p [:open]
63
+ p "Status: #{ws.status}"
64
+ ws.send(body)
65
+ end
66
+
67
+ ws.on :message do |event|
68
+ # To run websocket_methods_wrapper_spec.rb, uncomment code below
69
+ # to return info about connection estabilished.
70
+ # Also, read the comment from the top of the mentioned file.
71
+ #
72
+ # if ENV['test'] == 'true'
73
+ # result = block_given? ? block.call(event.data) : event.data
74
+ # return result
75
+ # end
76
+
77
+ if block_given?
78
+ block.call(event.data)
79
+ else
80
+ puts event.data
81
+ end
82
+ end
83
+
84
+ ws.on :close do |event|
85
+ p [:close, event.code, event.reason]
86
+
87
+ @retries += 1
88
+ if @retries <= RETRIES_LIMIT
89
+ puts 'Retry...'
90
+ # It restarts the websocket connection.
91
+ connect(body, &block)
92
+ else
93
+ ws = nil
94
+ puts 'Retries limit reached, closing. Wrong cluster address or unhealthy node might be a reason, please check.'
95
+ EM.stop
96
+ end
97
+ end
98
+ }
99
+ rescue Timeout::Error,
100
+ Net::HTTPError,
101
+ Net::HTTPNotFound,
102
+ Net::HTTPClientException,
103
+ Net::HTTPFatalError,
104
+ Net::ReadTimeout => e
105
+ fail ApiError.new(message: e.message)
106
+ rescue StandardError => e
107
+ message = "#{e.class} #{e.message}\n Backtrace: \n #{e.backtrace}"
108
+ fail ApiError.new(message: message)
109
+ end
110
+ end
111
+ end