solana_rpc_ruby 1.0.0.pre → 1.1.1

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.
@@ -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.pre'
4
+ VERSION = '1.1.1'
5
5
  end
@@ -0,0 +1,129 @@
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
+ @subscription_info = nil
31
+
32
+ message = 'Websocket cluster is missing. Please provide default cluster in config or pass it to the client directly.'
33
+ raise ArgumentError, message unless @cluster
34
+ end
35
+
36
+ # Connects with cluster's websocket.
37
+ #
38
+ # @param body [String]
39
+ # @param &block [Proc]
40
+ #
41
+ # @return [String] # messages from websocket
42
+ def connect(body, &block)
43
+ EM.run {
44
+ # ping option sends some data to the server periodically,
45
+ # which prevents the connection to go idle.
46
+ ws ||= Faye::WebSocket::Client.new(@cluster, nil)
47
+ EM::PeriodicTimer.new(KEEPALIVE_TIME) do
48
+ while !ws.ping
49
+ @retries += 1
50
+
51
+ unless @retries <= 3
52
+ puts '3 ping retries failed, close connection.'
53
+ ws.close
54
+ break
55
+ end
56
+
57
+ puts 'Ping failed, sleep for 10 seconds...'
58
+ sleep SLEEP_TIME
59
+ end
60
+ end
61
+
62
+ # Uncomment to disconnect websocket.
63
+ # EM::Timer.new(2) do
64
+ # ws.send(unsubscribe_body(body))
65
+ # end
66
+
67
+ ws.on :open do |event|
68
+ p [:open]
69
+ p "Status: #{ws.status}"
70
+ ws.send(body)
71
+ end
72
+
73
+ ws.on :message do |event|
74
+ # To run websocket_methods_wrapper_spec.rb, uncomment code below
75
+ # to return info about connection estabilished.
76
+ # Also, read the comment from the top of the mentioned file.
77
+ #
78
+ # if ENV['test'] == 'true'
79
+ # result = block_given? ? block.call(event.data) : event.data
80
+ # return result
81
+ # end
82
+ @subscription_info = event.data unless @subscription_info
83
+
84
+ if block_given?
85
+ block.call(event.data)
86
+ else
87
+ puts event.data
88
+ end
89
+ end
90
+
91
+ ws.on :close do |event|
92
+ p [:close, event.code, event.reason]
93
+
94
+ @retries += 1
95
+ if @retries <= RETRIES_LIMIT
96
+ puts 'Retry...'
97
+ # It restarts the websocket connection.
98
+ connect(body, &block)
99
+ else
100
+ ws = nil
101
+ puts 'Retries limit reached, closing. Wrong cluster address or unhealthy node might be a reason, please check.'
102
+ EM.stop
103
+ end
104
+ end
105
+ }
106
+ rescue Timeout::Error,
107
+ Net::HTTPError,
108
+ Net::HTTPNotFound,
109
+ Net::HTTPClientException,
110
+ Net::HTTPFatalError,
111
+ Net::ReadTimeout => e
112
+ fail ApiError.new(message: e.message)
113
+ rescue StandardError => e
114
+ message = "#{e.class} #{e.message}\n Backtrace: \n #{e.backtrace}"
115
+ fail ApiError.new(message: message)
116
+ end
117
+
118
+ def unsubscribe_body(body)
119
+ method = JSON.parse(body)['method']
120
+ info = JSON.parse(@subscription_info)
121
+
122
+ subscription_id = info['result']
123
+ id = info['id']
124
+ unsubscribe_method = method.gsub('Sub', 'Unsub')
125
+
126
+ create_json_body(unsubscribe_method, method_params: [subscription_id], id: id)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,300 @@
1
+ require 'json'
2
+ require_relative 'request_body'
3
+ require_relative 'helper_methods'
4
+
5
+ module SolanaRpcRuby
6
+ ##
7
+ # WebsocketsMethodsWrapper class serves as a wrapper for solana JSON RPC API websocket methods.
8
+ # All informations about params:
9
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#subscription-websocket
10
+ class WebsocketsMethodsWrapper
11
+ include RequestBody
12
+ include HelperMethods
13
+
14
+ # Determines which cluster will be used to send requests.
15
+ # @return [SolanaRpcRuby::WebsocketClient]
16
+ attr_accessor :websocket_client
17
+
18
+ # Cluster where requests will be sent.
19
+ # @return [String]
20
+ attr_accessor :cluster
21
+
22
+ # Unique client-generated identifying integer.
23
+ # @return [Integer]
24
+ attr_accessor :id
25
+
26
+ # Initialize object with cluster address where requests will be sent.
27
+ #
28
+ # @param api_client [ApiClient]
29
+ # @param cluster [String] cluster where requests will be sent.
30
+ # @param id [Integer] unique client-generated identifying integer.
31
+ def initialize(
32
+ websocket_client: WebsocketClient,
33
+ cluster: SolanaRpcRuby.ws_cluster,
34
+ id: rand(1...99_999)
35
+ )
36
+ @websocket_client = websocket_client.new(cluster: cluster)
37
+ @id = id
38
+ end
39
+
40
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#accountsubscribe
41
+ # Subscribe to an account to receive notifications when the lamports or data for a given account public key changes
42
+ #
43
+ # @param account_pubkey [String]
44
+ # @param commitment [String]
45
+ # @param encoding [String]
46
+ # @param &block [Proc]
47
+ #
48
+ # @return [Integer] Subscription id (needed to unsubscribe)
49
+ def account_subscribe(account_pubkey, commitment: nil, encoding: '', &block)
50
+ method = create_method_name(__method__)
51
+
52
+ params = []
53
+ params_hash = {}
54
+ params_hash['commitment'] = commitment unless blank?(commitment)
55
+ params_hash['encoding'] = encoding unless blank?(encoding)
56
+
57
+ params << account_pubkey
58
+ params << params_hash if params_hash.any?
59
+
60
+ subscribe(method, method_params: params, &block)
61
+ end
62
+
63
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#accountunsubscribe
64
+ # Unsubscribe from account change notifications
65
+ #
66
+ # @param subscription_id [Integer]
67
+ #
68
+ # @return [Bool] unsubscribe success message
69
+ def account_unsubscribe(subscription_id)
70
+ method = create_method_name(__method__)
71
+ unsubscribe(method, subscription_id: subscription_id)
72
+ end
73
+
74
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#logssubscribe
75
+ # Subscribe to transaction logging
76
+ #
77
+ # @param filter [String]|[Hash]
78
+ # @option filter [Array] :mentions
79
+ # @param commitment [String]
80
+ # @param &block [Proc]
81
+ #
82
+ # @return [Integer] Subscription id (needed to unsubscribe)
83
+ def logs_subscribe(filter, commitment: nil, &block)
84
+ method = create_method_name(__method__)
85
+
86
+ params = []
87
+ params_hash = {}
88
+ params_hash['commitment'] = commitment unless blank?(commitment)
89
+
90
+ params << filter
91
+ params << params_hash
92
+
93
+ subscribe(method, method_params: params, &block)
94
+ end
95
+
96
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#logsunsubscribe
97
+ # Unsubscribe from transaction logging
98
+ #
99
+ # @param subscription_id [Integer]
100
+ #
101
+ # @return [Bool] unsubscribe success message
102
+
103
+ def logs_unsubscribe(subscription_id)
104
+ method = create_method_name(__method__)
105
+ unsubscribe(method, subscription_id: subscription_id)
106
+ end
107
+
108
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#programsubscribe
109
+ # Subscribe to a program to receive notifications when the lamports or data for a given account owned by the program changes
110
+ #
111
+ # @param account_pubkey [String]
112
+ # @param commitment [String]
113
+ # @param encoding [String]
114
+ # @param filters [Array]
115
+ # @param &block [Proc]
116
+ #
117
+ # @return [Integer] Subscription id (needed to unsubscribe)
118
+ def program_subscribe(
119
+ program_id_pubkey,
120
+ commitment: nil,
121
+ encoding: '',
122
+ filters: [],
123
+ &block
124
+ )
125
+ method = create_method_name(__method__)
126
+
127
+ params = []
128
+ params_hash = {}
129
+ params_hash['commitment'] = commitment unless blank?(commitment)
130
+ params_hash['encoding'] = encoding unless blank?(encoding)
131
+ params_hash['filters'] = filters unless blank?(filters)
132
+
133
+ params << program_id_pubkey
134
+ params << params_hash if params_hash.any?
135
+
136
+ subscribe(method, method_params: params, &block)
137
+ end
138
+
139
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#programunsubscribe
140
+ # Unsubscribe from program-owned account change notifications
141
+ #
142
+ # @param subscription_id [Integer]
143
+ #
144
+ # @return [Bool] unsubscribe success message
145
+
146
+ def program_unsubscribe(subscription_id)
147
+ method = create_method_name(__method__)
148
+ unsubscribe(method, subscription_id: subscription_id)
149
+ end
150
+
151
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#signaturesubscribe
152
+ # Subscribe to a transaction signature to receive notification when the transaction is confirmed
153
+ # On signatureNotification, the subscription is automatically cancelled
154
+ #
155
+ # @param transaction_signature [String]
156
+ # @param commitment [String]
157
+ # @param &block [Proc]
158
+ #
159
+ # @return [Integer] Subscription id (needed to unsubscribe)
160
+ def signature_subscribe(
161
+ transaction_signature,
162
+ commitment: nil,
163
+ &block
164
+ )
165
+ method = create_method_name(__method__)
166
+
167
+ params = []
168
+ params_hash = {}
169
+ params_hash['commitment'] = commitment unless blank?(commitment)
170
+
171
+ params << transaction_signature
172
+ params << params_hash
173
+
174
+ subscribe(method, method_params: params, &block)
175
+ end
176
+
177
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#signatureunsubscribe
178
+ # Unsubscribe from signature confirmation notification
179
+ #
180
+ # @param subscription_id [Integer]
181
+ #
182
+ # @return [Bool] unsubscribe success message
183
+ def signature_unsubscribe(subscription_id)
184
+ method = create_method_name(__method__)
185
+ unsubscribe(method, subscription_id: subscription_id)
186
+ end
187
+
188
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#slotsubscribe
189
+ # Subscribe to receive notification anytime a slot is processed by the validator
190
+ #
191
+ # @param &block [Proc]
192
+ #
193
+ # @return [Integer] Subscription id (needed to unsubscribe)
194
+ def slot_subscribe(&block)
195
+ method = create_method_name(__method__)
196
+
197
+ subscribe(method, &block)
198
+ end
199
+
200
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#slotunsubscribe
201
+ # Unsubscribe from slot notifications
202
+ #
203
+ # @param subscription_id [Integer]
204
+ #
205
+ # @return [Bool] unsubscribe success message
206
+ def slot_unsubscribe(subscription_id)
207
+ method = create_method_name(__method__)
208
+ unsubscribe(method, subscription_id: subscription_id)
209
+ end
210
+
211
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#slotsupdatessubscribe---unstable
212
+ #
213
+ # This subscription is unstable; the format of this subscription may change in the future and it may not always be supported
214
+ # Subscribe to receive a notification from the validator on a variety of updates on every slot
215
+ #
216
+ # @param &block [Proc]
217
+ #
218
+ # @return [Integer] Subscription id (needed to unsubscribe)
219
+ def slots_updates_subscribe(&block)
220
+ method = create_method_name(__method__)
221
+
222
+ subscribe(method, &block)
223
+ end
224
+
225
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#slotsupdatesunsubscribe
226
+ # Unsubscribe from slot-update notifications
227
+ #
228
+ # @param subscription_id [Integer]
229
+ #
230
+ # @return [Bool] unsubscribe success message
231
+ def slots_updates_unsubscribe(subscription_id)
232
+ method = create_method_name(__method__)
233
+ unsubscribe(method, subscription_id: subscription_id)
234
+ end
235
+
236
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#rootsubscribe
237
+ #
238
+ # Subscribe to receive notification anytime a new root is set by the validator.
239
+ #
240
+ # @param &block [Proc]
241
+ #
242
+ # @return [Integer] Subscription id (needed to unsubscribe)
243
+ def root_subscribe(&block)
244
+ method = create_method_name(__method__)
245
+
246
+ subscribe(method, &block)
247
+ end
248
+
249
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#rootunsubscribe
250
+ # Unsubscribe from root notifications
251
+ #
252
+ # @param subscription_id [Integer]
253
+ #
254
+ # @return [Bool] unsubscribe success message
255
+ def root_unsubscribe(subscription_id)
256
+ method = create_method_name(__method__)
257
+ unsubscribe(method, subscription_id: subscription_id)
258
+ end
259
+
260
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#votesubscribe---unstable-disabled-by-default
261
+ #
262
+ # This subscription is unstable and only available if the validator was started with the --rpc-pubsub-enable-vote-subscription flag.
263
+ # The format of this subscription may change in the future
264
+ #
265
+ # Subscribe to receive notification anytime a new vote is observed in gossip.
266
+ # These votes are pre-consensus therefore there is no guarantee these votes will enter the ledger.
267
+ #
268
+ # @param &block [Proc]
269
+ #
270
+ # @return [Integer] Subscription id (needed to unsubscribe)
271
+ def vote_subscribe(&block)
272
+ method = create_method_name(__method__)
273
+
274
+ subscribe(method, &block)
275
+ end
276
+
277
+ # @see https://docs.solana.com/developing/clients/jsonrpc-api#rootunsubscribe
278
+ # Unsubscribe from vote notifications
279
+ #
280
+ # @param subscription_id [Integer]
281
+ #
282
+ # @return [Bool] unsubscribe success message
283
+ def vote_unsubscribe(subscription_id)
284
+ method = create_method_name(__method__)
285
+ unsubscribe(method, subscription_id: subscription_id)
286
+ end
287
+
288
+ private
289
+
290
+ def subscribe(method, method_params: [], &block)
291
+ body = create_json_body(method, method_params: method_params)
292
+ @websocket_client.connect(body, &block)
293
+ end
294
+
295
+ def unsubscribe(method, subscription_id:)
296
+ body = create_json_body(method, method_params: [subscription_id])
297
+ @websocket_client.connect(body)
298
+ end
299
+ end
300
+ end
@@ -2,6 +2,8 @@ require_relative 'solana_rpc_ruby/api_client'
2
2
  require_relative 'solana_rpc_ruby/api_error'
