steem-ruby 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d2df68c704478da2f78e2cdee3a423523950f27
4
- data.tar.gz: f8e6912e7b808be2bb7c065dfb177d0ef6a98a53
3
+ metadata.gz: 25b2e7d4eb5445c89b02aac685677e49a0c551c8
4
+ data.tar.gz: 9b5f21517af8ea96a85c751d0a470463cff09ea7
5
5
  SHA512:
6
- metadata.gz: 8b93714f563a05179fdbcf1c45af4e2279047343b86d1e932bb5ea306a8c7a508f8769b8a22a9415f4344effd591d0f5f86bcfc02a004bf34478fdd140a599ec
7
- data.tar.gz: 12bf547182d8a9abae9d0182cb21f509e176c46f283b3683cebc7b57fc9efb6b3cdabf94538ba1b86550824e5483526257b13d45476d63bf7402bff5b393902e
6
+ metadata.gz: d960cab6d69f53dc73366084c85bb98ca4534829f225306125ca517b4adb391d108483836be355659d32143058daa19b7fa2685bfa4dd2a8b192677cb786e272
7
+ data.tar.gz: ba077e084d71823040fb8470aa5793439356f8d7002da3763b60a4c4939d3655ff78a41ae920a98a8b15709a035b1c0ef9f39a296207675f296213201f2fbb6c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- steem-ruby (0.9.1)
4
+ steem-ruby (0.9.2)
5
5
  bitcoin-ruby (~> 0.0, >= 0.0.18)
6
6
  ffi (~> 1.9, >= 1.9.23)
7
7
  hashie (~> 3.5, >= 3.5.7)
@@ -50,7 +50,7 @@ GEM
50
50
  addressable (>= 2.3.6)
51
51
  crack (>= 0.3.2)
52
52
  hashdiff
53
- yard (0.9.14)
53
+ yard (0.9.15)
54
54
 
55
55
  PLATFORMS
56
56
  ruby
data/README.md CHANGED
@@ -70,6 +70,79 @@ end
70
70
 
