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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +73 -0
- data/Rakefile +128 -31
- data/lib/steem.rb +1 -0
- data/lib/steem/api.rb +23 -15
- data/lib/steem/base_error.rb +29 -10
- data/lib/steem/block_api.rb +23 -3
- data/lib/steem/broadcast.rb +1 -1
- data/lib/steem/mixins/retriable.rb +22 -13
- data/lib/steem/rpc/base_client.rb +1 -2
- data/lib/steem/rpc/http_client.rb +1 -1
- data/lib/steem/stream.rb +377 -0
- data/lib/steem/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25b2e7d4eb5445c89b02aac685677e49a0c551c8
|
4
|
+
data.tar.gz: 9b5f21517af8ea96a85c751d0a470463cff09ea7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
155
|
-
|
156
|
-
|
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
|
-
|
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
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.
|
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
|
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
|
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
|
129
|
+
def signature(rpc_method_name)
|
130
|
+
url = rpc_client.uri.to_s
|
131
|
+
|
125
132
|
@@signatures ||= {}
|
126
|
-
@@signatures[
|
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
|
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 =
|
164
|
+
expected_args = signature(rpc_method_name).args || []
|
157
165
|
expected_args_key_string = if expected_args.size > 0
|
158
|
-
" (#{
|
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 =
|
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
|
-
|
193
|
+
raise_error_response rpc_method_name, rpc_args, response
|
186
194
|
else
|
187
195
|
raise Steem::ArgumentError, response.error.inspect
|
188
196
|
end
|
data/lib/steem/base_error.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
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
|
data/lib/steem/block_api.rb
CHANGED
@@ -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,
|
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
|
44
|
-
|
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 = []
|
data/lib/steem/broadcast.rb
CHANGED
@@ -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
|
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,
|
16
|
-
IOError]
|
15
|
+
TIMEOUT_ERRORS = [Net::ReadTimeout, Errno::EBADF, IOError]
|
17
16
|
|
18
17
|
def initialize(options = {})
|
19
18
|
@chain = options[:chain] || :steem
|
data/lib/steem/stream.rb
ADDED
@@ -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
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.
|
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-
|
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
|