radiator 0.3.4 → 0.3.6

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: d0086bea776358375494997aec2c65747f764d6e
4
- data.tar.gz: ae4cb6a0609f53d64b918dd07169ffd0b5eba060
3
+ metadata.gz: 956f02673186f4855cd07bc5c5a1a9163c329cbf
4
+ data.tar.gz: 3a18bd23240d54c747ec4b5735a316c60a9e5ef6
5
5
  SHA512:
6
- metadata.gz: c22901174d31457a25dd9990a64c2b4dcef9a7f98cf3bf1f9c7994cab317b2dced12ae160db654eef1e268ca01bb751c01aad5cfc5ebc6afe91816dd8e2dc0d4
7
- data.tar.gz: c8c61c3bc5920bd003fe80b1c1ce06c50a5c080261f4f0867b554a087d603942932c3ce8fa050a7c221c5a5c5b059811b93192fc6b4869bc7439182a79fa9f88
6
+ metadata.gz: 6982a8ac692ee87bb34041d854ca80034745505f389f6f94e12deaafae6975a3336d6147b4d116a4d8a8d03f85f8d051561751ab782e74df511341a8b084169d
7
+ data.tar.gz: 6a6438a16389022332cda72466bad910f6d1f4b474e892176078dd37c5f6a821f0cdd7422ceebe3140e450b7bf3b5c9b9bae92f207aacce9683337ca2b1af78c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- radiator (0.3.4)
4
+ radiator (0.3.6)
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)
@@ -16,7 +16,7 @@ GEM
16
16
  addressable (2.5.2)
17
17
  public_suffix (>= 2.0.2, < 4.0)
18
18
  awesome_print (1.8.0)
19
- bitcoin-ruby (0.0.11)
19
+ bitcoin-ruby (0.0.12)
20
20
  connection_pool (2.2.1)
21
21
  crack (0.4.3)
22
22
  safe_yaml (~> 1.0.0)
@@ -68,4 +68,4 @@ DEPENDENCIES
68
68
  yard (~> 0.9.9)
69
69
 
70
70
  BUNDLED WITH
71
- 1.15.4
71
+ 1.16.0.pre.3
data/README.md CHANGED
@@ -397,9 +397,9 @@ Radiator supports failover for situations where a node has, for example, become
397
397
 
398
398
  ```ruby
399
399
  options = {
400
- ur: 'https://steemd.steemit.com',
400
+ ur: 'https://api.steemit.com',
401
401
  failover_urls: [
402
- 'https://steemd.steemitstage.com',
402
+ 'https://api.steemitstage.com',
403
403
  'https://gtg.steem.house:8090'
404
404
  ]
405
405
  }
@@ -421,6 +421,16 @@ There is another rare scenario involving `::Transaction` broadcasts that's handl
421
421
  tx = Radiator::Transaction.new(wif: wif, recover_transactions_on_error: false)
422
422
  ```
423
423
 
424
+ ## Debugging
425
+
426
+ To enable debugging, set environment `LOG=DEBUG` before launching your app. E.g.:
427
+
428
+ ```bash
429
+ $ LOG=DEBUG irb -rradiator
430
+ ```
431
+
432
+ This will enable debugging for the `irb` session.
433
+
424
434
  ## Troubleshooting
425
435
 
426
436
  ## Problem: My log is full of `Unable to perform request ... retrying ...` messages.
@@ -465,7 +475,10 @@ Verify your code is not doing too much between blocks.
465
475
  * `rake`
466
476
  * To run tests with parallelization and local code coverage:
467
477
  * `HELL_ENABLED=true rake`
468
-
478
+ * To run a stream test on the live STEEM blockchain with debug logging enabled:
479
+ * `LOG=DEBUG rake test_live_stream`
480
+ * To run a stream test on the live GOLOS blockchain with debug logging enabled:
481
+ * `LOG=DEBUG rake test_live_stream[golos]`
469
482
  ---
470
483
 
471
484
  <center>
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
  require 'yard'
4
+ require 'radiator'
4
5
 
5
6
  Rake::TestTask.new(:test) do |t|
6
7
  t.libs << 'test'
@@ -19,14 +20,62 @@ end
19
20
 
