radiator 0.3.9 → 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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: