radiator 0.3.9 → 0.3.10

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: cd04edb4782257e159679b3ba504958f7bebff66
4
- data.tar.gz: c02f7c6a4f03d7083781768e8698dbca4dd9e7f2
3
+ metadata.gz: c4ff8172c52320522f9a22b4a710a1051837aa9d
4
+ data.tar.gz: 827457f802605807383f2c42350f671754fad7c0
5
5
  SHA512:
6
- metadata.gz: 5dff44fb5eb36711bf5d3bf608f75ac6c45febd7a1f1f8595737325e6a6a22141e70380009d9d70c0d86bcb1ecc36dba0a5b178eb650c69eb2d7ca7edc37c876
7
- data.tar.gz: 69342f5a584c97d19693134127587c83d6f67f60c99a4d189c8ada1e40164cc267df39eb14961b83f19012ee85e76dc057bc9318633cf2845d96a0b8640aa078
6
+ metadata.gz: f79ed7cbb0e5a64a7a21c17e6b25bc254591b9e31b24ab733ffc93872de6949a994339844f4479780c86e34ac56546dbfb91d391875c861fc03ea4818f83c9a1
7
+ data.tar.gz: 53c4aa2777f751221c0e0bee02b2dc47abbed64dde1ae92d66db77ae57a4363d5554d552012f1372b480bd928c6f17ea07b0595f59d104397a3562e66de5a09b
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- radiator (0.3.9)
4
+ radiator (0.3.10)
5
5
  awesome_print (~> 1.7, >= 1.7.0)
6
6
  bitcoin-ruby (~> 0.0, >= 0.0.11)
7
7
  ffi (~> 1.9, >= 1.9.18)
8
8
  hashie (~> 3.5, >= 3.5.5)
9
9
  json (~> 2.0, >= 2.0.2)
10
10
  logging (~> 2.2, >= 2.2.0)
11
- net-http-persistent (~> 3.0, >= 2.5.2)
11
+ net-http-persistent (>= 2.5.2)
12
12
 
13
13
  GEM
14
14
  remote: https://rubygems.org/
data/Rakefile CHANGED
@@ -52,16 +52,21 @@ task :test_live_broadcast, [:account, :wif, :chain] do |t, args|
52
52
  end
53
53
  end
54
54
 
55
- desc 'Tests the ability to stream live data.'
56
- task :test_live_stream, :chain do |t, args|
55
+ desc 'Tests the ability to stream live data. defaults: chain = steem; persist = true.'
56
+ task :test_live_stream, [:chain, :persist] do |t, args|
57
57
  chain = args[:chain] || 'steem'
58
+ persist = (args[:persist] || 'true') == 'true'
58
59
  last_block_number = 0
59
- options = {chain: chain}
60
+ options = {chain: chain, persist: persist}
60
61
  api = Radiator::Api.new(options)
61
62
  total_ops = 0.0
62
63
  total_vops = 0.0
64
+ elapsed = 0
65
+ count = 0
63
66
 
64
67
  Radiator::Stream.new(options).blocks do |b, n|
68
+ start = Time.now.utc
69
+
65
70
  if last_block_number == 0
66
71
  # skip test
67
72
  elsif last_block_number + 1 == n
@@ -80,7 +85,9 @@ task :test_live_stream, :chain do |t, args|
80
85
  0
81
86
  end
82
87
 
83
- puts "#{n}: #{b.witness}; transactions: #{t_size}; operations: #{op_size}, virtual operations: #{vop_size} (cumulative vop ratio: #{('%.2f' % (vop_ratio * 100))} %)"
88
+ elapsed += Time.now.utc - start
89
+ count += 1
90
+ puts "#{n}: #{b.witness}; trx: #{t_size}; op: #{op_size}, vop: #{vop_size} (cumulative vop ratio: #{('%.2f' % (vop_ratio * 100))} %; average #{((elapsed / count) * 1000).to_i}ms)"
84
91
  end
85
92
  else
86
93
  # This should not happen. If it does, there's likely a bug in Radiator.
data/lib/golos.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Golos chain client for broadcasting common operations.
2
+ #
3
+ # @see Radiator::Chain
4
+ class Golos < Radiator::Chain
5
+ def initialize(options = {})
6
+ super(options.merge(chain: :golos))
7
+ end
8
+ end
data/lib/radiator/api.rb CHANGED
@@ -150,12 +150,17 @@ module Radiator
150
150
 
151
151
  DEFAULT_GOLOS_FAILOVER_URLS = [
152
152
  DEFAULT_GOLOS_URL,
153
- 'https://api.golos.cf'
153
+ 'https://api.golos.cf',
154
+ # not recommended:
155
+ # 'http://golos-seed.arcange.eu',
156
+ # not recommended, requires option ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE
157
+ # 'https://golos-seed.arcange.eu',
154
158
  ]
155
159
 
156
160
  # @private
157
161
  POST_HEADERS = {
158
- 'Content-Type' => 'application/json'
162
+ 'Content-Type' => 'application/json',
163
+ 'User-Agent' => Radiator::AGENT_ID
159
164
  }
160
165
 
161
166
  # @private
