mixin_bot 2.6.0 → 3.0.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 +3 -3
- data/CHANGELOG.md +11 -0
- data/README.md +42 -5
- data/docs/agent/cookbook.md +26 -0
- data/examples/blaze_async.rb +6 -4
- data/lib/mixin_bot/api/blaze_async.rb +5 -1
- data/lib/mixin_bot/blaze/reactor.rb +244 -0
- data/lib/mixin_bot/configuration.rb +52 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +1 -0
- data/lib/puma/plugin/mixin_blaze.rb +263 -0
- data/llms.txt +1 -1
- metadata +20 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c6e5421b12eabf2c32c0500c1b828228fd24a10855047f2f7f1bf605dc62ada
|
|
4
|
+
data.tar.gz: a18da86395abe9053e52d70965538359cbbada5bc13439196c73536382d05c87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a20f4ef42062cff12ff5ecb280f8d447528ee2f7882e0425783af545b57a3fe7faecf36ca7c70387e5c3dbeb08cdcea02729b919cddadb386fe76b5b6d45fa6
|
|
7
|
+
data.tar.gz: f9f88a933a44502b2709fd282c8170adf73653d46721c83044358b59434444b10efac60ef82b84cb470cffaf7d4694d7bbeb270dc851c54ba5a66e40ff285713
|
data/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md — MixinBot
|
|
2
2
|
|
|
3
|
-
Ruby gem (
|
|
3
|
+
Ruby gem (v3.0.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
|
|
|
@@ -34,13 +34,13 @@ docs/agent/ # LLM-oriented CLI and cookbook docs
|
|
|
34
34
|
|
|
35
35
|
## CI and release
|
|
36
36
|
|
|
37
|
-
- **CI** (`.github/workflows/ci.yml`): `pull_request` and `push` to `main` — `rake test` on Ruby
|
|
37
|
+
- **CI** (`.github/workflows/ci.yml`): `pull_request` and `push` to `main` — `rake test` on Ruby 4.0, `rake rubocop` (4.0), `rake mixin_bot:api_coverage`.
|
|
38
38
|
- **Release** (`.github/workflows/release.yml`): push tag `v*` (must match `MixinBot::VERSION`, e.g. tag `v2.1.0` for `VERSION = '2.1.0'`) → `rake build` → RubyGems via [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC; workflow `release.yml`, no repo secret) → GitHub Release (notes from `CHANGELOG.md`, `.gem` attached).
|
|
39
39
|
- **Dependabot** (`.github/dependabot.yml`): weekly Bundler and GitHub Actions updates; Dependabot PRs use the same CI workflow.
|
|
40
40
|
|
|
41
41
|
## Conventions
|
|
42
42
|
|
|
43
|
-
- **Ruby** >=
|
|
43
|
+
- **Ruby** >= 4.0 (CI: 4.0)
|
|
44
44
|
- **HTTP responses**: `MixinBot::Models::ApiEnvelope` — use `res['data']` or delegated keys
|
|
45
45
|
- **Safe transfers**: require `spend_key`; prefer `create_safe_transfer` / `create_transfer` (Safe pipeline)
|
|
46
46
|
- **Legacy APIs**: `create_legacy_transfer`, `POST /transfers` — deprecated, warns once
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.0.0] - 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Puma plugin hosting the Blaze connection** (`plugin :mixin_blaze`) — Solid Queue-style in-process hosting, removing the standalone Blaze deployment artifact. Dual run modes via the `mixin_blaze_mode` Puma DSL option: `:fork` (default) forks a dedicated Blaze child on `on_booted` with a monitor thread that stops Puma if the child dies, terminated on `on_stopped`/`on_restart`; `:async` runs the reactor on a Puma background thread in-process. Handler resolution comes from `MixinBot` configuration (resolved lazily after app boot) with an explicit ack policy (on-receipt vs after-handler); Puma 6/7 lifecycle-event compatibility; cluster mode requires `preload_app!` and fails loudly without it. Puma remains a host-app concern — no new gem dependency.
|
|
15
|
+
- **`MixinBot::Blaze::Reactor`** — reusable Blaze loop (connect, keepalive ping, read → handler → ack, reconnect with backoff) wrapping `API#blaze_async`; extracted from `examples/blaze_async.rb` so the plugin shell stays thin and the loop is unit-testable offline.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Breaking**: Ruby >= 4.0 is now required — `required_ruby_version` bumped from `>= 3.2.0` to `>= 4.0.0`. The CI matrix, RuboCop target, and Release workflow all run Ruby 4.0; the dev-only `parallel` version pin (kept for the old Ruby 3.2 floor) was dropped.
|
|
20
|
+
|
|
10
21
|
## [2.6.0] - 2026-09-07
|
|
11
22
|
|
|
12
23
|
### Added
|
data/README.md
CHANGED
|
@@ -6,12 +6,12 @@ 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: **
|
|
9
|
+
Current gem version: **3.0.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
|
-
- **Ruby** ≥
|
|
14
|
-
- **Bundler** 2.
|
|
13
|
+
- **Ruby** ≥ 4.0 (CI runs 4.0).
|
|
14
|
+
- **Bundler** 2.6+ recommended.
|
|
15
15
|
- Optional: the **`mixin`** CLI in `PATH` if you use `MixinBot::API#encode_raw_transaction_native` / `#decode_raw_transaction_native` or the experimental `MixinBot::NodeCLI` helpers.
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
@@ -290,6 +290,43 @@ ensure
|
|
|
290
290
|
end
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
### Running Blaze inside Puma (`plugin :mixin_blaze`)
|
|
294
|
+
|
|
295
|
+
For Rails/Puma apps, the gem ships a Puma plugin (Solid Queue-style) that hosts the Blaze connection inside the web process tree — no standalone Blaze process to deploy or supervise:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
# config/puma.rb
|
|
299
|
+
plugin :mixin_blaze
|
|
300
|
+
mixin_blaze_mode :fork # default; see table below
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# config/initializers/mixin_bot.rb
|
|
305
|
+
MixinBot.configure do
|
|
306
|
+
# ... credentials ...
|
|
307
|
+
self.blaze_handler = ->(envelope) { MyBot.process! envelope }
|
|
308
|
+
self.blaze_ack_policy = :on_receipt # default; or :after_handler
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The handler receives each decoded message envelope (a Hash with `action` and `data` keys) and runs serially on the hosting thread — safe for ActiveRecord. Handlers run one message at a time; the keepalive ping runs separately.
|
|
313
|
+
|
|
314
|
+
| Mode | Where the connection lives | Notes |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| `:fork` (default) | Dedicated child process forked from the Puma launcher | Crash-isolated; if the child dies, Puma is stopped so the process manager restarts the unit |
|
|
317
|
+
| `:async` | Background thread inside the Puma process itself | In cluster mode that process is the master; no child process |
|
|
318
|
+
|
|
319
|
+
Ack policy: `:on_receipt` acknowledges each message immediately (at-most-once dispatch — a handler crash loses that message); `:after_handler` acknowledges only after the handler completes without raising (at-least-once — handlers must be idempotent, since unacknowledged messages are redelivered on reconnect).
|
|
320
|
+
|
|
321
|
+
Receipt confirmations (`ACKNOWLEDGE_MESSAGE_RECEIPT` frames the server pushes for your own outgoing/acknowledged messages) are consumed internally: the handler never sees them and they are never re-acknowledged.
|
|
322
|
+
|
|
323
|
+
Operational notes:
|
|
324
|
+
|
|
325
|
+
- **Remove any standalone Blaze process when enabling the plugin** — two connections double your handler invocations (or split deliveries).
|
|
326
|
+
- **Cluster mode requires `preload_app!`** — plugins run in the launcher process, which holds no application code without preloading; the plugin reports an error and stays off otherwise.
|
|
327
|
+
- **Phased restarts keep the old handler code** in the hosting process until a full restart; the same applies to Solid Queue's plugin.
|
|
328
|
+
- Workers that need to *send* messages should use the REST API (`create_message` / `create_messages`), not the socket.
|
|
329
|
+
|
|
293
330
|
## Deep links and bot auth
|
|
294
331
|
|
|
295
332
|
```ruby
|
|
@@ -396,8 +433,8 @@ Examples under `examples/` expect `examples/config.yml` (copy from `examples/con
|
|
|
396
433
|
|
|
397
434
|
GitHub Actions runs on every pull request and on pushes to `main`:
|
|
398
435
|
|
|
399
|
-
- **Test** — Ruby
|
|
400
|
-
- **RuboCop** — Ruby
|
|
436
|
+
- **Test** — Ruby 4.0: `bundle exec rake test`
|
|
437
|
+
- **RuboCop** — Ruby 4.0: `bundle exec rake rubocop`
|
|
401
438
|
- **API coverage** — `bundle exec rake mixin_bot:api_coverage`
|
|
402
439
|
|
|
403
440
|
### Release
|
data/docs/agent/cookbook.md
CHANGED
|
@@ -130,6 +130,32 @@ end
|
|
|
130
130
|
|
|
131
131
|
See [examples/blaze.rb](../../examples/blaze.rb).
|
|
132
132
|
|
|
133
|
+
## Blaze inside Puma (`plugin :mixin_blaze`)
|
|
134
|
+
|
|
135
|
+
For Rails/Puma apps the gem ships a Puma plugin (Solid Queue-style) that hosts the Blaze connection inside the web process tree — no standalone Blaze process. Requires a configured `blaze_handler`; without one the plugin logs an error and stays off.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# config/puma.rb
|
|
139
|
+
plugin :mixin_blaze
|
|
140
|
+
mixin_blaze_mode :fork # default (:fork = supervised child process); :async = in-process thread
|
|
141
|
+
|
|
142
|
+
# config/initializers/mixin_bot.rb
|
|
143
|
+
MixinBot.configure do
|
|
144
|
+
# ... credentials ...
|
|
145
|
+
self.blaze_handler = ->(envelope) { MyBot.process! envelope } # required
|
|
146
|
+
self.blaze_ack_policy = :on_receipt # default; :after_handler = ack after handler success
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Behavior: reconnect with bounded backoff, keepalive pings, serial handler dispatch (ActiveRecord-safe), handler exceptions logged without ending delivery, graceful shutdown with Puma (child killed on stop; in `:fork` mode a dead child stops Puma so the unit restarts). `ACKNOWLEDGE_MESSAGE_RECEIPT` confirmations are consumed internally — the handler never sees them and they are never re-acknowledged.
|
|
151
|
+
|
|
152
|
+
Operational notes:
|
|
153
|
+
|
|
154
|
+
- **Remove any standalone Blaze process when enabling the plugin** — duplicate connections duplicate handler invocations.
|
|
155
|
+
- **Puma cluster mode requires `preload_app!`** — plugins run in the launcher process; without preloading there is no app code to resolve the handler and the plugin reports an error.
|
|
156
|
+
- **Phased restarts keep old handler code** in the hosting process until a full restart.
|
|
157
|
+
- Workers that need to *send* messages use the REST API (`create_message`), not the socket.
|
|
158
|
+
|
|
133
159
|
## Discover API methods
|
|
134
160
|
|
|
135
161
|
```bash
|
data/examples/blaze_async.rb
CHANGED
|
@@ -40,10 +40,12 @@ def send_frame(connection, bytes)
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
Async do |task|
|
|
43
|
-
# endpoint_options: is forwarded to Async::HTTP::Endpoint.parse
|
|
44
|
-
# timeout here
|
|
45
|
-
#
|
|
46
|
-
|
|
43
|
+
# endpoint_options: is forwarded to Async::HTTP::Endpoint.parse. No
|
|
44
|
+
# timeout: here — it would become the socket's per-read timeout and kill
|
|
45
|
+
# quiet-but-healthy connections; liveness is the keepalive ping below.
|
|
46
|
+
# handler: stays default; pass an Async::WebSocket::Connection subclass to
|
|
47
|
+
# hook the raw frame events (e.g. PONG correlation).
|
|
48
|
+
connection = API.blaze_async
|
|
47
49
|
p [Time.now.to_s, :connected]
|
|
48
50
|
|
|
49
51
|
# liveness is the caller's policy: nothing on the wire goes stale quietly
|
|
@@ -33,7 +33,11 @@ module MixinBot
|
|
|
33
33
|
# +handler:+ is yield-through so the caller can supply an
|
|
34
34
|
# +Async::WebSocket::Connection+ subclass (e.g. one that correlates
|
|
35
35
|
# PONG frames). +endpoint_options:+ are forwarded to
|
|
36
|
-
# +Async::HTTP::Endpoint.parse
|
|
36
|
+
# +Async::HTTP::Endpoint.parse+. Prefer not passing +timeout:+ there: it
|
|
37
|
+
# becomes the socket's per-read timeout, so a quiet-but-healthy
|
|
38
|
+
# connection dies when no frame arrives within that window. Bound the
|
|
39
|
+
# connect phase at the call site instead (see
|
|
40
|
+
# MixinBot::Blaze::Reactor#connect!).
|
|
37
41
|
def blaze_async(handler: Async::WebSocket::Connection, endpoint_options: {})
|
|
38
42
|
access_token = access_token('GET', '/', '')
|
|
39
43
|
authorization = format('Bearer %<access_token>s', access_token:)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'protocol/websocket'
|
|
6
|
+
|
|
7
|
+
module MixinBot
|
|
8
|
+
module Blaze
|
|
9
|
+
# Fiber-based Blaze receive loop — the hosted sibling of
|
|
10
|
+
# examples/blaze_async.rb.
|
|
11
|
+
#
|
|
12
|
+
# Drives API#blaze_async inside its own Async reactor (created by #run,
|
|
13
|
+
# so it can be called from any plain thread — e.g. a Puma plugin's
|
|
14
|
+
# background thread or a forked child). The loop: connect, request
|
|
15
|
+
# pending messages, read frames serially, dispatch each decoded envelope
|
|
16
|
+
# to the configured handler, and acknowledge per the ack policy. Receipt
|
|
17
|
+
# confirmations (ACKNOWLEDGE_MESSAGE_RECEIPT frames) are consumed
|
|
18
|
+
# internally — never dispatched to the handler nor re-acknowledged.
|
|
19
|
+
# Dropped connections are re-established with bounded exponential backoff.
|
|
20
|
+
#
|
|
21
|
+
# Handlers run serially on the reactor thread — never in concurrent
|
|
22
|
+
# fibers — so handler code may use per-thread resources like ActiveRecord
|
|
23
|
+
# connections safely. Handler exceptions are logged and do not end
|
|
24
|
+
# message delivery.
|
|
25
|
+
#
|
|
26
|
+
# Reactor.new(handler: ->(envelope) { ... }).run # blocks forever
|
|
27
|
+
#
|
|
28
|
+
class Reactor
|
|
29
|
+
CONNECT_TIMEOUT = 10
|
|
30
|
+
KEEPALIVE_INTERVAL = 30
|
|
31
|
+
INITIAL_BACKOFF = 1
|
|
32
|
+
MAX_BACKOFF = 30
|
|
33
|
+
# a connection must live at least this long for the backoff to reset
|
|
34
|
+
STABLE_PERIOD = 30
|
|
35
|
+
|
|
36
|
+
attr_reader :handler, :ack_policy
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @param api [MixinBot::API] API used for frame codecs and connecting
|
|
40
|
+
# @param handler [#call] invoked with each decoded message envelope
|
|
41
|
+
# Hash (with +action+ and +data+ keys)
|
|
42
|
+
# @param ack_policy [Symbol] +:on_receipt+ (ack before the handler
|
|
43
|
+
# runs) or +:after_handler+ (ack only after the handler succeeds;
|
|
44
|
+
# failed messages are redelivered on reconnect)
|
|
45
|
+
# @param connection_factory [#call, nil] returns a connected
|
|
46
|
+
# +Async::WebSocket+ connection; defaults to +api.blaze_async+
|
|
47
|
+
# with a +connect_timeout+ connect phase
|
|
48
|
+
# @param keepalive_interval [Numeric] seconds between client pings
|
|
49
|
+
# @param connect_timeout [Numeric] seconds allowed for the connect phase
|
|
50
|
+
# (DNS + TCP + TLS + WebSocket upgrade); established connections are
|
|
51
|
+
# never timed out on read
|
|
52
|
+
# @param sleeper [#call, nil] backoff hook, called with seconds;
|
|
53
|
+
# defaults to +Kernel#sleep+ (inject for tests)
|
|
54
|
+
# @param logger [#call, nil] called with +(level, exception_or_message)+;
|
|
55
|
+
# defaults to +$stderr+
|
|
56
|
+
def initialize(handler:, api: MixinBot.api, ack_policy: MixinBot.config.blaze_ack_policy,
|
|
57
|
+
connection_factory: nil, keepalive_interval: KEEPALIVE_INTERVAL,
|
|
58
|
+
connect_timeout: CONNECT_TIMEOUT, sleeper: nil, logger: nil)
|
|
59
|
+
raise MixinBot::ArgumentError, 'handler must respond to #call' unless handler.respond_to?(:call)
|
|
60
|
+
unless %i[on_receipt after_handler].include?(ack_policy)
|
|
61
|
+
raise MixinBot::ArgumentError, "ack_policy must be :on_receipt or :after_handler, got #{ack_policy.inspect}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@api = api
|
|
65
|
+
@handler = handler
|
|
66
|
+
@ack_policy = ack_policy
|
|
67
|
+
# No socket-level timeout: endpoint timeout: would apply to every read
|
|
68
|
+
# (a quiet-but-healthy connection would be killed), not just the
|
|
69
|
+
# connect phase. Liveness is the keepalive ping + the connect-phase
|
|
70
|
+
# timeout applied in #connect!.
|
|
71
|
+
@connection_factory = connection_factory || -> { api.blaze_async }
|
|
72
|
+
@keepalive_interval = keepalive_interval
|
|
73
|
+
@connect_timeout = connect_timeout
|
|
74
|
+
@sleeper = sleeper || ->(seconds) { sleep seconds }
|
|
75
|
+
@logger = logger || ->(level, detail) { warn "[mixin_blaze] #{level}: #{detail}" }
|
|
76
|
+
@guard = Mutex.new
|
|
77
|
+
@stopping = false
|
|
78
|
+
@connection = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Runs the connect/read/reconnect loop, blocking the calling thread
|
|
83
|
+
# until #stop. Yields control to the fiber scheduler while waiting.
|
|
84
|
+
#
|
|
85
|
+
# @return [void]
|
|
86
|
+
def run
|
|
87
|
+
Async do |task|
|
|
88
|
+
@task = task
|
|
89
|
+
backoff = INITIAL_BACKOFF
|
|
90
|
+
|
|
91
|
+
until stopped?
|
|
92
|
+
started_at = monotonic_time
|
|
93
|
+
begin
|
|
94
|
+
run_connection
|
|
95
|
+
backoff = INITIAL_BACKOFF if stable?(started_at)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
log.call :error, e
|
|
98
|
+
backoff = INITIAL_BACKOFF if stable?(started_at)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
break if stopped?
|
|
102
|
+
|
|
103
|
+
sleep_backoff backoff
|
|
104
|
+
backoff = [(backoff * 2), MAX_BACKOFF].min
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Stops the loop and closes the current connection. Safe to call from
|
|
111
|
+
# any thread (including while #run blocks in another one) and safe to
|
|
112
|
+
# call more than once.
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
def stop
|
|
116
|
+
@guard.synchronize do
|
|
117
|
+
@stopping = true
|
|
118
|
+
begin
|
|
119
|
+
@connection&.close
|
|
120
|
+
rescue StandardError
|
|
121
|
+
# already dead; the read loop will notice on its own
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def stopped?
|
|
129
|
+
@guard.synchronize { @stopping }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# One connection lifetime: connect, keepalive, pending request, read
|
|
133
|
+
# loop. Returns on a clean close; raises on a broken one.
|
|
134
|
+
def run_connection
|
|
135
|
+
return unless connect!
|
|
136
|
+
|
|
137
|
+
log.call :connected, "pid=#{Process.pid}"
|
|
138
|
+
|
|
139
|
+
keepalive = start_keepalive
|
|
140
|
+
send_frame @api.list_pending_message
|
|
141
|
+
|
|
142
|
+
while (message = @connection.read)
|
|
143
|
+
dispatch message
|
|
144
|
+
end
|
|
145
|
+
log.call :closed, 'clean close'
|
|
146
|
+
ensure
|
|
147
|
+
keepalive&.stop
|
|
148
|
+
@connection&.close
|
|
149
|
+
@connection = nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Establishes the connection and registers it under the guard, so a
|
|
153
|
+
# concurrent #stop can always find and close it. The connect phase
|
|
154
|
+
# (DNS + TCP + TLS + WebSocket upgrade) is bounded by #connect_timeout —
|
|
155
|
+
# a hung connect becomes an error the run loop retries, never a
|
|
156
|
+
# permanently stuck loop. Returns nil when the reactor stopped
|
|
157
|
+
# mid-connect; the fresh connection is closed here.
|
|
158
|
+
def connect!
|
|
159
|
+
fresh = @task.with_timeout(@connect_timeout) { @connection_factory.call }
|
|
160
|
+
|
|
161
|
+
@guard.synchronize do
|
|
162
|
+
if @stopping
|
|
163
|
+
begin
|
|
164
|
+
fresh&.close
|
|
165
|
+
rescue StandardError
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@connection = fresh
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
fresh
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def start_keepalive
|
|
178
|
+
Async do
|
|
179
|
+
loop do
|
|
180
|
+
sleep @keepalive_interval
|
|
181
|
+
@connection&.send_ping
|
|
182
|
+
end
|
|
183
|
+
rescue Protocol::WebSocket::ProtocolError, IOError
|
|
184
|
+
# the connection is gone; the read loop is already winding down
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Serial dispatch — handlers never run in concurrent fibers.
|
|
189
|
+
def dispatch(message)
|
|
190
|
+
raw = decode(message)
|
|
191
|
+
return if raw.nil?
|
|
192
|
+
|
|
193
|
+
# Receipt confirmations echo a message_id the client already knows and
|
|
194
|
+
# are never redelivered by LIST_PENDING_MESSAGES — they are not
|
|
195
|
+
# dispatchable events, and acknowledging one back would only
|
|
196
|
+
# round-trip a no-op frame. Skip them before the ack policy sees them.
|
|
197
|
+
return if raw['action'] == 'ACKNOWLEDGE_MESSAGE_RECEIPT'
|
|
198
|
+
|
|
199
|
+
data = raw['data'].is_a?(Hash) ? raw['data'] : {}
|
|
200
|
+
message_id = data['message_id']
|
|
201
|
+
|
|
202
|
+
send_frame @api.acknowledge_message_receipt(message_id) if @ack_policy == :on_receipt && message_id
|
|
203
|
+
|
|
204
|
+
handler_ok =
|
|
205
|
+
begin
|
|
206
|
+
handler.call raw
|
|
207
|
+
true
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
log.call :handler_error, e
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
send_frame @api.acknowledge_message_receipt(message_id) if @ack_policy == :after_handler && handler_ok && message_id
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def decode(message)
|
|
217
|
+
JSON.parse @api.ws_message(message.to_str)
|
|
218
|
+
rescue Zlib::Error, JSON::ParserError => e
|
|
219
|
+
log.call :decode_error, e
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def send_frame(bytes)
|
|
224
|
+
@connection.write Protocol::WebSocket::BinaryMessage.new(bytes.pack('C*'))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def sleep_backoff(seconds)
|
|
228
|
+
@sleeper.call seconds
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def stable?(started_at)
|
|
232
|
+
monotonic_time - started_at >= STABLE_PERIOD
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def monotonic_time
|
|
236
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def log
|
|
240
|
+
@logger
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -59,6 +59,17 @@ module MixinBot
|
|
|
59
59
|
].freeze
|
|
60
60
|
attr_accessor(*CONFIGURABLE_ATTRS)
|
|
61
61
|
|
|
62
|
+
# Acknowledgement policies for the Blaze message loop:
|
|
63
|
+
# - +:on_receipt+ (default): ack immediately on receipt, before the handler
|
|
64
|
+
# runs (at-most-once dispatch).
|
|
65
|
+
# - +:after_handler+: ack only after the handler completes without raising
|
|
66
|
+
# (at-least-once dispatch; unacked messages are redelivered on reconnect).
|
|
67
|
+
BLAZE_ACK_POLICIES = %i[on_receipt after_handler].freeze
|
|
68
|
+
|
|
69
|
+
DEFAULT_BLAZE_ACK_POLICY = :on_receipt
|
|
70
|
+
|
|
71
|
+
attr_reader :blaze_handler, :blaze_ack_policy
|
|
72
|
+
|
|
62
73
|
##
|
|
63
74
|
# Initializes a new Configuration instance.
|
|
64
75
|
#
|
|
@@ -96,6 +107,10 @@ module MixinBot
|
|
|
96
107
|
|
|
97
108
|
@debug = kwargs[:debug] || false
|
|
98
109
|
|
|
110
|
+
@blaze_ack_policy = DEFAULT_BLAZE_ACK_POLICY
|
|
111
|
+
self.blaze_handler = kwargs[:blaze_handler]
|
|
112
|
+
self.blaze_ack_policy = kwargs[:blaze_ack_policy]
|
|
113
|
+
|
|
99
114
|
self.session_private_key = kwargs[:session_private_key] || kwargs[:private_key]
|
|
100
115
|
self.server_public_key = kwargs[:server_public_key] || kwargs[:pin_token]
|
|
101
116
|
self.spend_key = kwargs[:spend_key]
|
|
@@ -203,6 +218,43 @@ module MixinBot
|
|
|
203
218
|
end
|
|
204
219
|
end
|
|
205
220
|
|
|
221
|
+
##
|
|
222
|
+
# Sets the callable invoked for each decoded Blaze message envelope.
|
|
223
|
+
#
|
|
224
|
+
# Used by MixinBot::Blaze::Reactor (and the +plugin :mixin_blaze+ Puma
|
|
225
|
+
# plugin) to dispatch messages. The callable receives the full decoded
|
|
226
|
+
# message envelope Hash (with +action+ and +data+ keys).
|
|
227
|
+
#
|
|
228
|
+
# @param callable [#call, nil] the message handler; must respond to #call
|
|
229
|
+
# @raise [ArgumentError] if a non-callable, non-nil value is given
|
|
230
|
+
#
|
|
231
|
+
def blaze_handler=(callable)
|
|
232
|
+
unless callable.nil? || callable.respond_to?(:call)
|
|
233
|
+
raise ArgumentError,
|
|
234
|
+
"blaze_handler must respond to #call, got #{callable.inspect}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
@blaze_handler = callable
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
# Sets the Blaze acknowledgement policy.
|
|
242
|
+
#
|
|
243
|
+
# @param policy [Symbol] +:on_receipt+ (default) or +:after_handler+
|
|
244
|
+
# @raise [ArgumentError] for unknown policies
|
|
245
|
+
#
|
|
246
|
+
def blaze_ack_policy=(policy)
|
|
247
|
+
return if policy.nil?
|
|
248
|
+
|
|
249
|
+
policy = policy.to_sym
|
|
250
|
+
unless BLAZE_ACK_POLICIES.include?(policy)
|
|
251
|
+
raise ArgumentError,
|
|
252
|
+
"blaze_ack_policy must be one of #{BLAZE_ACK_POLICIES.join(', ')}, got #{policy.inspect}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
@blaze_ack_policy = policy
|
|
256
|
+
end
|
|
257
|
+
|
|
206
258
|
private
|
|
207
259
|
|
|
208
260
|
def decode_key(key)
|
data/lib/mixin_bot/version.rb
CHANGED
data/lib/mixin_bot.rb
CHANGED
|
@@ -26,6 +26,7 @@ require_relative 'mixin_bot/models'
|
|
|
26
26
|
require_relative 'mixin_bot/session_store'
|
|
27
27
|
require_relative 'mixin_bot/cache_store_adapter'
|
|
28
28
|
require_relative 'mixin_bot/api'
|
|
29
|
+
require_relative 'mixin_bot/blaze/reactor'
|
|
29
30
|
require_relative 'mixin_bot/bot_auth'
|
|
30
31
|
require_relative 'mixin_bot/cli'
|
|
31
32
|
require_relative 'mixin_bot/computer'
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Puma plugin hosting the Blaze connection inside the web process tree —
|
|
4
|
+
# the Solid Queue plugin architecture:
|
|
5
|
+
#
|
|
6
|
+
# # config/puma.rb
|
|
7
|
+
# plugin :mixin_blaze
|
|
8
|
+
# mixin_blaze_mode :fork # default; :async hosts the loop in-process
|
|
9
|
+
#
|
|
10
|
+
# # config/initializers/mixin_bot.rb
|
|
11
|
+
# MixinBot.configure do
|
|
12
|
+
# self.blaze_handler = ->(envelope) { MyBot.process! envelope }
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Modes:
|
|
16
|
+
# - *fork* (default): after boot a dedicated child process runs
|
|
17
|
+
# {MixinBot::Blaze::Reactor}; a background thread supervises it and stops
|
|
18
|
+
# Puma if the child dies (fail fast — silent non-reception is worse than a
|
|
19
|
+
# visible restart). The child stops with Puma and monitors the master.
|
|
20
|
+
# - *async*: the reactor runs in a Puma background thread inside this very
|
|
21
|
+
# process. In cluster mode that process is the master.
|
|
22
|
+
#
|
|
23
|
+
# Requires `preload_app!` in cluster mode: plugins run in the launcher
|
|
24
|
+
# process, and without preloading it holds no application code to resolve
|
|
25
|
+
# the handler.
|
|
26
|
+
require 'English'
|
|
27
|
+
require 'mixin_bot'
|
|
28
|
+
require 'puma/plugin'
|
|
29
|
+
|
|
30
|
+
module Puma
|
|
31
|
+
class DSL
|
|
32
|
+
# Selects where the Blaze connection lives (see file comment).
|
|
33
|
+
#
|
|
34
|
+
# @param mode [Symbol] +:fork+ (default) or +:async+
|
|
35
|
+
def mixin_blaze_mode(mode = :fork)
|
|
36
|
+
@options[:mixin_blaze_mode] = mode.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Puma::Plugin.create do
|
|
42
|
+
def start(launcher)
|
|
43
|
+
@launcher = launcher
|
|
44
|
+
@log_writer = launcher.log_writer
|
|
45
|
+
@puma_pid = $PROCESS_ID
|
|
46
|
+
@mode = launcher.options[:mixin_blaze_mode] || :fork
|
|
47
|
+
|
|
48
|
+
unless %i[fork async].include?(@mode)
|
|
49
|
+
@log_writer.error "mixin_blaze: mixin_blaze_mode must be fork or async, got #{@mode.inspect}; plugin not started"
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if launcher.options[:workers].to_i.positive? && !launcher.options[:preload_app]
|
|
54
|
+
@log_writer.error 'mixin_blaze: Puma cluster mode requires preload_app! (the launcher process hosts the Blaze handler); ' \
|
|
55
|
+
'enable preload_app! or drop to single mode; plugin not started'
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@mode == :async ? start_async_mode : start_fork_mode
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def start_fork_mode
|
|
65
|
+
in_background { monitor_blaze_fork }
|
|
66
|
+
|
|
67
|
+
register_lifecycle(
|
|
68
|
+
booted: -> { fork_blaze },
|
|
69
|
+
stopped: -> { stop_blaze_fork },
|
|
70
|
+
restart: -> { stop_blaze_fork }
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def start_async_mode
|
|
75
|
+
register_lifecycle(
|
|
76
|
+
booted: -> { start_blaze_async },
|
|
77
|
+
stopped: -> { stop_blaze_async },
|
|
78
|
+
restart: lambda {
|
|
79
|
+
stop_blaze_async
|
|
80
|
+
start_blaze_async
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Puma 7 renamed the lifecycle hooks; the old names remain as deprecated
|
|
86
|
+
# aliases (same branching as Solid Queue's plugin).
|
|
87
|
+
def register_lifecycle(booted:, stopped:, restart:)
|
|
88
|
+
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new('7')
|
|
89
|
+
@launcher.events.on_booted { booted.call }
|
|
90
|
+
@launcher.events.on_stopped { stopped.call }
|
|
91
|
+
@launcher.events.on_restart { restart.call }
|
|
92
|
+
else
|
|
93
|
+
@launcher.events.after_booted { booted.call }
|
|
94
|
+
@launcher.events.after_stopped { stopped.call }
|
|
95
|
+
@launcher.events.before_restart { restart.call }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# ---- fork mode ----
|
|
100
|
+
|
|
101
|
+
def fork_blaze
|
|
102
|
+
reactor = build_reactor
|
|
103
|
+
return if reactor.nil?
|
|
104
|
+
|
|
105
|
+
@blaze_pid = fork do
|
|
106
|
+
@blaze_pid = nil
|
|
107
|
+
# the master's INT trap must not run here; raise Interrupt on the main
|
|
108
|
+
# thread instead, so the reactor unwinds and closes the connection
|
|
109
|
+
Signal.trap(:INT) { raise Interrupt }
|
|
110
|
+
Signal.trap(:TERM) { exit!(0) }
|
|
111
|
+
Thread.new { monitor_puma_master }
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
reactor.run
|
|
115
|
+
ensure
|
|
116
|
+
# skip at-exit finalization, which can deadlock on inherited
|
|
117
|
+
# database handles (same rationale as Solid Queue's plugin)
|
|
118
|
+
exit!(0)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
log "mixin_blaze: forked Blaze child (pid #{@blaze_pid})"
|
|
123
|
+
rescue NotImplementedError
|
|
124
|
+
@log_writer.error 'mixin_blaze: fork is unavailable on this platform; use mixin_blaze_mode :async'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def stop_blaze_fork
|
|
128
|
+
return unless @blaze_pid
|
|
129
|
+
|
|
130
|
+
@shutting_down = true
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
Process.waitpid(@blaze_pid, Process::WNOHANG)
|
|
134
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
135
|
+
# already gone; the reap loop below handles it
|
|
136
|
+
end
|
|
137
|
+
log 'mixin_blaze: stopping Blaze child...'
|
|
138
|
+
begin
|
|
139
|
+
Process.kill(:INT, @blaze_pid)
|
|
140
|
+
rescue Errno::ESRCH
|
|
141
|
+
@blaze_pid = nil
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# bounded reap: never let a stuck child hold up Puma's shutdown
|
|
146
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10.0
|
|
147
|
+
reaped = false
|
|
148
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
149
|
+
begin
|
|
150
|
+
reaped = true if Process.waitpid(@blaze_pid, Process::WNOHANG)
|
|
151
|
+
break if reaped
|
|
152
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
153
|
+
reaped = true
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
sleep 0.05
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
unless reaped
|
|
160
|
+
log "mixin_blaze: Blaze child #{@blaze_pid} did not stop within 10s; killing"
|
|
161
|
+
begin
|
|
162
|
+
Process.kill(:KILL, @blaze_pid)
|
|
163
|
+
rescue Errno::ESRCH
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
begin
|
|
167
|
+
Process.waitpid(@blaze_pid) # KILL is fatal; reap the zombie
|
|
168
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
@blaze_pid = nil
|
|
173
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
174
|
+
@blaze_pid = nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def monitor_blaze_fork
|
|
178
|
+
loop do
|
|
179
|
+
if blaze_fork_dead?
|
|
180
|
+
log 'mixin_blaze: Blaze child has gone away, stopping Puma...'
|
|
181
|
+
stop_puma!
|
|
182
|
+
break
|
|
183
|
+
end
|
|
184
|
+
sleep 2
|
|
185
|
+
end
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
log "mixin_blaze: monitor thread ended (#{e.class}: #{e.message})"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def blaze_fork_dead?
|
|
191
|
+
return false if @shutting_down
|
|
192
|
+
return false unless @blaze_pid
|
|
193
|
+
|
|
194
|
+
Process.waitpid(@blaze_pid, Process::WNOHANG)
|
|
195
|
+
false
|
|
196
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
197
|
+
true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def monitor_puma_master
|
|
201
|
+
loop do
|
|
202
|
+
if Process.ppid != @puma_pid
|
|
203
|
+
Process.kill(:INT, $PROCESS_ID)
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
sleep 2
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ---- async mode ----
|
|
211
|
+
|
|
212
|
+
def start_blaze_async
|
|
213
|
+
reactor = build_reactor
|
|
214
|
+
return if reactor.nil?
|
|
215
|
+
|
|
216
|
+
@reactor = reactor
|
|
217
|
+
@blaze_thread = Thread.new do
|
|
218
|
+
Thread.current.name = 'puma plugin mixin_blaze' if Thread.current.respond_to?(:name=)
|
|
219
|
+
reactor.run
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
log "mixin_blaze: Blaze reactor running in-process (pid #{Process.pid})"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def stop_blaze_async(join_limit: 1)
|
|
226
|
+
reactor = @reactor
|
|
227
|
+
thread = @blaze_thread
|
|
228
|
+
@reactor = nil
|
|
229
|
+
@blaze_thread = nil
|
|
230
|
+
|
|
231
|
+
return unless reactor
|
|
232
|
+
|
|
233
|
+
reactor.stop
|
|
234
|
+
thread&.join(join_limit)
|
|
235
|
+
log 'mixin_blaze: in-process Blaze reactor stopped'
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# ---- shared ----
|
|
239
|
+
|
|
240
|
+
# Handler resolution happens at boot time (after the application has
|
|
241
|
+
# loaded — Puma starts plugin background work after load_and_bind).
|
|
242
|
+
def build_reactor
|
|
243
|
+
handler = MixinBot.config.blaze_handler
|
|
244
|
+
if handler.nil?
|
|
245
|
+
@log_writer.error 'mixin_blaze: no handler configured — set MixinBot.configure { self.blaze_handler = ->(envelope) { ... } }; ' \
|
|
246
|
+
'plugin not started'
|
|
247
|
+
return nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
MixinBot::Blaze::Reactor.new(handler: handler, logger: ->(level, detail) { log("mixin_blaze #{level}: #{detail}") })
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def stop_puma!
|
|
254
|
+
Process.kill(:INT, $PROCESS_ID)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def log(message)
|
|
258
|
+
@log_writer.log(message)
|
|
259
|
+
rescue Errno::EIO, Errno::EPIPE, Errno::EBADF
|
|
260
|
+
# the controlling terminal can disappear mid-shutdown; keep going
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
end
|
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
|
+
> Ruby SDK and CLI for Mixin Network: Safe UTXO transfers, REST API, Blaze messaging, transaction crypto, optional MVM helpers. Ruby >= 4.0. Gem version 3.0.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:
|
|
4
|
+
version: 3.0.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
|
|
@@ -24,6 +23,20 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: async
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
27
40
|
- !ruby/object:Gem::Dependency
|
|
28
41
|
name: async-websocket
|
|
29
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -333,6 +346,7 @@ files:
|
|
|
333
346
|
- lib/mixin_bot/api/turn.rb
|
|
334
347
|
- lib/mixin_bot/api/user.rb
|
|
335
348
|
- lib/mixin_bot/api/withdraw.rb
|
|
349
|
+
- lib/mixin_bot/blaze/reactor.rb
|
|
336
350
|
- lib/mixin_bot/bot_auth.rb
|
|
337
351
|
- lib/mixin_bot/cache_store_adapter.rb
|
|
338
352
|
- lib/mixin_bot/cli.rb
|
|
@@ -387,6 +401,7 @@ files:
|
|
|
387
401
|
- lib/mvm/nft.rb
|
|
388
402
|
- lib/mvm/registry.rb
|
|
389
403
|
- lib/mvm/scan.rb
|
|
404
|
+
- lib/puma/plugin/mixin_blaze.rb
|
|
390
405
|
- lib/tasks/api_coverage.rake
|
|
391
406
|
- llms.txt
|
|
392
407
|
homepage: https://github.com/an-lee/mixin_bot
|
|
@@ -394,7 +409,6 @@ licenses:
|
|
|
394
409
|
- MIT
|
|
395
410
|
metadata:
|
|
396
411
|
rubygems_mfa_required: 'true'
|
|
397
|
-
post_install_message:
|
|
398
412
|
rdoc_options: []
|
|
399
413
|
require_paths:
|
|
400
414
|
- lib
|
|
@@ -402,15 +416,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
402
416
|
requirements:
|
|
403
417
|
- - ">="
|
|
404
418
|
- !ruby/object:Gem::Version
|
|
405
|
-
version:
|
|
419
|
+
version: 4.0.0
|
|
406
420
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
407
421
|
requirements:
|
|
408
422
|
- - ">="
|
|
409
423
|
- !ruby/object:Gem::Version
|
|
410
424
|
version: '0'
|
|
411
425
|
requirements: []
|
|
412
|
-
rubygems_version:
|
|
413
|
-
signing_key:
|
|
426
|
+
rubygems_version: 4.0.16
|
|
414
427
|
specification_version: 4
|
|
415
428
|
summary: A Ruby SDK for Mixin Network
|
|
416
429
|
test_files: []
|