20
21
  task default: :test
21
22
 
23
+ desc 'Deletes test/fixtures/vcr_cassettes/*.yml so they can be rebuilt fresh.'
24
+ task :dump_vcr do |t|
25
+ exec 'rm -v test/fixtures/vcr_cassettes/*.yml'
26
+ end
27
+
28
+ desc 'Tests the ability to stream live data.'
29
+ task :test_live_stream, :chain do |t, args|
30
+ chain = (args[:chain] || 'steem').to_sym
31
+ last_block_number = 0
32
+ options = {chain: chain}
33
+ api = Radiator::Api.new(options)
34
+ total_ops = 0.0
35
+ total_vops = 0.0
36
+
37
+ Radiator::Stream.new(options).blocks do |b, n|
38
+ if last_block_number == 0
39
+ # skip test
40
+ elsif last_block_number + 1 == n
41
+ t = b.transactions
42
+ t_size = t.size
43
+ o = t.map(&:operations)
44
+ op_size = o.map(&:size).reduce(0, :+)
45
+ total_ops += op_size
46
+ api.get_ops_in_block(n, true) do |vops|
47
+ vop_size = vops.size
48
+ total_vops += vop_size
49
+
50
+ vop_ratio = if total_vops > 0
51
+ total_vops / total_ops
52
+ else
53
+ 0
54
+ end
55
+
56
+ puts "#{n}: #{b.witness}; transactions: #{t_size}; operations: #{op_size}, virtual operations: #{vop_size} (cumulative vop ratio: #{('%.2f' % (vop_ratio * 100))} %)"
57
+ end
58
+ else
59
+ # This should not happen. If it does, there's likely a bug in Radiator.
60
+
61
+ puts "Error, last block nunber was #{last_block_number}, did not expect #{n}."
62
+ end
63
+
64
+ last_block_number = n
65
+ end
66
+ end
67
+
68
+ desc 'Ruby console with radiator already required.'
22
69
  task :console do
23
70
  exec "irb -r radiator -I ./lib"
24
71
  end
25
72
 
73
+ desc 'Build a new version of the radiator gem.'
26
74
  task :build do
27
75
  exec 'gem build radiator.gemspec'
28
76
  end
29
77
 
78
+ desc 'Publish the current version of the radiator gem.'
30
79
  task :push do
31
80
  exec "gem push radiator-#{Radiator::VERSION}.gem"
32
81
  end
@@ -34,6 +83,7 @@ end
34
83
  # We're not going to yank on a regular basis, but this is how it's done if you
35
84
  # really want a task for that for some reason.
36
85
 
86
+ # desc 'Yank the current version of the radiator gem.'
37
87
  # task :yank do
38
88
  # exec "gem yank radiator -v #{Radiator::VERSION}"
39
89
  # end
data/lib/radiator/api.rb CHANGED
@@ -3,6 +3,7 @@ require 'base64'
3
3
  require 'hashie'
4
4
  require 'hashie/logger'
5
5
  require 'openssl'
6
+ require 'open-uri'
6
7
  require 'net/http/persistent'
7
8
 
8
9
  module Radiator
@@ -131,14 +132,15 @@ module Radiator
131
132
  # @see https://steemit.github.io/steemit-docs/#accounts
132
133
  #
133
134
  class Api
134
- DEFAULT_STEEM_URL = 'https://steemd.steemit.com'
135
+ include Utils
136
+
137
+ DEFAULT_STEEM_URL = 'https://api.steemit.com'
135
138
 
136
139
  DEFAULT_GOLOS_URL = 'https://ws.golos.io'
137
140
 
138
141
  DEFAULT_STEEM_FAILOVER_URLS = [
139
142
  DEFAULT_STEEM_URL,
140
- 'https://steemd-int.steemit.com',
141
- 'https://steemd.steemitstage.com',
143
+ 'https://api.steemitstage.com',
142
144
  'https://gtg.steem.house:8090',
143
145
  'https://seed.bitcoiner.me',
144
146
  'https://steemd.minnowsupportproject.org',
@@ -156,9 +158,8 @@ module Radiator
156
158
  'Content-Type' => 'application/json'
157
159
  }
158
160
 
