voilkruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +77 -0
  4. data/LICENSE +41 -0
  5. data/README.md +519 -0
  6. data/Rakefile +140 -0
  7. data/gource.sh +8 -0
  8. data/images/Anthony Martin.png +0 -0
  9. data/images/Marvin Hofmann.jpg +0 -0
  10. data/images/Marvin Hofmann.png +0 -0
  11. data/lib/voilk.rb +17 -0
  12. data/lib/voilkruby.rb +45 -0
  13. data/lib/voilkruby/account_by_key_api.rb +7 -0
  14. data/lib/voilkruby/account_history_api.rb +15 -0
  15. data/lib/voilkruby/api.rb +772 -0
  16. data/lib/voilkruby/base_error.rb +23 -0
  17. data/lib/voilkruby/block_api.rb +14 -0
  18. data/lib/voilkruby/broadcast_operations.json +497 -0
  19. data/lib/voilkruby/chain.rb +299 -0
  20. data/lib/voilkruby/chain_config.rb +22 -0
  21. data/lib/voilkruby/chain_stats_api.rb +15 -0
  22. data/lib/voilkruby/condenser_api.rb +99 -0
  23. data/lib/voilkruby/database_api.rb +5 -0
  24. data/lib/voilkruby/error_parser.rb +228 -0
  25. data/lib/voilkruby/follow_api.rb +7 -0
  26. data/lib/voilkruby/logger.rb +20 -0
  27. data/lib/voilkruby/market_history_api.rb +19 -0
  28. data/lib/voilkruby/methods.json +495 -0
  29. data/lib/voilkruby/mixins/acts_as_poster.rb +124 -0
  30. data/lib/voilkruby/mixins/acts_as_voter.rb +50 -0
  31. data/lib/voilkruby/mixins/acts_as_wallet.rb +67 -0
  32. data/lib/voilkruby/network_broadcast_api.rb +7 -0
  33. data/lib/voilkruby/operation.rb +101 -0
  34. data/lib/voilkruby/operation_ids.rb +98 -0
  35. data/lib/voilkruby/operation_types.rb +139 -0
  36. data/lib/voilkruby/stream.rb +527 -0
  37. data/lib/voilkruby/tag_api.rb +33 -0
  38. data/lib/voilkruby/transaction.rb +306 -0
  39. data/lib/voilkruby/type/amount.rb +57 -0
  40. data/lib/voilkruby/type/array.rb +17 -0
  41. data/lib/voilkruby/type/beneficiaries.rb +29 -0
  42. data/lib/voilkruby/type/future.rb +18 -0
  43. data/lib/voilkruby/type/hash.rb +17 -0
  44. data/lib/voilkruby/type/permission.rb +19 -0
  45. data/lib/voilkruby/type/point_in_time.rb +19 -0
  46. data/lib/voilkruby/type/price.rb +25 -0
  47. data/lib/voilkruby/type/public_key.rb +18 -0
  48. data/lib/voilkruby/type/serializer.rb +12 -0
  49. data/lib/voilkruby/type/u_int16.rb +19 -0
  50. data/lib/voilkruby/type/u_int32.rb +19 -0
  51. data/lib/voilkruby/utils.rb +170 -0
  52. data/lib/voilkruby/version.rb +4 -0
  53. data/voilkruby.gemspec +40 -0
  54. metadata +412 -0
