beowulf-ruby-testnet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,845 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'hashie'
4
+ require 'hashie/logger'
5
+ require 'openssl'
6
+ require 'open-uri'
7
+ require 'net/http/persistent'
8
+
9
+ module Beowulf
10
+ # Beowulf::Api allows you to call remote methods to interact with the Beowulf blockchain.
11
+ # The `Api` class is a shortened name for
12
+ # `Beowulf::CondenserApi`.
13
+ #
14
+ # Examples:
15
+ #
16
+ # api = Beowulf::Api.new
17
+ # response = api.get_dynamic_global_properties
18
+ # virtual_supply = response.result.virtual_supply
19
+ #
20
+ # ... or ...
21
+ #
22
+ # api = Beowulf::Api.new
23
+ # virtual_supply = api.get_dynamic_global_properties do |prop|
24
+ # prop.virtual_supply
25
+ # end
26
+ #
27
+ # If you need access to the `error` property, they can be accessed as follows:
28
+ #
29
+ # api = Beowulf::Api.new
30
+ # response = api.get_dynamic_global_properties
31
+ # if response.result.nil?
32
+ # puts response.error
33
+ # exit
34
+ # end
35
+ #
36
+ # virtual_supply = response.result.virtual_supply
37
+ #
38
+ # ... or ...
39
+ #
40
+ # api = Beowulf::Api.new
41
+ # virtual_supply = api.get_dynamic_global_properties do |prop, error|
42
+ # if prop.nil?
43
+ # puts error
44
+ # exis
45
+ # end
46
+ #
47
+ # prop.virtual_supply
48
+ # end
49
+ #
50
+ # List of remote methods:
51
+ #
52
+ # get_version
53
+ # get_block_header
54
+ # get_block
55
+ # get_config
56
+ # get_dynamic_global_properties
57
+ # get_supernode_schedule
58
+ # get_hardfork_version
59
+ # get_next_scheduled_hardfork
60
+ # get_accounts
61
+ # lookup_account_names
62
+ # lookup_accounts
63
+ # get_account_count
64
+ # get_owner_history
65
+ # get_transaction_hex
66
+ # get_transaction
67
+ # get_transaction_with_status
68
+ # get_pending_transaction_count
69
+ # get_required_signatures
70
+ # get_potential_signatures
71
+ # verify_authority
72
+ # get_supernodes
73
+ # get_supernode_by_accounts
74
+ # get_supernodes_by_vote
75
+ # lookup_supernode_accounts
76
+ # get_supernode_count
77
+ # get_active_supernodes
78
+ #
79
+ class Api
80
+ include Utils
81
+
82
+ DEFAULT_BEOWULF_URL = 'https://testnet-bw.beowulfchain.com/rpc'
83
+
84
+ DEFAULT_BEOWULF_FAILOVER_URLS = [
85
+ DEFAULT_BEOWULF_URL
86
+ ]
87
+
88
+ DEFAULT_RESTFUL_URL = 'https://testnet-bw.beowulfchain.com/rpc'
89
+
90
+ # @private
91
+ POST_HEADERS = {
92
+ 'Content-Type' => 'application/json',
93
+ 'User-Agent' => Beowulf::AGENT_ID
94
+ }
95
+
96
+ # @private
97
+ HEALTH_URI = '/health'
98
+
99
+ def self.default_url(chain)
100
+ case chain.to_sym
101
+ when :beowulf then DEFAULT_BEOWULF_URL
102
+ else; raise ApiError, "Unsupported chain: #{chain}"
103
+ end
104
+ end
105
+
106
+ def self.default_restful_url(chain)
107
+ case chain.to_sym
108
+ when :beowulf then DEFAULT_RESTFUL_URL
109
+ else; raise ApiError, "Unsupported chain: #{chain}"
110
+ end
111
+ end
112
+
113
+ def self.default_failover_urls(chain)
114
+ case chain.to_sym
115
+ when :beowulf then DEFAULT_BEOWULF_FAILOVER_URLS
116
+ else; raise ApiError, "Unsupported chain: #{chain}"
117
+ end
118
+ end
119
+
120
+ # Create a new instance of Beowulf::Api.
121
+ #
122
+ # Examples:
123
+ #
124
+ # api = Beowulf::Api.new(url: 'https://api.example.com')
125
+ #
126
+ # @param options [::Hash] The attributes to initialize the Beowulf::Api with.
127
+ # @option options [String] :url URL that points at a full node, like `https://testnet-bw.beowulfchain.com/rpc`. Default from DEFAULT_URL.
128
+ # @option options [::Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
129
+ # @option options [Logger] :logger An instance of `Logger` to send debug messages to.
130
+ # @option options [Boolean] :recover_transactions_on_error Have Beowulf try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
131
+ # @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
132
+ # @option options [Integer] :pool_size Maximum number of connections allowed.
133
+ # @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.
134
+ # @option options [Boolean] :persist Enable or disable Persistent HTTP. Using Persistent HTTP keeps the connection alive between API calls. Default: `true`
135
+ def initialize(options = {})
136
+ @user = options[:user]
137
+ @password = options[:password]
138
+ @chain = options[:chain] || :beowulf
139
+ @url = options[:url] || Api::default_url(@chain)
140
+ @restful_url = options[:restful_url] || Api::default_restful_url(@chain)
141
+ @preferred_url = @url.dup
142
+ @failover_urls = options[:failover_urls]
143
+ @debug = !!options[:debug]
144
+ @max_requests = options[:max_requests] || 30
145
+ @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
146
+ @ssl_version = options[:ssl_version]
147
+
148
+ @self_logger = false
149
+ @logger = if options[:logger].nil?
150
+ @self_logger = true
151
+ Beowulf.logger
152
+ else
153
+ options[:logger]
154
+ end
155
+
156
+ @self_hashie_logger = false
157
+ @hashie_logger = if options[:hashie_logger].nil?
158
+ @self_hashie_logger = true
159
+ Logger.new(nil)
160
+ else
161
+ options[:hashie_logger]
162
+ end
163
+
164
+ if @failover_urls.nil?
165
+ @failover_urls = Api::default_failover_urls(@chain) - [@url]
166
+ end
167
+
168
+ @failover_urls = [@failover_urls].flatten.compact
169
+ @preferred_failover_urls = @failover_urls.dup
170
+
171
+ unless @hashie_logger.respond_to? :warn
172
+ @hashie_logger = Logger.new(@hashie_logger)
173
+ end
174
+
175
+ @recover_transactions_on_error = if options.keys.include? :recover_transactions_on_error
176
+ options[:recover_transactions_on_error]
177
+ else
178
+ true
179
+ end
180
+
181
+ @persist_error_count = 0
182
+ @persist = if options.keys.include? :persist
183
+ options[:persist]
184
+ else
185
+ true
186
+ end
187
+
188
+ @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
189
+ options[:reuse_ssl_sessions]
190
+ else
191
+ true
192
+ end
193
+
194
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
195
+ options[:use_condenser_namespace]
196
+ else
197
+ true
198
+ end
199
+
200
+ if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
201
+ @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
202
+ end
203
+
204
+ Hashie.logger = @hashie_logger
205
+ @method_names = nil
206
+ @uri = nil
207
+ @http_id = nil
208
+ @http_memo = {}
209
+ @api_options = options.dup.merge(chain: @chain)
210
+ @api = nil
211
+ @block_api = nil
212
+ @backoff_at = nil
213
+ @jussi_supported = []
214
+ end
215
+
216
+ # Get a specific block or range of blocks.
217
+ #
218
+ # Example:
219
+ #
220
+ # api = Beowulf::Api.new
221
+ # blocks = api.get_blocks(10..20)
222
+ # transactions = blocks.flat_map(&:transactions)
223
+ #
224
+ # ... or ...
225
+ #
226
+ # api = Beowulf::Api.new
227
+ # transactions = []
228
+ # api.get_blocks(10..20) do |block|
229
+ # transactions += block.transactions
230
+ # end
231
+ #
232
+ # @param block_number [Fixnum || ::Array<Fixnum>]
233
+ # @param block the block to execute for each result, optional.
234
+ # @return [::Array]
235
+ def get_blocks(block_number, &block)
236
+ block_number = [*(block_number)].flatten
237
+
238
+ if !!block
239
+ block_number.each do |i|
240
+ if use_condenser_namespace?
241
+ yield api.get_block(i)
242
+ else
243
+ yield block_api.get_block(block_num: i).result, i
244
+ end
245
+ end
246
+ else
247
+ block_number.map do |i|
248
+ if use_condenser_namespace?
249
+ api.get_block(i)
250
+ else
251
+ block_api.get_block(block_num: i).result
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ # Stops the persistent http connections.
258
+ #
259
+ def shutdown
260
+ @uri = nil
261
+ @http_id = nil
262
+ @http_memo.each do |k|
263
+ v = @http_memo.delete(k)
264
+ if defined?(v.shutdown)
265
+ debug "Shutting down instance #{k} (#{v})"
266
+ v.shutdown
267
+ end
268
+ end
269
+ @api.shutdown if !!@api && @api != self
270
+ @api = nil
271
+ @block_api.shutdown if !!@block_api && @block_api != self
272
+ @block_api = nil
273
+
274
+ if @self_logger
275
+ if !!@logger && defined?(@logger.close)
276
+ if defined?(@logger.closed?)
277
+ @logger.close unless @logger.closed?
278
+ end
279
+ end
280
+ end
281
+
282
+ if @self_hashie_logger
283
+ if !!@hashie_logger && defined?(@hashie_logger.close)
284
+ if defined?(@hashie_logger.closed?)
285
+ @hashie_logger.close unless @hashie_logger.closed?
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ # @private
292
+ def method_names
293
+ return @method_names if !!@method_names
294
+ return CondenserApi::METHOD_NAMES if api_name == :condenser_api
295
+
296
+ @method_names = Beowulf::Api.methods(api_name).map do |e|
297
+ e['method'].to_sym
298
+ end
299
+ end
300
+
301
+ # @private
302
+ def api_name
303
+ :condenser_api
304
+ end
305
+
306
+ # @private
307
+ def respond_to_missing?(m, include_private = false)
308
+ method_names.nil? ? false : method_names.include?(m.to_sym)
309
+ end
310
+
311
+ # @private
312
+ def method_missing(m, *args, &block)
313
+ super unless respond_to_missing?(m)
314
+
315
+ current_rpc_id = rpc_id
316
+ method_name = [api_name, m].join('.')
317
+ response = nil
318
+ options = if api_name == :condenser_api
319
+ {
320
+ jsonrpc: "2.0",
321
+ method: method_name,
322
+ params: args,
323
+ id: current_rpc_id,
324
+ }
325
+ else
326
+ rpc_args = if args.empty?
327
+ {}
328
+ else
329
+ args.first
330
+ end
331
+ {
332
+ jsonrpc: "2.0",
333
+ method: method_name,
334
+ params: rpc_args,
335
+ id: current_rpc_id,
336
+ }
337
+ end
338
+
339
+ tries = 0
340
+ timestamp = Time.now.utc
341
+
342
+ loop do
343
+ tries += 1
344
+
345
+ if tries > 5 && flappy? && !check_file_open?
346
+ raise ApiError, 'PANIC: Out of file resources'
347
+ end
348
+
349
+ begin
350
+ if tries > 1 && @recover_transactions_on_error && api_name == :network_broadcast_api
351
+ signatures, exp = extract_signatures(options)
352
+
353
+ if !!signatures && signatures.any?
354
+ offset = [(exp - timestamp).abs, 30].min
355
+
356
+ if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
357
+ response = Hashie::Mash.new(response)
358
+ end
359
+ end
360
+ end
361
+
362
+ if response.nil?
363
+ response = request(options)
364
+
365
+ response = if response.nil?
366
+ error "No response, retrying ...", method_name
367
+ elsif !response.kind_of? Net::HTTPSuccess
368
+ warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
369
+ else
370
+ detect_jussi(response)
371
+
372
+ case response.code
373
+ when '200'
374
+ body = response.body
375
+ response = JSON[body]
376
+
377
+ if response['id'] != options[:id]
378
+ debug_payload(options, body) if ENV['DEBUG'] == 'true'
379
+
380
+ if !!response['id']
381
+ warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
382
+ else
383
+ # The node has broken the jsonrpc spec.
384
+ warning "Node did not provide jsonrpc id (expected: #{options[:id]}, got: nothing), retrying ...", method_name, true
385
+ end
386
+
387
+ if response.keys.include?('error')
388
+ handle_error(response, options, method_name, tries)
389
+ end
390
+ elsif response.keys.include?('error')
391
+ handle_error(response, options, method_name, tries)
392
+ else
393
+ Hashie::Mash.new(response)
394
+ end
395
+ when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
396
+ when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
397
+ when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
398
+ when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
399
+ when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
400
+ else
401
+ warning "Unknown code #{response.code}, retrying ...", method_name, true
402
+ warning response
403
+ end
404
+ end
405
+ end
406
+ rescue Net::HTTP::Persistent::Error => e
407
+ warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
408
+ if e.cause.class == Net::HTTPMethodNotAllowed
409
+ warning 'Node upstream is misconfigured.'
410
+ drop_current_failover_url method_name
411
+ end
412
+
413
+ @persist_error_count += 1
414
+ rescue ConnectionPool::Error => e
415
+ warning "Connection Pool Error (#{e.message}), retrying ...", method_name, true
416
+ rescue Errno::ECONNREFUSED => e
417
+ warning 'Connection refused, retrying ...', method_name, true
418
+ rescue Errno::EADDRNOTAVAIL => e
419
+ warning 'Node not available, retrying ...', method_name, true
420
+ rescue Errno::ECONNRESET => e
421
+ warning "Connection Reset (#{e.message}), retrying ...", method_name, true
422
+ rescue Errno::EBUSY => e
423
+ warning "Resource busy (#{e.message}), retrying ...", method_name, true
424
+ rescue Errno::ENETDOWN => e
425
+ warning "Network down (#{e.message}), retrying ...", method_name, true
426
+ rescue Net::ReadTimeout => e
427
+ warning 'Node read timeout, retrying ...', method_name, true
428
+ rescue Net::OpenTimeout => e
429
+ warning 'Node timeout, retrying ...', method_name, true
430
+ rescue RangeError => e
431
+ warning 'Range Error, retrying ...', method_name, true
432
+ rescue OpenSSL::SSL::SSLError => e
433
+ warning "SSL Error (#{e.message}), retrying ...", method_name, true
434
+ rescue SocketError => e
435
+ warning "Socket Error (#{e.message}), retrying ...", method_name, true
436
+ rescue JSON::ParserError => e
437
+ warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
438
+ drop_current_failover_url method_name if tries > 5
439
+ response = nil
440
+ rescue ApiError => e
441
+ warning "ApiError (#{e.message}), retrying ...", method_name, true
442
+ # rescue => e
443
+ # warning "Unknown exception from request, retrying ...", method_name, true
444
+ # warning e
445
+ end
446
+
447
+ if !!response
448
+ @persist_error_count = 0
449
+
450
+ if !!block
451
+ if api_name == :condenser_api
452
+ return yield(response.result, response.error, response.id)
453
+ else
454
+ if defined?(response.result.size) && response.result.size == 0
455
+ return yield(nil, response.error, response.id)
456
+ elsif (defined?(response.result.size) && response.result.size == 1 && defined?(response.result.values))
457
+ return yield(response.result.values.first, response.error, response.id)
458
+ else
459
+ return yield(response.result, response.error, response.id)
460
+ end
461
+ end
462
+ else
463
+ return response
464
+ end
465
+ end
466
+
467
+ backoff
468
+ end # loop
469
+ end
470
+
471
+ def inspect
472
+ properties = %w(
473
+ chain url backoff_at max_requests ssl_verify_mode ssl_version persist
474
+ recover_transactions_on_error reuse_ssl_sessions pool_size
475
+ use_condenser_namespace
476
+ ).map do |prop|
477
+ if !!(v = instance_variable_get("@#{prop}"))
478
+ "@#{prop}=#{v}"
479
+ end
480
+ end.compact.join(', ')
481
+
482
+ "#<#{self.class.name} [#{properties}]>"
483
+ end
484
+
485
+ def stopped?
486
+ http_active = if @http_memo.nil?
487
+ false
488
+ else
489
+ @http_memo.values.map do |http|
490
+ if defined?(http.active?)
491
+ http.active?
492
+ else
493
+ false
494
+ end
495
+ end.include?(true)
496
+ end
497
+
498
+ @uri.nil? && @http_id.nil? && !http_active && @api.nil? && @block_api.nil?
499
+ end
500
+
501
+ def use_condenser_namespace?
502
+ @use_condenser_namespace
503
+ end
504
+ private
505
+ def self.methods_json_path
506
+ @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
507
+ end
508
+
509
+ def self.methods(api_name)
510
+ @methods ||= {}
511
+ @methods[api_name] ||= JSON[File.read methods_json_path].map do |e|
512
+ e if e['api'].to_sym == api_name
513
+ end.compact.freeze
514
+ end
515
+
516
+ def self.apply_http_defaults(http, ssl_verify_mode)
517
+ http.read_timeout = 10
518
+ http.open_timeout = 10
519
+ http.verify_mode = ssl_verify_mode
520
+ http.ssl_timeout = 30 if defined? http.ssl_timeout
521
+ http
522
+ end
523
+
524
+ def api_options
525
+ @api_options.merge(failover_urls: @failover_urls, logger: @logger, hashie_logger: @hashie_logger)
526
+ end
527
+
528
+ def api
529
+ @api ||= self.class == Api ? self : Api.new(api_options)
530
+ end
531
+
532
+ def block_api
533
+ @block_api ||= self.class == BlockApi ? self : BlockApi.new(api_options)
534
+ end
535
+
536
+ def rpc_id
537
+ @rpc_id ||= 0
538
+ @rpc_id = @rpc_id + 1
539
+ end
540
+
541
+ def uri
542
+ @uri ||= URI.parse(@url)
543
+ end
544
+
545
+ def http_id
546
+ @http_id ||= "beowulf-#{Beowulf::VERSION}-#{api_name}-#{SecureRandom.uuid}"
547
+ end
548
+
549
+ def http
550
+ return @http_memo[http_id] if @http_memo.keys.include? http_id
551
+
552
+ @http_memo[http_id] = if @persist && @persist_error_count < 10
553
+ idempotent = api_name != :network_broadcast_api
554
+
555
+ http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
556
+ Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
557
+ else
558
+ # net-http-persistent < 3.0
559
+ Net::HTTP::Persistent.new(http_id)
560
+ end
561
+
562
+ http.keep_alive = 30
563
+ http.idle_timeout = idempotent ? 10 : nil
564
+ http.max_requests = @max_requests
565
+ http.retry_change_requests = idempotent
566
+ http.reuse_ssl_sessions = @reuse_ssl_sessions
567
+
568
+ http
569
+ else
570
+ http = Net::HTTP.new(uri.host, uri.port)
571
+ http.use_ssl = uri.scheme == 'https'
572
+ http
573
+ end
574
+
575
+ Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
576
+ end
577
+
578
+ def post_request
579
+ Net::HTTP::Post.new uri.request_uri, POST_HEADERS
580
+ end
581
+
582
+ def request(options)
583
+ request = post_request
584
+ request.body = JSON[options]
585
+
586
+ case http
587
+ when Net::HTTP::Persistent then http.request(uri, request)
588
+ when Net::HTTP then http.request(request)
589
+ else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
590
+ end
591
+ end
592
+
593
+ def jussi_supported?(url = @url)
594
+ @jussi_supported.include? url
595
+ end
596
+
597
+ def detect_jussi(response)
598
+ return if jussi_supported?(@url)
599
+
600
+ jussi_response_id = response['x-jussi-response-id']
601
+
602
+ if !!jussi_response_id
603
+ debug "Found a node that supports jussi: #{@url}"
604
+ @jussi_supported << @url
605
+ end
606
+ end
607
+
608
+ def recover_transaction(signatures, expected_rpc_id, after)
609
+ debug "Looking for signatures: #{signatures.map{|s| s[0..5]}} since: #{after}"
610
+
611
+ count = 0
612
+ start = Time.now.utc
613
+ block_range = api.get_dynamic_global_properties do |properties|
614
+ high = properties.head_block_number
615
+ low = high - 100
616
+ [*(low..(high))].reverse
617
+ end
618
+
619
+ # At most, we read 100 blocks
620
+ # but we also give up once the block time is before the `after` argument.
621
+ api.get_blocks(block_range) do |block, block_num|
622
+ unless defined? block.transaction_ids
623
+ error "Blockchain does not provide transaction ids in blocks, giving up."
624
+ return nil
625
+ end
626
+
627
+ count += 1
628
+ raise ApiError, "Race condition detected on remote node at: #{block_num}" if block.nil?
629
+
630
+ # In the future, it would be better to decode the operation and signature
631
+ # into the transaction id.
632
+ unless defined? block.transaction_ids
633
+ @recover_transactions_on_error = false
634
+ return
635
+ end
636
+
637
+ timestamp = Time.parse(block.timestamp + 'Z')
638
+ break if timestamp < after
639
+
640
+ block.transactions.each_with_index do |tx, index|
641
+ next unless ((tx['signatures'] || []) & signatures).any?
642
+
643
+ debug "Found transaction #{count} block(s) ago; took #{(Time.now.utc - start)} seconds to scan."
644
+
645
+ return {
646
+ id: expected_rpc_id,
647
+ recovered_by: http_id,
648
+ result: {
649
+ id: block.transaction_ids[index],
650
+ block_num: block_num,
651
+ trx_num: index,
652
+ expired: false
653
+ }
654
+ }
655
+ end
656
+ end
657
+
658
+ debug "Could not find transaction in #{count} block(s); took #{(Time.now.utc - start)} seconds to scan."
659
+
660
+ return nil
661
+ end
662
+
663
+ def reset_failover
664
+ @url = @preferred_url.dup
665
+ @failover_urls = @preferred_failover_urls.dup
666
+ warning "Failover reset, going back to #{@url} ..."
667
+ end
668
+
669
+ def pop_failover_url
670
+ reset_failover if @failover_urls.none?
671
+
672
+ until @failover_urls.none? || healthy?(url = @failover_urls.sample)
673
+ @failover_urls.delete(url)
674
+ end
675
+
676
+ url || (uri || @url).to_s
677
+ end
678
+
679
+ def bump_failover
680
+ @uri = nil
681
+ @url = pop_failover_url
682
+ warning "Failing over to #{@url} ..."
683
+ end
684
+
685
+ def flappy?
686
+ !!@backoff_at && Time.now.utc - @backoff_at < 300
687
+ end
688
+
689
+ # Note, this methods only removes the uri.to_s if present but it does not
690
+ # call bump_failover, in order to avoid a race condition.
691
+ def drop_current_failover_url(prefix)
692
+ if @preferred_failover_urls.size == 1
693
+ warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
694
+ else
695
+ warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
696
+ @preferred_failover_urls.delete(uri.to_s)
697
+ @failover_urls.delete(uri.to_s)
698
+ end
699
+ end
700
+
701
+ def handle_error(response, request_options, method_name, tries)
702
+ parser = ErrorParser.new(response)
703
+ _signatures, exp = extract_signatures(request_options)
704
+
705
+ if (!!exp && exp < Time.now.utc) || (tries > 2 && !parser.node_degraded?)
706
+ # Whatever the error was, it is already expired or tried too much. No need to try to recover.
707
+ debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
708
+ elsif parser.can_retry?
709
+ drop_current_failover_url method_name if !!exp && parser.expiry?
710
+ drop_current_failover_url method_name if parser.node_degraded?
711
+ debug "Error code #{parser} (attempt: #{tries}), retrying ..."
712
+ return nil
713
+ end
714
+
715
+ if !!parser.trx_id
716
+ # Turns out, the ErrorParser found a transaction id. It might come in
717
+ # handy, so let's append this to the result along with the error.
718
+
719
+ response[:result] = {
720
+ id: parser.trx_id,
721
+ block_num: -1,
722
+ trx_num: -1,
723
+ expired: false
724
+ }
725
+
726
+ if @recover_transactions_on_error
727
+ begin
728
+ if !!@restful_url
729
+ JSON[open("#{@restful_url}/account_history_api/get_transaction?id=#{parser.trx_id}").read].tap do |tx|
730
+ response[:result][:block_num] = tx['block_num']
731
+ response[:result][:trx_num] = tx['transaction_num']
732
+ end
733
+ else
734
+ # Node operators often disable this operation.
735
+ api.get_transaction(parser.trx_id) do |tx|
736
+ if !!tx
737
+ response[:result][:block_num] = tx.block_num
738
+ response[:result][:trx_num] = tx.transaction_num
739
+ end
740
+ end
741
+ end
742
+
743
+ response[:recovered_by] = http_id
744
+ response.delete('error') # no need for this, now
745
+ rescue
746
+ debug "Couldn't find block for trx_id: #{parser.trx_id}, giving up."
747
+ end
748
+ end
749
+ end
750
+
751
+ Hashie::Mash.new(response)
752
+ end
753
+
754
+ def healthy?(url)
755
+ begin
756
+ # Note, not all nodes support the /health uri. But even if they don't,
757
+ # they'll respond status code 200 OK, even if the body shows an error.
758
+ #
759
+ # But if the node supports the /health uri, it will do additional
760
+ # verifications on the block height.
761
+ #
762
+ # Also note, this check is done **without** net-http-persistent.
763
+
764
+ response = open(url + HEALTH_URI)
765
+ response = JSON[response.read]
766
+
767
+ if !!response['error']
768
+ if !!response['error']['data']
769
+ if !!response['error']['data']['message']
770
+ error "#{url} error: #{response['error']['data']['message']}"
771
+ end
772
+ elsif !!response['error']['message']
773
+ error "#{url} error: #{response['error']['message']}"
774
+ else
775
+ error "#{url} error: #{response['error']}"
776
+ end
777
+
778
+ false
779
+ elsif response['status'] == 'OK'
780
+ true
781
+ else
782
+ error "#{url} status: #{response['status']}"
783
+
784
+ false
785
+ end
786
+ rescue JSON::ParserError
787
+ # No JSON, but also no HTTP error code, so we're OK.
788
+
789
+ true
790
+ rescue => e
791
+ error "Health check failure for #{url}: #{e.inspect}"
792
+ sleep 0.2
793
+ false
794
+ end
795
+ end
796
+
797
+ def check_file_open?
798
+ File.exists?('.')
799
+ rescue
800
+ false
801
+ end
802
+
803
+ def debug_payload(request, response)
804
+ request = JSON.pretty_generate(request)
805
+ response = JSON.parse(response) rescue response
806
+ response = JSON.pretty_generate(response) rescue response
807
+
808
+ puts '=' * 80
809
+ puts "Request:"
810
+ puts request
811
+ puts '=' * 80
812
+ puts "Response:"
813
+ puts response
814
+ puts '=' * 80
815
+ end
816
+
817
+ def backoff
818
+ shutdown
819
+ bump_failover if flappy? || !healthy?(uri)
820
+ @backoff_at ||= Time.now.utc
821
+ @backoff_sleep ||= 0.01
822
+
823
+ @backoff_sleep *= 2
824
+ GC.start
825
+ sleep @backoff_sleep
826
+ ensure
827
+ if !!@backoff_at && Time.now.utc - @backoff_at > 300
828
+ @backoff_at = nil
829
+ @backoff_sleep = nil
830
+ end
831
+ end
832
+
833
+ def self.finalize(logger, hashie_logger)
834
+ proc {
835
+ if !!logger && defined?(logger.close) && !logger.closed?
836
+ logger.close
837
+ end
838
+
839
+ if !!hashie_logger && defined?(hashie_logger.close) && !hashie_logger.closed?
840
+ hashie_logger.close
841
+ end
842
+ }
843
+ end
844
+ end
845
+ end