71
71
  *See: [Broadcast](https://www.rubydoc.info/gems/steem-ruby/Steem/Broadcast)*
72
72
 
73
+ ### Streaming
74
+
75
+ The value passed to the block is an object, with the keys: `:type` and `:value`.
76
+
77
+ ```ruby
78
+ stream = Steem::Stream.new
79
+
80
+ stream.operations do |op|
81
+ puts "#{op.type}: #{op.value}"
82
+ end
83
+ ```
84
+
85
+ To start a stream from a specific block number, pass it as an argument:
86
+
87
+ ```ruby
88
+ stream = Steem::Stream.new
89
+
90
+ stream.operations(at_block_num: 9001) do |op|
91
+ puts "#{op.type}: #{op.value}"
92
+ end
93
+ ```
94
+
95
+ You can also grab the related transaction id and block number for each operation:
96
+
97
+ ```ruby
98
+ stream = Steem::Stream.new
99
+
100
+ stream.operations do |op, trx_id, block_num|
101
+ puts "#{block_num} :: #{trx_id}"
102
+ puts "#{op.type}: #{op.value}"
103
+ end
104
+ ```
105
+
106
+ To stream only certain operations:
107
+
108
+ ```ruby
109
+ stream = Steem::Stream.new
110
+
111
+ stream.operations(types: :vote_operation) do |op|
112
+ puts "#{op.type}: #{op.value}"
113
+ end
114
+ ```
115
+
116
+ Or pass an array of certain operations:
117
+
118
+ ```ruby
119
+ stream = Steem::Stream.new
120
+
121
+ stream.operations(types: [:comment_operation, :vote_operation]) do |op|
122
+ puts "#{op.type}: #{op.value}"
123
+ end
124
+ ```
125
+
126
+ Or (optionally) just pass the operation(s) you want as the only arguments. This is semantic sugar for when you want specific types and take all of the defaults.
127
+
128
+ ```ruby
129
+ stream = Steem::Stream.new
130
+
131
+ stream.operations(:vote_operation) do |op|
132
+ puts "#{op.type}: #{op.value}"
133
+ end
134
+ ```
135
+
136
+ To also include virtual operations:
137
+
138
+ ```ruby
139
+ stream = Steem::Stream.new
140
+
141
+ stream.operations(include_virtual: true) do |op|
142
+ puts "#{op.type}: #{op.value}"
143
+ end
144
+ ```
145
+
73
146
  ### Multisig
74
147
 
75
148
  You can use multisignature to broadcast an operation.
data/Rakefile CHANGED
@@ -138,44 +138,141 @@ end
138
138
 
139
139
  namespace :stream do
140
140
  desc 'Test the ability to stream a block range.'
141
- task :block_range do
142
- block_api = Steem::BlockApi.new(url: ENV['TEST_NODE'])
141
+ task :block_range, [:mode, :at_block_num] do |t, args|
142
+ mode = (args[:mode] || 'irreversible').to_sym
143
+ first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
144
+ stream = Steem::Stream.new(url: ENV['TEST_NODE'], mode: mode)
143
145
  api = Steem::Api.new(url: ENV['TEST_NODE'])
144
146
  last_block_num = nil
145
- first_block_num = nil
146
147
  last_timestamp = nil
148
+ range_complete = false
147
149
 
148
- loop do
149
- api.get_dynamic_global_properties do |properties|
150
- current_block_num = properties.last_irreversible_block_num
151
- # First pass replays latest a random number of blocks to test chunking.
152
- first_block_num ||= current_block_num - (rand * 200).to_i
150
+ api.get_dynamic_global_properties do |properties|
151
+ current_block_num = if mode == :head
152
+ properties.head_block_number
153
+ else
154
+ properties.last_irreversible_block_num
155
+ end
156
+
157
+ # First pass replays latest a random number of blocks to test chunking.
158
+ first_block_num ||= current_block_num - (rand * 200).to_i
159
+
160
+ range = first_block_num..current_block_num
161
+ puts "Initial block range: #{range.size}"
162
+
163
+ stream.blocks(at_block_num: range.first) do |block, block_num|
164
+ current_timestamp = Time.parse(block.timestamp + 'Z')
153
165
 
154
- if current_block_num >= first_block_num
155
- range = first_block_num..current_block_num
156
- puts "Got block range: #{range.size}"
157
- block_api.get_blocks(block_range: range) do |block, block_num|
158
- current_timestamp = Time.parse(block.timestamp + 'Z')
159
-
160
- if !!last_timestamp && block_num != last_block_num + 1
161
- puts "Bug: Last block number was #{last_block_num} then jumped to: #{block_num}"
162
- exit
163
- end
164
-
165
- if !!last_timestamp && current_timestamp < last_timestamp
166
- puts "Bug: Went back in time. Last timestamp was #{last_timestamp}, then jumped back to #{current_timestamp}"
167
- exit
168
- end
169
-
170
- puts "\t#{block_num} Timestamp: #{current_timestamp}, witness: #{block.witness}"
171
- last_block_num = block_num
172
- last_timestamp = current_timestamp
173
- end
174
-
175
- first_block_num = range.max + 1
166
+ if !range_complete && block_num > range.last
167
+ puts 'Done with initial range.'
168
+ range_complete = true
176
169
  end
177
170
 
178
- sleep 3
171
+ if !!last_timestamp && block_num != last_block_num + 1
172
+ puts "Bug: Last block number was #{last_block_num} then jumped to: #{block_num}"
173
+ exit
174
+ end
175
+
176
+ if !!last_timestamp && current_timestamp < last_timestamp
177
+ puts "Bug: Went back in time. Last timestamp was #{last_timestamp}, then jumped back to #{current_timestamp}"
178
+ exit
179
+ end
180
+
181
+ puts "\t#{block_num} Timestamp: #{current_timestamp}, witness: #{block.witness}"
182
+ last_block_num = block_num
183
+ last_timestamp = current_timestamp
184
+ end
185
+ end
186
+ end
187
+
188
+ desc 'Test the ability to stream a block range of transactions.'
189
+ task :trx_range, [:mode, :at_block_num] do |t, args|
190
+ mode = (args[:mode] || 'irreversible').to_sym
191
+ first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
192
+ stream = Steem::Stream.new(url: ENV['TEST_NODE'], mode: mode)
193
+ api = Steem::Api.new(url: ENV['TEST_NODE'])
194
+
195
+ api.get_dynamic_global_properties do |properties|
196
+ current_block_num = if mode == :head
197
+ properties.head_block_number
198
+ else
199
+ properties.last_irreversible_block_num
200
+ end
201
+
202
+ # First pass replays latest a random number of blocks to test chunking.
203
+ first_block_num ||= current_block_num - (rand * 200).to_i
204
+
205
+ stream.transactions(at_block_num: first_block_num) do |trx, trx_id, block_num|
206
+ puts "#{block_num} :: #{trx_id}; ops: #{trx.operations.map(&:type).join(', ')}"
207
+ end
208
+ end
209
+ end
210
+
211
+ desc 'Test the ability to stream a block range of operations.'
212
+ task :op_range, [:mode, :at_block_num] do |t, args|
213
+ mode = (args[:mode] || 'irreversible').to_sym
214
+ first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
215
+ stream = Steem::Stream.new(url: ENV['TEST_NODE'], mode: mode)
216
+ api = Steem::Api.new(url: ENV['TEST_NODE'])
217
+
218
+ api.get_dynamic_global_properties do |properties|
219
+ current_block_num = if mode == :head
220
+ properties.head_block_number
221
+ else
222
+ properties.last_irreversible_block_num
223
+ end
224
+
225
+ # First pass replays latest a random number of blocks to test chunking.
226
+ first_block_num ||= current_block_num - (rand * 200).to_i
227
+
228
+ stream.operations(at_block_num: first_block_num) do |op, trx_id, block_num|
229
+ puts "#{block_num} :: #{trx_id}; op: #{op.type}"
230
+ end
231
+ end
232
+ end
233
+
234
+ desc 'Test the ability to stream a block range of virtual operations.'
235
+ task :vop_range, [:mode, :at_block_num] do |t, args|
236
+ mode = (args[:mode] || 'irreversible').to_sym
237
+ first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
238
+ stream = Steem::Stream.new(url: ENV['TEST_NODE'], mode: mode)
239
+ api = Steem::Api.new(url: ENV['TEST_NODE'])
240
+
241
+ api.get_dynamic_global_properties do |properties|
242
+ current_block_num = if mode == :head
243
+ properties.head_block_number
244
+ else
245
+ properties.last_irreversible_block_num
246
+ end
247
+
248
+ # First pass replays latest a random number of blocks to test chunking.
249
+ first_block_num ||= current_block_num - (rand * 200).to_i
250
+
251
+ stream.operations(at_block_num: first_block_num, only_virtual: true) do |op, trx_id, block_num|
252
+ puts "#{block_num} :: #{trx_id}; op: #{op.type}"
253
+ end
254
+ end
255
+ end
256
+
257
+ desc 'Test the ability to stream a block range of all operations (including virtual).'
258
+ task :all_op_range, [:mode, :at_block_num] do |t, args|
259
+ mode = (args[:mode] || 'irreversible').to_sym
260
+ first_block_num = args[:at_block_num].to_i if !!args[:at_block_num]
261
+ stream = Steem::Stream.new(url: ENV['TEST_NODE'], mode: mode)
262
+ api = Steem::Api.new(url: ENV['TEST_NODE'])
263
+
264
+ api.get_dynamic_global_properties do |properties|
265
+ current_block_num = if mode == :head
266
+ properties.head_block_number
267
+ else
268
+ properties.last_irreversible_block_num
269
+ end
270
+
271
+ # First pass replays latest a random number of blocks to test chunking.
272
+ first_block_num ||= current_block_num - (rand * 200).to_i
273
+
274
+ stream.operations(at_block_num: first_block_num, include_virtual: true) do |op, trx_id, block_num|
275
+ puts "#{block_num} :: #{trx_id}; op: #{op.type}"
179
276
  end
180
277
  end
181
278
  end
data/lib/steem.rb CHANGED
@@ -19,6 +19,7 @@ require 'steem/jsonrpc'
19
19
  require 'steem/block_api'
20
20
  require 'steem/formatter'
21
21
  require 'steem/broadcast'
22
+ require 'steem/stream'
22
23
 
23
24
  module Steem
24
25
  def self.api_classes
data/lib/steem/api.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Steem
2
2
  # This ruby API works with
3
- # {https://github.com/steemit/steem/releases steemd-0.19.4} and other AppBase
3
+ # {https://github.com/steemit/steem/releases steemd-0.19.10} and other AppBase
4
4
  # compatible upstreams. To access different API namespaces, use the
5
5
  # following:
6
6
  #
@@ -36,7 +36,7 @@ module Steem
36
36
  #
37
37
  # Also see: {https://developers.steem.io/apidefinitions/ Complete API Definitions}
38
38
  class Api
39
- attr_accessor :chain, :methods
39
+ attr_accessor :chain, :methods, :rpc_client
40
40
 
41
41
  # Use this for debugging naive thread handler.
42
42
  # DEFAULT_RPC_CLIENT_CLASS = RPC::HttpClient
@@ -57,12 +57,17 @@ module Steem
57
57
  @api_name.to_s.split('_').map(&:capitalize).join
58
58
  end
59
59
 
60
- def self.jsonrpc=(jsonrpc)
61
- @jsonrpc = jsonrpc
60
+ def self.jsonrpc=(jsonrpc, url = nil)
61
+ @jsonrpc ||= {}
62
+ @jsonrpc[url || jsonrpc.rpc_client.uri.to_s] = jsonrpc
62
63
  end
63
64
 
64
- def self.jsonrpc
65
- @jsonrpc
65
+ def self.jsonrpc(url = nil)
66
+ if @jsonrpc.size < 2 && url.nil?
67
+ @jsonrpc.values.first
68
+ else
69
+ @jsonrpc[url]
70
+ end
66
71
  end
67
72
 
68
73
  # Override this if you want to use your own client.
@@ -89,7 +94,7 @@ module Steem
89
94
  # have access to instance options until now.
90
95
 
91
96
  Api::jsonrpc = Jsonrpc.new(options)
92
- @methods = Api::jsonrpc.get_api_methods
97
+ @methods = Api::jsonrpc(rpc_client.uri.to_s).get_api_methods
93
98
 
94
99
  unless !!@methods[@api_name]
95
100
  raise UnknownApiError, "#{@api_name} (known APIs: #{@methods.keys.join(' ')})"
@@ -115,19 +120,22 @@ module Steem
115
120
  end
116
121
  private
117
122
  # @private
118
- def self.args_keys_to_s(rpc_method_name)
123
+ def args_keys_to_s(rpc_method_name)
119
124
  args = signature(rpc_method_name).args
120
125
  args_keys = JSON[args.to_json]
121
126
  end
122
127
 
123
128
  # @private
124
- def self.signature(rpc_method_name)
129
+ def signature(rpc_method_name)
130
+ url = rpc_client.uri.to_s
131
+
125
132
  @@signatures ||= {}
126
- @@signatures[rpc_method_name] ||= jsonrpc.get_signature(method: rpc_method_name).result
133
+ @@signatures[url] ||= {}
134
+ @@signatures[url][rpc_method_name] ||= Api::jsonrpc(url).get_signature(method: rpc_method_name).result
127
135
  end
128
136
 
129
137
  # @private
130
- def self.raise_error_response(rpc_method_name, rpc_args, response)
138
+ def raise_error_response(rpc_method_name, rpc_args, response)
131
139
  raise UnknownError, "#{rpc_method_name}: #{response}" if response.error.nil?
132
140
 
133
141
  error = response.error
@@ -153,9 +161,9 @@ module Steem
153
161
  when :condenser_api then args
154
162
  when :jsonrpc then args.first
155
163
  else
156
- expected_args = Api::signature(rpc_method_name).args || []
164
+ expected_args = signature(rpc_method_name).args || []
157
165
  expected_args_key_string = if expected_args.size > 0
158
- " (#{Api::args_keys_to_s(rpc_method_name)})"
166
+ " (#{args_keys_to_s(rpc_method_name)})"
159
167
  end
160
168
  expected_args_size = expected_args.size
161
169
 
@@ -178,11 +186,11 @@ module Steem
178
186
  args
179
187
  end
180
188
 
181
- response = @rpc_client.rpc_execute(@api_name, m, rpc_args)
189
+ response = rpc_client.rpc_execute(@api_name, m, rpc_args)
182
190
 
183
191
  if defined?(response.error) && !!response.error
184
192
  if !!response.error.message
185
- Api::raise_error_response rpc_method_name, rpc_args, response
193
+ raise_error_response rpc_method_name, rpc_args, response
186
194
  else
187
195
  raise Steem::ArgumentError, response.error.inspect
188
196
  end
@@ -1,16 +1,16 @@
1
1
  module Steem
2
2
  class BaseError < StandardError
3
- def initialize(error, cause = nil)
3
+ def initialize(error = nil, cause = nil)
4
4
  @error = error
5
5
  @cause = cause
6
6
  end
7
7
 
8
8
  def to_s
9
- if !!@cause
10
- JSON[error: @error, cause: @cause] rescue {error: @error, cause: @cause}.to_s
11
- else
12
- JSON[@error] rescue @error.to_s
13
- end
9
+ detail = {}
10
+ detail[:error] = @error if !!@error
11
+ detail[:cause] = @cause if !!@cause
12
+
13
+ JSON[detail] rescue detai.to_s
14
14
  end
15
15
 
16
16
  def self.build_error(error, context)
@@ -22,6 +22,10 @@ module Steem
22
22
  raise Steem::RemoteNodeError.new, error.message, build_backtrace(error)
23
23
  end
24
24
 
25
+ if error.message.include? 'Server error'
26
+ raise Steem::RemoteNodeError.new, error.message, build_backtrace(error)
27
+ end
28
+
25
29
  if error.message.include? 'plugin not enabled'
26
30
  raise Steem::PluginNotEnabledError, error.message, build_backtrace(error)
27
31
  end
@@ -30,6 +34,10 @@ module Steem
30
34
  raise Steem::ArgumentError, "#{context}: #{error.message}", build_backtrace(error)
31
35
  end
32
36
 
37
+ if error.message.include? 'Invalid params'
38
+ raise Steem::ArgumentError, "#{context}: #{error.message}", build_backtrace(error)
39
+ end
40
+
33
41
  if error.message.start_with? 'Bad Cast:'
34
42
  raise Steem::ArgumentError, "#{context}: #{error.message}", build_backtrace(error)
35
43
  end
@@ -110,6 +118,10 @@ module Steem
110
118
  raise Steem::MissingOtherAuthorityError, "#{context}: #{error.message}", build_backtrace(error)
111
119
  end
112
120
 
121
+ if error.message.include? 'Upstream response error'
122
+ raise Steem::UpstreamResponseError, "#{context}: #{error.message}", build_backtrace(error)
123
+ end
124
+
113
125
  if error.message.include? 'Bad or missing upstream response'
114
126
  raise Steem::BadOrMissingUpstreamResponseError, "#{context}: #{error.message}", build_backtrace(error)
115
127
  end
@@ -122,6 +134,10 @@ module Steem
122
134
  raise Steem::InvalidAccountError, "#{context}: #{error.message}", build_backtrace(error)
123
135
  end
124
136
 
137
+ if error.message.include?('Method') && error.message.include?(' does not exist.')
138
+ raise Steem::UnknownMethodError, "#{context}: #{error.message}", build_backtrace(error)
139
+ end
140
+
125
141
  if error.message.include? 'Invalid operation name'
126
142
  raise Steem::UnknownOperationError, "#{context}: #{error.message}", build_backtrace(error)
127
143
  end
@@ -162,9 +178,6 @@ module Steem
162
178
 
163
179
  class UnsupportedChainError < BaseError; end
164
180
  class ArgumentError < BaseError; end
165
- class RemoteNodeError < BaseError; end
166
- class RemoteDatabaseLockError < RemoteNodeError; end
167
- class PluginNotEnabledError < RemoteNodeError; end
168
181
  class TypeError < BaseError; end
169
182
  class EmptyTransactionError < ArgumentError; end
170
183
  class InvalidAccountError < ArgumentError; end
@@ -186,12 +199,18 @@ module Steem
186
199
  class MissingOtherAuthorityError < MissingAuthorityError; end
187
200
  class IncorrectRequestIdError < BaseError; end
188
201
  class IncorrectResponseIdError < BaseError; end
189
- class BadOrMissingUpstreamResponseError < BaseError; end
202
+ class RemoteNodeError < BaseError; end
203
+ class UpstreamResponseError < RemoteNodeError; end
204
+ class RemoteDatabaseLockError < UpstreamResponseError; end
205
+ class PluginNotEnabledError < UpstreamResponseError; end
206
+ class BadOrMissingUpstreamResponseError < UpstreamResponseError; end
190
207
  class TransactionIndexDisabledError < BaseError; end
191
208
  class NotAppBaseError < BaseError; end
192
209
  class UnknownApiError < BaseError; end
210
+ class UnknownMethodError < BaseError; end
193
211
  class UnknownOperationError < BaseError; end
194
212
  class JsonRpcBatchMaximumSizeExceededError < BaseError; end
195
213
  class TooManyTimeoutsError < BaseError; end
214
+ class TooManyRetriesError < BaseError; end
196
215
  class UnknownError < BaseError; end
197
216
  end
@@ -12,11 +12,25 @@ module Steem
12
12
  super
13
13
  end
14
14
 
15
+ # Uses a batched requst on a range of block headers.
16
+ #
17
+ # @param options [Hash] The attributes to get a block range with.
18
+ # @option options [Range] :block_range starting on one block number and ending on an higher block number.
19
+ def get_block_headers(options = {block_range: (0..0)}, &block)
20
+ get_block_objects(options.merge(object: :block_header), block)
21
+ end
22
+
15
23
  # Uses a batched requst on a range of blocks.
16
24
  #
17
25
  # @param options [Hash] The attributes to get a block range with.
18
26
  # @option options [Range] :block_range starting on one block number and ending on an higher block number.
19
27
  def get_blocks(options = {block_range: (0..0)}, &block)
28
+ get_block_objects(options.merge(object: :block), block)
29
+ end
30
+ private
31
+ def get_block_objects(options = {block_range: (0..0)}, block = nil)
32
+ object = options[:object]
33
+ object_method = "get_#{object}".to_sym
20
34
  block_range = options[:block_range] || (0..0)
21
35
 
22
36
  if (start = block_range.first) < 1
@@ -33,15 +47,21 @@ module Steem
33
47
  request_object = []
34
48
 
35
49
  for i in sub_range do
36
- @rpc_client.put(self.class.api_name, :get_block, block_num: i, request_object: request_object)
50
+ @rpc_client.put(self.class.api_name, object_method, block_num: i, request_object: request_object)
37
51
  end
38
52
 
39
53
  if !!block
40
54
  index = 0
41
55
  @rpc_client.rpc_batch_execute(request_object: request_object) do |result, error, id|
42
56
  block_num = sub_range.to_a[index]
43
- index = index += 1
44
- yield(result.nil? ? nil : result.block, block_num)
57
+ index = index + 1
58
+
59
+ case object
60
+ when :block_header
61
+ block.call(result.nil? ? nil : result[:header], block_num)
62
+ else
63
+ block.call(result.nil? ? nil : result[object], block_num)
64
+ end
45
65
  end
46
66
  else
47
67
  blocks = []
@@ -1136,7 +1136,7 @@ module Steem
1136
1136
  end
1137
1137
  end
1138
1138
 
1139
- # @privats
1139
+ # @private
1140
1140
  def self.database_api(options)
1141
1141
  options[:database_api] ||= if !!options[:app_base]
1142
1142
  Steem::DatabaseApi.new(options)
@@ -13,24 +13,12 @@ module Steem
13
13
  IncorrectResponseIdError, RemoteDatabaseLockError
14
14
  ]
