solana_rpc_ruby 1.0.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -1
- data/LICENSE +20 -674
- data/README.md +158 -3
- data/lib/generators/templates/solana_rpc_ruby_config.rb +2 -0
- data/lib/solana_rpc_ruby/api_client.rb +5 -3
- data/lib/solana_rpc_ruby/api_error.rb +13 -2
- data/lib/solana_rpc_ruby/helper_methods.rb +13 -0
- data/lib/solana_rpc_ruby/methods_wrapper.rb +13 -9
- data/lib/solana_rpc_ruby/request_body.rb +9 -4
- data/lib/solana_rpc_ruby/version.rb +1 -1
- data/lib/solana_rpc_ruby/websocket_client.rb +111 -0
- data/lib/solana_rpc_ruby/websocket_methods_wrapper.rb +300 -0
- data/lib/solana_rpc_ruby.rb +6 -0
- data/solana_rpc_ruby.gemspec +4 -2
- metadata +23 -7
data/README.md
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-

|
2
|
+

|
3
|
+

|
4
|
+

|
5
|
+

|
6
|
+
[](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
|
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(
|
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
|
@@ -45,12 +45,14 @@ module SolanaRpcRuby
|
|
45
45
|
Net::HTTPNotFound,
|
46
46
|
Net::HTTPClientException,
|
47
47
|
Net::HTTPFatalError,
|
48
|
-
Net::ReadTimeout
|
49
|
-
|
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(
|
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
|
-
|
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
|
-
|
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":
|
35
|
+
"id": id
|
31
36
|
}
|
32
37
|
end
|
33
38
|
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
|