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,299 @@
1
+ module Rubybear
2
+ # Examples ...
3
+ #
4
+ # To vote on a post/comment:
5
+ #
6
+ # bears = Rubybear::Chain.new(chain: :bears, account_name: 'your account name', wif: 'your wif')
7
+ # bears.vote!(10000, 'author', 'post-or-comment-permlink')
8
+ #
9
+ # To post and vote in the same transaction:
10
+ #
11
+ # bears = Rubybear::Chain.new(chain: :bears, account_name: 'your account name', wif: 'your wif')
12
+ # bears.post!(title: 'title of my post', body: 'body of my post', tags: ['tag'], self_upvote: 10000)
13
+ #
14
+ # To post and vote with declined payout:
15
+ #
16
+ # bears = Rubybear::Chain.new(chain: :bears, account_name: 'your account name', wif: 'your wif')
17
+ #
18
+ # options = {
19
+ # title: 'title of my post',
20
+ # body: 'body of my post',
21
+ # tags: ['tag'],
22
+ # self_upvote: 10000,
23
+ # percent_bears_dollars: 0
24
+ # }
25
+ #
26
+ # bears.post!(options)
27
+ #
28
+ class Chain
29
+ include Mixins::ActsAsPoster
30
+ include Mixins::ActsAsVoter
31
+ include Mixins::ActsAsWallet
32
+
33
+ VALID_OPTIONS = %w(
34
+ chain account_name wif url failover_urls
35
+ ).map(&:to_sym)
36
+ VALID_OPTIONS.each { |option| attr_accessor option }
37
+
38
+ def self.parse_slug(*args)
39
+ args = [args].flatten
40
+
41
+ if args.size == 1
42
+ case args[0]
43
+ when String then split_slug(args[0])
44
+ when ::Hash then [args[0]['author'], args[0]['permlink']]
45
+ end
46
+ else
47
+ args
48
+ end
49
+ end
50
+
51
+ def initialize(options = {})
52
+ options = options.dup
53
+ options.each do |k, v|
54
+ k = k.to_sym
55
+ if VALID_OPTIONS.include?(k.to_sym)
56
+ options.delete(k)
57
+ send("#{k}=", v)
58
+ end
59
+ end
60
+
61
+ @account_name ||= ENV['ACCOUNT_NAME']
62
+ @wif ||= ENV['WIF']
63
+
64
+ reset
65
+ end
66
+
67
+ # Find a specific block by block number.
68
+ #
69
+ # Example:
70
+ #
71
+ # bears = Rubybear::Chain.new(chain: :bears)
72
+ # block = bears.find_block(12345678)
73
+ # transactions = block.transactions
74
+ #
75
+ # @param block_number [Fixnum]
76
+ # @return [::Hash]
77
+ def find_block(block_number)
78
+ api.get_blocks(block_number).first
79
+ end
80
+
81
+ # Find a specific account by name.
82
+ #
83
+ # Example:
84
+ #
85
+ # bears = Rubybear::Chain.new(chain: :bears)
86
+ # ned = bears.find_account('ned')
87
+ # coining_shares = ned.coining_shares
88
+ #
89
+ # @param account_name [String] Name of the account to find.
90
+ # @return [::Hash]
91
+ def find_account(account_name)
92
+ api.get_accounts([account_name]) do |accounts, err|
93
+ raise ChainError, ErrorParser.new(err) if !!err
94
+
95
+ accounts[0]
96
+ end
97
+ end
98
+
99
+ # Find a specific comment by author and permlink or slug.
100
+ #
101
+ # Example:
102
+ #
103
+ # bears = Rubybear::Chain.new(chain: :bears)
104
+ # comment = bears.find_comment('inertia', 'kinda-spooky') # by account, permlink
105
+ # active_votes = comment.active_votes
106
+ #
107
+ # ... or ...
108
+ #
109
+ # comment = bears.find_comment('@inertia/kinda-spooky') # by slug
110
+ #
111
+ # @param args [String || ::Array<String>] Slug or author, permlink of comment.
112
+ # @return [::Hash]
113
+ def find_comment(*args)
114
+ author, permlink = Chain.parse_slug(args)
115
+
116
+ api.get_content(author, permlink) do |comment, err|
117
+ raise ChainError, ErrorParser.new(err) if !!err
118
+
119
+ comment unless comment.id == 0
120
+ end
121
+ end
122
+
123
+ # Current dynamic global properties, cached for 3 seconds. This is useful
124
+ # for reading properties without worrying about actually fetching it over
125
+ # rpc more than needed.
126
+ def properties
127
+ @properties ||= nil
128
+
129
+ if !!@properties && Time.now.utc - Time.parse(@properties.time + 'Z') > 3
130
+ @properties = nil
131
+ end
132
+
133
+ return @properties if !!@properties
134
+
135
+ api.get_dynamic_global_properties do |properties|
136
+ @properties = properties
137
+ end
138
+ end
139
+
140
+ def block_time
141
+ Time.parse(properties.time + 'Z')
142
+ end
143
+
144
+ # Returns the current base (e.g. BEARS) price in the coin asset (e.g.
145
+ # COINS).
146
+ #
147
+ def base_per_mcoin
148
+ total_coining_fund_bears = properties.total_coining_fund_bears.to_f
149
+ total_coining_shares_mcoin = properties.total_coining_shares.to_f / 1e6
150
+
151
+ total_coining_fund_bears / total_coining_shares_mcoin
152
+ end
153
+
154
+ # Returns the current base (e.g. BEARS) price in the debt asset (e.g BSD).
155
+ #
156
+ def base_per_debt
157
+ api.get_feed_history do |feed_history|
158
+ current_median_history = feed_history.current_median_history
159
+ base = current_median_history.base
160
+ base = base.split(' ').first.to_f
161
+ quote = current_median_history.quote
162
+ quote = quote.split(' ').first.to_f
163
+
164
+ (base / quote) * base_per_mcoin
165
+ end
166
+ end
167
+
168
+ # List of accounts followed by account.
169
+ #
170
+ # @param account_name String Name of the account.
171
+ # @return [::Array<String>]
172
+ def followed_by(account_name)
173
+ return [] if account_name.nil?
174
+
175
+ followers = []
176
+ count = -1
177
+
178
+ until count == followers.size
179
+ count = followers.size
180
+ follow_api.get_followers(account: account_name, start: followers.last, type: 'blog', limit: 1000) do |follows, err|
181
+ raise ChainError, ErrorParser.new(err) if !!err
182
+
183
+ followers += follows.map(&:follower)
184
+ followers = followers.uniq
185
+ end
186
+ end
187
+
188
+ followers
189
+ end
190
+
191
+ # List of accounts following account.
192
+ #
193
+ # @param account_name String Name of the account.
194
+ # @return [::Array<String>]
195
+ def following(account_name)
196
+ return [] if account_name.nil?
197
+
198
+ following = []
199
+ count = -1
200
+
201
+ until count == following.size
202
+ count = following.size
203
+ follow_api.get_following(account: account_name, start: following.last, type: 'blog', limit: 100) do |follows, err|
204
+ raise ChainError, ErrorParser.new(err) if !!err
205
+
206
+ following += follows.map(&:following)
207
+ following = following.uniq
208
+ end
209
+ end
210
+
211
+ following
212
+ end
213
+
214
+ # Clears out queued properties.
215
+ def reset_properties
216
+ @properties = nil
217
+ end
218
+
219
+ # Clears out queued operations.
220
+ def reset_operations
221
+ @operations = []
222
+ end
223
+
224
+ # Clears out all properties and operations.
225
+ def reset
226
+ reset_properties
227
+ reset_operations
228
+
229
+ @api = @block_api = @follow_api = nil
230
+ end
231
+
232
+ # Broadcast queued operations.
233
+ #
234
+ # @param auto_reset [boolean] clears operations no matter what, even if there's an error.
235
+ def broadcast!(auto_reset = false)
236
+ raise ChainError, "Required option: chain" if @chain.nil?
237
+ raise ChainError, "Required option: account_name, wif" if @account_name.nil? || @wif.nil?
238
+
239
+ begin
240
+ transaction = Rubybear::Transaction.new(build_options)
241
+ transaction.operations = @operations
242
+ response = transaction.process(true)
243
+ rescue => e
244
+ reset if auto_reset
245
+ raise e
246
+ end
247
+
248
+ if !!response.result
249
+ reset
250
+ response
251
+ else
252
+ reset if auto_reset
253
+ ErrorParser.new(response)
254
+ end
255
+ end
256
+ private
257
+ def self.split_slug(slug)
258
+ slug = slug.split('@').last
259
+ author = slug.split('/')[0]
260
+ permlink = slug.split('/')[1..-1].join('/')
261
+ permlink = permlink.split('#')[0]
262
+
263
+ [author, permlink]
264
+ end
265
+
266
+ def build_options
267
+ {
268
+ chain: chain,
269
+ wif: wif,
270
+ url: url,
271
+ failover_urls: failover_urls
272
+ }
273
+ end
274
+
275
+ def api
276
+ @api ||= Api.new(build_options)
277
+ end
278
+
279
+ def block_api
280
+ @block_api ||= BlockApi.new(build_options)
281
+ end
282
+
283
+ def follow_api
284
+ @follow_api ||= FollowApi.new(build_options)
285
+ end
286
+
287
+ def default_max_acepted_payout
288
+ "1000000.000 #{default_debt_asset}"
289
+ end
290
+
291
+ def default_debt_asset
292
+ case chain
293
+ when :bears then ChainConfig::NETWORKS_BEARS_DEBT_ASSET
294
+ when :test then ChainConfig::NETWORKS_TEST_DEBT_ASSET
295
+ else; raise ChainError, "Unknown chain: #{chain}"
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,22 @@
1
+ module Rubybear
2
+ module ChainConfig
3
+ EXPIRE_IN_SECS = 600
4
+ EXPIRE_IN_SECS_PROPOSAL = 24 * 60 * 60
5
+
6
+ NETWORKS_BEARS_CHAIN_ID = 'b510834141c312c2aa8837040734605f2333f1ecc4f634576372f9c12dc7e8b2'
7
+ NETWORKS_BEARS_ADDRESS_PREFIX = 'SHR'
8
+ NETWORKS_BEARS_CORE_ASSET = 'BEARS'
9
+ NETWORKS_BEARS_DEBT_ASSET = 'BSD'
10
+ NETWORKS_BEARS_COIN_ASSET = 'COINS'
11
+ NETWORKS_BEARS_DEFAULT_NODE = 'https://api.bearshares.com'
12
+
13
+ NETWORKS_TEST_CHAIN_ID = '18dcf0a285365fc58b71f18b3d3fec954aa0c141c44e4e5cb4cf777b9eab274e'
14
+ NETWORKS_TEST_ADDRESS_PREFIX = 'TST'
15
+ NETWORKS_TEST_CORE_ASSET = 'CORE'
16
+ NETWORKS_TEST_DEBT_ASSET = 'TEST'
17
+ NETWORKS_TEST_COIN_ASSET = 'CESTS'
18
+ NETWORKS_TEST_DEFAULT_NODE = 'https://test.bears.ws'
19
+
20
+ NETWORK_CHAIN_IDS = [NETWORKS_BEARS_CHAIN_ID, NETWORKS_TEST_CHAIN_ID]
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Rubybear
2
+ class ChainStatsApi < Api
3
+ def method_names
4
+ @method_names ||= [
5
+ :get_stats_for_time,
6
+ :get_stats_for_interval,
7
+ :get_lifetime_stats
8
+ ].freeze
9
+ end
10
+
11
+ def api_name
12
+ :chain_stats_api
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,99 @@
1
+ module Rubybear
2
+ class CondenserApi < Api
3
+ METHOD_NAMES = [
4
+ :broadcast_block,
5
+ :broadcast_transaction,
6
+ :broadcast_transaction_synchronous,
7
+ :get_account_bandwidth,
8
+ :get_account_count,
9
+ :get_account_history,
10
+ :get_account_references,
11
+ :get_account_reputations,
12
+ :get_account_votes,
13
+ :get_accounts,
14
+ :get_active_votes,
15
+ :get_active_witnesses,
16
+ :get_block,
17
+ :get_block_header,
18
+ :get_blog,
19
+ :get_blog_authors,
20
+ :get_blog_entries,
21
+ :get_chain_properties,
22
+ :get_comment_discussions_by_payout,
23
+ :get_config,
24
+ :get_content,
25
+ :get_content_replies,
26
+ :get_conversion_requests,
27
+ :get_current_median_history_price,
28
+ :get_discussions_by_active,
29
+ :get_discussions_by_author_before_date,
30
+ :get_discussions_by_blog,
31
+ :get_discussions_by_cashout,
32
+ :get_discussions_by_children,
33
+ :get_discussions_by_comments,
34
+ :get_discussions_by_created,
35
+ :get_discussions_by_feed,
36
+ :get_discussions_by_hot,
37
+ :get_discussions_by_promoted,
38
+ :get_discussions_by_trending,
39
+ :get_discussions_by_votes,
40
+ :get_dynamic_global_properties,
41
+ :get_escrow,
42
+ :get_expiring_coining_delegations,
43
+ :get_feed,
44
+ :get_feed_entries,
45
+ :get_feed_history,
46
+ :get_follow_count,
47
+ :get_followers,
48
+ :get_following,
49
+ :get_hardfork_version,
50
+ :get_key_references,
51
+ :get_market_history,
52
+ :get_market_history_buckets,
53
+ :get_next_scheduled_hardfork,
54
+ :get_open_orders,
55
+ :get_ops_in_block,
56
+ :get_order_book,
57
+ :get_owner_history,
58
+ :get_post_discussions_by_payout,
59
+ :get_potential_signatures,
60
+ :get_reblogged_by,
61
+ :get_recent_trades,
62
+ :get_recovery_request,
63
+ :get_replies_by_last_update,
64
+ :get_required_signatures,
65
+ :get_reward_fund,
66
+ :get_savings_withdraw_from,
67
+ :get_savings_withdraw_to,
68
+ :get_state,
69
+ :get_tags_used_by_author,
70
+ :get_ticker,
71
+ :get_trade_history,
72
+ :get_transaction,
73
+ :get_transaction_hex,
74
+ :get_trending_tags,
75
+ :get_version,
76
+ :get_coining_delegations,
77
+ :get_volume,
78
+ :get_withdraw_routes,
79
+ :get_witness_by_account,
80
+ :get_witness_count,
81
+ :get_witness_schedule,
82
+ :get_witnesses,
83
+ :get_witnesses_by_vote,
84
+ :lookup_account_names,
85
+ :lookup_accounts,
86
+ :lookup_witness_accounts,
87
+ :verify_account_authority,
88
+ :verify_authority
89
+ ].freeze
90
+
91
+ def method_names
92
+ METHOD_NAMES
93
+ end
94
+
95
+ def api_name
96
+ :condenser_api
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ module Rubybear
2
+ # @see Api
3
+ class DatabaseApi < Api
4
+ end
5
+ end
@@ -0,0 +1,228 @@
1
+ module Rubybear
2
+ class ErrorParser
3
+ include Utils
4
+
5
+ attr_reader :response, :error, :error_code, :error_message,
6
+ :api_name, :api_method, :api_params,
7
+ :expiry, :can_retry, :can_reprepare, :node_degraded, :trx_id, :debug
8
+
9
+ alias expiry? expiry
10
+ alias can_retry? can_retry
11
+ alias can_reprepare? can_reprepare
12
+ alias node_degraded? node_degraded
13
+
14
+ REPREPARE_WHITELIST = [
15
+ 'is_canonical( c ): signature is not canonical',
16
+ 'now < trx.expiration: '
17
+ ]
18
+
19
+ DUPECHECK = '(skip & skip_transaction_dupe_check) || trx_idx.indices().get<by_trx_id>().find(trx_id) == trx_idx.indices().get<by_trx_id>().end(): Duplicate transaction check failed'
20
+
21
+ REPREPARE_BLACKLIST = [DUPECHECK]
22
+
23
+ def initialize(response)
24
+ @response = response
25
+
26
+ @error = nil
27
+ @error_code = nil
28
+ @error_message = nil
29
+ @api_name = nil
30
+ @api_method = nil
31
+ @api_params = nil
32
+
33
+ @expiry = nil
34
+ @can_retry = nil
35
+ @can_reprepare = nil
36
+ @trx_id = nil
37
+ @debug = nil
38
+
39
+ parse_error_response
40
+ end
41
+
42
+ def parse_error_response
43
+ if response.nil?
44
+ @expiry = false
45
+ @can_retry = false
46
+ @can_reprepare = false
47
+
48
+ return
49
+ end
50
+
51
+ @response = JSON[response] if response.class == String
52
+
53
+ @error = if !!@response['error']
54
+ response['error']
55
+ else
56
+ response
57
+ end
58
+
59
+ begin
60
+ if !!@error['data']
61
+ # These are, by far, the more interesting errors, so we try to pull
62
+ # them out first, if possible.
63
+
64
+ @error_code = @error['data']['code']
65
+ stacks = @error['data']['stack']
66
+ stack_formats = nil
67
+
68
+ @error_message = if !!stacks
69
+ stack_formats = stacks.map { |s| s['format'] }
70
+ stack_datum = stacks.map { |s| s['data'] }
71
+ data_call_method = stack_datum.find { |data| data['call.method'] == 'call' }
72
+ data_name = stack_datum.find { |data| !!data['name'] }
73
+
74
+ # See if we can recover a transaction id out of this hot mess.
75
+ data_trx_ix = stack_datum.find { |data| !!data['trx_ix'] }
76
+ @trx_id = data_trx_ix['trx_ix'] if !!data_trx_ix
77
+
78
+ stack_formats.reject(&:empty?).join('; ')
79
+ else
80
+ @error_code ||= @error['code']
81
+ @error['message']
82
+ end
83
+
84
+ @api_name, @api_method, @api_params = if !!data_call_method
85
+ @api_name = data_call_method['call.params']
86
+ end
87
+ else
88
+ @error_code = @error['code']
89
+ @error_message = @error['message']
90
+ @expiry = false
91
+ @can_retry = false
92
+ @can_reprepare = false
93
+ end
94
+
95
+ case @error_code
96
+ when -32603
97
+ if error_match?('Internal Error')
98
+ @expiry = false
99
+ @can_retry = true
100
+ @can_reprepare = true
101
+ end
102
+ when -32003
103
+ if error_match?('Unable to acquire database lock')
104
+ @expiry = false
105
+ @can_retry = true
106
+ @can_reprepare = true
107
+ end
108
+ when -32000
109
+ @expiry = false
110
+ @can_retry = coerce_backtrace
111
+ @can_reprepare = if @api_name == 'network_broadcast_api'
112
+ error_match(REPREPARE_WHITELIST)
113
+ else
114
+ false
115
+ end
116
+ when 10
117
+ @expiry = false
118
+ @can_retry = coerce_backtrace
119
+ @can_reprepare = !!stack_formats && (stack_formats & REPREPARE_WHITELIST).any?
120
+ when 13
121
+ @error_message = @error['data']['message']
122
+ @expiry = false
123
+ @can_retry = false
124
+ @can_reprepare = false
125
+ when 3030000
126
+ @error_message = @error['data']['message']
127
+ @expiry = false
128
+ @can_retry = false
129
+ @can_reprepare = false
130
+ when 4030100
131
+ # Code 4030100 is "transaction_expiration_exception: transaction
132
+ # expiration exception". If we assume the expiration was valid, the
133
+ # node might be bad and needs to be dropped.
134
+
135
+ @expiry = true
136
+ @can_retry = true
137
+ @can_reprepare = false
138
+ when 4030200
139
+ # Code 4030200 is "transaction tapos exception". They are recoverable
140
+ # if the transaction hasn't expired yet. A tapos exception can be
141
+ # retried in situations where the node is behind and the tapos is
142
+ # based on a block the node doesn't know about yet.
143
+
144
+ @expiry = false
145
+ @can_retry = true
146
+
147
+ # Allow fall back to reprepare if retry fails.
148
+ @can_reprepare = true
149
+ else
150
+ @expiry = false
151
+ @can_retry = false
152
+ @can_reprepare = false
153
+ end
154
+ rescue => e
155
+ if defined? ap
156
+ if ENV['DEBUG'] == 'true'
157
+ ap error_parser_exception: e, original_response: response, backtrace: e.backtrace
158
+ else
159
+ ap error_parser_exception: e, original_response: response
160
+ end
161
+ end
162
+
163
+ @expiry = false
164
+ @can_retry = false
165
+ @can_reprepare = false
166
+ end
167
+ end
168
+
169
+ def coerce_backtrace
170
+ can_retry = false
171
+
172
+ case @error['code']
173
+ when -32003
174
+ any_of = [
175
+ 'Internal Error"',
176
+ '_api_plugin not enabled.'
177
+ ]
178
+
179
+ can_retry = error_match?('Unable to acquire database lock')
180
+
181
+ if !can_retry && error_match?(any_of)
182
+ can_retry = true
183
+ @node_degraded = true
184
+ else
185
+ @node_degraded = false
186
+ end
187
+ when -32002
188
+ can_retry = @node_degraded = error_match?('Could not find API')
189
+ when 1
190
+ can_retry = @node_degraded = error_match?('no method with name \'condenser_api')
191
+ end
192
+
193
+ can_retry
194
+ end
195
+
196
+ def error_match?(matches)
197
+ matches = [matches].flatten
198
+
199
+ any = matches.map do |match|
200
+ case match
201
+ when String
202
+ @error['message'] && @error['message'].include?(match)
203
+ when ::Array
204
+ if @error['message']
205
+ match.map { |m| m.include?(match) }.include? true
206
+ else
207
+ false
208
+ end
209
+ else; false
210
+ end
211
+ end
212
+
213
+ any.include?(true)
214
+ end
215
+
216
+ def to_s
217
+ if !!error_message && !error_message.empty?
218
+ "#{error_code}: #{error_message}"
219
+ else
220
+ error_code.to_s
221
+ end
222
+ end
223
+
224
+ def inspect
225
+ "#<#{self.class.name} [#{to_s}]>"
226
+ end
227
+ end
228
+ end