@@ -203,7 +208,6 @@ module Radiator
203
208
  @hashie_logger = options[:hashie_logger] || Logger.new(nil)
204
209
  @max_requests = options[:max_requests] || 30
205
210
  @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
206
- @reuse_ssl_sessions = !!options[:reuse_ssl_sessions]
207
211
  @ssl_version = options[:ssl_version]
208
212
 
209
213
  if @failover_urls.nil?
@@ -223,17 +227,31 @@ module Radiator
223
227
  true
224
228
  end
225
229
 
230
+ @persist = if options.keys.include? :persist
231
+ options[:persist]
232
+ else
233
+ true
234
+ end
235
+
236
+ @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
237
+ options[:reuse_ssl_sessions]
238
+ else
239
+ true
240
+ end
241
+
226
242
  if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
227
243
  @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
228
244
  end
229
245
 
230
246
  Hashie.logger = @hashie_logger
231
247
  @method_names = nil
232
- @http = nil
248
+ @http_memo = {}
233
249
  @api_options = options.dup.merge(chain: @chain)
234
250
  @api = nil
235
251
  @block_api = nil
236
252
  @backoff_at = nil
253
+
254
+ ObjectSpace.define_finalizer(self, self.class.finalize(self))
237
255
  end
238
256
 
239
257
  # Get a specific block or range of blocks.
@@ -355,8 +373,13 @@ module Radiator
355
373
  def shutdown
356
374
  @uri = nil
357
375
  @http_id = nil
358
- @http.shutdown if !!@http && defined?(@http.shutdown)
359
- @http = nil
376
+ @http_memo.each do |k|
377
+ v = @http_memo.delete(k)
378
+ if defined?(v.shutdown)
379
+ debug "Shutting down instance #{k} (#{v})"
380
+ v.shutdown
381
+ end
382
+ end
360
383
  @api.shutdown if !!@api && @api != self
361
384
  @api = nil
362
385
  @block_api.shutdown if !!@block_api && @block_api != self
@@ -485,6 +508,19 @@ module Radiator
485
508
  backoff
486
509
  end # loop
487
510
  end
511
+
512
+ def inspect
513
+ properties = %w(
514
+ chain url backoff_at max_requests ssl_verify_mode ssl_version persist
515
+ recover_transactions_on_error reuse_ssl_sessions pool_size
516
+ ).map do |prop|
517
+ if !!(v = instance_variable_get("@#{prop}"))
518
+ "@#{prop}=#{v}"
519
+ end
520
+ end.compact.join(', ')
521
+
522
+ "#<#{self.class.name} [#{properties}]>"
523
+ end
488
524
  private
489
525
  def self.methods_json_path
490
526
  @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
@@ -497,6 +533,14 @@ module Radiator
497
533
  end.compact.freeze
498
534
  end
499
535
 
536
+ def self.apply_http_defaults(http, ssl_verify_mode)
537
+ http.read_timeout = 10
538
+ http.open_timeout = 10
539
+ http.verify_mode = ssl_verify_mode
540
+ http.ssl_timeout = 30
541
+ http
542
+ end
543
+
500
544
  def api
501
545
  @api ||= self.class == Api ? self : Api.new(@api_options)
502
546
  end
@@ -519,26 +563,32 @@ module Radiator
519
563
  end
520
564
 
521
565
  def http
522
- idempotent = api_name != :network_broadcast_api
566
+ return @http_memo[http_id] if @http_memo.keys.include? http_id
567
+
568
+ @http_memo[http_id] = if @persist
569
+ idempotent = api_name != :network_broadcast_api
570
+
571
+ http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
572
+ Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
573
+ else
574
+ # net-http-persistent < 3.0
575
+ Net::HTTP::Persistent.new(http_id)
576
+ end
577
+
578
+ http.keep_alive = 30
579
+ http.idle_timeout = idempotent ? 10 : nil
580
+ http.max_requests = @max_requests
581
+ http.retry_change_requests = idempotent
582
+ http.reuse_ssl_sessions = @reuse_ssl_sessions
523
583
 
524
- @http ||= if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
525
- Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
584
+ http
526
585
  else
527
- # net-http-persistent < 3.0
528
- Net::HTTP::Persistent.new(http_id)
586
+ http = Net::HTTP.new(uri.host, uri.port)
587
+ http.use_ssl = uri.scheme == 'https'
588
+ http
529
589
  end
530
590
 
531
- @http.keep_alive = 30
532
- @http.read_timeout = 10
533
- @http.open_timeout = 10
534
- @http.idle_timeout = idempotent ? 10 : nil
535
- @http.max_requests = @max_requests
536
- @http.retry_change_requests = idempotent
537
- @http.verify_mode = @ssl_verify_mode
538
- @http.reuse_ssl_sessions = @reuse_ssl_sessions
539
- @http.ssl_version = @ssl_version
540
-
541
- @http
591
+ Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
542
592
  end
543
593
 
544
594
  def post_request
@@ -548,7 +598,12 @@ module Radiator
548
598
  def request(options)
549
599
  request = post_request
550
600
  request.body = JSON[options]
551
- http.request(uri, request)
601
+
602
+ case http
603
+ when Net::HTTP::Persistent then http.request(uri, request)
604
+ when Net::HTTP then http.request(request)
605
+ else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
606
+ end
552
607
  end
