mixin_bot 2.2.2 → 2.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +5 -1
- data/docs/agent/cli.md +18 -1
- data/lib/mixin_bot/cli/errors.rb +8 -3
- data/lib/mixin_bot/cli/output.rb +5 -0
- data/lib/mixin_bot/client/error_mapper.rb +43 -23
- data/lib/mixin_bot/client.rb +22 -1
- data/lib/mixin_bot/errors.rb +190 -16
- data/lib/mixin_bot/monitor.rb +1 -10
- data/lib/mixin_bot/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 419e39522e8235c79d12c0506cb4529d34ed130170acc3805fd5827eb63e7428
|
|
4
|
+
data.tar.gz: f47598cf481cd07be7ca023c329018320a63b0b111a5ba4c68b646c8bfa2819d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20330faef09d018e2c1e914a3766270648c681d79d633fefd633b6880ff40542f9b730df77ff267974507d25ebdc107bfd5005cacc15f5f0c3968c3cd4cf824f
|
|
7
|
+
data.tar.gz: db4cfbd8fd5520d076927326316ebf07682c7065597478d8892e8f94c8a13f72a17d6e3f603f1770f6a66a61fb2c1cf17562060f6c4b9f596c0b67de1baa5031
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.0] - 2026-05-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`MixinBot::APIError`** — structured base for API failures with `code`, `description`, `request_id`, `extra`, and helpers `retryable?`, `throttle?`, `client_error?`.
|
|
15
|
+
- **Typed API errors** — `RateLimitError`, `ValidationError`, `ConflictError`, `TransferError`, `TransientError`, `AppUpdateRequiredError`, `InvalidAddressFormatError`, `ServerError`.
|
|
16
|
+
- **`MixinBot.retryable?(error)`** — canonical retry policy for network timeouts, server errors (500+), and transient codes (10104, 10105).
|
|
17
|
+
- **CLI** — structured error kind `rate_limit`; API errors in JSON output include `code`, `request_id`, and `throttle` when available.
|
|
18
|
+
- **`test/mixin_bot/client/test_error_mapper.rb`** — unit tests for full official error-code catalog, legacy codes, HTTP fallbacks, and retry/throttle semantics.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **BREAKING: Error code mapping** — aligned to [Mixin API error codes](https://developers.mixin.one/docs/api/error-codes). Code `429` raises `RateLimitError` (not `ForbiddenError`). Codes `10002` and `20116` raise `ValidationError` and `ConflictError` respectively. `ForbiddenError` is reserved for code `403`.
|
|
23
|
+
- **`Monitor.check_retryable_error`** — delegates to `MixinBot.retryable?` (no longer retries on `"insufficient"` message substrings).
|
|
24
|
+
- **`Client#parse_response!`** — HTTP status fallback for 401/403/429/5xx when JSON `error` is absent (CDN/proxy edge cases).
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- Rescue rate limits explicitly: `rescue MixinBot::RateLimitError` (or check `error.throttle?`).
|
|
29
|
+
- Replace string-based retry heuristics with `MixinBot.retryable?(error)`.
|
|
30
|
+
- If you rescued `ForbiddenError` expecting throttling, update to handle `RateLimitError` separately.
|
|
31
|
+
|
|
10
32
|
## [2.2.2] - 2026-05-24
|
|
11
33
|
|
|
12
34
|
### Fixed
|
data/README.md
CHANGED
|
@@ -194,7 +194,11 @@ Top-level helpers on **`MixinBot::API`**: `access_token`, `encode_raw_transactio
|
|
|
194
194
|
|
|
195
195
|
### Errors
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
API failures raise structured `MixinBot::APIError` subclasses with `code`, `description`, `request_id`, and behavior helpers (`retryable?`, `throttle?`). The full mapping follows the [Mixin error codes](https://developers.mixin.one/docs/api/error-codes) table — notably `429` → `RateLimitError`, not `ForbiddenError`.
|
|
198
|
+
|
|
199
|
+
Use `MixinBot.retryable?(error)` for job retry decisions; use `error.throttle?` on `RateLimitError` for global backoff. `Monitor.check_retryable_error` delegates to the same policy.
|
|
200
|
+
|
|
201
|
+
Local validation errors (`ArgumentError`, `InvalidInvoiceFormatError`, …) and `InsufficientAppBillingError` (billing preflight) are separate from API envelope errors. See `lib/mixin_bot/errors.rb`.
|
|
198
202
|
|
|
199
203
|
### Multiple bots
|
|
200
204
|
|
data/docs/agent/cli.md
CHANGED
|
@@ -72,7 +72,24 @@ Error (stderr, exit 1):
|
|
|
72
72
|
}
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
API failures may also include `code`, `request_id`, and `throttle` (true for rate limits):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"status": "error",
|
|
80
|
+
"error": {
|
|
81
|
+
"kind": "rate_limit",
|
|
82
|
+
"message": "GET | /me | {}, errcode: 429, ...",
|
|
83
|
+
"code": 429,
|
|
84
|
+
"request_id": "abc-123",
|
|
85
|
+
"throttle": true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Error kinds: `invalid_args`, `auth`, `not_found`, `rate_limit`, `api_error`, `billing`, `unsupported`, `conflict`, `internal`.
|
|
91
|
+
|
|
92
|
+
For retry logic in automation, prefer `MixinBot.retryable?(exception)` in Ruby; CLI kinds mark `rate_limit` as non-retryable (use global backoff when `throttle` is true).
|
|
76
93
|
|
|
77
94
|
## Commands
|
|
78
95
|
|
data/lib/mixin_bot/cli/errors.rb
CHANGED
|
@@ -9,6 +9,7 @@ module MixinBot
|
|
|
9
9
|
invalid_args: { retryable: false, description: 'Invalid or missing arguments' },
|
|
10
10
|
auth: { retryable: false, description: 'Authentication or authorization failed' },
|
|
11
11
|
not_found: { retryable: false, description: 'Requested resource was not found' },
|
|
12
|
+
rate_limit: { retryable: false, description: 'API rate limit exceeded; slow down globally' },
|
|
12
13
|
api_error: { retryable: false, description: 'Mixin API returned an error' },
|
|
13
14
|
unsupported: { retryable: false, description: 'Operation is not supported in this context' },
|
|
14
15
|
conflict: { retryable: false, description: 'Resource exists with incompatible configuration' },
|
|
@@ -30,16 +31,19 @@ module MixinBot
|
|
|
30
31
|
|
|
31
32
|
def kind_for_exception(error)
|
|
32
33
|
case error
|
|
33
|
-
when MixinBot::ArgumentError, ::ArgumentError
|
|
34
|
+
when MixinBot::ArgumentError, ::ArgumentError, ValidationError, InvalidAddressFormatError
|
|
34
35
|
:invalid_args
|
|
36
|
+
when RateLimitError
|
|
37
|
+
:rate_limit
|
|
35
38
|
when UnauthorizedError, ForbiddenError, PinError, ConfigurationNotValidError
|
|
36
39
|
:auth
|
|
37
40
|
when NotFoundError, UserNotFoundError
|
|
38
41
|
:not_found
|
|
39
42
|
when InsufficientAppBillingError
|
|
40
43
|
:billing
|
|
41
|
-
when ResponseError, RequestError, HttpError,
|
|
42
|
-
InsufficientBalanceError, UtxoInsufficientError, InsufficientPoolError
|
|
44
|
+
when ResponseError, RequestError, HttpError, ServerError,
|
|
45
|
+
InsufficientBalanceError, UtxoInsufficientError, InsufficientPoolError,
|
|
46
|
+
ConflictError, TransferError, TransientError, AppUpdateRequiredError
|
|
43
47
|
:api_error
|
|
44
48
|
else
|
|
45
49
|
:internal
|
|
@@ -50,6 +54,7 @@ module MixinBot
|
|
|
50
54
|
msg = message.to_s.downcase
|
|
51
55
|
return :auth if msg.include?('unauthorized') || msg.include?('authentication')
|
|
52
56
|
return :not_found if msg.include?('not found') || msg.include?('404')
|
|
57
|
+
return :rate_limit if msg.include?('too many requests') || msg.include?('errcode: 429')
|
|
53
58
|
return :unsupported if msg.include?('unsupported') || msg.include?('not supported')
|
|
54
59
|
return :invalid_args if msg.include?('invalid') || msg.include?('unknown')
|
|
55
60
|
|
data/lib/mixin_bot/cli/output.rb
CHANGED
|
@@ -84,6 +84,11 @@ module MixinBot
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
error_body['error']['hint'] = hint if hint.present?
|
|
87
|
+
if exception.is_a?(MixinBot::APIError)
|
|
88
|
+
error_body['error']['code'] = exception.code unless exception.code.nil?
|
|
89
|
+
error_body['error']['request_id'] = exception.request_id if exception.request_id.present?
|
|
90
|
+
error_body['error']['throttle'] = true if exception.respond_to?(:throttle?) && exception.throttle?
|
|
91
|
+
end
|
|
87
92
|
warn(JSON.generate(error_body))
|
|
88
93
|
else
|
|
89
94
|
warn(format_error(message))
|
|
@@ -6,34 +6,54 @@ module MixinBot
|
|
|
6
6
|
# Maps Mixin API +error+ objects to Ruby exceptions.
|
|
7
7
|
#
|
|
8
8
|
module ErrorMapper
|
|
9
|
+
CODE_MAP = {
|
|
10
|
+
400 => ValidationError,
|
|
11
|
+
401 => UnauthorizedError,
|
|
12
|
+
403 => ForbiddenError,
|
|
13
|
+
404 => NotFoundError,
|
|
14
|
+
429 => RateLimitError,
|
|
15
|
+
10_002 => ValidationError,
|
|
16
|
+
10_006 => AppUpdateRequiredError,
|
|
17
|
+
10_104 => TransientError,
|
|
18
|
+
10_105 => TransientError,
|
|
19
|
+
10_404 => UserNotFoundError,
|
|
20
|
+
20_116 => ConflictError,
|
|
21
|
+
20_117 => InsufficientBalanceError,
|
|
22
|
+
20_118 => PinError,
|
|
23
|
+
20_119 => PinError,
|
|
24
|
+
20_120 => TransferError,
|
|
25
|
+
20_121 => UnauthorizedError,
|
|
26
|
+
20_123 => ConflictError,
|
|
27
|
+
20_124 => InsufficientBalanceError,
|
|
28
|
+
20_125 => ConflictError,
|
|
29
|
+
20_127 => TransferError,
|
|
30
|
+
20_131 => ValidationError,
|
|
31
|
+
20_133 => ConflictError,
|
|
32
|
+
20_134 => TransferError,
|
|
33
|
+
20_135 => TransferError,
|
|
34
|
+
20_150 => ValidationError,
|
|
35
|
+
30_102 => InvalidAddressFormatError,
|
|
36
|
+
30_103 => InsufficientPoolError,
|
|
37
|
+
500 => ServerError,
|
|
38
|
+
7000 => ServerError,
|
|
39
|
+
7001 => ServerError
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
9
42
|
module_function
|
|
10
43
|
|
|
11
44
|
def raise_for!(verb:, path:, body:, response:, result:)
|
|
12
45
|
err = result['error'] || {}
|
|
13
|
-
code = err['code']
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
code = err['code']&.to_i
|
|
47
|
+
klass = CODE_MAP[code] || default_class_for_code(code)
|
|
48
|
+
raise APIError.build(klass, verb:, path:, body:, response:, result:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build(klass, verb:, path:, body:, response:, result:, **)
|
|
52
|
+
APIError.build(klass, verb:, path:, body:, response:, result:, **)
|
|
53
|
+
end
|
|
18
54
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
raise UnauthorizedError, errmsg
|
|
22
|
-
when 403, 20_116, 10_002, 429
|
|
23
|
-
raise ForbiddenError, errmsg
|
|
24
|
-
when 404
|
|
25
|
-
raise NotFoundError, errmsg
|
|
26
|
-
when 20_117
|
|
27
|
-
raise InsufficientBalanceError, errmsg
|
|
28
|
-
when 20_118, 20_119
|
|
29
|
-
raise PinError, errmsg
|
|
30
|
-
when 30_103
|
|
31
|
-
raise InsufficientPoolError, errmsg
|
|
32
|
-
when 10_404
|
|
33
|
-
raise UserNotFoundError, errmsg
|
|
34
|
-
else
|
|
35
|
-
raise ResponseError, errmsg
|
|
36
|
-
end
|
|
55
|
+
def default_class_for_code(_code)
|
|
56
|
+
ResponseError
|
|
37
57
|
end
|
|
38
58
|
end
|
|
39
59
|
end
|
data/lib/mixin_bot/client.rb
CHANGED
|
@@ -125,9 +125,30 @@ module MixinBot
|
|
|
125
125
|
|
|
126
126
|
def parse_response!(verb:, path:, body:, response:)
|
|
127
127
|
result = response.body
|
|
128
|
-
|
|
128
|
+
result = {} unless result.is_a?(Hash)
|
|
129
|
+
|
|
130
|
+
if result['error'].blank?
|
|
131
|
+
raise_http_status_error!(verb:, path:, body:, response:) if http_error_status?(response.status)
|
|
132
|
+
return MixinBot::Models::ApiEnvelope.new(result)
|
|
133
|
+
end
|
|
129
134
|
|
|
130
135
|
ErrorMapper.raise_for!(verb:, path:, body:, response:, result:)
|
|
131
136
|
end
|
|
137
|
+
|
|
138
|
+
def http_error_status?(status)
|
|
139
|
+
[401, 403, 429].include?(status) || status >= 500
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def raise_http_status_error!(verb:, path:, body:, response:)
|
|
143
|
+
klass =
|
|
144
|
+
case response.status
|
|
145
|
+
when 429 then RateLimitError
|
|
146
|
+
when 401 then UnauthorizedError
|
|
147
|
+
when 403 then ForbiddenError
|
|
148
|
+
else ServerError
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
raise ErrorMapper.build(klass, verb:, path:, body:, response:, result: {})
|
|
152
|
+
end
|
|
132
153
|
end
|
|
133
154
|
end
|
data/lib/mixin_bot/errors.rb
CHANGED
|
@@ -7,7 +7,7 @@ module MixinBot
|
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
|
|
9
9
|
##
|
|
10
|
-
# Raised when invalid arguments are provided.
|
|
10
|
+
# Raised when invalid arguments are provided (local validation, not API code 400).
|
|
11
11
|
#
|
|
12
12
|
class ArgumentError < StandardError; end
|
|
13
13
|
|
|
@@ -22,34 +22,196 @@ module MixinBot
|
|
|
22
22
|
class RequestError < Error; end
|
|
23
23
|
|
|
24
24
|
##
|
|
25
|
-
#
|
|
25
|
+
# Base class for Mixin API error responses with structured metadata.
|
|
26
26
|
#
|
|
27
|
-
class
|
|
27
|
+
class APIError < Error
|
|
28
|
+
attr_reader :code, :description, :status, :http_status, :request_id, :server_time,
|
|
29
|
+
:retry_after, :extra, :path, :verb, :body
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Metrics/ParameterLists -- structured API error metadata
|
|
32
|
+
def initialize(message = nil, code: nil, description: nil, status: nil, http_status: nil,
|
|
33
|
+
request_id: nil, server_time: nil, retry_after: nil, extra: nil,
|
|
34
|
+
path: nil, verb: nil, body: nil)
|
|
35
|
+
@code = code&.to_i
|
|
36
|
+
@description = description
|
|
37
|
+
@status = status&.to_i
|
|
38
|
+
@http_status = http_status&.to_i
|
|
39
|
+
@request_id = request_id
|
|
40
|
+
@server_time = server_time
|
|
41
|
+
@retry_after = retry_after
|
|
42
|
+
@extra = extra
|
|
43
|
+
@path = path
|
|
44
|
+
@verb = verb
|
|
45
|
+
@body = body
|
|
46
|
+
super(message || formatted_message)
|
|
47
|
+
end
|
|
48
|
+
# rubocop:enable Metrics/ParameterLists
|
|
49
|
+
|
|
50
|
+
def client_error?
|
|
51
|
+
c = code.to_i
|
|
52
|
+
return true if c.between?(400, 499)
|
|
53
|
+
return true if http_status == 202 && c.positive? && c < 500
|
|
54
|
+
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def retryable?
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def throttle?
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def formatted_message
|
|
67
|
+
format(
|
|
68
|
+
'%<verb>s | %<path>s | %<body>s, errcode: %<code>s, errmsg: %<description>s, ' \
|
|
69
|
+
'request_id: %<request_id>s, server_time: %<server_time>s',
|
|
70
|
+
verb: verb.to_s.upcase,
|
|
71
|
+
path: path.to_s,
|
|
72
|
+
body: body.to_s,
|
|
73
|
+
code: code,
|
|
74
|
+
description: description,
|
|
75
|
+
request_id: request_id,
|
|
76
|
+
server_time: server_time
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
def build(klass, verb:, path:, body:, response:, result:, code: nil, description: nil)
|
|
82
|
+
err = result.is_a?(Hash) ? (result['error'] || {}) : {}
|
|
83
|
+
resolved_code = code || err['code'] || infer_code_from_http(response)
|
|
84
|
+
resolved_description = description || err['description'] || http_status_description(response&.status)
|
|
85
|
+
headers = response&.headers || {}
|
|
86
|
+
retry_after = headers['Retry-After'] if klass <= RateLimitError
|
|
87
|
+
|
|
88
|
+
klass.new(
|
|
89
|
+
code: resolved_code,
|
|
90
|
+
description: resolved_description,
|
|
91
|
+
status: err['status'],
|
|
92
|
+
http_status: response&.status,
|
|
93
|
+
request_id: headers['X-Request-Id'],
|
|
94
|
+
server_time: headers['X-Server-Time'],
|
|
95
|
+
retry_after: retry_after,
|
|
96
|
+
extra: err['extra'],
|
|
97
|
+
path: path,
|
|
98
|
+
verb: verb,
|
|
99
|
+
body: body
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def infer_code_from_http(response)
|
|
106
|
+
return nil unless response
|
|
107
|
+
|
|
108
|
+
case response.status
|
|
109
|
+
when 401 then 401
|
|
110
|
+
when 403 then 403
|
|
111
|
+
when 429 then 429
|
|
112
|
+
else
|
|
113
|
+
response.status if response.status >= 500
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def http_status_description(status)
|
|
118
|
+
case status
|
|
119
|
+
when 401 then 'Unauthorized'
|
|
120
|
+
when 403 then 'Forbidden'
|
|
121
|
+
when 429 then 'Too Many Requests'
|
|
122
|
+
when 500.. then 'Internal Server Error'
|
|
123
|
+
else
|
|
124
|
+
"HTTP #{status}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
28
129
|
|
|
29
130
|
##
|
|
30
|
-
# Raised when
|
|
131
|
+
# Raised when Mixin API returns an error response (unmapped or generic codes).
|
|
31
132
|
#
|
|
32
|
-
class
|
|
133
|
+
class ResponseError < APIError
|
|
134
|
+
def retryable?
|
|
135
|
+
code.to_i >= 500
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# Raised when a requested resource is not found (error code 404).
|
|
141
|
+
#
|
|
142
|
+
class NotFoundError < APIError; end
|
|
33
143
|
|
|
34
144
|
##
|
|
35
145
|
# Raised when a user is not found (error code 10404).
|
|
36
146
|
#
|
|
37
|
-
class UserNotFoundError <
|
|
147
|
+
class UserNotFoundError < APIError; end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
# Raised when authentication fails (error codes 401, 20121).
|
|
151
|
+
#
|
|
152
|
+
class UnauthorizedError < APIError; end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Raised when access is forbidden (error code 403).
|
|
156
|
+
#
|
|
157
|
+
class ForbiddenError < APIError; end
|
|
158
|
+
|
|
159
|
+
##
|
|
160
|
+
# Raised when the API rate limit is exceeded (error code 429).
|
|
161
|
+
#
|
|
162
|
+
class RateLimitError < APIError
|
|
163
|
+
def throttle?
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
##
|
|
169
|
+
# Raised when request data is invalid (error codes 400, 10002, 20131, 20150).
|
|
170
|
+
#
|
|
171
|
+
class ValidationError < APIError; end
|
|
172
|
+
|
|
173
|
+
##
|
|
174
|
+
# Raised when a resource conflict or capacity limit applies (error codes 20116, 20123, 20125, 20133).
|
|
175
|
+
#
|
|
176
|
+
class ConflictError < APIError; end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# Raised for transfer/withdraw amount or fee constraints.
|
|
180
|
+
#
|
|
181
|
+
class TransferError < APIError; end
|
|
182
|
+
|
|
183
|
+
##
|
|
184
|
+
# Raised for transient conditions that may succeed on retry (error codes 10104, 10105).
|
|
185
|
+
#
|
|
186
|
+
class TransientError < APIError
|
|
187
|
+
def retryable?
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
end
|
|
38
191
|
|
|
39
192
|
##
|
|
40
|
-
# Raised when
|
|
193
|
+
# Raised when the app must be updated (error code 10006).
|
|
41
194
|
#
|
|
42
|
-
class
|
|
195
|
+
class AppUpdateRequiredError < APIError; end
|
|
43
196
|
|
|
44
197
|
##
|
|
45
|
-
# Raised when
|
|
198
|
+
# Raised when an address format is invalid (error code 30102).
|
|
46
199
|
#
|
|
47
|
-
class
|
|
200
|
+
class InvalidAddressFormatError < APIError; end
|
|
48
201
|
|
|
49
202
|
##
|
|
50
|
-
# Raised
|
|
203
|
+
# Raised for server-side failures (error codes 500, 7000, 7001).
|
|
51
204
|
#
|
|
52
|
-
class
|
|
205
|
+
class ServerError < APIError
|
|
206
|
+
def retryable?
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# Raised when there is insufficient balance for a transaction (error codes 20117, 20124).
|
|
213
|
+
#
|
|
214
|
+
class InsufficientBalanceError < APIError; end
|
|
53
215
|
|
|
54
216
|
##
|
|
55
217
|
# Raised when app prepaid billing credit lacks headroom for a billed operation.
|
|
@@ -77,8 +239,8 @@ module MixinBot
|
|
|
77
239
|
class UtxoInsufficientError < InsufficientBalanceError
|
|
78
240
|
attr_reader :total_input, :total_output, :output_size
|
|
79
241
|
|
|
80
|
-
def initialize(message, total_input: nil, total_output: nil, output_size: nil)
|
|
81
|
-
super(message)
|
|
242
|
+
def initialize(message = nil, total_input: nil, total_output: nil, output_size: nil, **)
|
|
243
|
+
super(message, **)
|
|
82
244
|
@total_input = total_input
|
|
83
245
|
@total_output = total_output
|
|
84
246
|
@output_size = output_size
|
|
@@ -88,12 +250,12 @@ module MixinBot
|
|
|
88
250
|
##
|
|
89
251
|
# Raised when there is insufficient pool for a transaction (error code 30103).
|
|
90
252
|
#
|
|
91
|
-
class InsufficientPoolError <
|
|
253
|
+
class InsufficientPoolError < APIError; end
|
|
92
254
|
|
|
93
255
|
##
|
|
94
256
|
# Raised when PIN verification fails (error codes 20118, 20119).
|
|
95
257
|
#
|
|
96
|
-
class PinError <
|
|
258
|
+
class PinError < APIError; end
|
|
97
259
|
|
|
98
260
|
##
|
|
99
261
|
# Raised when NFO memo format is invalid.
|
|
@@ -119,4 +281,16 @@ module MixinBot
|
|
|
119
281
|
# Raised when invoice format is invalid.
|
|
120
282
|
#
|
|
121
283
|
class InvalidInvoiceFormatError < Error; end
|
|
284
|
+
|
|
285
|
+
class << self
|
|
286
|
+
##
|
|
287
|
+
# Canonical retry decision for API and network failures.
|
|
288
|
+
#
|
|
289
|
+
def retryable?(error)
|
|
290
|
+
return true if error.is_a?(Faraday::TimeoutError) || error.is_a?(Faraday::ConnectionFailed)
|
|
291
|
+
return error.retryable? if error.respond_to?(:retryable?)
|
|
292
|
+
|
|
293
|
+
false
|
|
294
|
+
end
|
|
295
|
+
end
|
|
122
296
|
end
|
data/lib/mixin_bot/monitor.rb
CHANGED
|
@@ -61,16 +61,7 @@ module MixinBot
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def check_retryable_error(error)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
reason = error.message.to_s.downcase
|
|
67
|
-
return true if reason.include?('timeout')
|
|
68
|
-
return true if reason.include?('internal server')
|
|
69
|
-
return true if reason.include?('insufficient')
|
|
70
|
-
return true if reason.include?('inputs locked by')
|
|
71
|
-
return true if reason.include?('by other transaction')
|
|
72
|
-
|
|
73
|
-
false
|
|
64
|
+
MixinBot.retryable?(error)
|
|
74
65
|
end
|
|
75
66
|
end
|
|
76
67
|
end
|
data/lib/mixin_bot/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mixin_bot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- an-lee
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|