3
3
  require_relative 'solana_rpc_ruby/methods_wrapper'
4
4
  require_relative 'solana_rpc_ruby/response'
5
+ require_relative 'solana_rpc_ruby/websocket_client'
6
+ require_relative 'solana_rpc_ruby/websocket_methods_wrapper'
5
7
 
6
8
  # Namespace for classes and modules that handle connection with solana JSON RPC API.
7
9
  module SolanaRpcRuby
@@ -10,14 +12,14 @@ module SolanaRpcRuby
10
12
  # @return [String] cluster address.
11
13
  attr_accessor :cluster
12
14
 
15
+ # Default websocket cluster address that will be used if not passed.
16
+ # @return [String] websocket cluster address.
17
+ attr_accessor :ws_cluster
18
+
13
19
  # Default json rpc version that will be used.
14
20
  # @return [String] json rpc version.
15
21
  attr_accessor :json_rpc_version
16
22
 
17
- # Default encoding that will be used.
18
- # @return [String] encoding.
19
- attr_accessor :encoding
20
-
21
23
  # Config set from initializer.
22
24
  # @return [String] encoding.
23
25
  def config
@@ -23,15 +23,21 @@ Gem::Specification.new do |spec|
23
23
  'Rakefile'
24
24
  ]
25
25
  spec.extra_rdoc_files = ['README.md']
