scale_rb 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,202 @@
1
+ require 'async'
2
+ require 'async/websocket/client'
3
+ require 'async/http/endpoint'
4
+ require 'async/queue'
5
+ require 'json'
6
+
7
+ require_relative 'client_ext'
8
+
9
+ module ScaleRb
10
+ class WsClient
11
+ include ClientExt
12
+ attr_accessor :supported_methods
13
+
14
+ def initialize
15
+ @queue = Async::Queue.new
16
+ @response_handler = ResponseHandler.new
17
+ @subscription_handler = SubscriptionHandler.new
18
+ @request_id = 1
19
+ end
20
+
21
+ def request(method, params = [])
22
+ # don't check for rpc_methods, because there is no @supported_methods when initializing
23
+ if method != 'rpc_methods' && !@supported_methods.include?(method)
24
+ raise "Method `#{method}` is not supported. It should be in [#{@supported_methods.join(', ')}]."
25
+ end
26
+
27
+ response_future = Async::Notification.new
28
+
29
+ @response_handler.register(@request_id, proc { |response|
30
+ # this is running in the main task
31
+ response_future.signal(response['result'])
32
+ })
33
+
34
+ request = JsonRpcRequest.new(@request_id, method, params)
35
+ @queue.enqueue(request)
36
+
37
+ @request_id += 1
38
+
39
+ response_future.wait
40
+ end
41
+
42
+ def subscribe(method, params = [], &block)
43
+ return unless method.include?('subscribe')
44
+
45
+ subscription_id = request(method, params)
46
+ @subscription_handler.subscribe(subscription_id, block)
47
+ subscription_id
48
+ end
49
+
50
+ def unsubscribe(method, subscription_id)
51
+ result = request(method, [subscription_id])
52
+ @subscription_handler.unsubscribe(subscription_id)
53
+ result
54
+ end
55
+
56
+ def next_request
57
+ @queue.dequeue
58
+ end
59
+
60
+ def handle_response(response)
61
+ if response.key?('id')
62
+ @response_handler.handle(response)
63
+ elsif response.key?('method')
64
+ @subscription_handler.handle(response)
65
+ else
66
+ puts "Received an unknown message: #{response}"
67
+ end
68
+ end
69
+
70
+ def respond_to_missing?(*_args)
71
+ true
72
+ end
73
+
74
+ def method_missing(method, *args)
75
+ ScaleRb.logger.debug "#{method}(#{args.join(', ')})"
76
+
77
+ method = method.to_s
78
+ if method.include?('unsubscribe')
79
+ unsubscribe(method, args[0])
80
+ elsif method.include?('subscribe')
81
+ raise "A subscribe method needs a block" unless block_given?
82
+
83
+ subscribe(method, args) do |notification|
84
+ yield notification['params']['result']
85
+ end
86
+ else
87
+ request(method, args)
88
+ end
89
+ end
90
+
91
+ def self.start(url)
92
+ Async do |task|
93
+ endpoint = Async::HTTP::Endpoint.parse(url, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
94
+ client = WsClient.new
95
+
96
+ task.async do
97
+ Async::WebSocket::Client.connect(endpoint) do |connection|
98
+ Async do
99
+ while request = client.next_request
100
+ ScaleRb.logger.debug "Sending request: #{request.to_json}"
101
+ connection.write(request.to_json)
102
+ end
103
+ end
104
+
105
+ # inside main task
106
+ while message = connection.read
107
+ data = JSON.parse(message)
108
+ ScaleRb.logger.debug "Received message: #{data}"
109
+
110
+ # 可以简单的理解为,这里的handle_response就是通知wait中的request,可以继续了.
111
+ Async do
112
+ client.handle_response(data)
113
+ rescue => e
114
+ ScaleRb.logger.error "#{e.class}: #{e.message}"
115
+ ScaleRb.logger.error e.backtrace.join("\n")
116
+ task.stop
117
+ end
118
+ end
119
+ rescue => e
120
+ ScaleRb.logger.error "#{e.class}: #{e.message}"
121
+ ScaleRb.logger.error e.backtrace.join("\n")
122
+ ensure
123
+ task.stop
124
+ end
125
+ end
126
+
127
+ task.async do
128
+ client.supported_methods = client.request('rpc_methods')['methods']
129
+ yield client
130
+ rescue => e
131
+ ScaleRb.logger.error "#{e.class}: #{e.message}"
132
+ ScaleRb.logger.error e.backtrace.join("\n")
133
+ task.stop
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ class JsonRpcRequest
140
+ attr_reader :id, :method, :params
141
+
142
+ def initialize(id, method, params = {})
143
+ @id = id
144
+ @method = method
145
+ @params = params
146
+ end
147
+
148
+ def to_json(*_args)
149
+ { jsonrpc: '2.0', id: @id, method: @method, params: @params }.to_json
150
+ end
151
+
152
+ # def to_s
153
+ # to_json
154
+ # end
155
+ end
156
+
157
+ class ResponseHandler
158
+ def initialize
159
+ @handlers = {}
160
+ end
161
+
162
+ # handler: a proc with response data as param
163
+ def register(id, handler)
164
+ @handlers[id] = handler
165
+ end
166
+
167
+ def handle(response)
168
+ id = response['id']
169
+ if @handlers.key?(id)
170
+ handler = @handlers[id]
171
+ handler.call(response)
172
+ @handlers.delete(id)
173
+ else
174
+ ScaleRb.logger.debug "Received a message with unknown id: #{response}"
175
+ end
176
+ end
177
+ end
178
+
179
+ class SubscriptionHandler
180
+ def initialize
181
+ @subscriptions = {}
182
+ end
183
+
184
+ def subscribe(subscription_id, handler)
185
+ @subscriptions[subscription_id] = handler
186
+ end
187
+
188
+ def unsubscribe(subscription_id)
189
+ @subscriptions.delete(subscription_id)
190
+ end
191
+
192
+ def handle(notification)
193
+ subscription_id = notification.dig('params', 'subscription')
194
+ if subscription_id && @subscriptions.key?(subscription_id)
195
+ @subscriptions[subscription_id].call(notification)
196
+ else
197
+ ScaleRb.logger.debug "Received a notification with unknown subscription id: #{notification}"
198
+ end
199
+ end
200
+ end
201
+
202
+ end
data/lib/codec.rb CHANGED
File without changes
data/lib/hasher.rb CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/lib/registry.rb CHANGED
File without changes
@@ -1,3 +1,3 @@
1
1
  module ScaleRb
2
- VERSION = '0.2.2'
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/scale_rb.rb CHANGED
@@ -20,15 +20,15 @@ require 'metadata/metadata'
20
20
  require 'hasher'
21
21
  require 'storage_helper'
22
22
 
23
- # client
24
- require 'client/http_client'
25
- require 'client/abstract_ws_client'
26
-
27
23
  # get registry from config
28
24
  require 'registry'
29
25
 
30
26
  require 'address'
31
27
 
28
+ # clients
29
+ require 'client/http_client'
30
+ require 'client/ws_client'
31
+
32
32
  module ScaleRb
33
33
  class << self
34
34
  attr_accessor :logger
File without changes
data/scale_rb.gemspec CHANGED
@@ -6,11 +6,11 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ['Aki Wu']
7
7
  spec.email = ['wuminzhe@gmail.com']
8
8
 
9
- spec.summary = 'New Ruby SCALE Codec Library'
10
- spec.description = 'Ruby implementation of the parity SCALE data format'
9
+ spec.summary = 'A Ruby SCALE Codec Library, and, Substrate RPC Client'
10
+ spec.description = 'This gem includes a ruby implementation of SCALE Codec, a general Substrate Http JSONRPC Client, and, a general Substrate Websocket JSON-RPC Client.'
11
11
  spec.homepage = 'https://github.com/wuminzhe/scale_rb'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.1.1') # async gem's requirement
14
14
 
15
15
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
16
16
 
@@ -34,4 +34,8 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency 'base58'
35
35
  spec.add_dependency 'blake2b_rs', '~> 0.1.4'
36
36
  spec.add_dependency 'xxhash'
37
+ # for websocket client
38
+ spec.add_dependency 'async'
39
+ spec.add_dependency 'async-http', '~> 0.69.0'
40
+ spec.add_dependency 'async-websocket', '~> 0.26.2'
37
41
  end
data/tea.yaml ADDED
@@ -0,0 +1,6 @@
1
+ # https://tea.xyz/what-is-this-file
2
+ ---
3
+ version: 1.0.0
4
+ codeOwners:
5
+ - '0x08D5966A5226C74f59407bC3aB3a63a68488Da1D'
6
+ quorum: 1
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scale_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aki Wu
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base58
@@ -52,7 +52,50 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description: Ruby implementation of the parity SCALE data format
55
+ - !ruby/object:Gem::Dependency
56
+ name: async
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: async-http
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.69.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.69.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: async-websocket
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.26.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.26.2
97
+ description: This gem includes a ruby implementation of SCALE Codec, a general Substrate
98
+ Http JSONRPC Client, and, a general Substrate Websocket JSON-RPC Client.
56
99
  email:
57
100
  - wuminzhe@gmail.com
58
101
  executables:
@@ -72,13 +115,17 @@ files:
72
115
  - Rakefile
73
116
  - bin/console
74
117
  - bin/setup
118
+ - examples/http_client_1.rb
119
+ - examples/http_client_2.rb
120
+ - examples/ws_client_1.rb
121
+ - examples/ws_client_2.rb
122
+ - examples/ws_client_3.rb
123
+ - examples/ws_client_4.rb
75
124
  - exe/metadata
76
125
  - lib/address.rb
77
- - lib/client/abstract_ws_client.rb
126
+ - lib/client/client_ext.rb
78
127
  - lib/client/http_client.rb
79
- - lib/client/http_client_metadata.rb
80
- - lib/client/http_client_storage.rb
81
- - lib/client/rpc_request_builder.rb
128
+ - lib/client/ws_client.rb
82
129
  - lib/codec.rb
83
130
  - lib/hasher.rb
84
131
  - lib/metadata/metadata.rb
@@ -95,13 +142,14 @@ files:
95
142
  - lib/scale_rb/version.rb
96
143
  - lib/storage_helper.rb
97
144
  - scale_rb.gemspec
145
+ - tea.yaml
98
146
  homepage: https://github.com/wuminzhe/scale_rb
99
147
  licenses:
100
148
  - MIT
101
149
  metadata:
102
150
  bug_tracker_uri: https://github.com/wuminzhe/scale_rb/issues/
103
151
  source_code_uri: https://github.com/wuminzhe/scale_rb.git
104
- post_install_message:
152
+ post_install_message:
105
153
  rdoc_options: []
106
154
  require_paths:
107
155
  - lib
@@ -109,15 +157,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
157
  requirements:
110
158
  - - ">="
111
159
  - !ruby/object:Gem::Version
112
- version: 2.6.0
160
+ version: 3.1.1
113
161
  required_rubygems_version: !ruby/object:Gem::Requirement
114
162
  requirements:
115
163
  - - ">="
116
164
  - !ruby/object:Gem::Version
117
165
  version: '0'
118
166
  requirements: []
119
- rubygems_version: 3.3.7
120
- signing_key:
167
+ rubygems_version: 3.4.19
168
+ signing_key:
121
169
  specification_version: 4
122
- summary: New Ruby SCALE Codec Library
170
+ summary: A Ruby SCALE Codec Library, and, Substrate RPC Client
123
171
  test_files: []
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './rpc_request_builder'
4
-
5
- module ScaleRb
6
- class AbstractWsClient
7
- extend RpcRequestBuilder
8
- attr_accessor :metadata, :registry
9
-
10
- def initialize
11
- @id = 0
12
- @metadata = nil
13
- @registry = nil
14
- @callbacks = {}
15
- @subscription_callbacks = {}
16
- end
17
-
18
- def send_json_rpc(_body)
19
- raise 'WsClient is a abstract base class for websocket client, please use its sub-class'
20
- end
21
-
22
- # changes: [
23
- # [
24
- # "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7", # storage key
25
- # "0x0400000000000000d887690900000000020000" # change
26
- # ]
27
- # ]
28
- def process(resp)
29
- # handle id
30
- @callbacks[resp['id']]&.call(resp['id'], resp) if resp['id']
31
-
32
- # handle storage subscription
33
- return unless resp['params'] && resp['params']['subscription']
34
- return unless @metadata && @registry
35
-
36
- subscription = resp['params']['subscription']
37
- changes = resp['params']['result']['changes']
38
- block = resp['params']['result']['block']
39
- p "block: #{block}"
40
-
41
- return unless @subscription_callbacks[subscription]
42
-
43
- pallet_name, item_name, subscription_callback = @subscription_callbacks[subscription]
44
- storage_item = Metadata.get_storage_item(pallet_name, item_name, @metadata)
45
- storages = decode_storages(changes.map(&:last), storage_item, registry)
46
- subscription_callback.call(storages)
47
- end
48
-
49
- def get_metadata(callback = nil)
50
- if callback.nil?
51
- callback = lambda do |id, resp|
52
- return unless resp['id'] && resp['result']
53
- return if resp['id'] != id
54
-
55
- metadata_hex = resp['result']
56
- metadata = Metadata.decode_metadata(metadata_hex.strip._to_bytes)
57
- return unless metadata
58
-
59
- @metadata = metadata
60
- @registry = Metadata.build_registry(@metadata)
61
- end
62
- end
63
-
64
- id = bind_id_to(callback)
65
- body = state_getMetadata(id)
66
- send_json_rpc(body)
67
- end
68
-
69
- def subscribe_storage(pallet_name, item_name, subscription_callback, key = nil, registry = nil)
70
- callback = create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
71
- id = bind_id_to(callback)
72
- body = derived_state_subscribe_storage(id, pallet_name, item_name, key, registry)
73
- send_json_rpc(body)
74
- end
75
-
76
- private
77
-
78
- def bind_id_to(callback)
79
- @callbacks[@id] = callback
80
- old = @id
81
- @id += 1
82
- old
83
- end
84
-
85
- def decode_storages(datas, storage_item, registry)
86
- datas.map do |data|
87
- StorageHelper.decode_storage2(data, storage_item, registry)
88
- end
89
- end
90
-
91
- def create_callback_for_subscribe_storage(pallet_name, item_name, subscription_callback)
92
- lambda do |id, resp|
93
- return unless resp['id'] && resp['result']
94
- return if resp['id'] != id
95
-
96
- @subscription_callbacks[resp['result']] = [
97
- pallet_name,
98
- item_name,
99
- subscription_callback
100
- ]
101
- end
102
- end
103
- end
104
- end
@@ -1,78 +0,0 @@
1
- require 'json'
2
- require 'fileutils'
3
-
4
- module ScaleRb
5
- module HttpClient
6
- class << self
7
- def get_metadata(url, at = nil)
8
- hex = state_getMetadata(url, at)
9
- Metadata.decode_metadata(hex.strip._to_bytes)
10
- end
11
-
12
- # cached version of get_metadata
13
- # get metadata from cache first
14
- def get_metadata_cached(url, at: nil, dir: nil)
15
- dir = ENV['SCALE_RB_METADATA_DIR'] || File.join(Dir.pwd, 'metadata') if dir.nil?
16
-
17
- at = ScaleRb::HttpClient.chain_getFinalizedHead(url) if at.nil?
18
- spec_name, spec_version = get_spec(url, at)
19
-
20
- # get metadata from cache first
21
- metadata = metadata_cached(
22
- spec_name: spec_name,
23
- spec_version: spec_version,
24
- dir: dir
25
- )
26
- return metadata if metadata
27
-
28
- # get metadata from rpc
29
- metadata = ScaleRb::HttpClient.get_metadata(url, at)
30
-
31
- # cache it
32
- ScaleRb.logger.debug "caching metadata `#{spec_name}_#{spec_version}.json`"
33
- save_metadata_to_file(
34
- spec_name: spec_name,
35
- spec_version: spec_version,
36
- metadata: metadata,
37
- dir: dir
38
- )
39
-
40
- metadata
41
- end
42
-
43
- private
44
-
45
- def get_spec(url, at)
46
- runtime_version = ScaleRb::HttpClient.state_getRuntimeVersion(url, at)
47
- spec_name = runtime_version['specName']
48
- spec_version = runtime_version['specVersion']
49
- [spec_name, spec_version]
50
- end
51
-
52
- def metadata_cached(spec_name:, spec_version:, dir:)
53
- raise 'spec_version is required' unless spec_version
54
- raise 'spec_name is required' unless spec_name
55
-
56
- file_path = File.join(dir, "#{spec_name}_#{spec_version}.json")
57
- return unless File.exist?(file_path)
58
-
59
- ScaleRb.logger.debug "found metadata `#{spec_name}_#{spec_version}.json` in cache"
60
- JSON.parse(File.read(file_path))
61
- end
62
-
63
- def save_metadata_to_file(spec_name:, spec_version:, metadata:, dir:)
64
- FileUtils.mkdir_p(dir)
65
-
66
- File.open(File.join(dir, "#{spec_name}_#{spec_version}.json"), 'w') do |f|
67
- f.write(JSON.pretty_generate(metadata))
68
- end
69
- end
70
-
71
- def require_block_hash_correct(url, block_hash)
72
- return unless ScaleRb::HttpClient.chain_getHeader(url, block_hash).nil?
73
-
74
- raise 'Unable to retrieve header and parent from supplied hash'
75
- end
76
- end
77
- end
78
- end