mixin_bot 2.2.2 → 2.4.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/AGENTS.md +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +6 -2
- data/docs/agent/cli.md +18 -1
- data/lib/mixin_bot/address.rb +7 -7
- data/lib/mixin_bot/api/address.rb +1 -1
- data/lib/mixin_bot/api/blaze.rb +1 -1
- data/lib/mixin_bot/api/encrypted_message.rb +6 -6
- data/lib/mixin_bot/api/legacy_transaction.rb +1 -1
- data/lib/mixin_bot/api/legacy_user.rb +7 -4
- data/lib/mixin_bot/api/payment.rb +3 -0
- data/lib/mixin_bot/api/transaction.rb +1 -1
- data/lib/mixin_bot/api/user.rb +2 -2
- 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/invoice.rb +13 -13
- data/lib/mixin_bot/monitor.rb +1 -10
- data/lib/mixin_bot/nfo.rb +18 -18
- data/lib/mixin_bot/transaction/decoder.rb +1 -1
- data/lib/mixin_bot/transaction/encoder.rb +73 -76
- data/lib/mixin_bot/utils/address.rb +1 -1
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mvm/registry.rb +3 -3
- data/llms.txt +1 -1
- metadata +5 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06d699c83730cba7bfd93dedbc47f96b1d97c1cd2d4816babd0d310effce3f99
|
|
4
|
+
data.tar.gz: 8ac62f6fbd15d257d3fae717eb3e3ae1fbca0a46caf9f7f6a03b81797ec7fdf7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ded5aea75050e8ea99a1fd95de4473229fd0dc8c38fa819b01f039ed1917b791e4ccb8183e519521973447b82f909a333f467b438aacd1a9e50f8dff679ea85d
|
|
7
|
+
data.tar.gz: e4041c6a976a0127cb4d2740cda7315f5c16d72cd6edc0b0ca54f978fac8ce64d33d464e730863181473d361b1f5b69d98ae0903e746f5fea0ba979db8d0e354
|
data/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md — MixinBot
|
|
2
2
|
|
|
3
|
-
Ruby gem (v2.
|
|
3
|
+
Ruby gem (v2.3.0): Mixin Network REST SDK + `mixinbot` CLI. Parity targets: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client), [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client).
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.0] - 2026-07-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Blaze WebSocket User-Agent** — the client now sends a `User-Agent` header on Blaze WebSocket connections.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`sha3` dependency** — upgraded from `~> 1.0` to `~> 2.2` (sha3 2.x renames `SHA3::Digest::SHA256` to `SHA3::Digest::SHA3_256`; hash output is unchanged).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Payment URL formatting** — `safe_pay_url` now formats amounts without scientific notation for very small or large values.
|
|
23
|
+
|
|
24
|
+
### Performance
|
|
25
|
+
|
|
26
|
+
- **Encoder allocations** — replaced `Array` splat patterns with `Array#concat` in `Transaction::Encoder`, `Nfo`, `InvoiceEntry`, `EncryptedMessage`, and `MVM::Registry` to avoid O(n²) reallocation.
|
|
27
|
+
- **Bytes pack caching** — cached `bytes.pack` results in transaction encoders to reduce duplicate allocations.
|
|
28
|
+
|
|
29
|
+
## [2.3.0] - 2026-05-27
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`MixinBot::APIError`** — structured base for API failures with `code`, `description`, `request_id`, `extra`, and helpers `retryable?`, `throttle?`, `client_error?`.
|
|
34
|
+
- **Typed API errors** — `RateLimitError`, `ValidationError`, `ConflictError`, `TransferError`, `TransientError`, `AppUpdateRequiredError`, `InvalidAddressFormatError`, `ServerError`.
|
|
35
|
+
- **`MixinBot.retryable?(error)`** — canonical retry policy for network timeouts, server errors (500+), and transient codes (10104, 10105).
|
|
36
|
+
- **CLI** — structured error kind `rate_limit`; API errors in JSON output include `code`, `request_id`, and `throttle` when available.
|
|
37
|
+
- **`test/mixin_bot/client/test_error_mapper.rb`** — unit tests for full official error-code catalog, legacy codes, HTTP fallbacks, and retry/throttle semantics.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **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`.
|
|
42
|
+
- **`Monitor.check_retryable_error`** — delegates to `MixinBot.retryable?` (no longer retries on `"insufficient"` message substrings).
|
|
43
|
+
- **`Client#parse_response!`** — HTTP status fallback for 401/403/429/5xx when JSON `error` is absent (CDN/proxy edge cases).
|
|
44
|
+
|
|
45
|
+
### Migration
|
|
46
|
+
|
|
47
|
+
- Rescue rate limits explicitly: `rescue MixinBot::RateLimitError` (or check `error.throttle?`).
|
|
48
|
+
- Replace string-based retry heuristics with `MixinBot.retryable?(error)`.
|
|
49
|
+
- If you rescued `ForbiddenError` expecting throttling, update to handle `RateLimitError` separately.
|
|
50
|
+
|
|
10
51
|
## [2.2.2] - 2026-05-24
|
|
11
52
|
|
|
12
53
|
### Fixed
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Ruby SDK and CLI for [Mixin Network](https://developers.mixin.one/docs): authent
|
|
|
6
6
|
|
|
7
7
|
The gem aims for **parity with the official [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client)** Go SDK and **[bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client)** Node SDK. See [API_COVERAGE.md](API_COVERAGE.md) for the full mapping; run `rake mixin_bot:api_coverage` to confirm no gaps are marked missing.
|
|
8
8
|
|
|
9
|
-
Current gem version: **2.
|
|
9
|
+
Current gem version: **2.3.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -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/address.rb
CHANGED
|
@@ -109,7 +109,7 @@ module MixinBot
|
|
|
109
109
|
|
|
110
110
|
self.payload = prefix + msg
|
|
111
111
|
|
|
112
|
-
checksum = SHA3::Digest::
|
|
112
|
+
checksum = SHA3::Digest::SHA3_256.digest(MIX_ADDRESS_PREFIX + payload)
|
|
113
113
|
data = payload + checksum[0...4]
|
|
114
114
|
data = Base58.binary_to_base58 data, :bitcoin
|
|
115
115
|
self.address = "#{MIX_ADDRESS_PREFIX}#{data}"
|
|
@@ -126,10 +126,10 @@ module MixinBot
|
|
|
126
126
|
raise ArgumentError, 'invalid address, length invalid' if data.length < 3 + 16 + 4
|
|
127
127
|
|
|
128
128
|
self.payload = data[...-4]
|
|
129
|
-
checksum = SHA3::Digest::
|
|
129
|
+
checksum = SHA3::Digest::SHA3_256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
|
|
130
130
|
raise ArgumentError, 'invalid address, checksum invalid' unless checksum == data[-4..]
|
|
131
131
|
else
|
|
132
|
-
checksum = SHA3::Digest::
|
|
132
|
+
checksum = SHA3::Digest::SHA3_256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
|
|
133
133
|
data = payload + checksum
|
|
134
134
|
data = Base58.binary_to_base58 data, :bitcoin
|
|
135
135
|
self.address = "#{MIX_ADDRESS_PREFIX}#{data}"
|
|
@@ -171,7 +171,7 @@ module MixinBot
|
|
|
171
171
|
|
|
172
172
|
def encode
|
|
173
173
|
msg = MAIN_ADDRESS_PREFIX + public_key
|
|
174
|
-
checksum = SHA3::Digest::
|
|
174
|
+
checksum = SHA3::Digest::SHA3_256.digest msg
|
|
175
175
|
data = public_key + checksum[0...4]
|
|
176
176
|
base58 = Base58.binary_to_base58 data, :bitcoin
|
|
177
177
|
self.address = "#{MAIN_ADDRESS_PREFIX}#{base58}"
|
|
@@ -188,7 +188,7 @@ module MixinBot
|
|
|
188
188
|
payload = data[...-4]
|
|
189
189
|
|
|
190
190
|
msg = MAIN_ADDRESS_PREFIX + payload
|
|
191
|
-
checksum = SHA3::Digest::
|
|
191
|
+
checksum = SHA3::Digest::SHA3_256.digest msg
|
|
192
192
|
|
|
193
193
|
raise ArgumentError, 'invalid address' unless checksum[0...4] == data[-4..]
|
|
194
194
|
|
|
@@ -200,8 +200,8 @@ module MixinBot
|
|
|
200
200
|
def self.burning_address
|
|
201
201
|
seed = "\0" * 64
|
|
202
202
|
|
|
203
|
-
digest1 = SHA3::Digest::
|
|
204
|
-
digest2 = SHA3::Digest::
|
|
203
|
+
digest1 = SHA3::Digest::SHA3_256.digest seed
|
|
204
|
+
digest2 = SHA3::Digest::SHA3_256.digest digest1
|
|
205
205
|
src = digest1 + digest2
|
|
206
206
|
|
|
207
207
|
spend_key = MixinBot::Utils.shared_public_key(seed)
|
data/lib/mixin_bot/api/blaze.rb
CHANGED
|
@@ -10,7 +10,7 @@ module MixinBot
|
|
|
10
10
|
Faye::WebSocket::Client.new(
|
|
11
11
|
format('wss://%<host>s/', host: config.blaze_host),
|
|
12
12
|
['Mixin-Blaze-1'],
|
|
13
|
-
headers: { 'Authorization' => authorization },
|
|
13
|
+
headers: { 'Authorization' => authorization, 'User-Agent' => "mixin_bot/#{MixinBot::VERSION}" },
|
|
14
14
|
ping: 60
|
|
15
15
|
)
|
|
16
16
|
end
|
|
@@ -126,8 +126,8 @@ module MixinBot
|
|
|
126
126
|
ciphertext = encrypter.update(Base64.urlsafe_decode64(data)) + encrypter.final + encrypter.auth_tag
|
|
127
127
|
|
|
128
128
|
bytes = [1]
|
|
129
|
-
bytes
|
|
130
|
-
bytes
|
|
129
|
+
bytes.concat([sessions.size].pack('v*').bytes)
|
|
130
|
+
bytes.concat(JOSE::JWA::Ed25519.pk_to_curve25519(pk).bytes)
|
|
131
131
|
|
|
132
132
|
sessions.each do |session|
|
|
133
133
|
aes_key = JOSE::JWA::X25519.shared_secret(
|
|
@@ -143,12 +143,12 @@ module MixinBot
|
|
|
143
143
|
iv = encrypter.random_iv
|
|
144
144
|
encrypter.iv = iv
|
|
145
145
|
|
|
146
|
-
bytes
|
|
147
|
-
bytes
|
|
146
|
+
bytes.concat((MixinBot::UUID.new(hex: session['session_id']).packed + iv).bytes)
|
|
147
|
+
bytes.concat(encrypter.update(key + padtext).bytes)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
bytes
|
|
151
|
-
bytes
|
|
150
|
+
bytes.concat(nounce.bytes)
|
|
151
|
+
bytes.concat(ciphertext.bytes)
|
|
152
152
|
|
|
153
153
|
Base64.urlsafe_encode64 bytes.pack('C*'), padding: false
|
|
154
154
|
end
|
|
@@ -10,12 +10,15 @@ module MixinBot
|
|
|
10
10
|
kl = keystore.with_indifferent_access
|
|
11
11
|
priv = OpenSSL::PKey::RSA.new(kl[:private_key])
|
|
12
12
|
token = Base64.decode64(kl[:pin_token])
|
|
13
|
+
# Use string keys with the dash form (`'oaep-label'`) for Ruby 4.0
|
|
14
|
+
# compatibility; the underscore form `oaep_label` is no longer
|
|
15
|
+
# recognized by the OpenSSL EVP_PKEY_CTX in Ruby 4.0.
|
|
13
16
|
key_bytes = priv.decrypt(
|
|
14
17
|
token,
|
|
15
|
-
rsa_padding_mode
|
|
16
|
-
rsa_oaep_md
|
|
17
|
-
rsa_mgf1_md
|
|
18
|
-
|
|
18
|
+
'rsa_padding_mode' => 'oaep',
|
|
19
|
+
'rsa_oaep_md' => 'sha256',
|
|
20
|
+
'rsa_mgf1_md' => 'sha1',
|
|
21
|
+
'oaep-label' => kl[:session_id]
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
pin_byte = kl[:pin].to_s.b
|
|
@@ -12,6 +12,9 @@ module MixinBot
|
|
|
12
12
|
trace_id = kwargs[:trace_id] || SecureRandom.uuid
|
|
13
13
|
|
|
14
14
|
mix_address = MixinBot.utils.build_mix_address(members:, threshold:)
|
|
15
|
+
# Format amount without scientific notation so the Mixin web UI's URL
|
|
16
|
+
# parser accepts it. Mirrors `MixinBot::Utils::Address#build_safe_recipient`.
|
|
17
|
+
amount = format('%.8f', amount.to_d.to_r).gsub(/\.?0+\z/, '')
|
|
15
18
|
|
|
16
19
|
"https://mixin.one/pay/#{mix_address}?amount=#{amount}&asset=#{asset_id}&memo=#{memo}&trace=#{trace_id}"
|
|
17
20
|
end
|
|
@@ -138,7 +138,7 @@ module MixinBot
|
|
|
138
138
|
aid = h[:asset_id]
|
|
139
139
|
raise ArgumentError, 'utxo asset_id or asset is required' if aid.blank?
|
|
140
140
|
|
|
141
|
-
SHA3::Digest::
|
|
141
|
+
SHA3::Digest::SHA3_256.hexdigest(aid)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
asset = mixin_asset_for.call(utxos[0])
|
data/lib/mixin_bot/api/user.rb
CHANGED
|
@@ -164,9 +164,9 @@ module MixinBot
|
|
|
164
164
|
signing_key_hex = keypair[1].unpack1('H*')
|
|
165
165
|
|
|
166
166
|
# NOTE: the Go SDK's +crypto.Sha256Hash+ is misleadingly named — it
|
|
167
|
-
# actually computes SHA3-256, so +SHA3::Digest::
|
|
167
|
+
# actually computes SHA3-256, so +SHA3::Digest::SHA3_256+ is the correct
|
|
168
168
|
# match. See bot-api-go-client safe_user.go +RegisterSafeBareUser+.
|
|
169
|
-
app_id_hash = SHA3::Digest::
|
|
169
|
+
app_id_hash = SHA3::Digest::SHA3_256.hexdigest config.app_id
|
|
170
170
|
signature = Base64.urlsafe_encode64(
|
|
171
171
|
JOSE::JWA::Ed25519.sign([app_id_hash].pack('H*'), keypair[1]),
|
|
172
172
|
padding: false
|
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/invoice.rb
CHANGED
|
@@ -54,7 +54,7 @@ module MixinBot
|
|
|
54
54
|
payload = payload.pack('C*')
|
|
55
55
|
|
|
56
56
|
# Calculate checksum
|
|
57
|
-
checksum = SHA3::Digest::
|
|
57
|
+
checksum = SHA3::Digest::SHA3_256.digest(INVOICE_PREFIX + payload)[0...4]
|
|
58
58
|
|
|
59
59
|
# Combine everything and encode to base64
|
|
60
60
|
self.address = INVOICE_PREFIX + Base64.urlsafe_encode64(payload + checksum, padding: false)
|
|
@@ -68,7 +68,7 @@ module MixinBot
|
|
|
68
68
|
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice payload size' if data.size < 3 + 23 + 1
|
|
69
69
|
|
|
70
70
|
payload = data[...-4]
|
|
71
|
-
checksum = SHA3::Digest::
|
|
71
|
+
checksum = SHA3::Digest::SHA3_256.digest(INVOICE_PREFIX + payload)[0...4]
|
|
72
72
|
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice checksum' unless checksum == data[-4..]
|
|
73
73
|
|
|
74
74
|
payload = payload.bytes
|
|
@@ -141,29 +141,29 @@ module MixinBot
|
|
|
141
141
|
def encode
|
|
142
142
|
bytes = []
|
|
143
143
|
|
|
144
|
-
bytes
|
|
145
|
-
bytes
|
|
144
|
+
bytes.concat MixinBot::UUID.new(hex: trace_id).packed.bytes
|
|
145
|
+
bytes.concat MixinBot::UUID.new(hex: asset_id).packed.bytes
|
|
146
146
|
|
|
147
147
|
amount_string = amount.to_d.to_s('F')
|
|
148
148
|
amount_bytes = amount_string.bytes
|
|
149
|
-
bytes
|
|
150
|
-
bytes
|
|
149
|
+
bytes.concat MixinBot.utils.encode_int(amount_bytes.size)
|
|
150
|
+
bytes.concat amount_bytes
|
|
151
151
|
|
|
152
152
|
extra_bytes = extra.bytes
|
|
153
|
-
bytes
|
|
154
|
-
bytes
|
|
153
|
+
bytes.concat MixinBot.utils.encode_uint16(extra_bytes.size)
|
|
154
|
+
bytes.concat extra_bytes
|
|
155
155
|
|
|
156
156
|
references_count = (index_references || []).size + (hash_references || []).size
|
|
157
|
-
bytes
|
|
157
|
+
bytes.concat MixinBot.utils.encode_int(references_count)
|
|
158
158
|
|
|
159
159
|
index_references&.each do |index|
|
|
160
|
-
bytes
|
|
161
|
-
bytes
|
|
160
|
+
bytes.concat MixinBot.utils.encode_int(1)
|
|
161
|
+
bytes.concat MixinBot.utils.encode_int(index)
|
|
162
162
|
end
|
|
163
163
|
|
|
164
164
|
hash_references&.each do |hash|
|
|
165
|
-
bytes
|
|
166
|
-
bytes
|
|
165
|
+
bytes.concat MixinBot.utils.encode_int(0)
|
|
166
|
+
bytes.concat [hash].pack('H*').bytes
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
bytes
|
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/nfo.rb
CHANGED
|
@@ -39,10 +39,10 @@ module MixinBot
|
|
|
39
39
|
|
|
40
40
|
def unique_token_id
|
|
41
41
|
bytes = []
|
|
42
|
-
bytes
|
|
43
|
-
bytes
|
|
44
|
-
bytes
|
|
45
|
-
bytes
|
|
42
|
+
bytes.concat MixinBot::UUID.new(hex: chain).packed.bytes
|
|
43
|
+
bytes.concat [nm_class].pack('H*').bytes
|
|
44
|
+
bytes.concat MixinBot::UUID.new(hex: collection).packed.bytes
|
|
45
|
+
bytes.concat MixinBot.utils.encode_int token
|
|
46
46
|
|
|
47
47
|
md5 = Digest::MD5.new
|
|
48
48
|
md5.update bytes.pack('c*')
|
|
@@ -67,33 +67,33 @@ module MixinBot
|
|
|
67
67
|
def encode
|
|
68
68
|
bytes = []
|
|
69
69
|
|
|
70
|
-
bytes
|
|
71
|
-
bytes
|
|
70
|
+
bytes.concat prefix.bytes
|
|
71
|
+
bytes.push(version)
|
|
72
72
|
|
|
73
73
|
if mask.zero?
|
|
74
|
-
bytes
|
|
74
|
+
bytes << 0
|
|
75
75
|
else
|
|
76
|
-
bytes
|
|
77
|
-
bytes
|
|
78
|
-
bytes
|
|
76
|
+
bytes << 1
|
|
77
|
+
bytes.concat MixinBot.utils.encode_uint64 mask
|
|
78
|
+
bytes.concat MixinBot::UUID.new(hex: chain).packed.bytes
|
|
79
79
|
|
|
80
80
|
class_bytes = [nm_class].pack('H*').bytes
|
|
81
|
-
bytes
|
|
82
|
-
bytes
|
|
81
|
+
bytes.concat MixinBot.utils.encode_int class_bytes.size
|
|
82
|
+
bytes.concat class_bytes
|
|
83
83
|
|
|
84
84
|
collection_bytes = collection.split('-').pack('H* H* H* H* H*').bytes
|
|
85
|
-
bytes
|
|
86
|
-
bytes
|
|
85
|
+
bytes.concat MixinBot.utils.encode_int collection_bytes.size
|
|
86
|
+
bytes.concat collection_bytes
|
|
87
87
|
|
|
88
88
|
# token_bytes = memo[:token].split('-').pack('H* H* H* H* H*').bytes
|
|
89
89
|
token_bytes = MixinBot.utils.encode_int token
|
|
90
|
-
bytes
|
|
91
|
-
bytes
|
|
90
|
+
bytes.concat MixinBot.utils.encode_int token_bytes.size
|
|
91
|
+
bytes.concat token_bytes
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
extra_bytes = [extra].pack('H*').bytes
|
|
95
|
-
bytes
|
|
96
|
-
bytes
|
|
95
|
+
bytes.concat MixinBot.utils.encode_int extra_bytes.size
|
|
96
|
+
bytes.concat extra_bytes
|
|
97
97
|
|
|
98
98
|
@raw = bytes.pack('C*')
|
|
99
99
|
@hex = raw.unpack1('H*')
|
|
@@ -17,28 +17,25 @@ module MixinBot
|
|
|
17
17
|
|
|
18
18
|
bytes = []
|
|
19
19
|
|
|
20
|
-
bytes
|
|
21
|
-
bytes
|
|
22
|
-
bytes
|
|
20
|
+
bytes.concat Transaction::MAGIC
|
|
21
|
+
bytes.push(0, @tx.version)
|
|
22
|
+
bytes.concat [@tx.asset].pack('H*').bytes
|
|
23
23
|
|
|
24
|
-
bytes
|
|
25
|
-
bytes
|
|
26
|
-
bytes
|
|
24
|
+
bytes.concat encode_inputs
|
|
25
|
+
bytes.concat encode_outputs
|
|
26
|
+
bytes.concat encode_references if @tx.version >= Transaction::REFERENCES_TX_VERSION
|
|
27
27
|
|
|
28
28
|
extra_bytes = @tx.extra.bytes
|
|
29
29
|
raise InvalidTransactionFormatError, 'extra is too long' if extra_bytes.size > Transaction::MAX_EXTRA_SIZE
|
|
30
30
|
|
|
31
|
-
bytes
|
|
32
|
-
bytes
|
|
31
|
+
bytes.concat MixinBot.utils.encode_uint32 extra_bytes.size
|
|
32
|
+
bytes.concat extra_bytes
|
|
33
33
|
|
|
34
|
-
bytes
|
|
35
|
-
encode_signatures
|
|
36
|
-
else
|
|
37
|
-
encode_aggregated_signature
|
|
38
|
-
end
|
|
34
|
+
bytes.concat(@tx.aggregated.nil? ? encode_signatures : encode_aggregated_signature)
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
@tx.
|
|
36
|
+
packed = bytes.pack('C*')
|
|
37
|
+
@tx.hash = SHA3::Digest::SHA3_256.hexdigest packed
|
|
38
|
+
@tx.hex = packed.unpack1('H*')
|
|
42
39
|
|
|
43
40
|
@tx
|
|
44
41
|
end
|
|
@@ -48,63 +45,63 @@ module MixinBot
|
|
|
48
45
|
def encode_inputs
|
|
49
46
|
bytes = []
|
|
50
47
|
|
|
51
|
-
bytes
|
|
48
|
+
bytes.concat MixinBot.utils.encode_uint16(@tx.inputs.size)
|
|
52
49
|
|
|
53
50
|
@tx.inputs.each do |input|
|
|
54
|
-
bytes
|
|
55
|
-
bytes
|
|
51
|
+
bytes.concat [input['hash']].pack('H*').bytes
|
|
52
|
+
bytes.concat MixinBot.utils.encode_uint16(input['index'])
|
|
56
53
|
|
|
57
54
|
genesis = input['genesis'] || ''
|
|
58
55
|
if genesis.empty?
|
|
59
|
-
bytes
|
|
56
|
+
bytes.concat Transaction::NULL_BYTES
|
|
60
57
|
else
|
|
61
58
|
genesis_bytes = [genesis].pack('H*').bytes
|
|
62
|
-
bytes
|
|
63
|
-
bytes
|
|
59
|
+
bytes.concat MixinBot.utils.encode_uint16 genesis_bytes.size
|
|
60
|
+
bytes.concat genesis_bytes
|
|
64
61
|
end
|
|
65
62
|
|
|
66
63
|
deposit = input['deposit']
|
|
67
64
|
if deposit.nil?
|
|
68
|
-
bytes
|
|
65
|
+
bytes.concat Transaction::NULL_BYTES
|
|
69
66
|
else
|
|
70
|
-
bytes
|
|
71
|
-
bytes
|
|
67
|
+
bytes.concat Transaction::MAGIC
|
|
68
|
+
bytes.concat [deposit['chain']].pack('H*').bytes
|
|
72
69
|
|
|
73
70
|
asset_bytes = [deposit['asset']].pack('H*')
|
|
74
|
-
bytes
|
|
75
|
-
bytes
|
|
71
|
+
bytes.concat MixinBot.utils.encode_uint16 asset_bytes.size
|
|
72
|
+
bytes.concat asset_bytes.bytes
|
|
76
73
|
|
|
77
74
|
transaction_bytes = [deposit['transaction']].pack('H*')
|
|
78
|
-
bytes
|
|
79
|
-
bytes
|
|
75
|
+
bytes.concat MixinBot.utils.encode_uint16 transaction_bytes.size
|
|
76
|
+
bytes.concat transaction_bytes.bytes
|
|
80
77
|
|
|
81
|
-
bytes
|
|
78
|
+
bytes.concat MixinBot.utils.encode_uint64 deposit['index']
|
|
82
79
|
|
|
83
80
|
amount_bytes = MixinBot.utils.bytes_of deposit['amount']
|
|
84
|
-
bytes
|
|
85
|
-
bytes
|
|
81
|
+
bytes.concat MixinBot.utils.encode_uint16 amount_bytes.size
|
|
82
|
+
bytes.concat amount_bytes
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
mint = input['mint']
|
|
89
86
|
if mint.nil?
|
|
90
|
-
bytes
|
|
87
|
+
bytes.concat Transaction::NULL_BYTES
|
|
91
88
|
else
|
|
92
|
-
bytes
|
|
89
|
+
bytes.concat Transaction::MAGIC
|
|
93
90
|
|
|
94
91
|
group = mint['group'] || ''
|
|
95
92
|
if group.empty?
|
|
96
|
-
bytes
|
|
93
|
+
bytes.concat Transaction::NULL_BYTES
|
|
97
94
|
else
|
|
98
95
|
group_bytes = [group].pack('H*')
|
|
99
|
-
bytes
|
|
100
|
-
bytes
|
|
96
|
+
bytes.concat MixinBot.utils.encode_uint16 group_bytes.size
|
|
97
|
+
bytes.concat group_bytes.bytes
|
|
101
98
|
end
|
|
102
99
|
|
|
103
|
-
bytes
|
|
100
|
+
bytes.concat MixinBot.utils.encode_uint64 mint['batch']
|
|
104
101
|
|
|
105
102
|
amount_bytes = MixinBot.utils.encode_int mint['amount']
|
|
106
|
-
bytes
|
|
107
|
-
bytes
|
|
103
|
+
bytes.concat MixinBot.utils.encode_uint16 amount_bytes.size
|
|
104
|
+
bytes.concat amount_bytes
|
|
108
105
|
end
|
|
109
106
|
end
|
|
110
107
|
|
|
@@ -114,55 +111,55 @@ module MixinBot
|
|
|
114
111
|
def encode_outputs
|
|
115
112
|
bytes = []
|
|
116
113
|
|
|
117
|
-
bytes
|
|
114
|
+
bytes.concat MixinBot.utils.encode_uint16 @tx.outputs.size
|
|
118
115
|
|
|
119
116
|
@tx.outputs.each do |output|
|
|
120
117
|
type = output['type'] || 0
|
|
121
|
-
bytes
|
|
118
|
+
bytes.push(0x00, type)
|
|
122
119
|
|
|
123
120
|
amount_bytes = MixinBot.utils.encode_int((output['amount'].to_d * 1e8).round)
|
|
124
|
-
bytes
|
|
125
|
-
bytes
|
|
121
|
+
bytes.concat MixinBot.utils.encode_uint16 amount_bytes.size
|
|
122
|
+
bytes.concat amount_bytes
|
|
126
123
|
|
|
127
|
-
bytes
|
|
124
|
+
bytes.concat MixinBot.utils.encode_uint16 output['keys'].size
|
|
128
125
|
output['keys'].each do |key|
|
|
129
|
-
bytes
|
|
126
|
+
bytes.concat [key].pack('H*').bytes
|
|
130
127
|
end
|
|
131
128
|
|
|
132
|
-
bytes
|
|
129
|
+
bytes.concat [output['mask']].pack('H*').bytes
|
|
133
130
|
|
|
134
131
|
script_bytes = [output['script']].pack('H*').bytes
|
|
135
|
-
bytes
|
|
136
|
-
bytes
|
|
132
|
+
bytes.concat MixinBot.utils.encode_uint16 script_bytes.size
|
|
133
|
+
bytes.concat script_bytes
|
|
137
134
|
|
|
138
135
|
withdrawal = output['withdrawal']
|
|
139
136
|
if withdrawal.nil?
|
|
140
|
-
bytes
|
|
137
|
+
bytes.concat Transaction::NULL_BYTES
|
|
141
138
|
else
|
|
142
|
-
bytes
|
|
139
|
+
bytes.concat Transaction::MAGIC
|
|
143
140
|
|
|
144
|
-
bytes
|
|
141
|
+
bytes.concat [withdrawal['chain']].pack('H*').bytes
|
|
145
142
|
|
|
146
143
|
asset_bytes = [withdrawal['asset']].pack('H*')
|
|
147
|
-
bytes
|
|
148
|
-
bytes
|
|
144
|
+
bytes.concat MixinBot.utils.encode_uint16 asset_bytes.bytesize
|
|
145
|
+
bytes.concat asset_bytes.bytes
|
|
149
146
|
|
|
150
147
|
address = withdrawal['address'] || ''
|
|
151
148
|
if address.empty?
|
|
152
|
-
bytes
|
|
149
|
+
bytes.concat Transaction::NULL_BYTES
|
|
153
150
|
else
|
|
154
151
|
address_bytes = [address].pack('H*').bytes
|
|
155
|
-
bytes
|
|
156
|
-
bytes
|
|
152
|
+
bytes.concat MixinBot.utils.encode_uint16 address_bytes.size
|
|
153
|
+
bytes.concat address_bytes
|
|
157
154
|
end
|
|
158
155
|
|
|
159
156
|
tag = withdrawal['tag'] || ''
|
|
160
157
|
if tag.empty?
|
|
161
|
-
bytes
|
|
158
|
+
bytes.concat Transaction::NULL_BYTES
|
|
162
159
|
else
|
|
163
160
|
tag_bytes = [tag].pack('H*').bytes
|
|
164
|
-
bytes
|
|
165
|
-
bytes
|
|
161
|
+
bytes.concat MixinBot.utils.encode_uint16 tag_bytes.size
|
|
162
|
+
bytes.concat tag_bytes
|
|
166
163
|
end
|
|
167
164
|
end
|
|
168
165
|
end
|
|
@@ -174,10 +171,10 @@ module MixinBot
|
|
|
174
171
|
bytes = []
|
|
175
172
|
|
|
176
173
|
references = Array(@tx.references)
|
|
177
|
-
bytes
|
|
174
|
+
bytes.concat MixinBot.utils.encode_uint16 references.size
|
|
178
175
|
|
|
179
176
|
references.each do |reference|
|
|
180
|
-
bytes
|
|
177
|
+
bytes.concat [reference].pack('H*').bytes
|
|
181
178
|
end
|
|
182
179
|
|
|
183
180
|
bytes
|
|
@@ -186,14 +183,14 @@ module MixinBot
|
|
|
186
183
|
def encode_aggregated_signature
|
|
187
184
|
bytes = []
|
|
188
185
|
|
|
189
|
-
bytes
|
|
190
|
-
bytes
|
|
191
|
-
bytes
|
|
186
|
+
bytes.concat MixinBot.utils.encode_uint16 Transaction::MAX_ENCODE_INT
|
|
187
|
+
bytes.concat MixinBot.utils.encode_uint16 Transaction::AGGREGATED_SIGNATURE_PREFIX
|
|
188
|
+
bytes.concat [@tx.aggregated['signature']].pack('H*').bytes
|
|
192
189
|
|
|
193
190
|
signers = @tx.aggregated['signers'] || []
|
|
194
191
|
if signers.empty?
|
|
195
|
-
bytes
|
|
196
|
-
bytes
|
|
192
|
+
bytes.concat Transaction::AGGREGATED_SIGNATURE_ORDINAY_MASK
|
|
193
|
+
bytes.concat Transaction::NULL_BYTES
|
|
197
194
|
return bytes
|
|
198
195
|
end
|
|
199
196
|
|
|
@@ -205,17 +202,17 @@ module MixinBot
|
|
|
205
202
|
max = signers.last
|
|
206
203
|
sig_byte_len = [@tx.aggregated['signature']].pack('H*').bytes.size
|
|
207
204
|
if ((max / 8) + 1) > sig_byte_len
|
|
208
|
-
bytes
|
|
209
|
-
bytes
|
|
210
|
-
signers.each { |signer| bytes
|
|
205
|
+
bytes.concat Transaction::AGGREGATED_SIGNATURE_SPARSE_MASK
|
|
206
|
+
bytes.concat MixinBot.utils.encode_uint16 signers.size
|
|
207
|
+
signers.each { |signer| bytes.concat MixinBot.utils.encode_uint16(signer) }
|
|
211
208
|
else
|
|
212
209
|
masks_bytes = Array.new((max / 8) + 1, 0)
|
|
213
210
|
signers.each do |signer|
|
|
214
211
|
masks_bytes[signer / 8] ^= (1 << (signer % 8))
|
|
215
212
|
end
|
|
216
|
-
bytes
|
|
217
|
-
bytes
|
|
218
|
-
bytes
|
|
213
|
+
bytes.concat Transaction::AGGREGATED_SIGNATURE_ORDINAY_MASK
|
|
214
|
+
bytes.concat MixinBot.utils.encode_uint16 masks_bytes.size
|
|
215
|
+
bytes.concat masks_bytes
|
|
219
216
|
end
|
|
220
217
|
|
|
221
218
|
bytes
|
|
@@ -233,18 +230,18 @@ module MixinBot
|
|
|
233
230
|
|
|
234
231
|
raise ArgumentError, 'signatures overflow' if sl == Transaction::MAX_ENCODE_INT
|
|
235
232
|
|
|
236
|
-
bytes
|
|
233
|
+
bytes.concat MixinBot.utils.encode_uint16 sl
|
|
237
234
|
|
|
238
235
|
if sl.positive?
|
|
239
236
|
@tx.signatures.each do |signature|
|
|
240
|
-
bytes
|
|
237
|
+
bytes.concat MixinBot.utils.encode_uint16 signature.keys.size
|
|
241
238
|
|
|
242
239
|
signature.keys.sort.each do |key|
|
|
243
240
|
signature_bytes = [signature[key]].pack('H*').bytes
|
|
244
241
|
raise ArgumentError, 'Signature should be 64 bytes' if signature_bytes.size != 64
|
|
245
242
|
|
|
246
|
-
bytes
|
|
247
|
-
bytes
|
|
243
|
+
bytes.concat MixinBot.utils.encode_uint16 key
|
|
244
|
+
bytes.concat signature_bytes
|
|
248
245
|
end
|
|
249
246
|
end
|
|
250
247
|
end
|
data/lib/mixin_bot/version.rb
CHANGED
data/lib/mvm/registry.rb
CHANGED
|
@@ -53,9 +53,9 @@ module MVM
|
|
|
53
53
|
|
|
54
54
|
def contract_from_multisig(user_ids, threshold)
|
|
55
55
|
bytes = []
|
|
56
|
-
bytes
|
|
57
|
-
bytes
|
|
58
|
-
bytes
|
|
56
|
+
bytes.concat(MixinBot.utils.encode_uint16(user_ids.length))
|
|
57
|
+
bytes.concat([user_ids.sort.join.gsub('-', '')].pack('H*').bytes)
|
|
58
|
+
bytes.concat(MixinBot.utils.encode_uint16(threshold))
|
|
59
59
|
|
|
60
60
|
hash = Eth::Util.bin_to_prefixed_hex(Eth::Util.keccak256(bytes.pack('C*')))
|
|
61
61
|
@rpc.call @registry, 'contracts', hash.to_i(16)
|
data/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MixinBot
|
|
2
2
|
|
|
3
|
-
> Ruby SDK and CLI for Mixin Network: Safe UTXO transfers, REST API, Blaze messaging, transaction crypto, optional MVM helpers. Ruby >= 3.2. Gem version 2.
|
|
3
|
+
> Ruby SDK and CLI for Mixin Network: Safe UTXO transfers, REST API, Blaze messaging, transaction crypto, optional MVM helpers. Ruby >= 3.2. Gem version 2.3.0.
|
|
4
4
|
|
|
5
5
|
Important notes:
|
|
6
6
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mixin_bot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- an-lee
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -232,14 +231,14 @@ dependencies:
|
|
|
232
231
|
requirements:
|
|
233
232
|
- - "~>"
|
|
234
233
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: '
|
|
234
|
+
version: '2.2'
|
|
236
235
|
type: :runtime
|
|
237
236
|
prerelease: false
|
|
238
237
|
version_requirements: !ruby/object:Gem::Requirement
|
|
239
238
|
requirements:
|
|
240
239
|
- - "~>"
|
|
241
240
|
- !ruby/object:Gem::Version
|
|
242
|
-
version: '
|
|
241
|
+
version: '2.2'
|
|
243
242
|
- !ruby/object:Gem::Dependency
|
|
244
243
|
name: thor
|
|
245
244
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -375,7 +374,6 @@ licenses:
|
|
|
375
374
|
- MIT
|
|
376
375
|
metadata:
|
|
377
376
|
rubygems_mfa_required: 'true'
|
|
378
|
-
post_install_message:
|
|
379
377
|
rdoc_options: []
|
|
380
378
|
require_paths:
|
|
381
379
|
- lib
|
|
@@ -390,8 +388,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
390
388
|
- !ruby/object:Gem::Version
|
|
391
389
|
version: '0'
|
|
392
390
|
requirements: []
|
|
393
|
-
rubygems_version:
|
|
394
|
-
signing_key:
|
|
391
|
+
rubygems_version: 4.0.16
|
|
395
392
|
specification_version: 4
|
|
396
393
|
summary: A Ruby SDK for Mixin Network
|
|
397
394
|
test_files: []
|