voilkruby 0.0.1

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