553
608
 
554
609
  def recover_transaction(signatures, expected_rpc_id, after)
@@ -610,7 +665,7 @@ module Radiator
610
665
  @failover_urls.delete(url)
611
666
  end
612
667
 
613
- url || @url
668
+ url || (uri || @url).to_s
614
669
  end
615
670
 
616
671
  def bump_failover
@@ -625,11 +680,11 @@ module Radiator
625
680
 
626
681
  def drop_current_failover_url(prefix)
627
682
  if @preferred_failover_urls.size == 1
628
- warning "Node #{@url} appears to be misconfigured but no other node is available, retrying ...", prefix
683
+ warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
629
684
  else
630
- warning "Removing misconfigured node from failover urls: #{@url}, retrying ...", prefix
631
- @preferred_failover_urls.delete(@url)
632
- @failover_urls.delete(@url)
685
+ warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
686
+ @preferred_failover_urls.delete(uri)
687
+ @failover_urls.delete(uri)
633
688
  end
634
689
  end
635
690
 
@@ -700,7 +755,7 @@ module Radiator
700
755
 
701
756
  def backoff
702
757
  shutdown
703
- bump_failover if flappy? || !healthy?(@url)
758
+ bump_failover if flappy? || !healthy?(uri)
704
759
  @backoff_at ||= Time.now.utc
705
760
  @backoff_sleep ||= 0.01
706
761
 
@@ -712,5 +767,12 @@ module Radiator
712
767
  @backoff_sleep = nil
713
768
  end
714
769
  end
770
+
771
+ def self.finalize(obj)
772
+ proc {
773
+ puts "DESTROY OBJECT #{obj.inspect}" if ENV['LOG'] == 'TRACE'
774
+ obj.shutdown
775
+ }
776
+ end
715
777
  end
716
778
  end
@@ -20,3 +20,4 @@ module Radiator; class StreamError < BaseError; end; end
20
20
  module Radiator; class TypeError < BaseError; end; end
21
21
  module Radiator; class OperationError < BaseError; end; end
22
22
  module Radiator; class TransactionError < BaseError; end; end