15
15
 
16
- # Expontential backoff.
17
- #
18
- # @private
19
- def backoff
20
- @backoff ||= 0.1
21
- @backoff *= 2
22
- @backoff = 0.1 if @backoff > MAX_BACKOFF
23
-
24
- sleep @backoff
25
- end
26
-
27
16
  def can_retry?(e = nil)
28
17
  @retry_count ||= 0
29
- @first_retry_at ||= Time.now.utc
30
18
 
31
19
  return false if @retry_count >= MAX_RETRY_COUNT
32
20
 
33
- @retry_count = if Time.now.utc - @first_retry_at > MAX_RETRY_ELAPSE
21
+ @retry_count = if retry_reset?
34
22
  @first_retry_at = nil
35
23
  else
36
24
  @retry_count + 1
@@ -45,5 +33,26 @@ module Steem
45
33
 
46
34
  can_retry
47
35
  end
36
+ private
37
+ # @private
38
+ def first_retry_at
39
+ @first_retry_at ||= Time.now.utc
40
+ end
41
+
42
+ # @private
43
+ def retry_reset?
44
+ Time.now.utc - first_retry_at > MAX_RETRY_ELAPSE
45
+ end
46
+
47
+ # Expontential backoff.
48
+ #
49
+ # @private
50
+ def backoff
51
+ @backoff ||= 0.1
52
+ @backoff *= 2
53
+ @backoff = 0.1 if @backoff > MAX_BACKOFF
54
+
55
+ sleep @backoff
56
+ end
48
57
  end
