rubybear 0.0.2

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