23
+ module Radiator; class ChainError < BaseError; end; end
@@ -0,0 +1,335 @@
1
+ module Radiator
2
+ # Examples ...
3
+ #
4
+ # To vote on a post/comment:
5
+ #
6
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
7
+ # steem.vote!(10000, 'author', 'post-or-comment-permlink')
8
+ #
9
+ # To post and vote in the same transaction:
10
+ #
11
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
12
+ # steem.post!(title: 'title of my post', body: 'body of my post', tags: ['tag'], self_upvote: 10000)
13
+ #
14
+ # To post and vote with declined payout:
15
+ #
16
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
17
+ #
18
+ # options = {
19
+ # title: 'title of my post',
20
+ # body: 'body of my post',
21
+ # tags: ['tag'],
22
+ # self_upvote: 10000,
23
+ # percent_steem_dollars: 0
24
+ # }
25
+ #
26
+ # steem.post!(options)
27
+ #
28
+ class Chain
29
+ VALID_OPTIONS = %w(
30
+ chain account_name wif
31
+ ).map(&:to_sym)
32
+ VALID_OPTIONS.each { |option| attr_accessor option }
33
+
34
+ def initialize(options = {})
35
+ options = options.dup
36
+ options.each do |k, v|
37
+ k = k.to_sym
38
+ if VALID_OPTIONS.include?(k.to_sym)
39
+ options.delete(k)
40
+ send("#{k}=", v)
41
+ end
42
+ end
43
+
44
+ @account_name ||= ENV['ACCOUNT_NAME']
45
+ @wif ||= ENV['WIF']
46
+
47
+ raise ChainError, "Required option: chain" if @chain.nil?
48
+ raise ChainError, "Required option: account_name, wif" if @account_name.nil? || @wif.nil?
49
+
50
+ reset
51
+ end
52
+
53
+ # Clears out queued operations.
54
+ def reset
55
+ @operations = []
56
+ end
57
+
58
+ # Broadcast queued operations.
59
+ #
60
+ # @param auto_reset [boolean] clears operations no matter what, even if there's an error.
61
+ def broadcast!(auto_reset = false)
62
+ begin
63
+ transaction = Radiator::Transaction.new(build_options)
64
+ transaction.operations = @operations
65
+ response = transaction.process(true)
66
+ rescue => e
67
+ reset if auto_reset
68
+ raise e
69
+ end
70
+
71
+ if !!response.result
72
+ reset
73
+ response
74
+ else
75
+ reset if auto_reset
76
+ ErrorParser.new(response)
77
+ end
78
+ end
79
+
80
+ # Create a vote operation.
81
+ #
82
+ # Examples:
83
+ #
84
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
85
+ # steem.vote(10000, 'author', 'permlink')
86
+ # steem.broadcast!
87
+ #
88
+ # ... or ...
89
+ #
90
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
91
+ # steem.vote(10000, '@author/permlink')
92
+ # steem.broadcast!
93
+ #
94
+ # @param weight [Integer] value between -10000 and 10000.
95
+ # @param args [author, permlink || slug] pass either `author` and `permlink` or string containing both like `@author/permlink`.
96
+ def vote(weight, *args)
97
+ author, permlink = if args.size == 1
98
+ author, permlink = parse_slug(args[0])
99
+ else
100
+ author, permlink = args
101
+ end
102
+
103
+ @operations << {
104
+ type: :vote,
105
+ voter: account_name,
106
+ author: author,
107
+ permlink: permlink,
108
+ weight: weight
109
+ }
110
+
111
+ self
112
+ end
113
+
114
+ # Create a vote operation and broadcasts it right away.
115
+ #
116
+ # Examples:
117
+ #
118
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
119
+ # steem.vote!(10000, 'author', 'permlink')
120
+ #
121
+ # ... or ...
122
+ #
123
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
124
+ # steem.vote!(10000, '@author/permlink')
125
+ #
126
+ # @see vote
127
+ def vote!(weight, *args); vote(weight, *args).broadcast!(true); end
128
+
129
+ # Creates a post operation.
130
+ #
131
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
132
+ # options = {
133
+ # title: 'This is my fancy post title.',
134
+ # body: 'This is my fancy post body.',
135
+ # tags: %w(thess are my fancy tags)
136
+ # }
137
+ # steem.post(options)
138
+ # steem.broadcast!
139
+ #
140
+ # @param options [Hash] options
141
+ # @option options [String] :title Title of the post.
142
+ # @option options [String] :body Body of the post.
143
+ # @option options [Array<String>] :tags Tags of the post.
144
+ # @option options [String] :permlink (automatic) Permlink of the post, defaults to formatted title.
145
+ # @option options [String] :parent_permlink (automatic) Parent permlink of the post, defaults to first tag.
146
+ # @option options [String] :parent_author (optional) Parent author of the post (only used if reply).
147
+ # @option options [String] :max_accepted_payout (1000000.000 SBD) Maximum accepted payout, set to '0.000 SBD' to deline payout
148
+ # @option options [Integer] :percent_steem_dollars (5000) Percent STEEM Dollars is used to set 50/50 or 100% STEEM Power
149
+ # @option options [Integer] :allow_votes (true) Allow votes for this post.
150
+ # @option options [Integer] :allow_curation_rewards (true) Allow curation rewards for this post.
151
+ def post(options = {})
152
+ tags = [options[:tags] || []].flatten
153
+ title = options[:title].to_s
154
+ permlink = options[:permlink] || title.downcase.gsub(/[^a-z0-9\-]+/, '-')
155
+ parent_permlink = options[:parent_permlink] || tags[0]
156
+
157
+ raise ChainError, 'At least one tag is required or set the parent_permlink directy.' if parent_permlink.nil?
158
+
159
+ body = options[:body]
160
+ parent_author = options[:parent_author] || ''
161
+ max_accepted_payout = options[:max_accepted_payout] || default_max_acepted_payout
162
+ percent_steem_dollars = options[:percent_steem_dollars]
163
+ allow_votes = options[:allow_votes] || true
164
+ allow_curation_rewards = options[:allow_curation_rewards] || true
165
+ self_vote = options[:self_vote]
166
+
167
+ tags.insert(0, parent_permlink)
168
+ tags = tags.compact.uniq
169
+
170
+ metadata = {
171
+ app: Radiator::AGENT_ID
172
+ }
173
+ metadata[:tags] = tags if tags.any?
174
+
175
+ @operations << {
176
+ type: :comment,
177
+ parent_permlink: parent_permlink,
178
+ author: account_name,
179
+ permlink: permlink,
180
+ title: title,
181
+ body: body,
182
+ json_metadata: metadata.to_json,
183
+ parent_author: parent_author
184
+ }
185
+
186
+ if (!!max_accepted_payout &&
187
+ max_accepted_payout != default_max_acepted_payout
188
+ ) || !!percent_steem_dollars || !allow_votes || !allow_curation_rewards
189
+ @operations << {
190
+ type: :comment_options,
191
+ author: account_name,
192
+ permlink: permlink,
193
+ max_accepted_payout: max_accepted_payout,
194
+ percent_steem_dollars: percent_steem_dollars,
195
+ allow_votes: allow_votes,
196
+ allow_curation_rewards: allow_curation_rewards,
197
+ extensions: []
198
+ }
199
+ end
200
+
201
+ vote(self_vote, account_name, permlink) if !!self_vote
202
+
203
+ self
204
+ end
205
+
206
+ # Create a vote operation and broadcasts it right away.
207
+ #
208
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
209
+ # options = {
210
+ # title: 'This is my fancy post title.',
211
+ # body: 'This is my fancy post body.',
212
+ # tags: %w(thess are my fancy tags)
213
+ # }
214
+ # steem.post!(options)
215
+ #
216
+ # @see post
217
+ def post!(options = {}); post(options).broadcast!(true); end
218
+
219
+ # Create a delete_comment operation.
220
+ #
221
+ # Examples:
222
+ #
223
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
224
+ # steem.delete_comment('permlink')
225
+ # steem.broadcast!
226
+ #
227
+ # @param permlink
228
+ def delete_comment(permlink)
229
+ @operations << {
230
+ type: :delete_comment,
231
+ author: account_name,
232
+ permlink: permlink
233
+ }
234
+
235
+ self
236
+ end
237
+
238
+ # Create a delete_comment operation and broadcasts it right away.
239
+ #
240
+ # Examples:
241
+ #
242
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
243
+ # steem.delete_comment!('permlink')
244
+ #
245
+ # @see delete_comment
246
+ def delete_comment!(permlink); delete_comment(permlink).broadcast!(true); end
247
+
248
+ # Create a claim_reward_balance operation.
249
+ #
250
+ # Examples:
251
+ #
252
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
253
+ # steem.claim_reward_balance(reward_sbd: '100.000 SBD')
254
+ # steem.broadcast!
255
+ #
256
+ # @param options [Hash] options
257
+ # @option options [String] :reward_steem The amount of STEEM to claim, like: `100.000 STEEM`
258
+ # @option options [String] :reward_sbd The amount of SBD to claim, like: `100.000 SBD`
259
+ # @option options [String] :reward_vests The amount of VESTS to claim, like: `100.000000 VESTS`
260
+ def claim_reward_balance(options)
261
+ reward_steem = options[:reward_steem] || '0.000 STEEM'
262
+ reward_sbd = options[:reward_sbd] || '0.000 SBD'
263
+ reward_vests = options[:reward_vests] || '0.000000 VESTS'
264
+
265
+ @operations << {
266
+ type: :claim_reward_balance,
267
+ account: account_name,
268
+ reward_steem: reward_steem,
269
+ reward_sbd: reward_sbd,
270
+ reward_vests: reward_vests
271
+ }
272
+
273
+ self
274
+ end
275
+
276
+ # Create a claim_reward_balance operation and broadcasts it right away.
277
+ #
278
+ # Examples:
279
+ #
280
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
281
+ # steem.claim_reward_balance!(reward_sbd: '100.000 SBD')
282
+ #
283
+ # @see claim_reward_balance
284
+ def claim_reward_balance!(permlink); claim_reward_balance(permlink).broadcast!(true); end
285
+
286
+ # Create a transfer operation.
287
+ #
288
+ # steem = Steem.new(account_name: 'your account name', wif: 'your active wif')
289
+ # steem.transfer(amount: '1.000 SBD', to: 'account name', memo: 'this is a memo')
290
+ # steem.broadcast!
291
+ #
292
+ # @param options [Hash] options
293
+ # @option options [String] :amount The amount to transfer, like: `100.000 STEEM`
294
+ # @option options [String] :to The account receiving the transfer.
295
+ # @option options [String] :memo ('') The memo for the transfer.
296
+ def transfer(options = {})
297
+ @operations << options.merge(type: :transfer, from: account_name)
298
+
299
+ self
300
+ end
301
+
302
+ # Create a transfer operation and broadcasts it right away.
303
+ #
304
+ # steem = Steem.new(account_name: 'your account name', wif: 'your wif')
305
+ # steem.transfer!(amount: '1.000 SBD', to: 'account name', memo: 'this is a memo')
306
+ #
307
+ # @see transfer
308
+ def transfer!(options = {}); transfer(options).broadcast!(true); end
309
+ private
310
+ def build_options
311
+ {
312
+ chain: chain,
313
+ wif: wif
314
+ }
315
+ end
316
+
317
+ def parse_slug(slug)
318
+ slug = slug.split('@').last
319
+ author = slug.split('/')[0]
320
+ [author, slug.split('/')[1..-1].join('/')]
321
+ end
322
+
323
+ def default_max_acepted_payout
324
+ "1000000.000 #{default_debt_asset}"
325
+ end
326
+
327
+ def default_debt_asset
328
+ case chain
329
+ when :steem then 'SBD'
330
+ when :golos then 'GBG'
331
+ else; raise ChainError, "Unknown chain: #{chain}"
332
+ end
333
+ end
334
+ end
335
+ end
@@ -1,8 +1,10 @@
1
+ require 'awesome_print'
2
+
1
3
  module Radiator