159
- # These are known SSL versions supported by:
160
- # https://github.com/ruby/openssl/blob/master/lib/openssl/ssl.rb
161
- SSL_VERSIONS = [:TLSv1_2, :TLSv1_1, :TLSv1, :SSLv3, :SSLv2, :SSLv23]
161
+ # @private
162
+ HEALTH_URI = '/health'
162
163
 
163
164
  def self.default_url(chain)
164
165
  case chain.to_sym
@@ -183,11 +184,12 @@ module Radiator
183
184
  # api = Radiator::Api.new(url: 'https://api.example.com')
184
185
  #
185
186
  # @param options [Hash] The attributes to initialize the Radiator::Api with.
186
- # @option options [String] :url URL that points at a full node, like `https://steemd.steemit.com`. Default from DEFAULT_URL.
187
+ # @option options [String] :url URL that points at a full node, like `https://api.steemit.com`. Default from DEFAULT_URL.
187
188
  # @option options [Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
188
189
  # @option options [Logger] :logger An instance of `Logger` to send debug messages to.
189
190
  # @option options [Boolean] :recover_transactions_on_error Have Radiator try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
190
191
  # @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
192
+ # @option options [Boolean] :reuse_ssl_sessions Reuse a previously opened SSL session for a new connection. There's a slight performance improvement by enabling this, but at the expense of reliability during long execution. Default false.
191
193
  def initialize(options = {})
192
194
  @user = options[:user]
193
195
  @password = options[:password]
@@ -199,7 +201,9 @@ module Radiator
199
201
  @logger = options[:logger] || Radiator.logger
200
202
  @hashie_logger = options[:hashie_logger] || Logger.new(nil)
201
203
  @max_requests = options[:max_requests] || 30
202
- @ssl_version = nil # default
204
+ @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
205
+ @reuse_ssl_sessions = !!options[:reuse_ssl_sessions]
206
+ @ssl_version = options[:ssl_version]
203
207
 
204
208
  if @failover_urls.nil?
205
209
  @failover_urls = Api::default_failover_urls(@chain) - [@url]
@@ -220,6 +224,7 @@ module Radiator
220
224
 
221
225
  Hashie.logger = @hashie_logger
222
226
  @method_names = nil
227
+ @http = nil
223
228
  @api_options = options.dup.merge(chain: @chain)
224
229
  @api = nil
225
230
  @block_api = nil
@@ -376,12 +381,13 @@ module Radiator
376
381
  def method_missing(m, *args, &block)
377
382
  super unless respond_to_missing?(m)
378
383
 
384
+ current_rpc_id = rpc_id
379
385
  method_name = [api_name, m].join('.')
380
386
  response = nil
381
387
  options = {
382
388
  jsonrpc: "2.0",
383
389
  params: [api_name, m, args],
384
- id: rpc_id,
390
+ id: current_rpc_id,
385
391
  method: "call"
386
392
  }
387
393
 
@@ -398,8 +404,8 @@ module Radiator
398
404
  if tries > 1 && !!signatures && signatures.any?
399
405
  offset = [(exp - timestamp).abs, 300].min
400
406
 
401
- if !!(response = recover_transaction(signatures, rpc_id, timestamp - offset))
402
- warning 'Found recovered transaction after retry.', method_name
407
+ if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
408
+ warning 'Found recovered transaction after retry.', method_name, true
403
409
  response = Hashie::Mash.new(response)
404
410
  end
405
411
  end
@@ -411,7 +417,7 @@ module Radiator
411
417
  response = if response.nil?
412
418
  error "No response, retrying ...", method_name
413
419
  elsif !response.kind_of? Net::HTTPSuccess
414
- warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name
420
+ warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
415
421
  else
416
422
  case response.code
417
423
  when '200'
@@ -419,52 +425,48 @@ module Radiator
419
425
  response = JSON[body]
420
426
 
421
427
  if response['id'] != options[:id]
422
- warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name
428
+ warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
423
429
  elsif response.keys.include?('error')
424
- case response['error']['code']
425
- when -32601 # Assert Exception:method_itr != api_itr->second.end(): Could not find method ...
426
- nil
427
- else
428
- Hashie::Mash.new(response)
429
- end
430
+ handle_error(response, options, method_name, tries)
430
431
  else