26
-
26
+ spec.metadata= {
27
+ 'documentation_uri' => 'https://www.rubydoc.info/github/Block-Logic/solana-rpc-ruby',
28
+ 'source_code_uri' => 'https://github.com/Block-Logic/solana-rpc-ruby'
29
+ }
30
+
31
+ spec.add_dependency 'faye-websocket', '~> 0.11'
32
+
27
33
  spec.add_development_dependency 'rubocop', '~> 1.15'
28
34
  spec.add_development_dependency 'rubocop-performance', '~> 1.11'
29
35
  spec.add_development_dependency 'rubocop-rspec', '~> 2.3'
30
- spec.add_development_dependency 'pry', '~> 0.13.1'
36
+ spec.add_development_dependency 'pry', '~> 0.14'
31
37
 
32
38
  spec.add_development_dependency 'codecov', '~> 0.4'
33
39
  spec.add_development_dependency 'dotenv', '~> 2.7'
34
- spec.add_development_dependency 'rails', '~> 6.1.3'
40
+ spec.add_development_dependency 'rails', '~> 6.1'
35
41
  spec.add_development_dependency 'rake', '~> 13.0'
36
42
  spec.add_development_dependency 'rspec', '~> 3.10'
37
43
  spec.add_development_dependency 'rspec-rails', '~> 4.0'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana_rpc_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Block Logic Team
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-13 00:00:00.000000000 Z
11
+ date: 2021-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faye-websocket
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.11'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rubocop
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +72,14 @@ dependencies:
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: 0.13.1
75
+ version: '0.14'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: 0.13.1
82
+ version: '0.14'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: codecov
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +114,14 @@ dependencies:
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: 6.1.3
117
+ version: '6.1'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: 6.1.3
124
+ version: '6.1'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rake
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -218,12 +232,16 @@ files:
218
232
  - lib/solana_rpc_ruby/request_body.rb