2
4
  class ErrorParser
3
5
  include Utils
4
6
 
5
- attr_reader :response, :error_code, :error_message,
7
+ attr_reader :response, :error, :error_code, :error_message,
6
8
  :api_name, :api_method, :api_params,
7
9
  :expiry, :can_retry, :can_reprepare, :trx_id, :debug
8
10
 
@@ -22,6 +24,7 @@ module Radiator
22
24
  def initialize(response)
23
25
  @response = response
24
26
 
27
+ @error = nil
25
28
  @error_code = nil
26
29
  @error_message = nil
27
30
  @api_name = nil
@@ -38,53 +41,85 @@ module Radiator
38
41
  end
39
42
 
40
43
  def parse_error_response
41
- return if response.nil?
42
-
43
- @error_code = response['error']['data']['code']
44
- stacks = response['error']['data']['stack']
45
- stack_formats = stacks.map { |s| s['format'] }
46
- stack_datum = stacks.map { |s| s['data'] }
47
- data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
48
-
49
- @error_message = stack_formats.reject(&:empty?).join('; ')
50
-
51
- @api_name, @api_method, @api_params = if !!data_call_method
52
- @api_name = data_call_method['call.params']
53
- end
54
-
55
- # See if we can recover a transaction id out of this hot mess.
56
- data_trx_ix = stack_datum.find { |data| !!data['trx_ix'] }
57
- @trx_id = data_trx_ix['trx_ix'] if !!data_trx_ix
58
-
59
- case @error_code
60
- when 10
44
+ if response.nil?
61
45
  @expiry = false