@@ -0,0 +1,140 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'yard'
4
+ require 'voilkruby'
5
+ require 'awesome_print'
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/*_test.rb']
11
+ t.ruby_opts << if ENV['HELL_ENABLED']
12
+ '-W2'
13
+ else
14
+ '-W1'
15
+ end
16
+ end
17
+
18
+ YARD::Rake::YardocTask.new do |t|
19
+ t.files = ['lib/**/*.rb']
20
+ end
21
+
22
+ task default: :test
23
+
24
+ namespace :clean do
25
+ desc 'Deletes test/fixtures/vcr_cassettes/*.yml so they can be rebuilt fresh.'
26
+ task :vcr do |t|
27
+ exec 'rm -v test/fixtures/vcr_cassettes/*.yml'
28
+ end
29
+ end
30
+
31
+ desc 'Tests the ability to broadcast live data. This task broadcasts a claim_reward_balance of 0.0000001 COINS.'
32
+ task :test_live_broadcast, [:account, :wif, :chain] do |t, args|
33
+ account_name = args[:account] || 'social'
34
+ posting_wif = args[:wif] || '5JrvPrQeBBvCRdjv29iDvkwn3EQYZ9jqfAHzrCyUvfbEbRkrYFC'
35
+ chain = args[:chain] || 'voilk'
36
+ # url = 'https://testnet.voilkdev.com/' # use testnet
37
+ url = nil # use default
38
+ options = {chain: chain, wif: posting_wif, url: url}
39
+ tx = VoilkRuby::Transaction.new(options)
40
+ tx.operations << {
41
+ type: :claim_reward_balance,
42
+ account: account_name,
43
+ reward_voilk: '0.000 VOILK',
44
+ reward_vsd: '0.000 VSD',
45
+ reward_coins: '0.000001 COINS'
46
+ }
47
+
48
+ response = tx.process(true)
49
+ ap response
50
+
51
+ if !!response.result
52
+ result = response.result
53
+
54
+ puts "https://explorer.voilk.com/b/#{result[:block_num]}" if !!result[:block_num]
55
+ puts "https://explorer.voilk.com/tx/#{result[:id]}" if !!result[:id]
56
+ end
57
+ end
58
+
59
+ desc 'Tests the ability to stream live data. defaults: chain = voilk; persist = true.'
60
+ task :test_live_stream, [:chain, :persist] do |t, args|
61
+ chain = args[:chain] || 'voilk'
62
+ persist = (args[:persist] || 'true') == 'true'
63
+ last_block_number = 0
64
+ # url = 'https://testnet.voilkdev.com/'
65
+ url = nil # use default
66
+ options = {chain: chain, persist: persist, url: url}
67
+ total_ops = 0.0
68
+ total_vops = 0.0
69
+ elapsed = 0
70
+ count = 0
71
+
72
+ VoilkRuby::Stream.new(options).blocks do |b, n, api|
73
+ start = Time.now.utc
74
+
75
+ if last_block_number == 0
76
+ # skip test
77
+ elsif last_block_number + 1 == n
78
+ t = b.transactions
79
+ t_size = t.size
80
+ o = t.map(&:operations)
81
+ op_size = o.map(&:size).reduce(0, :+)
82
+ total_ops += op_size
83
+
84
+ api.get_ops_in_block(n, true) do |vops, error|
85
+ if !!error
86
+ puts "Error on get_ops_in_block for block #{n}"
87
+ ap error if defined? ap
88
+ end
89
+
90
+ puts "Problem: vops is nil!" if vops.nil?
91
+
92
+ # Did we reach this point with an unhandled error that wasn't retried?
93
+ # If so, vops might be nil and we might need this error to get handled
94
+ # instead of checking for vops.nil?.
95
+
96
+ vop_size = vops.size
97
+ total_vops += vop_size
98
+
99
+ vop_ratio = if total_vops > 0
100
+ total_vops / total_ops
101
+ else
102
+ 0
103
+ end
104
+
105
+ elapsed += Time.now.utc - start
106
+ count += 1
107
+ 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)"
108
+ end
109
+ else
110
+ # This should not happen. If it does, there's likely a bug in VoilkRuby.
111
+
112
+ puts "Error, last block number was #{last_block_number}, did not expect #{n}."
113
+ end
114
+
115
+ last_block_number = n
116
+ end
117
+ end
118
+
119
+ desc 'Ruby console with voilkruby already required.'
120
+ task :console do
121
+ exec "irb -r voilkruby -I ./lib"
122
+ end
123
+
124
+ desc 'Build a new version of the voilkruby gem.'
125
+ task :build do
126
+ exec 'gem build voilkruby.gemspec'
127
+ end
128
+
129
+ desc 'Publish the current version of the voilkruby gem.'
130
+ task :push do
131
+ exec "gem push voilkruby-#{VoilkRuby::VERSION}.gem"
132
+ end
133
+
134
+ # We're not going to yank on a regular basis, but this is how it's done if you
135
+ # really want a task for that for some reason.
136
+
137
+ # desc 'Yank the current version of the voilkruby gem.'
138
+ # task :yank do
139
+ # exec "gem yank voilkruby -v #{VoilkRuby::VERSION}"
140
+ # end
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ gource ./ --user-image-dir images --hide usernames -s 0.5 -b 000000 \
4
+ --start-date '2017-05-27 17:23' \
5
+ -1280x720 --output-ppm-stream - |\
6
+ ffmpeg -y -r 28 -f image2pipe -vcodec ppm -i - -vcodec libx264 -preset slow \
7
+ -crf 28 -threads 0 output.mp4
8
+
Binary file
Binary file
Binary file
@@ -0,0 +1,17 @@
1
+ # Voilk chain client for broadcasting common operations.
2
+ #
3
+ # @see VoilkRuby::Chain
4
+ # @deprecated Using Voilk class provided by VoilkRuby is deprecated. Please use: VoilkRuby::Chain.new(chain: :voilk)
5
+ class Voilk < VoilkRuby::Chain
6
+ def initialize(options = {})
7
+ unless defined? @@deprecated_warning_shown
8
+ warn "[DEPRECATED] Using Voilk class provided by VoilkRuby is deprecated. Please use: VoilkRuby::Chain.new(chain: :voilk)"
9
+ @@deprecated_warning_shown = true
10
+ end
11
+
12
+ super(options.merge(chain: :voilk))
13
+ end
14
+
15
+ alias voilk_per_mcoin base_per_mcoin
16
+ alias voilk_per_usd base_per_debt
17
+ end
@@ -0,0 +1,45 @@
1
+ require 'voilkruby/version'
2
+ require 'json'
3
+ require 'awesome_print' if ENV['USE_AWESOME_PRINT'] == 'true'
4
+
5
+ module VoilkRuby
6
+ require 'voilkruby/utils'
7
+ require 'voilkruby/type/serializer'
8
+ require 'voilkruby/type/amount'
9
+ require 'voilkruby/type/u_int16'
10
+ require 'voilkruby/type/u_int32'
11
+ require 'voilkruby/type/point_in_time'
12
+ require 'voilkruby/type/permission'
13
+ require 'voilkruby/type/public_key'
14
+ require 'voilkruby/type/beneficiaries'
15
+ require 'voilkruby/type/price'
16
+ require 'voilkruby/type/array'
17
+ require 'voilkruby/type/hash'
18
+ require 'voilkruby/type/future'
19
+ require 'voilkruby/logger'
20
+ require 'voilkruby/chain_config'
21
+ require 'voilkruby/api'
22
+ require 'voilkruby/database_api'
23
+ require 'voilkruby/follow_api'
24
+ require 'voilkruby/tag_api'
25
+ require 'voilkruby/market_history_api'
26
+ require 'voilkruby/network_broadcast_api'
27
+ require 'voilkruby/chain_stats_api'
28
+ require 'voilkruby/account_by_key_api'
29
+ require 'voilkruby/account_history_api'
30
+ require 'voilkruby/condenser_api'
31
+ require 'voilkruby/block_api'
32
+ require 'voilkruby/stream'
33
+ require 'voilkruby/operation_ids'
34
+ require 'voilkruby/operation_types'
35
+ require 'voilkruby/operation'
36
+ require 'voilkruby/transaction'
37
+ require 'voilkruby/base_error'
38
+ require 'voilkruby/error_parser'
39
+ require 'voilkruby/mixins/acts_as_poster'
40
+ require 'voilkruby/mixins/acts_as_voter'
41
+ require 'voilkruby/mixins/acts_as_wallet'
42
+ require 'voilkruby/chain'
43
+ require 'voilk'
44
+ extend self
45
+ end
@@ -0,0 +1,7 @@
1
+ module VoilkRuby
2
+ class AccountByKeyApi < Api
3
+ def api_name
4
+ :account_by_key_api
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module VoilkRuby
2
+ class AccountHistoryApi < Api
3
+ def method_names
4
+ @method_names ||= [
5
+ :get_account_history,
6
+ :get_ops_in_block,
7
+ :get_transaction
8
+ ].freeze
9
+ end
10
+
11
+ def api_name
12
+ :account_history_api
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,772 @@
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 VoilkRuby
10
+ class Api
11
+ include Utils
12
+
13
+ DEFAULT_VOILK_URL = 'https://api.voilk.com'
14
+
15
+ DEFAULT_VOILK_FAILOVER_URLS = [
16
+ DEFAULT_VOILK_URL,
17
+ 'https://api.voilk.com',
18
+ ]
19
+
20
+ # @private
21
+ POST_HEADERS = {
22
+ 'Content-Type' => 'application/json',
23
+ 'User-Agent' => VoilkRuby::AGENT_ID
24
+ }
25
+
26
+ # @private
27
+ HEALTH_URI = '/health'
28
+
29
+ def self.default_url(chain)
30
+ case chain.to_sym
31
+ when :voilk then DEFAULT_VOILK_URL
32
+ else; raise ApiError, "Unsupported chain: #{chain}"
33
+ end
34
+ end
35
+
36
+ def self.default_failover_urls(chain)
37
+ case chain.to_sym
38
+ when :voilk then DEFAULT_VOILK_FAILOVER_URLS
39
+ else; raise ApiError, "Unsupported chain: #{chain}"
40
+ end
41
+ end
42
+
43
+ # Cretes a new instance of VoilkRuby::Api.
44
+ #
45
+ # Examples:
46
+ #
47
+ # api = VoilkRuby::Api.new(url: 'https://api.example.com')
48
+ #
49
+ # @param options [::Hash] The attributes to initialize the VoilkRuby::Api with.
50
+ # @option options [String] :url URL that points at a full node, like `https://api.voilk.com`. Default from DEFAULT_URL.
51
+ # @option options [::Array<String>] :failover_urls An array that contains one or more full nodes to fall back on. Default from DEFAULT_FAILOVER_URLS.
52
+ # @option options [Logger] :logger An instance of `Logger` to send debug messages to.
53
+ # @option options [Boolean] :recover_transactions_on_error Have VoilkRuby try to recover transactions that are accepted but could not be confirmed due to an error like network timeout. Default: `true`
54
+ # @option options [Integer] :max_requests Maximum number of requests on a connection before it is considered expired and automatically closed.
55
+ # @option options [Integer] :pool_size Maximum number of connections allowed.
56
+ # @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.
57
+ # @option options [Boolean] :persist Enable or disable Persistent HTTP. Using Persistent HTTP keeps the connection alive between API calls. Default: `true`
58
+ def initialize(options = {})
59
+ @user = options[:user]
60
+ @password = options[:password]
61
+ @chain = options[:chain] || :voilk
62
+ @url = options[:url] || Api::default_url(@chain)
63
+ @preferred_url = @url.dup
64
+ @failover_urls = options[:failover_urls]
65
+ @debug = !!options[:debug]
66
+ @max_requests = options[:max_requests] || 30
67
+ @ssl_verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
68
+ @ssl_version = options[:ssl_version]
69
+
70
+ @self_logger = false
71
+ @logger = if options[:logger].nil?
72
+ @self_logger = true
73
+ VoilkRuby.logger
74
+ else
75
+ options[:logger]
76
+ end
77
+
78
+ @self_hashie_logger = false
79
+ @hashie_logger = if options[:hashie_logger].nil?
80
+ @self_hashie_logger = true
81
+ Logger.new(nil)
82
+ else
83
+ options[:hashie_logger]
84
+ end
85
+
86
+ if @failover_urls.nil?
87
+ @failover_urls = Api::default_failover_urls(@chain) - [@url]
88
+ end
89
+
90
+ @failover_urls = [@failover_urls].flatten.compact
91
+ @preferred_failover_urls = @failover_urls.dup
92
+
93
+ unless @hashie_logger.respond_to? :warn
94
+ @hashie_logger = Logger.new(@hashie_logger)
95
+ end
96
+
97
+ @recover_transactions_on_error = if options.keys.include? :recover_transactions_on_error
98
+ options[:recover_transactions_on_error]
99
+ else
100
+ true
101
+ end
102
+
103
+ @persist_error_count = 0
104
+ @persist = if options.keys.include? :persist
105
+ options[:persist]
106
+ else
107
+ true
108
+ end
109
+
110
+ @reuse_ssl_sessions = if options.keys.include? :reuse_ssl_sessions
111
+ options[:reuse_ssl_sessions]
112
+ else
113
+ true
114
+ end
115
+
116
+ @use_condenser_namespace = if options.keys.include? :use_condenser_namespace
117
+ options[:use_condenser_namespace]
118
+ else
119
+ true
120
+ end
121
+
122
+ if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
123
+ @pool_size = options[:pool_size] || Net::HTTP::Persistent::DEFAULT_POOL_SIZE
124
+ end
125
+
126
+ Hashie.logger = @hashie_logger
127
+ @method_names = nil
128
+ @uri = nil
129
+ @http_id = nil
130
+ @http_memo = {}
131
+ @api_options = options.dup.merge(chain: @chain)
132
+ @api = nil
133
+ @block_api = nil
134
+ @backoff_at = nil
135
+ @jussi_supported = []
136
+ end
137
+
138
+ # Get a specific block or range of blocks.
139
+ #
140
+ # Example:
141
+ #
142
+ # api = VoilkRuby::Api.new
143
+ # blocks = api.get_blocks(10..20)
144
+ # transactions = blocks.flat_map(&:transactions)
145
+ #
146
+ # ... or ...
147
+ #
148
+ # api = VoilkRuby::Api.new
149
+ # transactions = []
150
+ # api.get_blocks(10..20) do |block|
151
+ # transactions += block.transactions
152
+ # end
153
+ #
154
+ # @param block_number [Fixnum || ::Array<Fixnum>]
155
+ # @param block the block to execute for each result, optional.
156
+ # @return [::Array]
157
+ def get_blocks(block_number, &block)
158
+ block_number = [*(block_number)].flatten
159
+
160
+ if !!block
161
+ block_number.each do |i|
162
+ if use_condenser_namespace?
163
+ yield api.get_block(i)
164
+ else
165
+ yield block_api.get_block(block_num: i).result, i
166
+ end
167
+ end
168
+ else
169
+ block_number.map do |i|
170
+ if use_condenser_namespace?
171
+ api.get_block(i)
172
+ else
173
+ block_api.get_block(block_num: i).result
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ # Stops the persistant http connections.
180
+ #
181
+ def shutdown
182
+ @uri = nil
183
+ @http_id = nil
184
+ @http_memo.each do |k|
185
+ v = @http_memo.delete(k)
186
+ if defined?(v.shutdown)
187
+ debug "Shutting down instance #{k} (#{v})"
188
+ v.shutdown
189
+ end
190
+ end
191
+ @api.shutdown if !!@api && @api != self
192
+ @api = nil
193
+ @block_api.shutdown if !!@block_api && @block_api != self
194
+ @block_api = nil
195
+
196
+ if @self_logger
197
+ if !!@logger && defined?(@logger.close)
198
+ if defined?(@logger.closed?)
199
+ @logger.close unless @logger.closed?
200
+ end
201
+ end
202
+ end
203
+
204
+ if @self_hashie_logger
205
+ if !!@hashie_logger && defined?(@hashie_logger.close)
206
+ if defined?(@hashie_logger.closed?)
207
+ @hashie_logger.close unless @hashie_logger.closed?
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ # @private
214
+ def method_names
215
+ return @method_names if !!@method_names
216
+ return CondenserApi::METHOD_NAMES if api_name == :condenser_api
217
+
218
+ @method_names = VoilkRuby::Api.methods(api_name).map do |e|
219
+ e['method'].to_sym
220
+ end
221
+ end
222
+
223
+ # @private
224
+ def api_name
225
+ :condenser_api
226
+ end
227
+
228
+ # @private
229
+ def respond_to_missing?(m, include_private = false)
230
+ method_names.nil? ? false : method_names.include?(m.to_sym)
231
+ end
232
+
233
+ # @private
234
+ def method_missing(m, *args, &block)
235
+ super unless respond_to_missing?(m)
236
+
237
+ current_rpc_id = rpc_id
238
+ method_name = [api_name, m].join('.')
239
+ response = nil
240
+ options = if api_name == :condenser_api
241
+ {
242
+ jsonrpc: "2.0",
243
+ method: method_name,
244
+ params: args,
245
+ id: current_rpc_id,
246
+ }
247
+ else
248
+ rpc_args = if args.empty?
249
+ {}
250
+ else
251
+ args.first
252
+ end
253
+
254
+ {
255
+ jsonrpc: "2.0",
256
+ method: method_name,
257
+ params: rpc_args,
258
+ id: current_rpc_id,
259
+ }
260
+ end
261
+
262
+ tries = 0
263
+ timestamp = Time.now.utc
264
+
265
+ loop do
266
+ tries += 1
267
+
268
+ if tries > 5 && flappy? && !check_file_open?
269
+ raise ApiError, 'PANIC: Out of file resources'
270
+ end
271
+
272
+ begin
273
+ if tries > 1 && @recover_transactions_on_error && api_name == :network_broadcast_api
274
+ signatures, exp = extract_signatures(options)
275
+
276
+ if !!signatures && signatures.any?
277
+ offset = [(exp - timestamp).abs, 30].min
278
+
279
+ if !!(response = recover_transaction(signatures, current_rpc_id, timestamp - offset))
280
+ response = Hashie::Mash.new(response)
281
+ end
282
+ end
283
+ end
284
+
285
+ if response.nil?
286
+ response = request(options)
287
+
288
+ response = if response.nil?
289
+ error "No response, retrying ...", method_name
290
+ elsif !response.kind_of? Net::HTTPSuccess
291
+ warning "Unexpected response (code: #{response.code}): #{response.inspect}, retrying ...", method_name, true
292
+ else
293
+ detect_jussi(response)
294
+
295
+ case response.code
296
+ when '200'
297
+ body = response.body
298
+ response = JSON[body]
299
+
300
+ if response['id'] != options[:id]
301
+ debug_payload(options, body) if ENV['DEBUG'] == 'true'
302
+
303
+ if !!response['id']
304
+ warning "Unexpected rpc_id (expected: #{options[:id]}, got: #{response['id']}), retrying ...", method_name, true
305
+ else
306
+ # The node has broken the jsonrpc spec.
307
+ warning "Node did not provide jsonrpc id (expected: #{options[:id]}, got: nothing), retrying ...", method_name, true
308
+ end
309
+
310
+ if response.keys.include?('error')
311
+ handle_error(response, options, method_name, tries)
312
+ end
313
+ elsif response.keys.include?('error')
314
+ handle_error(response, options, method_name, tries)
315
+ else
316
+ Hashie::Mash.new(response)
317
+ end
318
+ when '400' then warning 'Code 400: Bad Request, retrying ...', method_name, true
319
+ when '429' then warning 'Code 429: Too Many Requests, retrying ...', method_name, true
320
+ when '502' then warning 'Code 502: Bad Gateway, retrying ...', method_name, true
321
+ when '503' then warning 'Code 503: Service Unavailable, retrying ...', method_name, true
322
+ when '504' then warning 'Code 504: Gateway Timeout, retrying ...', method_name, true
323
+ else
324
+ warning "Unknown code #{response.code}, retrying ...", method_name, true
325
+ warning response
326
+ end
327
+ end
328
+ end
329
+ rescue Net::HTTP::Persistent::Error => e
330
+ warning "Unable to perform request: #{e} :: #{!!e.cause ? "cause: #{e.cause.message}" : ''}, retrying ...", method_name, true
331
+ if e.cause.class == Net::HTTPMethodNotAllowed
332
+ warning 'Node upstream is misconfigured.'
333
+ drop_current_failover_url method_name
334
+ end
335
+
336
+ @persist_error_count += 1
337
+ rescue ConnectionPool::Error => e
338
+ warning "Connection Pool Error (#{e.message}), retrying ...", method_name, true
339
+ rescue Errno::ECONNREFUSED => e
340
+ warning 'Connection refused, retrying ...', method_name, true
341
+ rescue Errno::EADDRNOTAVAIL => e
342
+ warning 'Node not available, retrying ...', method_name, true
343
+ rescue Errno::ECONNRESET => e
344
+ warning "Connection Reset (#{e.message}), retrying ...", method_name, true
345
+ rescue Errno::EBUSY => e
346
+ warning "Resource busy (#{e.message}), retrying ...", method_name, true
347
+ rescue Errno::ENETDOWN => e
348
+ warning "Network down (#{e.message}), retrying ...", method_name, true
349
+ rescue Net::ReadTimeout => e
350
+ warning 'Node read timeout, retrying ...', method_name, true
351
+ rescue Net::OpenTimeout => e
352
+ warning 'Node timeout, retrying ...', method_name, true
353
+ rescue RangeError => e
354
+ warning 'Range Error, retrying ...', method_name, true
355
+ rescue OpenSSL::SSL::SSLError => e
356
+ warning "SSL Error (#{e.message}), retrying ...", method_name, true
357
+ rescue SocketError => e
358
+ warning "Socket Error (#{e.message}), retrying ...", method_name, true
359
+ rescue JSON::ParserError => e
360
+ warning "JSON Parse Error (#{e.message}), retrying ...", method_name, true
361
+ drop_current_failover_url method_name if tries > 5
362
+ response = nil
363
+ rescue ApiError => e
364
+ warning "ApiError (#{e.message}), retrying ...", method_name, true
365
+ # rescue => e
366
+ # warning "Unknown exception from request, retrying ...", method_name, true
367
+ # warning e
368
+ end
369
+
370
+ if !!response
371
+ @persist_error_count = 0
372
+
373
+ if !!block
374
+ if api_name == :condenser_api
375
+ return yield(response.result, response.error, response.id)
376
+ else
377
+ if defined?(response.result.size) && response.result.size == 0
378
+ return yield(nil, response.error, response.id)
379
+ elsif (
380
+ defined?(response.result.size) && response.result.size == 1 &&
381
+ defined?(response.result.values)
382
+ )
383
+ return yield(response.result.values.first, response.error, response.id)
384
+ else
385
+ return yield(response.result, response.error, response.id)
386
+ end
387
+ end
388
+ else
389
+ return response
390
+ end
391
+ end
392
+
393
+ backoff
394
+ end # loop
395
+ end
396
+
397
+ def inspect
398
+ properties = %w(
399
+ chain url backoff_at max_requests ssl_verify_mode ssl_version persist
400
+ recover_transactions_on_error reuse_ssl_sessions pool_size
401
+ use_condenser_namespace
402
+ ).map do |prop|
403
+ if !!(v = instance_variable_get("@#{prop}"))
404
+ "@#{prop}=#{v}"
405
+ end
406
+ end.compact.join(', ')
407
+
408
+ "#<#{self.class.name} [#{properties}]>"
409
+ end
410
+
411
+ def stopped?
412
+ http_active = if @http_memo.nil?
413
+ false
414
+ else
415
+ @http_memo.values.map do |http|
416
+ if defined?(http.active?)
417
+ http.active?
418
+ else
419
+ false
420
+ end
421
+ end.include?(true)
422
+ end
423
+
424
+ @uri.nil? && @http_id.nil? && !http_active && @api.nil? && @block_api.nil?
425
+ end
426
+
427
+ def use_condenser_namespace?
428
+ @use_condenser_namespace
429
+ end
430
+ private
431
+ def self.methods_json_path
432
+ @methods_json_path ||= "#{File.dirname(__FILE__)}/methods.json"
433
+ end
434
+
435
+ def self.methods(api_name)
436
+ @methods ||= {}
437
+ @methods[api_name] ||= JSON[File.read methods_json_path].map do |e|
438
+ e if e['api'].to_sym == api_name
439
+ end.compact.freeze
440
+ end
441
+
442
+ def self.apply_http_defaults(http, ssl_verify_mode)
443
+ http.read_timeout = 10
444
+ http.open_timeout = 10
445
+ http.verify_mode = ssl_verify_mode
446
+ http.ssl_timeout = 30 if defined? http.ssl_timeout
447
+ http
448
+ end
449
+
450
+ def api_options
451
+ @api_options.merge(failover_urls: @failover_urls, logger: @logger, hashie_logger: @hashie_logger)
452
+ end
453
+
454
+ def api
455
+ @api ||= self.class == Api ? self : Api.new(api_options)
456
+ end
457
+
458
+ def block_api
459
+ @block_api ||= self.class == BlockApi ? self : BlockApi.new(api_options)
460
+ end
461
+
462
+ def rpc_id
463
+ @rpc_id ||= 0
464
+ @rpc_id = @rpc_id + 1
465
+ end
466
+
467
+ def uri
468
+ @uri ||= URI.parse(@url)
469
+ end
470
+
471
+ def http_id
472
+ @http_id ||= "voilkruby-#{VoilkRuby::VERSION}-#{api_name}-#{SecureRandom.uuid}"
473
+ end
474
+
475
+ def http
476
+ return @http_memo[http_id] if @http_memo.keys.include? http_id
477
+
478
+ @http_memo[http_id] = if @persist && @persist_error_count < 10
479
+ idempotent = api_name != :network_broadcast_api
480
+
481
+ http = if defined? Net::HTTP::Persistent::DEFAULT_POOL_SIZE
482
+ Net::HTTP::Persistent.new(name: http_id, pool_size: @pool_size)
483
+ else
484
+ # net-http-persistent < 3.0
485
+ Net::HTTP::Persistent.new(http_id)
486
+ end
487
+
488
+ http.keep_alive = 30
489
+ http.idle_timeout = idempotent ? 10 : nil
490
+ http.max_requests = @max_requests
491
+ http.retry_change_requests = idempotent
492
+ http.reuse_ssl_sessions = @reuse_ssl_sessions
493
+
494
+ http
495
+ else
496
+ http = Net::HTTP.new(uri.host, uri.port)
497
+ http.use_ssl = uri.scheme == 'https'
498
+ http
499
+ end
500
+
501
+ Api::apply_http_defaults(@http_memo[http_id], @ssl_verify_mode)
502
+ end
503
+
504
+ def post_request
505
+ Net::HTTP::Post.new uri.request_uri, POST_HEADERS
506
+ end
507
+
508
+ def request(options)
509
+ request = post_request
510
+ request.body = JSON[options]
511
+
512
+ case http
513
+ when Net::HTTP::Persistent then http.request(uri, request)
514
+ when Net::HTTP then http.request(request)
515
+ else; raise ApiError, "Unsuppored scheme: #{http.inspect}"
516
+ end
517
+ end
518
+
519
+ def jussi_supported?(url = @url)
520
+ @jussi_supported.include? url
521
+ end
522
+
523
+ def detect_jussi(response)
524
+ return if jussi_supported?(@url)
525
+
526
+ jussi_response_id = response['x-jussi-response-id']
527
+
528
+ if !!jussi_response_id
529
+ debug "Found a node that supports jussi: #{@url}"
530
+ @jussi_supported << @url
531
+ end
532
+ end
533
+
534
+ def recover_transaction(signatures, expected_rpc_id, after)
535
+ debug "Looking for signatures: #{signatures.map{|s| s[0..5]}} since: #{after}"
536
+
537
+ count = 0
538
+ start = Time.now.utc
539
+ block_range = api.get_dynamic_global_properties do |properties|
540
+ high = properties.head_block_number
541
+ low = high - 100
542
+ [*(low..(high))].reverse
543
+ end
544
+
545
+ # It would be nice if Voilk, Inc. would add an API method like
546
+ # `get_transaction`, call it `get_transaction_by_signature`, so we didn't
547
+ # have to scan the latest blocks like this. At most, we read 100 blocks
548
+ # but we also give up once the block time is before the `after` argument.
549
+
550
+ api.get_blocks(block_range) do |block, block_num|
551
+ unless defined? block.transaction_ids
552
+ error "Blockchain does not provide transaction ids in blocks, giving up."
553
+ return nil
554
+ end
555
+
556
+ count += 1
557
+ raise ApiError, "Race condition detected on remote node at: #{block_num}" if block.nil?
558
+
559
+ # TODO Some blockchains (like Golos) do not have transaction_ids. In
560
+ # the future, it would be better to decode the operation and signature
561
+ # into the transaction id.
562
+ # See: https://github.com/voilknetwork/voilk/issues/187
563
+ # See: https://github.com/GolosChain/golos/issues/281
564
+ unless defined? block.transaction_ids
565
+ @recover_transactions_on_error = false
566
+ return
567
+ end
568
+
569
+ timestamp = Time.parse(block.timestamp + 'Z')
570
+ break if timestamp < after
571
+
572
+ block.transactions.each_with_index do |tx, index|
573
+ next unless ((tx['signatures'] || []) & signatures).any?
574
+
575
+ debug "Found transaction #{count} block(s) ago; took #{(Time.now.utc - start)} seconds to scan."
576
+
577
+ return {
578
+ id: expected_rpc_id,
579
+ recovered_by: http_id,
580
+ result: {
581
+ id: block.transaction_ids[index],
582
+ block_num: block_num,
583
+ trx_num: index,
584
+ expired: false
585
+ }
586
+ }
587
+ end
588
+ end
589
+
590
+ debug "Could not find transaction in #{count} block(s); took #{(Time.now.utc - start)} seconds to scan."
591
+
592
+ return nil
593
+ end
594
+
595
+ def reset_failover
596
+ @url = @preferred_url.dup
597
+ @failover_urls = @preferred_failover_urls.dup
598
+ warning "Failover reset, going back to #{@url} ..."
599
+ end
600
+
601
+ def pop_failover_url
602
+ reset_failover if @failover_urls.none?
603
+
604
+ until @failover_urls.none? || healthy?(url = @failover_urls.sample)
605
+ @failover_urls.delete(url)
606
+ end
607
+
608
+ url || (uri || @url).to_s
609
+ end
610
+
611
+ def bump_failover
612
+ @uri = nil
613
+ @url = pop_failover_url
614
+ warning "Failing over to #{@url} ..."
615
+ end
616
+
617
+ def flappy?
618
+ !!@backoff_at && Time.now.utc - @backoff_at < 300
619
+ end
620
+
621
+ # Note, this methods only removes the uri.to_s if present but it does not
622
+ # call bump_failover, in order to avoid a race condition.
623
+ def drop_current_failover_url(prefix)
624
+ if @preferred_failover_urls.size == 1
625
+ warning "Node #{uri} appears to be misconfigured but no other node is available, retrying ...", prefix
626
+ else
627
+ warning "Removing misconfigured node from failover urls: #{uri}, retrying ...", prefix
628
+ @preferred_failover_urls.delete(uri.to_s)
629
+ @failover_urls.delete(uri.to_s)
630
+ end
631
+ end
632
+
633
+ def handle_error(response, request_options, method_name, tries)
634
+ parser = ErrorParser.new(response)
635
+ _signatures, exp = extract_signatures(request_options)
636
+
637
+ if (!!exp && exp < Time.now.utc) || (tries > 2 && !parser.node_degraded?)
638
+ # Whatever the error was, it is already expired or tried too much. No
639
+ # need to try to recover.
640
+
641
+ debug "Error code #{parser} but transaction already expired or too many tries, giving up (attempt: #{tries})."
642
+ elsif parser.can_retry?
643
+ drop_current_failover_url method_name if !!exp && parser.expiry?
644
+ drop_current_failover_url method_name if parser.node_degraded?
645
+ debug "Error code #{parser} (attempt: #{tries}), retrying ..."
646
+ return nil
647
+ end
648
+
649
+ if !!parser.trx_id
650
+ # Turns out, the ErrorParser found a transaction id. It might come in
651
+ # handy, so let's append this to the result along with the error.
652
+
653
+ response[:result] = {
654
+ id: parser.trx_id,
655
+ block_num: -1,
656
+ trx_num: -1,
657
+ expired: false
658
+ }
659
+
660
+ if @recover_transactions_on_error
661
+ begin
662
+ # Node operators often disable this operation.
663
+ api.get_transaction(parser.trx_id) do |tx|
664
+ if !!tx
665
+ response[:result][:block_num] = tx.block_num
666
+ response[:result][:trx_num] = tx.transaction_num
667
+ response[:recovered_by] = http_id
668
+ response.delete('error') # no need for this, now
669
+ end
670
+ end
671
+ rescue
672
+ debug "Couldn't find block for trx_id: #{parser.trx_id}, giving up."
673
+ end
674
+ end
675
+ end
676
+
677
+ Hashie::Mash.new(response)
678
+ end
679
+
680
+ def healthy?(url)
681
+ begin
682
+ # Note, not all nodes support the /health uri. But even if they don't,
683
+ # they'll respond status code 200 OK, even if the body shows an error.
684
+
685
+ # But if the node supports the /health uri, it will do additional
686
+ # verifications on the block height.
687
+ # See: https://github.com/voilknetwork/voilk/blob/master/contrib/healthcheck.sh
688
+
689
+ # Also note, this check is done **without** net-http-persistent.
690
+
691
+ response = open(url + HEALTH_URI)
692
+ response = JSON[response.read]
693
+
694
+ if !!response['error']
695
+ if !!response['error']['data']
696
+ if !!response['error']['data']['message']
697
+ error "#{url} error: #{response['error']['data']['message']}"
698
+ end
699
+ elsif !!response['error']['message']
700
+ error "#{url} error: #{response['error']['message']}"
701
+ else
702
+ error "#{url} error: #{response['error']}"
703
+ end
704
+
705
+ false
706
+ elsif response['status'] == 'OK'
707
+ true
708
+ else
709
+ error "#{url} status: #{response['status']}"
710
+
711
+ false
712
+ end
713
+ rescue JSON::ParserError
714
+ # No JSON, but also no HTTP error code, so we're OK.
715
+
716
+ true
717
+ rescue => e
718
+ error "Health check failure for #{url}: #{e.inspect}"
719
+ sleep 0.2
720
+ false
721
+ end
722
+ end
723
+
724
+ def check_file_open?
725
+ File.exists?('.')
726
+ rescue
727
+ false
728
+ end
729
+
730
+ def debug_payload(request, response)
731
+ request = JSON.pretty_generate(request)
732
+ response = JSON.parse(response) rescue response
733
+ response = JSON.pretty_generate(response) rescue response
734
+
735
+ puts '=' * 80
736
+ puts "Request:"
737
+ puts request
738
+ puts '=' * 80
739
+ puts "Response:"
740
+ puts response
741
+ puts '=' * 80
742
+ end
743
+
744
+ def backoff
745
+ shutdown
746
+ bump_failover if flappy? || !healthy?(uri)
747
+ @backoff_at ||= Time.now.utc
748
+ @backoff_sleep ||= 0.01
749
+
750
+ @backoff_sleep *= 2
751
+ GC.start
752
+ sleep @backoff_sleep
753
+ ensure
754
+ if !!@backoff_at && Time.now.utc - @backoff_at > 300
755
+ @backoff_at = nil
756
+ @backoff_sleep = nil
757
+ end
758
+ end
759
+
760
+ def self.finalize(logger, hashie_logger)
761
+ proc {
762
+ if !!logger && defined?(logger.close) && !logger.closed?
763
+ logger.close
764
+ end
765
+
766
+ if !!hashie_logger && defined?(hashie_logger.close) && !hashie_logger.closed?
767
+ hashie_logger.close
768
+ end
769
+ }
770
+ end
771
+ end
772
+ end