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,139 @@
1
+ module Rubybear
2
+
3
+ # See: https://github.com/bearshares/bears-js/blob/766746adb5ded86380be982c844f4c269f7800ae/src/auth/serializer/src/operations.js
4
+ module OperationTypes
5
+ TYPES = {
6
+ transfer: {
7
+ amount: Type::Amount
8
+ },
9
+ transfer_to_coining: {
10
+ amount: Type::Amount
11
+ },
12
+ withdraw_coining: {
13
+ coining_shares: Type::Amount
14
+ },
15
+ limit_order_create: {
16
+ orderid: Type::Uint32,
17
+ amount_to_sell: Type::Amount,
18
+ min_to_receive: Type::Amount,
19
+ expiration: Type::PointInTime
20
+ },
21
+ limit_order_cancel: {
22
+ orderid: Type::Uint32
23
+ },
24
+ feed_publish: {
25
+ exchange_rate: Type::Price
26
+ },
27
+ convert: {
28
+ requestid: Type::Uint32,
29
+ amount: Type::Amount
30
+ },
31
+ account_create: {
32
+ fee: Type::Amount,
33
+ owner: Type::Permission,
34
+ active: Type::Permission,
35
+ posting: Type::Permission,
36
+ memo: Type::PublicKey
37
+ },
38
+ create_claimed_account: {
39
+ owner: Type::Permission,
40
+ active: Type::Permission,
41
+ posting: Type::Permission,
42
+ memo: Type::PublicKey
43
+ },
44
+ account_update: {
45
+ owner: Type::Permission,
46
+ active: Type::Permission,
47
+ posting: Type::Permission,
48
+ memo: Type::PublicKey
49
+ },
50
+ custom: {
51
+ id: Type::Uint16
52
+ },
53
+ comment_options: {
54
+ max_accepted_payout: Type::Amount,
55
+ allow_replies: Type::Future
56
+ },
57
+ set_withdraw_coining_route: {
58
+ percent: Type::Uint16
59
+ },
60
+ limit_order_create2: {
61
+ orderid: Type::Uint32,
62
+ amount_to_sell: Type::Amount,
63
+ exchange_rate: Type::Price,
64
+ expiration: Type::PointInTime
65
+ },
66
+ request_account_recovery: {
67
+ new_owner_Permission: Type::Permission
68
+ },
69
+ recover_account: {
70
+ new_owner_Permission: Type::Permission,
71
+ recent_owner_Permission: Type::Permission
72
+ },
73
+ escrow_transfer: {
74
+ bsd_amount: Type::Amount,
75
+ bears_amount: Type::Amount,
76
+ escrow_id: Type::Uint32,
77
+ fee: Type::Amount,
78
+ ratification_deadline: Type::PointInTime,
79
+ escrow_expiration: Type::PointInTime
80
+ },
81
+ escrow_dispute: {
82
+ escrow_id: Type::Uint32
83
+ },
84
+ escrow_release: {
85
+ escrow_id: Type::Uint32,
86
+ bsd_amount: Type::Amount,
87
+ bears_amount: Type::Amount
88
+ },
89
+ escrow_approve: {
90
+ escrow_id: Type::Uint32
91
+ },
92
+ transfer_to_savings: {
93
+ amount: Type::Amount
94
+ },
95
+ transfer_from_savings: {
96
+ request_id: Type::Uint32,
97
+ amount: Type::Amount
98
+ },
99
+ cancel_transfer_from_savings: {
100
+ request_id: Type::Uint32
101
+ },
102
+ reset_account: {
103
+ new_owner_permission: Type::Amount
104
+ },
105
+ set_reset_account: {
106
+ reward_bears: Type::Amount,
107
+ reward_bsd: Type::Amount,
108
+ reward_coins: Type::Amount
109
+ },
110
+ claim_reward_balance: {
111
+ reward_bears: Type::Amount,
112
+ reward_bsd: Type::Amount,
113
+ reward_coins: Type::Amount
114
+ },
115
+ delegate_coining_shares: {
116
+ coining_shares: Type::Amount
117
+ },
118
+ claim_account: {
119
+ fee: Type::Amount
120
+ },
121
+ witness_update: {
122
+ block_signing_key: Type::PublicKey,
123
+ props: Type::Array
124
+ },
125
+ witness_set_properties: {
126
+ props: Type::Hash
127
+ }
128
+ }
129
+
130
+ def type(key, param, value)
131
+ return if value.nil?
132
+
133
+ t = TYPES[key] or return value
134
+ p = t[param] or return value
135
+
136
+ p.new(value)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,527 @@
1
+ module Rubybear
2
+ # Rubybear::Stream allows a live view of the BEARS blockchain.
3
+ #
4
+ # All values returned by `get_dynamic_global_properties` can be streamed.
5
+ #
6
+ # For example, if you want to know which witness is currently signing blocks,
7
+ # use the following:
8
+ #
9
+ # stream = Rubybear::Stream.new
10
+ # stream.current_witness do |witness|
11
+ # puts witness
12
+ # end
13
+ #
14
+ # More importantly, full blocks, transactions, and operations can be streamed.
15
+ class Stream < Api
16
+
17
+ # @private
18
+ INITIAL_TIMEOUT = 0.0200
19
+
20
+ # Note, even though block production is advertised at 3 seconds, often
21
+ # blocks are available in 1.5 seconds. However, we still keep our
22
+ # expectations at 3 seconds.
23
+ # @private
24
+ BLOCK_PRODUCTION = 3.0
25
+
26
+ # @private
27
+ MAX_TIMEOUT = 80
28
+
29
+ # @private
30
+ MAX_BLOCKS_PER_NODE = 10000
31
+
32
+ RANGE_BEHIND_WARNING = 400
33
+
34
+ def initialize(options = {})
35
+ super
36
+ end
37
+
38
+ # Returns the latest operations from the blockchain.
39
+ #
40
+ # stream = Rubybear::Stream.new
41
+ # stream.operations do |op|
42
+ # puts op.to_json
43
+ # end
44
+ #
45
+ # If symbol are passed, then only that operation is returned. Expected
46
+ # symbols are:
47
+ #
48
+ # account_create
49
+ # account_create_with_delegation
50
+ # account_update
51
+ # account_witness_proxy
52
+ # account_witness_vote
53
+ # cancel_transfer_from_savings
54
+ # change_recovery_account
55
+ # claim_reward_balance
56
+ # comment
57
+ # comment_options
58
+ # convert
59
+ # custom
60
+ # custom_json
61
+ # decline_voting_rights
62
+ # delegate_coining_shares
63
+ # delete_comment
64
+ # escrow_approve
65
+ # escrow_dispute
66
+ # escrow_release
67
+ # escrow_transfer
68
+ # feed_publish
69
+ # limit_order_cancel
70
+ # limit_order_create
71
+ # limit_order_create2
72
+ # pow
73
+ # pow2
74
+ # recover_account
75
+ # request_account_recovery
76
+ # set_withdraw_coining_route
77
+ # transfer
78
+ # transfer_from_savings
79
+ # transfer_to_savings
80
+ # transfer_to_coining
81
+ # vote
82
+ # withdraw_coining
83
+ # witness_update
84
+ #
85
+ # For example, to stream only votes:
86
+ #
87
+ # stream = Rubybear::Stream.new
88
+ # stream.operations(:vote) do |vote|
89
+ # puts vote.to_json
90
+ # end
91
+ #
92
+ # You can also stream virtual operations:
93
+ #
94
+ # stream = Rubybear::Stream.new
95
+ # stream.operations(:author_reward) do |vop|
96
+ # puts "#{vop.author} got paid for #{vop.permlink}: #{[vop.bsd_payout, vop.bears_payout, vop.coining_payout]}"
97
+ # end
98
+ #
99
+ # ... or multiple virtual operation types;
100
+ #
101
+ # stream = Rubybear::Stream.new
102
+ # stream.operations([:producer_reward, :author_reward]) do |vop|
103
+ # puts vop
104
+ # end
105
+ #
106
+ # ... or all types, inluding virtual operation types;
107
+ #
108
+ # stream = Rubybear::Stream.new
109
+ # stream.operations(nil, nil, :head, include_virtual: true) do |vop|
110
+ # puts vop
111
+ # end
112
+ #
113
+ # Expected virtual operation types:
114
+ #
115
+ # producer_reward
116
+ # author_reward
117
+ # curation_reward
118
+ # fill_convert_request
119
+ # fill_order
120
+ # fill_coining_withdraw
121
+ # interest
122
+ # shutdown_witness
123
+ #
124
+ # @param type [symbol || ::Array<symbol>] the type(s) of operation, optional.
125
+ # @param start starting block
126
+ # @param mode we have the choice between
127
+ # * :head the last block
128
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
129
+ # @param block the block to execute for each result, optional. Yields: |op, trx_id, block_num, api|
130
+ # @param options [::Hash] additional options
131
+ # @option options [Boollean] :include_virtual Also stream virtual options. Setting this true will impact performance. Default: false.
132
+ # @return [::Hash]
133
+ def operations(type = nil, start = nil, mode = :irreversible, options = {include_virtual: false}, &block)
134
+ type = [type].flatten.compact.map(&:to_sym)
135
+ include_virtual = !!options[:include_virtual]
136
+
137
+ if virtual_op_type?(type)
138
+ include_virtual = true
139
+ end
140
+
141
+ latest_block_number = -1
142
+
143
+ transactions(start, mode) do |transaction, trx_id, block_number|
144
+ virtual_ops_collected = latest_block_number == block_number
145
+ latest_block_number = block_number
146
+
147
+ ops = transaction.operations.map do |t, op|
148
+ t = t.to_sym
149
+ if type.size == 1 && type.first == t
150
+ op
151
+ elsif type.none? || type.include?(t)
152
+ {t => op}
153
+ end
154
+ end.compact
155
+
156
+ if include_virtual && !virtual_ops_collected
157
+ catch :pop_vops do; begin
158
+ api.get_ops_in_block(block_number, true) do |vops, error|
159
+ if !!error
160
+ standby "Node responded with: #{error.message || 'unknown error'}, retrying ...", {
161
+ error: error,
162
+ and: {throw: :pop_vops}
163
+ }
164
+ end
165
+
166
+ vops.each do |vtx|
167
+ next unless defined? vtx.op
168
+
169
+ t = vtx.op.first.to_sym
170
+ op = vtx.op.last
171
+ if type.size == 1 && type.first == t
172
+ ops << op
173
+ elsif type.none? || type.include?(t)
174
+ ops << {t => op}
175
+ end
176
+ end
177
+ end
178
+ end; end
179
+
180
+ virtual_ops_collected = true
181
+ end
182
+
183
+ next if ops.none?
184
+
185
+ return ops unless !!block
186
+
187
+ ops.each do |op|
188
+ yield op, trx_id, block_number, api
189
+ end
190
+ end
191
+ end
192
+
193
+ # Returns the latest transactions from the blockchain.
194
+ #
195
+ # stream = Rubybear::Stream.new
196
+ # stream.transactions do |tx, trx_id|
197
+ # puts "[#{trx_id}] #{tx.to_json}"
198
+ # end
199
+ #
200
+ # @param start starting block
201
+ # @param mode we have the choice between
202
+ # * :head the last block
203
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
204
+ # @param block the block to execute for each result, optional. Yields: |tx, trx_id, api|
205
+ # @return [::Hash]
206
+ def transactions(start = nil, mode = :irreversible, &block)
207
+ blocks(start, mode) do |b, block_number|
208
+ next if (_transactions = b.transactions).nil?
209
+ return _transactions unless !!block
210
+
211
+ _transactions.each_with_index do |transaction, index|
212
+ trx_id = if !!b['transaction_ids']
213
+ b['transaction_ids'][index]
214
+ end
215
+
216
+ yield transaction, trx_id, block_number, api
217
+ end
218
+ end
219
+ end
220
+
221
+ # Returns the latest blocks from the blockchain.
222
+ #
223
+ # stream = Rubybear::Stream.new
224
+ # stream.blocks do |bk, num|
225
+ # puts "[#{num}] #{bk.to_json}"
226
+ # end
227
+ #
228
+ # For convenience and memory management, the api used to poll the current
229
+ # block data is also available inside the block, e.g.:
230
+ #
231
+ # stream = Rubybear::Stream.new
232
+ # stream.blocks do |bk, num, api|
233
+ # puts "[#{num}] #{bk.to_json}"
234
+ #
235
+ # api.get_ops_in_block(num, true) do |vops, error|
236
+ # puts vops
237
+ # end
238
+ # end
239
+ #
240
+ # This idiom is useful for very long running scripts.
241
+ #
242
+ # @param start starting block
243
+ # @param mode we have the choice between
244
+ # * :head the last block
245
+ # * :irreversible the block that is confirmed by 2/3 of all block producers and is thus irreversible!
246
+ # @param max_blocks_per_node the number of blocks to read before trying a new node
247
+ # @param block the block to execute for each result, optional. Yields: |bk, num, api|
248
+ # @return [::Hash]
249
+ def blocks(start = nil, mode = :irreversible, max_blocks_per_node = MAX_BLOCKS_PER_NODE, &block)
250
+ reset_api
251
+
252
+ replay = !!start
253
+ counter = 0
254
+ latest_block_number = -1
255
+ @api_options[:max_requests] = [max_blocks_per_node * 2, @api_options[:max_requests].to_i].max
256
+
257
+ loop do
258
+ break if stop?
259
+
260
+ catch :sequence do; begin
261
+ head_block = api.get_dynamic_global_properties do |properties, error|
262
+ if !!error
263
+ standby "Node responded with: #{error.message || 'unknown error'}, retrying ...", {
264
+ error: error,
265
+ and: {throw: :sequence}
266
+ }
267
+ end
268
+
269
+ break if stop?
270
+
271
+ if properties.head_block_number.nil?
272
+ # This can happen if a reverse proxy is acting up.
273
+ standby "Bad block sequence after height: #{latest_block_number}", {
274
+ and: {throw: :sequence}
275
+ }
276
+ end
277
+
278
+ case mode.to_sym
279
+ when :head then properties.head_block_number
280
+ when :irreversible then properties.last_irreversible_block_num
281
+ else; raise StreamError, '"mode" has to be "head" or "irreversible"'
282
+ end
283
+ end
284
+
285
+ if head_block == latest_block_number
286
+ # This can happen when there's a delay in block production.
287
+
288
+ if current_timeout > BLOCK_PRODUCTION * 6
289
+ standby "Stream has stalled severely ...", {
290
+ and: {backoff: api, throw: :sequence}
291
+ }
292
+ elsif current_timeout > BLOCK_PRODUCTION * 3
293
+ warning "Stream has stalled ..."
294
+ end
295
+
296
+ timeout and throw :sequence
297
+ elsif head_block < latest_block_number
298
+ # This can happen if a reverse proxy is acting up.
299
+ standby "Invalid block sequence at height: #{head_block}", {
300
+ and: {backoff: api, throw: :sequence}
301
+ }
302
+ end
303
+
304
+ reset_timeout
305
+ start ||= head_block
306
+ range = (start..head_block)
307
+
308
+ for n in range
309
+ break if stop?
310
+
311
+ if (counter += 1) > max_blocks_per_node
312
+ reset_api
313
+ counter = 0
314
+ end
315
+
316
+ if !replay && range.size > RANGE_BEHIND_WARNING
317
+ # When the range is above RANGE_BEHIND_WARNING blocks, it's time
318
+ # to warn, unless we're replaying.
319
+
320
+ r = [*range]
321
+ index = r.index(n)
322
+ current_range = r[index..-1]
323
+
324
+ if current_range.size % RANGE_BEHIND_WARNING == 0
325
+ warning "Stream behind by #{current_range.size} blocks (about #{(current_range.size * 3) / 60.0} minutes)."
326
+ end
327
+ end
328
+
329
+ scoped_api, block_options = if use_condenser_namespace?
330
+ [api, n]
331
+ else
332
+ [block_api, {block_num: n}]
333
+ end
334
+
335
+ scoped_api.get_block(n) do |current_block, error|
336
+ if !!error
337
+ if error.message == 'Unable to acquire database lock'
338
+ start = n
339
+ timeout
340
+ standby "Node was unable to acquire database lock, retrying ...", {
341
+ and: {throw: :sequence}
342
+ }
343
+ else
344
+ standby "Node responded with: #{error.message || 'unknown error'}, retrying ...", {
345
+ error: error,
346
+ and: {throw: :sequence}
347
+ }
348
+ end
349
+ elsif current_block.nil?
350
+ standby "Node responded with: empty block, retrying ...", {
351
+ and: {throw: :sequence}
352
+ }
353
+ end
354
+
355
+ latest_block_number = n
356
+ return current_block, n if block.nil?
357
+ yield current_block, n, api
358
+ end
359
+
360
+ start = head_block + 1
361
+ sleep BLOCK_PRODUCTION / range.size
362
+ end
363
+ rescue StreamError; raise
364
+ # rescue => e
365
+ # warning "Unknown streaming error: #{e.inspect}, retrying ... "
366
+ # warning e
367
+ # redo
368
+ end; end
369
+ end
370
+ end
371
+
372
+ # Stops the persistant http connections.
373
+ #
374
+ def shutdown
375
+ flappy = false
376
+
377
+ begin
378
+ unless @api.nil?
379
+ flappy = @api.send(:flappy?)
380
+ @api.shutdown
381
+ end
382
+
383
+ unless @block_api.nil?
384
+ flappy = @block_api.send(:flappy?) unless flappy
385
+ @block_api.shutdown
386
+ end
387
+ rescue => e
388
+ warning("Unable to shut down: #{e}")
389
+ end
390
+
391
+ @api = nil
392
+ @block_api = nil
393
+ GC.start
394
+ end
395
+
396
+ # @private
397
+ def method_names
398
+ @method_names ||= [
399
+ :head_block_number,
400
+ :head_block_id,
401
+ :time,
402
+ :current_witness,
403
+ :total_pow,
404
+ :num_pow_witnesses,
405
+ :virtual_supply,
406
+ :current_supply,
407
+ :confidential_supply,
408
+ :current_bsd_supply,
409
+ :confidential_bsd_supply,
410
+ :total_coining_fund_bears,
411
+ :total_coining_shares,
412
+ :total_reward_fund_bears,
413
+ :total_reward_shares2,
414
+ :total_activity_fund_bears,
415
+ :total_activity_fund_shares,
416
+ :bsd_interest_rate,
417
+ :average_block_size,
418
+ :maximum_block_size,
419
+ :current_aslot,
420
+ :recent_slots_filled,
421
+ :participation_count,
422
+ :last_irreversible_block_num,
423
+ :max_virtual_bandwidth,
424
+ :current_reserve_ratio,
425
+ :block_numbers,
426
+ :blocks
427
+ ].freeze
428
+ end
429
+
430
+ # @private
431
+ def method_params(method)
432
+ case method
433
+ when :block_numbers then {head_block_number: nil}
434
+ when :blocks then {get_block: :head_block_number}
435
+ else; nil
436
+ end
437
+ end
438
+ private
439
+ def method_missing(m, *args, &block)
440
+ super unless respond_to_missing?(m)
441
+
442
+ @latest_values ||= []
443
+ @latest_values.shift(5) if @latest_values.size > 20
444
+ loop do
445
+ break if stop?
446
+
447
+ value = if (n = method_params(m)).nil?
448
+ key_value = api.get_dynamic_global_properties.result[m]
449
+ else
450
+ key = n.keys.first
451
+ if !!n[key]
452
+ r = api.get_dynamic_global_properties.result
453
+ key_value = param = r[n[key]]
454
+ result = nil
455
+ loop do
456
+ break if stop?
457
+
458
+ response = api.send(key, param)
459
+ raise StreamError, JSON[response.error] if !!response.error
460
+ result = response.result
461
+ break if !!result
462
+ warning "#{key}: #{param} result missing, retrying with timeout: #{current_timeout} seconds"
463
+ reset_api
464
+ timeout
465
+ end
466
+ reset_timeout
467
+ result
468
+ else
469
+ key_value = api.get_dynamic_global_properties.result[key]
470
+ end
471
+ end
472
+ unless @latest_values.include? key_value
473
+ @latest_values << key_value
474
+ if !!block
475
+ yield value
476
+ else
477
+ return value
478
+ end
479
+ end
480
+ sleep current_timeout
481
+ end
482
+ end
483
+
484
+ def reset_api
485
+ shutdown
486
+ !!api && !!block_api
487
+ end
488
+
489
+ def timeout
490
+ @timeout ||= INITIAL_TIMEOUT
491
+ @timeout *= 2
492
+ reset_timeout if @timeout > MAX_TIMEOUT
493
+ sleep @timeout || INITIAL_TIMEOUT
494
+ @timeout
495
+ end
496
+
497
+ def current_timeout
498
+ @timeout || INITIAL_TIMEOUT
499
+ end
500
+
501
+ def reset_timeout
502
+ @timeout = nil
503
+ end
504
+
505
+ def virtual_op_type?(type)
506
+ type = [type].flatten.compact.map(&:to_sym)
507
+
508
+ (Rubybear::OperationTypes::TYPES.keys && type).any?
509
+ end
510
+
511
+ def stop?
512
+ @api.nil? || @block_api.nil?
513
+ end
514
+
515
+ def standby(message, options = {})
516
+ error = options[:error]
517
+ secondary = options[:and] || {}
518
+ backoff_api = secondary[:backoff]
519
+ throwable = secondary[:throw]
520
+
521
+ warning message
522
+ warning error if !!error
523
+ backoff_api.send :backoff if !!backoff_api
524
+ throw throwable if !!throwable
525
+ end
526
+ end
527
+ end