49
58
  end
@@ -12,8 +12,7 @@ module Steem
12
12
  MAX_TIMEOUT_BACKOFF = 30
13
13
 
14
14
  # @private
15
- TIMEOUT_ERRORS = [Net::ReadTimeout, Errno::EBADF, Errno::ECONNREFUSED,
16
- IOError]
15
+ TIMEOUT_ERRORS = [Net::ReadTimeout, Errno::EBADF, IOError]
17
16
 
18
17
  def initialize(options = {})
19
18
  @chain = options[:chain] || :steem
@@ -17,7 +17,7 @@ module Steem
17
17
  #
18
18
  # @private
19
19
  TIMEOUT_ERRORS = [Net::OpenTimeout, JSON::ParserError, Net::ReadTimeout,
20
- Errno::EBADF, Errno::ECONNREFUSED, IOError]
20
+ Errno::EBADF, IOError, Errno::ENETDOWN]
21
21
 
22
22
  # @private
23
23
  POST_HEADERS = {
@@ -0,0 +1,377 @@
1
+ module Steem
2
+ # Steem::Stream allows a live view of the STEEM blockchain.
3
+ #
4
+ # Example streaming blocks:
5
+ #
6
+ # stream = Steem::Stream.new
7
+ #
8
+ # stream.blocks do |block, block_num|
9
+ # puts "#{block_num} :: #{block.witness}"
10
+ # end
11
+ #
12
+ # Example streaming transactions:
13
+ #
14
+ # stream = Steem::Stream.new
15
+ #
16
+ # stream.transactions do |trx, trx_id, block_num|
17
+ # puts "#{block_num} :: #{trx_id} :: operations: #{trx.operations.size}"
18
+ # end
19
+ #
20
+ # Example streaming operations:
21
+ #
22
+ # stream = Steem::Stream.new
23
+ #
24
+ # stream.operations do |op, trx_id, block_num|
25
+ # puts "#{block_num} :: #{trx_id} :: #{op.type}: #{op.value.to_json}"
26
+ # end
27
+ #
28
+ # Allows streaming of block headers, full blocks, transactions, operations and
29
+ # virtual operations.
30
+ class Stream
31
+ attr_reader :database_api, :block_api, :account_history_api, :mode
32
+
33
+ BLOCK_INTERVAL = 3
34
+ MAX_BACKOFF_BLOCK_INTERVAL = 30
35
+ MAX_RETRY_COUNT = 10
36
+
37
+ VOP_TRX_ID = ('0' * 40).freeze
38
+
39
+ # @param options [Hash] additional options
40
+ # @option options [Steem::DatabaseApi] :database_api
41
+ # @option options [Steem::BlockApi] :block_api
42
+ # @option options [Steem::AccountHistoryApi || Steem::CondenserApi] :account_history_api
43
+ # @option options [Symbol] :mode we have the choice between
44
+ # * :head the last block
45
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
46
+ # @option options [Boolean] :no_warn do not generate warnings
47
+ def initialize(options = {mode: :irreversible})
48
+ @instance_options = options
49
+ @database_api = options[:database_api] || Steem::DatabaseApi.new(options)
50
+ @block_api = options[:block_api] || Steem::BlockApi.new(options)
51
+ @account_history_api = options[:account_history_api]
52
+ @mode = options[:mode] || :irreversible
53
+ @no_warn = !!options[:no_warn]
54
+ end
55
+
56
+ # Use this method to stream block numbers. This is significantly faster
57
+ # than requesting full blocks and even block headers. Basically, the only
58
+ # thing this method does is call {Steem::Database#get_dynamic_global_properties} at 3 second
59
+ # intervals.
60
+ #
61
+ # @param options [Hash] additional options
62
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
63
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
64
+ def block_numbers(options = {}, &block)
65
+ block_objects(options.merge(object: :block_numbers), block)
66
+ end
67
+
68
+ # Use this method to stream block headers. This is quite a bit faster than
69
+ # requesting full blocks.
70
+ #
71
+ # @param options [Hash] additional options
72
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
73
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
74
+ def block_headers(options = {}, &block)
75
+ block_objects(options.merge(object: :block_headers), block)
76
+ end
77
+
78
+ # Use this method to stream full blocks.
79
+ #
80
+ # @param options [Hash] additional options
81
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
82
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
83
+ def blocks(options = {}, &block)
84
+ block_objects(options.merge(object: :blocks), block)
85
+ end
86
+
87
+ # Use this method to stream each transaction.
88
+ #
89
+ # @param options [Hash] additional options
90
+ # @option options [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
91
+ # @option options [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
92
+ def transactions(options = {}, &block)
93
+ blocks(options) do |block, block_num|
94
+ block.transactions.each_with_index do |transaction, index|
95
+ trx_id = block.transaction_ids[index]
96
+
97
+ yield transaction, trx_id, block_num
98
+ end
99
+ end
100
+ end
101
+
102
+ # Returns the latest operations from the blockchain.
103
+ #
104
+ # stream = Steem::Stream.new
105
+ # stream.operations do |op|
106
+ # puts op.to_json
107
+ # end
108
+ #
109
+ # If symbol are passed to `types` option, then only that operation is
110
+ # returned. Expected symbols are:
111
+ #
112
+ # account_create_operation
113
+ # account_create_with_delegation_operation
114
+ # account_update_operation
115
+ # account_witness_proxy_operation
116
+ # account_witness_vote_operation
117
+ # cancel_transfer_from_savings_operation
118
+ # change_recovery_account_operation
119
+ # claim_reward_balance_operation
120
+ # comment_operation
121
+ # comment_options_operation
122
+ # convert_operation
123
+ # custom_operation
124
+ # custom_json_operation
125
+ # decline_voting_rights_operation
126
+ # delegate_vesting_shares_operation
127
+ # delete_comment_operation
128
+ # escrow_approve_operation
129
+ # escrow_dispute_operation
130
+ # escrow_release_operation
131
+ # escrow_transfer_operation
132
+ # feed_publish_operation
133
+ # limit_order_cancel_operation
134
+ # limit_order_create_operation
135
+ # limit_order_create2_operation
136
+ # pow_operation
137
+ # pow2_operation
138
+ # recover_account_operation
139
+ # request_account_recovery_operation
140
+ # set_withdraw_vesting_route_operation
141
+ # transfer_operation
142
+ # transfer_from_savings_operation
143
+ # transfer_to_savings_operation
144
+ # transfer_to_vesting_operation
145
+ # vote_operation
146
+ # withdraw_vesting_operation
147
+ # witness_update_operation
148
+ #
149
+ # For example, to stream only votes:
150
+ #
151
+ # stream = Steem::Stream.new
152
+ # stream.operations(types: :vote_operation) do |vote|
153
+ # puts vote.to_json
154
+ # end
155
+ #
156
+ # ... Or ...
157
+ #
158
+ # stream = Steem::Stream.new
159
+ # stream.operations(:vote_operation) do |vote|
160
+ # puts vote.to_json
161
+ # end
162
+ #
163
+ # You can also stream virtual operations:
164
+ #
165
+ # stream = Steem::Stream.new
166
+ # stream.operations(types: :author_reward_operation, only_virtual: true) do |vop|
167
+ # v = vop.value
168
+ # puts "#{v.author} got paid for #{v.permlink}: #{[v.sbd_payout, v.steem_payout, v.vesting_payout]}"
169
+ # end
170
+ #
171
+ # ... or multiple virtual operation types;
172
+ #
173
+ # stream = Steem::Stream.new
174
+ # stream.operations(types: [:producer_reward_operation, :author_reward_operation], only_virtual: true) do |vop|
175
+ # puts vop.to_json
176
+ # end
177
+ #
178
+ # ... or all types, including virtual operation types from the head block number:
179
+ #
180
+ # stream = Steem::Stream.new(mode: :head)
181
+ # stream.operations(include_virtual: true) do |op|
182
+ # puts op.to_json
183
+ # end
184
+ #
185
+ # Expected virtual operation types:
186
+ #
187
+ # producer_reward_operation
188
+ # author_reward_operation
189
+ # curation_reward_operation
190
+ # fill_convert_request_operation
191
+ # fill_order_operation
192
+ # fill_vesting_withdraw_operation
193
+ # interest_operation
194
+ # shutdown_witness_operation
195
+ #
196
+ # @param args [Symbol || Array<Symbol> || Hash] the type(s) of operation or hash of expanded options, optional.
197
+ # @option args [Integer] :at_block_num Starts the stream at the given block number. Default: nil.
198
+ # @option args [Integer] :until_block_num Ends the stream at the given block number. Default: nil.
199
+ # @option args [Symbol || Array<Symbol>] :types the type(s) of operation, optional.
200
+ # @option args [Boolean] :only_virtual Only stream virtual options. Setting this true will improve performance because the stream only needs block numbers to then retrieve virtual operations. Default: false.
201
+ # @option args [Boolean] :include_virtual Also stream virtual options. Setting this true will impact performance. Default: false.
202
+ # @param block the block to execute for each result. Yields: |op, trx_id, block_num|
203
+ def operations(*args, &block)
204
+ options = {}
205
+ types = []
206
+ only_virtual = false
207
+ include_virtual = false
208
+ last_block_num = nil
209
+
210
+ case args.first
211
+ when Hash
212
+ options = args.first
213
+ types = transform_types(options[:types])
214
+ only_virtual = !!options[:only_virtual] || false
215
+ include_virtual = !!options[:include_virtual] || only_virtual || false
216
+ when Symbol, Array then types = transform_types(args)
217
+ end
218
+
219
+ if only_virtual
220
+ block_numbers(options) do |block_num|
221
+ get_virtual_ops(types, block_num, block)
222
+ end
223
+ else
224
+ transactions(options) do |transaction, trx_id, block_num|
225
+ transaction.operations.each do |op|
226
+ yield op, trx_id, block_num if types.none? || types.include?(op.type)
227
+
228
+ next unless last_block_num != block_num
229
+
230
+ last_block_num = block_num
231
+
232
+ get_virtual_ops(types, block_num, block) if include_virtual
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ def account_history_api
239
+ @account_history_api ||= begin
240
+ Steem::AccountHistoryApi.new(@instance_options)
241
+ rescue Steem::UnknownApiError => e
242
+ warn "#{e.inspect}, falling back to Steem::CondenserApi." unless @no_warn
243
+ Steem::CondenserApi.new(@instance_options)
244
+ end
245
+ end
246
+ private
247
+ # @private
248
+ def block_objects(options = {}, block)
249
+ object = options[:object]
250
+ object_method = "get_#{object}".to_sym
251
+ block_interval = BLOCK_INTERVAL
252
+
253
+ at_block_num, until_block_num = if !!block_range = options[:block_range]
254
+ [block_range.first, block_range.last]
255
+ else
256
+ [options[:at_block_num], options[:until_block_num]]
257
+ end
258
+
259
+ loop do
260
+ break if !!until_block_num && !!at_block_num && until_block_num < at_block_num
261
+
262
+ database_api.get_dynamic_global_properties do |properties|
263
+ current_block_num = find_block_number(properties)
264
+ current_block_num = [current_block_num, until_block_num].compact.min
265
+ at_block_num ||= current_block_num
266
+
267
+ if current_block_num >= at_block_num
268
+ range = at_block_num..current_block_num
269
+
270
+ if object == :block_numbers
271
+ range.each do |n|
272
+ block.call n
273
+ block_interval = BLOCK_INTERVAL
274
+ end
275
+ else
276
+ block_api.send(object_method, block_range: range) do |b, n|
277
+ block.call b, n
278
+ block_interval = BLOCK_INTERVAL
279
+ end
280
+ end
281
+
282
+ at_block_num = range.max + 1
283
+ else
284
+ # The stream has stalled, so let's back off and let the node sync
285
+ # up. We'll catch up with a bigger batch in the next cycle.
286
+ block_interval = [block_interval * 2, MAX_BACKOFF_BLOCK_INTERVAL].min
287
+ end
288
+ end
289
+
290
+ sleep block_interval
291
+ end
292
+ end
293
+
294
+ # @private
295
+ def find_block_number(properties)
296
+ block_num = case mode
297
+ when :head then properties.head_block_number
298
+ when :irreversible then properties.last_irreversible_block_num
299
+ else; raise Steem::ArgumentError, "Unknown mode: #{mode}"
300
+ end
301
+
302
+ block_num
303
+ end
304
+
305
+ # @private
306
+ def transform_types(types)
307
+ [types].compact.flatten.map do |type|
308
+ type = type.to_s
309
+
310
+ unless type.end_with? '_operation'
311
+ warn "Op type #{type} is deprecated. Use #{type}_operation instead." unless @no_warn
312
+ type += '_operation'
313
+ end
314
+
315
+ type
316
+ end
317
+ end
318
+
319
+ # @private
320
+ def get_virtual_ops(types, block_num, block)
321
+ retries = 0
322
+
323
+ loop do
324
+ get_ops_in_block_options = case account_history_api
325
+ when Steem::CondenserApi
326
+ [block_num, true]
327
+ when Steem::AccountHistoryApi
328
+ {
329
+ block_num: block_num,
330
+ only_virtual: true
331
+ }
332
+ end
333
+
334
+ response = account_history_api.get_ops_in_block(*get_ops_in_block_options)
335
+ result = response.result
336
+
337
+ if result.nil?
338
+ if retries < MAX_RETRY_COUNT
339
+ warn "Retrying get_ops_in_block on block #{block_num}" unless @no_warn
340
+ retries = retries + 1
341
+ sleep 9
342
+ redo
343
+ else
344
+ raise TooManyRetriesError, "unable to get valid result while finding virtual operations for block: #{block_num}"
345
+ end
346
+ end
347
+
348
+ ops = case account_history_api
349
+ when Steem::CondenserApi
350
+ result.map do |trx|
351
+ op = {type: trx.op[0] + '_operation', value: trx.op[1]}
352
+ op = Hashie::Mash.new(op)
353
+ end
354
+ when Steem::AccountHistoryApi then result.ops.map { |trx| trx.op }
355
+ end
356
+
357
+ if ops.empty?
358
+ if retries < MAX_RETRY_COUNT
359
+ sleep 3
360
+ retries = retries + 1
361
+ redo
362
+ else
363
+ raise TooManyRetriesError, "unable to find virtual operations for block: #{block_num}"
364
+ end
365
+ end
366
+
367
+ ops.each do |op|
368
+ next if types.any? && !types.include?(op.type)
369
+
370
+ block.call op, VOP_TRX_ID, block_num
371
+ end
372
+
373
+ break
374
+ end
375
+ end
376
+ end
377
+ end
data/lib/steem/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Steem
2
- VERSION = '0.9.1'
2
+ VERSION = '0.9.2'
3
3
  AGENT_ID = "steem-ruby/#{VERSION}"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: steem-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Martin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-28 00:00:00.000000000 Z
11
+ date: 2018-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -358,6 +358,7 @@ files:
358
358
  - lib/steem/rpc/base_client.rb
359
359
  - lib/steem/rpc/http_client.rb
360
360
  - lib/steem/rpc/thread_safe_http_client.rb
361
+ - lib/steem/stream.rb
361
362
  - lib/steem/transaction_builder.rb
362
363
  - lib/steem/type/amount.rb
363
364
  - lib/steem/type/base_type.rb