radiator 0.3.4 → 0.3.6

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: 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