431
432
  Hashie::Mash.new(response)
432
433
  end
433
- when '400' then warning 'Code 400: Bad Request, retrying ...', method_name
434
- when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name
435
- when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name
436
- when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name
437
- when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name
434
+ when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
435
+ when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
436
+ when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
437
+ when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
438
+ when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
438
439
  else
439
- warning "Unknown code #{response.code}, retrying ...", method_name
440
+ warning "Unknown code #{response.code}, retrying ...", method_name, true
440
441
  ap response
441
442
  end
442
443
  end
443
444
  end
444
445
  rescue Net::HTTP::Persistent::Error => e
445
- warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name
446
+ warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
446
447
  rescue Errno::ECONNREFUSED => e
447
- warning 'Connection refused, retrying ...', method_name
448
+ warning 'Connection refused, retrying ...', method_name, true
448
449
  rescue Errno::EADDRNOTAVAIL => e
449
- warning 'Node not available, retrying ...', method_name
450
+ warning 'Node not available, retrying ...', method_name, true
451
+ rescue Errno::ECONNRESET => e
452
+ warning "Connection Reset (#{e.message}), retrying ...", method_name, true
450
453
  rescue Net::ReadTimeout => e
451
- warning 'Node read timeout, retrying ...', method_name
454
+ warning 'Node read timeout, retrying ...', method_name, true
452
455
  rescue Net::OpenTimeout => e
453
- warning 'Node timeout, retrying ...', method_name
456
+ warning 'Node timeout, retrying ...', method_name, true
454
457
  rescue RangeError => e
455
- warning 'Range Error, retrying ...', method_name
458
+ warning 'Range Error, retrying ...', method_name, true
456
459
  rescue OpenSSL::SSL::SSLError => e
457
- @ssl_version = SSL_VERSIONS.sample
458
- warning "SSL Error (#{e.message}), switching to #{@ssl_version} and retrying ...", method_name
460
+ warning "SSL Error (#{e.message}), retrying ...", method_name, true
459
461
  rescue SocketError => e
460
- warning "Socket Error (#{e.message}), retrying ...", method_name
462
+ warning "Socket Error (#{e.message}), retrying ...", method_name, true
461
463
  rescue JSON::ParserError => e
462
- warning "JSON Parse Error (#{e.message}), retrying ...", method_name
464
+ warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
463
465
  response = nil
464
466
  rescue ApiError => e
465
- warning "ApiError (#{e.message}), retrying ...", method_name
467
+ warning "ApiError (#{e.message}), retrying ...", method_name, true
466
468
  # rescue => e
467
- # warning "Unknown exception from request, retrying ...", method_name
469
+ # warning "Unknown exception from request, retrying ...", method_name, true
468
470
  # ap e if defined? ap
469
471
  end
470
472
 
@@ -528,13 +530,9 @@ module Radiator
528
530
  @http.idle_timeout = idempotent ? 10 : nil
529
531
  @http.max_requests = @max_requests
530
532
  @http.retry_change_requests = idempotent
531
-
532
- if flappy?
533
- @http.reuse_ssl_sessions = false
534
- @http.ssl_version = @ssl_version
535
- else
536
- @http.reuse_ssl_sessions = true
537
- end
533
+ @http.verify_mode = @ssl_verify_mode
534
+ @http.reuse_ssl_sessions = @reuse_ssl_sessions
535
+ @http.ssl_version = @ssl_version
538
536
 
539
537
  @http
540
538
  end
@@ -549,25 +547,7 @@ module Radiator
549
547
  http.request(uri, request)
550
548
  end
551
549
 
552
- def extract_signatures(options)
553
- params = options[:params]
554
-
555
- signatures = params.map do |param|
556
- next unless defined? param.map
557
-
558
- param.map { |tx| tx[:signatures] }
559
- end.flatten.compact
560
-
561
- expirations = params.map do |param|
562
- next unless defined? param.map
563
-
564
- param.map { |tx| Time.parse(tx[:expiration] + 'Z') }
565
- end.flatten.compact
566
-
567
- [signatures, expirations.min]
568
- end
569
-
570
- def recover_transaction(signatures, rpc_id, after)
550
+ def recover_transaction(signatures, expected_rpc_id, after)
571
551
  block_range = api.get_dynamic_global_properties do |properties|