62
46
  @can_retry = false
63
- @can_reprepare = if @api_name == 'network_broadcast_api'
64
- (stack_formats & REPREPARE_WHITELIST).any?
65
- else
66
- false
47
+ @can_reprepare = false
48
+
49
+ return
50
+ end
51
+
52
+ @response = JSON[response] if response.class == String
53
+
54
+ @error = if !!@response['error']
55
+ response['error']
56
+ else
57
+ response
58
+ end
59
+
60
+ begin
61
+ @error_code = @error['data']['code']
62
+ stacks = @error['data']['stack']
63
+ stack_formats = stacks.map { |s| s['format'] }
64
+ stack_datum = stacks.map { |s| s['data'] }
65
+ data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
66
+
67
+ @error_message = stack_formats.reject(&:empty?).join('; ')
68
+
69
+ @api_name, @api_method, @api_params = if !!data_call_method
70
+ @api_name = data_call_method['call.params']
67
71
  end
68
- when 4030100
69
- # Code 4030100 is "transaction_expiration_exception: transaction
70
- # expiration exception". If we assume the expiration was valid, the
71
- # node might be bad and needs to be dropped.
72
72
 
73
- @expiry = true
74
- @can_retry = true
75
- @can_reprepare = false
76
- when 4030200
77
- # Code 4030200 is "transaction tapos exception". They are recoverable
78
- # if the transaction hasn't expired yet. A tapos exception can be
79
- # retried in situations where the node is behind and the tapos is
80
- # based on a block the node doesn't know about yet.
73
+ # See if we can recover a transaction id out of this hot mess.
74
+ data_trx_ix = stack_datum.find { |data| !!data['trx_ix'] }
75
+ @trx_id = data_trx_ix['trx_ix'] if !!data_trx_ix
81
76
 
82
- @expiry = false
83
- @can_retry = true
77
+ case @error_code
78
+ when 10
79
+ @expiry = false
80
+ @can_retry = false
81
+ @can_reprepare = if @api_name == 'network_broadcast_api'
82
+ (stack_formats & REPREPARE_WHITELIST).any?
83
+ else
84
+ false
85
+ end
86
+ when 13
87
+ @error_message = @error['data']['message']
88
+ @expiry = false
89
+ @can_retry = false
90
+ @can_reprepare = false
91
+ when 3030000
92
+ @error_message = @error['data']['message']
93
+ @expiry = false
94
+ @can_retry = false
95
+ @can_reprepare = false
96
+ when 4030100
97
+ # Code 4030100 is "transaction_expiration_exception: transaction
98
+ # expiration exception". If we assume the expiration was valid, the
99
+ # node might be bad and needs to be dropped.
100
+
101
+ @expiry = true
102
+ @can_retry = true
103
+ @can_reprepare = false
104
+ when 4030200
105
+ # Code 4030200 is "transaction tapos exception". They are recoverable
106
+ # if the transaction hasn't expired yet. A tapos exception can be
107
+ # retried in situations where the node is behind and the tapos is
108
+ # based on a block the node doesn't know about yet.
109
+
110
+ @expiry = false
111
+ @can_retry = true
112
+
113
+ # Allow fall back to reprepare if retry fails.
114
+ @can_reprepare = true
115
+ else
116
+ @expiry = false
117
+ @can_retry = false
118
+ @can_reprepare = false
119
+ end
120
+ rescue => e
121
+ ap error_perser_exception: e, original_response: response
84
122
 
85
- # Allow fall back to reprepare if retry fails.
86
- @can_reprepare = true
87
- else
88
123
  @expiry = false
89
124
  @can_retry = false
90
125
  @can_reprepare = false
@@ -98,5 +133,9 @@ module Radiator
98
133
  error_code.to_s
99
134
  end
100
135
  end
136
+
137
+ def inspect
138
+ "#<#{self.class.name} [#{to_s}]>"
139
+ end
101
140
  end
102
141
  end
@@ -23,6 +23,8 @@ module Radiator
23
23
  # @private
24
24
  MAX_BLOCKS_PER_NODE = 1000
25
25
 
26
+ RANGE_BEHIND_WARNING = 400
27
+
26
28
  def initialize(options = {})
27
29
  super
28
30
  end
@@ -212,17 +214,24 @@ module Radiator
212
214
  # @param mode we have the choice between
213
215
  # * :head the last block
214
216
  # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
215
- # * :max_blocks_per_node the number of blocks to read before trying a new node
217
+ # @param max_blocks_per_node the number of blocks to read before trying a new node
216
218
  # @param block the block to execute for each result, optional.
217
219
  # @return [Hash]
218
220
  def blocks(start = nil, mode = :irreversible, max_blocks_per_node = MAX_BLOCKS_PER_NODE, &block)
