steem-ruby 0.9.1 → 0.9.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 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