572
552
  high = properties.head_block_number
573
553
  low = high - 100
@@ -589,7 +569,7 @@ module Radiator
589
569
  next unless ((tx['signatures'] || []) & signatures).any?
590
570
 
591
571
  return {
592
- id: rpc_id,
572
+ id: expected_rpc_id,
593
573
  result: {
594
574
  id: block.transaction_ids[index],
595
575
  block_num: block_num,
@@ -610,12 +590,16 @@ module Radiator
610
590
  end
611
591
 
612
592
  def pop_failover_url
613
- @failover_urls.delete(@failover_urls.sample) || @url
593
+ reset_failover if @failover_urls.none?
594
+
595
+ until @failover_urls.none? || healthy?(url = @failover_urls.sample)
596
+ @failover_urls.delete(url)
597
+ end
598
+
599
+ url || @url
614
600
  end
615
601
 
616
602
  def bump_failover
617
- reset_failover if @failover_urls.none?
618
-
619
603
  @uri = nil
620
604
  @url = pop_failover_url
621
605
  warning "Failing over to #{@url} ..."
@@ -625,9 +609,56 @@ module Radiator
625
609
  !!@backoff_at && Time.now - @backoff_at < 300
626
610
  end
627
611
 
612
+ def drop_current_failover_url(prefix)
613
+ if @preferred_failover_urls.size == 1
614
+ warning "Node #{@url} appears to be misconfigured but no other node is available, retrying ...", prefix
615
+ else
616
+ warning "Removing misconfigured node from failover urls: #{@url}, retrying ...", prefix
617
+ @preferred_failover_urls.delete(@url)
618
+ @failover_urls.delete(@url)
619
+ end
620
+ end
621
+
622
+ def handle_error(response, request_options, method_name, tries)
623
+ parser = ErrorParser.new(response)
624
+ signatures, exp = extract_signatures(request_options)
625
+
626
+ if (!!exp && exp < Time.now.utc) || tries > 2
627
+ # Whatever the error was, it is already expired or tried too much. No
628
+ # need to try to recover.
629
+
630
+ debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
631
+ elsif parser.can_retry?
632
+ drop_current_failover_url method_name if !!exp && parser.expiry?
633
+ debug "Error code #{parser} (attempt: #{tries}), retrying ..."
634
+ return nil
635
+ end
636
+
637
+ Hashie::Mash.new(response)
638
+ end
639
+
640
+ def healthy?(url)
641
+ begin
642
+ # Note, not all nodes support the /health uri. But even if they don't,
643
+ # they'll respond status code 200 OK, even if the body shows an error.
644
+
645
+ # But if the node supports the /health uri, it will do additional
646
+ # verifications on the block height.
647
+ # See: https://github.com/steemit/steem/blob/master/contrib/healthcheck.sh
648
+
649
+ # Also note, this check is done **without** net-http-persistent.
650
+
651
+ !!open(url + HEALTH_URI)
652
+ rescue => e
653
+ error "Health check failure for #{url}: #{e.inspect}"
654
+ sleep 0.2
655
+ false
656
+ end
657
+ end
658
+
628
659
  def backoff
629
660
  shutdown
630
- bump_failover if flappy?
661
+ bump_failover if flappy? || !healthy?(@url)
631
662
  @backoff_at ||= Time.now
632
663
  @backoff_sleep ||= 0.01
633
664
 
@@ -639,18 +670,5 @@ module Radiator
639
670
  @backoff_sleep = nil
640
671
  end
641
672
  end
642
-
643
- def send_log(level, message, prefix = nil)
644
- if !!prefix
645
- @logger.send level, "#{prefix} :: #{message}"
646
- else
647
- @logger.send level, "#{message}"
648
- end
649
-
650
- nil
651
- end
652
-
653
- def error(message, prefix = nil); send_log(:error, message, prefix); end
654
- def warning(message, prefix = nil); send_log(:warn, message, prefix); end
655
673
  end
656
674
  end
@@ -1,11 +1,16 @@
1
1
  module Radiator
2
2
  class BaseError < StandardError
3
- def initialize(error)
3
+ def initialize(error, cause = nil)
4
4
  @error = error
5
+ @cause = cause
5
6
  end
6
7
 
7
8
  def to_s
8
- JSON[@error] rescue @error
9
+ if !!@cause
10
+ JSON[error: @error, cause: @cause] rescue {error: @error, cause: @cause}.to_s
11
+ else
12
+ JSON[@error] rescue @error
13
+ end
9
14
  end
10
15
  end
11
16
  end
@@ -8,7 +8,7 @@ module Radiator
8
8
  NETWORKS_STEEM_CORE_ASSET = 'STEEM'
9
9
  NETWORKS_STEEM_DEBT_ASSET = 'SBD'
10
10
  NETWORKS_STEEM_VEST_ASSET = 'VESTS'
11
- NETWORKS_STEEM_DEFAULT_NODE = 'https://steemd.steemit.com'
11
+ NETWORKS_STEEM_DEFAULT_NODE = 'https://api.steemit.com'
12
12
 
13
13
  NETWORKS_GOLOS_CHAIN_ID = '782a3039b478c839e4cb0c941ff4eaeb7df40bdd68bd441afd444b9da763de12'
14
14
  NETWORKS_GOLOS_ADDRESS_PREFIX = 'GLS'
@@ -0,0 +1,94 @@
1
+ module Radiator
2
+ class ErrorParser
3
+ include Utils
4
+
5
+ attr_reader :response, :error_code, :error_message,
6
+ :api_name, :api_method, :api_params,
7
+ :expiry, :can_retry, :can_reprepare, :debug
8
+
9
+ alias expiry? expiry
10
+ alias can_retry? can_retry
11
+ alias can_reprepare? can_reprepare
12
+
13
+ REPREPARE = [
14
+ 'is_canonical( c ): signature is not canonical',
15
+ 'now < trx.expiration: ',
16
+ '(skip & skip_transaction_dupe_check) || trx_idx.indices().get<by_trx_id>().find(trx_id) == trx_idx.indices().get<by_trx_id>().end(): Duplicate transaction check failed'
17
+ ]
18
+
19
+ def initialize(response)
20
+ @response = response
21
+
22
+ @error_code = nil
23
+ @error_message = nil
24
+ @api_name = nil
25
+ @api_method = nil
26
+ @api_params = nil
27
+
28
+ @expiry = nil
29
+ @can_retry = nil
30
+ @can_reprepare = nil
31
+ @debug = nil
32
+
33
+ parse_error_response
34
+ end
35
+
36
+ def parse_error_response
37
+ return if response.nil?
38
+
39
+ @error_code = response['error']['data']['code']
40
+ stacks = response['error']['data']['stack']
41
+ stack_formats = stacks.map { |s| s['format'] }
42
+ stack_datum = stacks.map { |s| s['data'] }
43
+ data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
44
+
45
+ @error_message = stack_formats.reject(&:empty?).join('; ')
46
+
47
+ @api_name, @api_method, @api_params = if !!data_call_method
48
+ @api_name = data_call_method['call.params']
49
+ end
50
+
51
+ case @error_code
52
+ when 10
53
+ @expiry = false
54
+ @can_retry = false
55
+ @can_reprepare = if @api_name == 'network_broadcast_api'
56
+ (stack_formats & REPREPARE).any?
57
+ else
58
+ false
59
+ end
60
+ when 4030100
61
+ # Code 4030100 is "transaction_expiration_exception: transaction
62
+ # expiration exception". If we assume the expiration was valid, the
63
+ # node might be bad and needs to be dropped.
64
+
65
+ @expiry = true
66
+ @can_retry = true
67
+ @can_reprepare = false
68
+ when 4030200
69
+ # Code 4030200 is "transaction tapos exception". They are recoverable
70
+ # if the transaction hasn't expired yet. A tapos exception can be
71
+ # retried in situations where the node is behind and the tapos is
72
+ # based on a block the node doesn't know about yet.
73
+
74
+ @expiry = false
75
+ @can_retry = true
76
+
77
+ # Allow fall back to reprepare if retry fails.
78
+ @can_reprepare = true
79
+ else
80
+ @expiry = false
81
+ @can_retry = false
82
+ @can_reprepare = false
83
+ end
84
+ end
85
+
86
+ def to_s
87
+ if !!error_message && !error_message.empty?
88
+ "#{error_code}: #{error_message}"
89
+ else
90
+ error_code.to_s
91
+ end
92
+ end
93
+ end
94
+ end
@@ -21,7 +21,7 @@ module Radiator
21
21
  MAX_TIMEOUT = 80
22
22
 
23
23
  # @private
24
- MAX_BLOCKS_PER_NODE = 100
24
+ MAX_BLOCKS_PER_NODE = 1000
25
25
 
26
26
  def initialize(options = {})
27
27
  super
@@ -27,12 +27,13 @@ module Radiator
27
27
 
28
28
  @logger = options[:logger] || Radiator.logger
29
29
  @chain ||= :steem
30
+ @chain = @chain.to_sym
30
31
  @chain_id = chain_id options[:chain_id]
31
32
  @url = options[:url] || url
32
33
  @operations = options[:operations] || []
33
34
 
34
35
  unless NETWORK_CHAIN_IDS.include? @chain_id
35
- @logger.warn "Unknown chain id: #{@chain_id}"
36
+ warning "Unknown chain id: #{@chain_id}"
36
37
  end
37
38
 
38
39
  if !!wif && !!private_key
@@ -70,7 +71,21 @@ module Radiator
70
71
  prepare
71
72
 
72
73
  if broadcast
73
- @network_broadcast_api.broadcast_transaction_synchronous(payload)
74
+ loop do
75
+ response = @network_broadcast_api.broadcast_transaction_synchronous(payload)
76
+
77
+ if !!response.error
78
+ parser = ErrorParser.new(response)
79
+
80
+ if parser.can_reprepare?
81
+ debug "Repreparing transaction ..."
82
+ prepare
83
+ redo
84
+ end
85
+ end
86
+
87
+ return response
88
+ end
74
89
  else
75
90
  self
76
91
  end
@@ -99,19 +114,56 @@ module Radiator
99
114
  def prepare
100
115
  raise TransactionError, "No wif or private key." unless !!@wif || !!@private_key
101
116
 
102
- @properties = @api.get_dynamic_global_properties.result
103
- @ref_block_num = @properties.head_block_number & 0xFFFF
104
- @ref_block_prefix = unhexlify(@properties.head_block_id[8..-1]).unpack('V*')[0]
105
-
106
- # The expiration allows for transactions to expire if they are not
107
- # included into a block by that time. Always update it to the current
108
- # time + EXPIRE_IN_SECS.
109
- #
110
- # Note, as of #1215, expiration exactly 'now' will be rejected:
111
- # https://github.com/steemit/steem/blob/57451b80d2cf480dcce9b399e48e56aa7af1d818/libraries/chain/database.cpp#L2870
112
- # https://github.com/steemit/steem/issues/1215
113
-
114
- @expiration = Time.parse(@properties.time + 'Z') + EXPIRE_IN_SECS
117
+ @api.get_dynamic_global_properties do |properties, error|
118
+ if !!error
119
+ raise TransactionError, "Unable to prepare transaction.", error
120
+ end
121
+
122
+ @properties = properties
123
+
124
+ case @chain
125
+ when :steem, :test
126
+ # You can actually go back as far as the TaPoS buffer will allow, which
127
+ # is something like 50,000 blocks.
128
+
129
+ block_number = @properties.last_irreversible_block_num
130
+
131
+ @api.get_block(block_number) do |block, error|
132
+ if !!error
133
+ raise TransactionError, "Unable to prepare transaction.", error
134
+ end
135
+
136
+ if block.nil?
137
+ raise TransactionError, "Unable to prepare transaction, block missing."
138
+ end
139
+
140
+ if block.block_id.nil?
141
+ raise TransactionError, "Unable to prepare transaction, block.block_id missing."
142
+ end
143
+
144
+ @ref_block_num = block_number & 0xFFFF
145
+ @ref_block_prefix = unhexlify(block.block_id[8..-1]).unpack('V*')[0]
146
+ end
147
+ when :golos
148
+ # No support for block_id in get_block on golos (yet), so just use the
149
+ # head block number.
150
+
151
+ @ref_block_num = @properties.head_block_number & 0xFFFF
152
+ @ref_block_prefix = unhexlify(@properties.head_block_id[8..-1]).unpack('V*')[0]
153
+ else
154
+ raise TransactionError, "Unable to prepare transaction, unsupported chain: #{@chain}"
155
+ end
156
+
157
+ # The expiration allows for transactions to expire if they are not
158
+ # included into a block by that time. Always update it to the current
159
+ # time + EXPIRE_IN_SECS.
160
+ #
161
+ # Note, as of #1215, expiration exactly 'now' will be rejected:
162
+ # https://github.com/steemit/steem/blob/57451b80d2cf480dcce9b399e48e56aa7af1d818/libraries/chain/database.cpp#L2870
163
+ # https://github.com/steemit/steem/issues/1215
164
+
165
+ @expiration = Time.parse(@properties.time + 'Z') + EXPIRE_IN_SECS
166
+ end
115
167
 
116
168
  self
117
169
  end
@@ -1,5 +1,59 @@
1
1
  module Radiator
2
2
  module Utils
3
+ def extract_signatures(options)
4
+ params = options[:params]
5
+
6
+ signatures = params.map do |param|
7
+ next unless defined? param.map
8
+
9
+ param.map do |tx|
10
+ tx[:signatures] rescue nil
11
+ end
12
+ end.flatten.compact
13
+
14
+ expirations = params.map do |param|
15
+ next unless defined? param.map
16
+
17
+ param.map do |tx|
18
+ Time.parse(tx[:expiration] + 'Z') rescue nil
19
+ end
20
+ end.flatten.compact
21
+
22
+ [signatures, expirations.min]
23
+ end
24
+
25
+ def send_log(level, message, prefix = nil)
26
+ log_message = if !!prefix
27
+ "#{prefix} :: #{message}"
28
+ else
29
+ message
30
+ end
31
+
32
+ if !!@logger
33
+ @logger.send level, log_message
34
+ else
35
+ puts "#{level}: #{log_message}"
36
+ end
37
+
38
+ nil
39
+ end
40
+
41
+ def error(message, prefix = nil)
42
+ send_log(:error, message, prefix)
43
+ end
44
+
45
+ def warning(message, prefix = nil, log_debug_node = false)
46
+ debug("Current node: #{@url}", prefix) if !!log_debug_node && @url
47
+
48
+ send_log(:warn, message, prefix)
49
+ end
50
+
51
+ def debug(message, prefix = nil)
52
+ if ENV['LOG'] == 'DEBUG'
53
+ send_log(:debug, message, prefix)
54
+ end
55
+ end
56
+
3
57
  def hexlify(s)
4
58
  a = []
5
59
  if s.respond_to? :each_byte
@@ -1,3 +1,3 @@
1
1
  module Radiator
2
- VERSION = '0.3.4'
2
+ VERSION = '0.3.6'
3
3
  end
data/lib/radiator.rb CHANGED
@@ -31,5 +31,6 @@ module Radiator
31
31
  require 'radiator/operation'
32
32
  require 'radiator/transaction'
33
33
  require 'radiator/base_error'
34
+ require 'radiator/error_parser'
34
35
  extend self
35
36
  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.4
4
+ version: 0.3.6
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-19 00:00:00.000000000 Z
11
+ date: 2017-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -335,6 +335,7 @@ files:
335
335
  - lib/radiator/chain_stats_api.rb
336
336
  - lib/radiator/condenser_api.rb
337
337
  - lib/radiator/database_api.rb
338
+ - lib/radiator/error_parser.rb
338
339
  - lib/radiator/follow_api.rb
339
340
  - lib/radiator/logger.rb
340
341
  - lib/radiator/market_history_api.rb
@@ -379,7 +380,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
379
380
  version: '0'
380
381
  requirements: []
381
382
  rubyforge_project:
382
- rubygems_version: 2.6.12
383
+ rubygems_version: 2.6.14
383
384
  signing_key:
384
385
  specification_version: 4
385
386
  summary: STEEM RPC Ruby Client