219
233
  - lib/solana_rpc_ruby/response.rb
220
234
  - lib/solana_rpc_ruby/version.rb
235
+ - lib/solana_rpc_ruby/websocket_client.rb
236
+ - lib/solana_rpc_ruby/websocket_methods_wrapper.rb
221
237
  - solana_rpc_ruby.gemspec
222
238
  homepage: https://github.com/Block-Logic/solana-rpc-ruby
223
239
  licenses:
224
240
  - MIT
225
- metadata: {}
226
- post_install_message:
241
+ metadata:
242
+ documentation_uri: https://www.rubydoc.info/github/Block-Logic/solana-rpc-ruby
243
+ source_code_uri: https://github.com/Block-Logic/solana-rpc-ruby
244
+ post_install_message:
227
245
  rdoc_options: []
228
246
  require_paths:
229
247
  - lib
@@ -234,12 +252,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
234
252
  version: 2.6.5
235
253
  required_rubygems_version: !ruby/object:Gem::Requirement
236
254
  requirements:
237
- - - ">"
255
+ - - ">="
238
256
  - !ruby/object:Gem::Version
239
- version: 1.3.1
257
+ version: '0'
240
258
  requirements: []
241
- rubygems_version: 3.0.8
242
- signing_key:
259
+ rubygems_version: 3.0.9
260
+ signing_key:
243
261
  specification_version: 4
244
262
  summary: Ruby wrapper for solana JSON RPC API.
245
263
  test_files: []