solana_rpc_ruby 1.0.0 → 1.1.2

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
- ![specs](https://github.com/Block-Logic/solana-rpc-ruby/actions/workflows/specs.yml/badge.svg?branch=177580443_create_wrapper_for_solana_rpc)
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,13 +40,163 @@ 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
- method_wrapper = SolanaRpcRuby::MethodsWrapper.new(cluster: 'https://api.testnet.solana.com')
47
+ method_wrapper = SolanaRpcRuby::MethodsWrapper.new(
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
54
+ )
55
+
41
56
  response = method_wrapper.get_account_info(account_pubkey)
42
57
  puts response
58
+
59
+ # You can check cluster and id that are used.
60
+ method_wrapper.cluster
61
+ method_wrapper.id
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
43
103
  ```
44
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.
199
+
45
200
  All info about methods you can find in the docs on: https://www.rubydoc.info/github/Block-Logic/solana-rpc-ruby/main/SolanaRpcRuby
46
201
 
47
202
  Also, as a reference you can use docs from solana: https://docs.solana.com/developing/clients/jsonrpc-api
@@ -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'
@@ -45,12 +45,14 @@ module SolanaRpcRuby
45
45
  Net::HTTPNotFound,
46
46
  Net::HTTPClientException,
47
47
  Net::HTTPFatalError,
48
- Net::ReadTimeout => e
49
- fail ApiError.new(message: e.message)
48
+ Net::ReadTimeout,
49
+ Errno::ECONNREFUSED,
50
+ SocketError => e
51
+ fail ApiError.new(error_class: e.class, message: e.message)
50
52
  rescue StandardError => e
51
53
 
52
54
  message = "#{e.class} #{e.message}\n Backtrace: \n #{e.backtrace}"
53
- fail ApiError.new(message: message)
55
+ fail ApiError.new(error_class: e.class, message: e.message)
54
56
  end
55
57
 
56
58
  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
@@ -19,12 +19,24 @@ module SolanaRpcRuby
19
19
  # @return [String]
20
20
  attr_accessor :cluster
21
21
 
22
+ # Unique client-generated identifying integer.
23
+ # @return [Integer]
24
+ attr_accessor :id
25
+
22
26
  # Initialize object with cluster address where requests will be sent.
23
27
  #
24
28
  # @param api_client [ApiClient]
25
29
  # @param cluster [String] cluster where requests will be sent.
26
- def initialize(api_client: ApiClient, cluster: SolanaRpcRuby.cluster)
30
+ # @param id [Integer] unique client-generated identifying integer.
31
+ def initialize(
32
+ api_client: ApiClient,
33
+ cluster: SolanaRpcRuby.cluster,
34
+ id: rand(1...99_999)
35
+ )
36
+
27
37
  @api_client = api_client.new(cluster)
38
+ @cluster = cluster
39
+ @id = id
28
40
  end
29
41
 
30
42
  # @see https://docs.solana.com/developing/clients/jsonrpc-api#getaccountinfo
@@ -1309,13 +1321,5 @@ module SolanaRpcRuby
1309
1321
  return response
1310
1322
  end
1311
1323
  end
1312
-
1313
- def create_method_name(method)
1314
- return '' unless method
1315
-
1316
- method.to_s.split('_').map.with_index do |string, i|
1317
- i == 0 ? string : string.capitalize
1318
- end.join
1319
- end
1320
1324
  end
1321
1325
  end
@@ -12,22 +12,27 @@ module SolanaRpcRuby
12
12
  #
13
13
  # @param method [string] method name.
14
14
  # @param method_params [Array] ordered array with required and/or optional params.
15
+ # @param id [Integer] Unique client-generated identifying integer.
15
16
  #
16
17
  # @return [Json] JSON string with body.
17
18
  #
18
- def create_json_body(method, method_params: [])
19
- body = base_body
19
+ def create_json_body(method, method_params: [], id: @id)
20
+ body = base_body(id: id)
20
21
  body[:method] = method
21
22
  body[:params] = method_params if method_params.any?
22
23
  body.to_json
23
24
  end
24
25
 
25
26
  # Hash with default body params.
27
+ # @param id [Integer] Unique client-generated identifying integer.
28
+ #
26
29
  # @return [Hash] hash with base params for every request.
27
- def base_body
30
+ def base_body(id: 1)
31
+ raise ArgumentError, 'id must be an integer' unless id.is_a?(Integer)
32
+
28
33
  {
29
34
  "jsonrpc": SolanaRpcRuby.json_rpc_version,
30
- "id": 1
35
+ "id": id
31
36
  }
32
37
  end
33
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolanaRpcRuby
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.2'
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