rubybear 0.0.2

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