221
+ reset_api
222
+
223
+ replay = !!start
219
224
  counter = 0
220
225
  latest_block_number = -1
221
226
  @api_options[:max_requests] = [max_blocks_per_node * 2, @api_options[:max_requests].to_i].max
222
227
 
223
228
  loop do
229
+ break if stop?
230
+
224
231
  catch :sequence do; begin
225
232
  head_block = api.get_dynamic_global_properties do |properties|
233
+ break if stop?
234
+
226
235
  if properties.head_block_number.nil?
227
236
  # This can happen if a reverse proxy is acting up.
228
237
  standby "Bad block sequence after height: #{latest_block_number}", {
@@ -251,18 +260,27 @@ module Radiator
251
260
  start ||= head_block
252
261
  range = (start..head_block)
253
262
 
254
- if range.size > 400
255
- # When the range is 400 blocks, the stream will be behind by about
256
- # 20 minutes. Time to warn.
257
- standby "Stream behind by #{range.size} blocks (about #{(range.size * 3) / 60.0} minutes)."
258
- end
259
-
260
- [*range].each do |n|
263
+ for n in range
264
+ break if stop?
265
+
261
266
  if (counter += 1) > max_blocks_per_node
262
- shutdown
267
+ reset_api
263
268
  counter = 0
264
269
  end
265
-
270
+
271
+ if !replay && range.size > RANGE_BEHIND_WARNING
272
+ # When the range is above RANGE_BEHIND_WARNING blocks, it's time
273
+ # to warn, unless we're replaying.
274
+
275
+ r = [*range]
276
+ index = r.index(n)
277
+ current_range = r[index..-1]
278
+
279
+ if current_range.size % RANGE_BEHIND_WARNING == 0
280
+ standby "Stream behind by #{current_range.size} blocks (about #{(current_range.size * 3) / 60.0} minutes)."
281
+ end
282
+ end
283
+
266
284
  block_api.get_block(n) do |current_block, error|
267
285
  if current_block.nil?
268
286
  standby "Node responded with: empty block, retrying ...", {
@@ -295,13 +313,24 @@ module Radiator
295
313
  # Stops the persistant http connections.
296
314
  #
297
315
  def shutdown
316
+ flappy = false
317
+
298
318
  begin
299
- @api.shutdown
319
+ unless @api.nil?
320
+ flappy = @api.send(:flappy?)
321
+ @api.shutdown
322
+ end
323
+
324
+ unless @block_api.nil?
325
+ flappy = @block_api.send(:flappy?) unless flappy
326
+ @block_api.shutdown
327
+ end
300
328
  rescue => e
301
329
  warning("Unable to shut down: #{e}")
302
330
  end
303
331
 
304
332
  @api = nil
333
+ @block_api = nil
305
334
  end
306
335
 
307
336
  # @private
@@ -353,6 +382,8 @@ module Radiator
353
382
  @latest_values ||= []
354
383
  @latest_values.shift(5) if @latest_values.size > 20
355
384
  loop do
385
+ break if stop?
386
+
356
387
  value = if (n = method_params(m)).nil?
357
388
  key_value = api.get_dynamic_global_properties.result[m]
358
389
  else
@@ -362,12 +393,14 @@ module Radiator
362
393
  key_value = param = r[n[key]]
363
394
  result = nil
364
395
  loop do
396
+ break if stop?
397
+
365
398
  response = api.send(key, param)
366
399
  raise StreamError, JSON[response.error] if !!response.error
367
400
  result = response.result
368
401
  break if !!result
369
402
  warnning "#{key}: #{param} result missing, retrying with timeout: #{@timeout || INITIAL_TIMEOUT} seconds"
370
- shutdown
403
+ reset_api
371
404
  sleep timeout
372
405
  end
373
406
  @timeout = INITIAL_TIMEOUT
@@ -388,6 +421,11 @@ module Radiator
388
421
  end
389
422
  end
390
423
 
424
+ def reset_api
425
+ shutdown
426
+ !!api && !!block_api
427
+ end
428
+
391
429
  def timeout
392
430
  @timeout ||= INITIAL_TIMEOUT
393
431
  @timeout *= 2
@@ -401,6 +439,10 @@ module Radiator
401
439
  (Radiator::OperationTypes::TYPES.keys && type).any?
402
440
  end
403
441
 
442
+ def stop?
443
+ @api.nil? || @block_api.nil?
444
+ end
445
+
404
446
  def standby(message, options = {})
405
447
  error = options[:error]
406
448
  secondary = options[:and] || {}
@@ -164,7 +164,7 @@ module Radiator
164
164
  if block.nil?
165
165
  warning "Block missing while trying to prepare transaction, retrying ..."
166
166
  else
167
- debug block if ENV['LOG'] == 'DEBUG'
167
+ debug block if %w(DEBUG TRACE).include? ENV['LOG']
168
168
 
169
169
  warning "Block structure while trying to prepare transaction, retrying ..."
170
170
  end
@@ -197,14 +197,17 @@ module Radiator
197
197
  Digest::SHA256.digest(to_bytes)
198
198
  end
199
199
 
200
+ # May not find all non-canonicals, see: https://github.com/lian/bitcoin-ruby/issues/196
200
201
  def signature
201
202
  public_key_hex = @private_key.pub
202
203
  ec = Bitcoin::OpenSSL_EC
203
204
  digest_hex = digest.freeze
205
+ count = 0
204
206
 
205
207
  loop do
206
- @expiration += 1 unless @immutable_expiration
207
- sig = ec.sign_compact(digest_hex, @private_key.priv, public_key_hex)
208
+ count += 1
209
+ debug "#{count} attempts to find canonical signature" if count % 40 == 0
210
+ sig = ec.sign_compact(digest_hex, @private_key.priv, public_key_hex, false)
208
211
 
209
212
  next if public_key_hex != ec.recover_compact(digest_hex, sig)
210
213
 
@@ -58,7 +58,7 @@ module Radiator
58
58
  end
59
59
 
60
60
  def debug(obj, prefix = nil)
61
- if ENV['LOG'] == 'DEBUG'
61
+ if %w(DEBUG TRACE).include? ENV['LOG']
62
62
  send_log(:debug, obj, prefix)
63
63
  end
64
64
  end
@@ -1,3 +1,4 @@
1
1
  module Radiator
2
- VERSION = '0.3.9'
2
+ VERSION = '0.3.10'
3
+ AGENT_ID = "radiator/#{VERSION}"
3
4
  end
data/lib/radiator.rb CHANGED
@@ -32,5 +32,8 @@ module Radiator
32
32
  require 'radiator/transaction'
33
33
  require 'radiator/base_error'
34
34
  require 'radiator/error_parser'
35
+ require 'radiator/chain'
36
+ require 'steem'
37
+ require 'golos'
35
38
  extend self
36
39
  end
data/lib/steem.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Steem chain client for broadcasting common operations.
2
+ #
3
+ # @see Radiator::Chain
4
+ class Steem < Radiator::Chain
5
+ def initialize(options = {})
6
+ super(options.merge(chain: :steem))
7
+ end
8
+ end
data/radiator.gemspec CHANGED
@@ -27,11 +27,13 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'vcr', '~> 3.0', '>= 3.0.3'
28
28
  spec.add_development_dependency 'yard', '~> 0.9.9'
29
29
 
30
- spec.add_dependency('net-http-persistent', '~> 3.0', '>= 2.5.2')
30
+ # net-http-persistent has an open-ended dependency because radiator directly
31
+ # supports net-http-persistent-3.0.0 as well as net-http-persistent-2.5.2.
32
+ spec.add_dependency('net-http-persistent', '>= 2.5.2')
31
33
  spec.add_dependency('json', '~> 2.0', '>= 2.0.2')
32
34
  spec.add_dependency('logging', '~> 2.2', '>= 2.2.0')
33
35
  spec.add_dependency('hashie', '~> 3.5', '>= 3.5.5')
34
36
  spec.add_dependency('bitcoin-ruby', '~> 0.0', '>= 0.0.11')
35
37
  spec.add_dependency('ffi', '~> 1.9', '>= 1.9.18')
36
- spec.add_dependency 'awesome_print', '~> 1.7', '>= 1.7.0'
38
+ spec.add_dependency('awesome_print', '~> 1.7', '>= 1.7.0')
37
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radiator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.3.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Martin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-26 00:00:00.000000000 Z
11
+ date: 2017-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -176,9 +176,6 @@ dependencies:
176
176
  name: net-http-persistent
177
177
  requirement: !ruby/object:Gem::Requirement
178
178
  requirements:
179
- - - "~>"
180
- - !ruby/object:Gem::Version
181
- version: '3.0'
182
179
  - - ">="
183
180
  - !ruby/object:Gem::Version
184
181
  version: 2.5.2
@@ -186,9 +183,6 @@ dependencies:
186
183
  prerelease: false
187
184
  version_requirements: !ruby/object:Gem::Requirement
188
185
  requirements:
189
- - - "~>"
190
- - !ruby/object:Gem::Version
191
- version: '3.0'
192
186
  - - ">="
193
187
  - !ruby/object:Gem::Version
194
188
  version: 2.5.2
@@ -331,12 +325,14 @@ files:
331
325
  - images/Anthony Martin.png
332
326
  - images/Marvin Hofmann.jpg
333
327
  - images/Marvin Hofmann.png
328
+ - lib/golos.rb
334
329
  - lib/radiator.rb
335
330
  - lib/radiator/account_by_key_api.rb
336
331
  - lib/radiator/api.rb
337
332
  - lib/radiator/base_error.rb
338
333
  - lib/radiator/block_api.rb
339
334
  - lib/radiator/broadcast_operations.json
335
+ - lib/radiator/chain.rb
340
336
  - lib/radiator/chain_config.rb
341
337
  - lib/radiator/chain_stats_api.rb
342
338
  - lib/radiator/condenser_api.rb
@@ -365,6 +361,7 @@ files:
365
361
  - lib/radiator/type/u_int32.rb
366
362
  - lib/radiator/utils.rb
367
363
  - lib/radiator/version.rb
364
+ - lib/steem.rb
368
365
  - radiator.gemspec
369
366
  homepage: https://github.com/inertia186/radiator
370
367
  licenses: