mixin_bot 2.0.0 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d03bd8cb9d6710487b3ef3c5ed57e88136a0bcb99c841d366fe7e24b3e8a43c
4
- data.tar.gz: 5e0c70f36c6ca291a0e65e11137434474ad804829e67c553a7b8fbee651b5ea9
3
+ metadata.gz: 2bbc251caedc50e5324a90713c291cf019b2bf0fa003a7ad4df3401af5b175fa
4
+ data.tar.gz: cb7e858d5073bb2fc8bd5e487027fc10a684b8ff05dbc2949aa48270808e1f0a
5
5
  SHA512:
6
- metadata.gz: a717d7b6103c5e1df8b1cb044303114c159cbc31cb47e6f0a33e9c7e7906a204c0fd3ab92507b9eb903979c603c5031e192a2f6bf392ee4d2636eea5f3f3c88c
7
- data.tar.gz: 75a350a5053a490aff9cfac0b556e70dad2d2e20bf4cafb4408640b6d5c5dd932ec2264bf2cf952a3dec511b4e71855d4c5996e7acb32ccf6ea201b17943c80f
6
+ metadata.gz: 758781312e8be1e922eee338ed4ddfd3154c44ea25fb1d7e2b67d9e3ea8298381e4cc4d6df33904c35b831545d136da14c4af1630fdabe7789d3761a1617d6d9
7
+ data.tar.gz: a5350e9812115ef75e39a906e6fb48aa55189692887ee037c0dc3f29cf67b00e70b912575fbe5b9d1f44d93be29c520c2a3142804671c1bcfd3bc96dcfee6b9a
data/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md — MixinBot
2
2
 
3
- Ruby gem (v2.0.0): Mixin Network REST SDK + `mixinbot` CLI. Parity target: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client).
3
+ Ruby gem (v2.2.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
 
@@ -35,7 +35,7 @@ docs/agent/ # LLM-oriented CLI and cookbook docs
35
35
  ## CI and release
36
36
 
37
37
  - **CI** (`.github/workflows/ci.yml`): `pull_request` and `push` to `main` — `rake test` on Ruby 3.2/3.3/4.0, `rake rubocop` (3.3), `rake mixin_bot:api_coverage`.
38
- - **Release** (`.github/workflows/release.yml`): push tag `v*` (must match `MixinBot::VERSION`, e.g. tag `v2.0.0` for `VERSION = '2.0.0'`) → `rake build` → RubyGems via [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC; workflow `release.yml`, no repo secret).
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
data/API_COVERAGE.md CHANGED
@@ -1,8 +1,10 @@
1
- # Mixin Go SDK API Coverage
1
+ # Mixin SDK API Coverage
2
2
 
3
- Reference: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client) (`package bot`).
3
+ Go reference: [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client) (`package bot`).
4
4
 
5
- Status values: `done` | `alias` | `n/a` (CLI-only / config)
5
+ Node reference: [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client) (`@mixin.dev/mixin-node-sdk`).
6
+
7
+ Status values: `done` | `alias` | `n/a` (CLI-only / config / platform-specific)
6
8
 
7
9
  | Go symbol | Ruby method | HTTP / notes | Status |
8
10
  |-----------|-------------|--------------|--------|
@@ -140,4 +142,79 @@ Status values: `done` | `alias` | `n/a` (CLI-only / config)
140
142
  | NewSafeUser | `MixinBot::Configuration` | config | n/a |
141
143
  | cli/*, examples/*, mixin/rpc main | `mixinbot call` / `mixinbot list` | CLI dispatch to `MixinBot::API` | done |
142
144
 
145
+ ## Node SDK
146
+
147
+ TS-only or Node-first REST surfaces. Ruby methods follow snake_case; aliases mirror TS names where helpful.
148
+
149
+ | TS symbol | Ruby method | HTTP / notes | Status |
150
+ |-----------|-------------|--------------|--------|
151
+ | **Circle** |
152
+ | circle.fetch | `API#circle` | GET `/circles/:id` | done |
153
+ | circle.fetchList | `API#circles` | GET `/circles` | done |
154
+ | circle.conversations | `API#circle_conversations` | GET `/circles/:id/conversations` | done |
155
+ | circle.create | `API#create_circle` | POST `/circles` | done |
156
+ | circle.update | `API#update_circle` | POST `/circles/:id` | done |
157
+ | circle.delete | `API#delete_circle` | POST `/circles/:id/delete` | done |
158
+ | circle.addUser | `API#add_user_to_circle` | POST `/users/:id/circles` | done |
159
+ | circle.removeUser | `API#remove_user_from_circle` | POST `/users/:id/circles` | done |
160
+ | circle.addConversation | `API#add_conversation_to_circle` | POST `/conversations/:id/circles` | done |
161
+ | circle.removeConversation | `API#remove_conversation_from_circle` | POST `/conversations/:id/circles` | done |
162
+ | **App** |
163
+ | app.fetch | `API#app` | GET `/apps/:id` | done |
164
+ | app.fetchList | `API#apps` | GET `/apps` | done |
165
+ | app.properties | `API#app_properties` | GET `/apps/property` | done |
166
+ | app.billing | `API#app_billing` | GET `/safe/apps/:id/billing` | done |
167
+ | app.create | `API#create_app` | POST `/apps` | done |
168
+ | app.update | `API#update_app` | POST `/apps/:id` | done |
169
+ | app.updateSecret | `API#rotate_app_secret` | POST `/apps/:id/secret` | done |
170
+ | app.updateSafeSession | `API#update_app_safe_session` | POST `/safe/apps/:id/session` | done |
171
+ | app.registerSafe | `API#register_app_safe` | POST `/safe/apps/:id/register` | done |
172
+ | app.favorite / unfavorite | `API#add_favorite_app` / `#remove_favorite_app` | POST `/apps/:id/favorite` | done |
173
+ | app.favorites | `API#favorite_apps` | GET `/users/:id/apps/favorite` | done |
174
+ | app.migrate | `API#transfer_app_ownership` | POST `/apps/:id/transfer` | done |
175
+ | **OAuth** |
176
+ | oauth.getToken | `API#oauth_token` | POST `/oauth/token` | done |
177
+ | oauth.authorize | `API#authorize_code` | POST `/oauth/authorize` | done |
178
+ | oauth.authorizations | `API#authorizations` | GET `/authorizations` | done |
179
+ | oauth.revokeAuthorize | `API#revoke_authorization` | POST `/oauth/cancel` | done |
180
+ | **User** |
181
+ | user.profile | `API#me` | GET `/me` | done |
182
+ | user.friends | `API#friends` | GET `/friends` | done |
183
+ | user.blockings | `API#blocking_users` | GET `/blocking_users` | done |
184
+ | user.rotateCode | `API#rotate_user_code` | GET `/me/code` | done |
185
+ | user.search | `API#search_user` | GET `/search/:q` | done |
186
+ | user.fetch | `API#user` | GET `/users/:id` | done |
187
+ | user.fetchList | `API#fetch_users` | POST `/users/fetch` | done |
188
+ | user.createBareUser | `API#create_user` | POST `/users` | done |
189
+ | user.update | `API#update_me` | POST `/me` | done |
190
+ | user.updatePreferences | `API#update_preferences` | POST `/me/preferences` | done |
191
+ | user.updateRelationships | `API#relationship` | POST `/relationships` | done |
192
+ | user.logs | `API#user_logs` | GET `/logs` | done |
193
+ | **Conversation** |
194
+ | conversation.mute / unmute | `API#mute_conversation` / `#unmute_conversation` | POST `/conversations/:id/mute` | done |
195
+ | conversation.disappearDuration | `API#set_conversation_disappear_duration` | POST `/conversations/:id/disappear` | done |
196
+ | conversation.updateGroupInfo | `API#update_conversation` | POST `/conversations/:id` | done |
197
+ | conversation.* (CRUD/participants) | `API#conversation`, `#create_*`, `#join_*`, etc. | various | done |
198
+ | **Message** |
199
+ | message.sendAcknowledgement(s) | `API#acknowledge_message` / `#acknowledge_messages` | POST `/acknowledgements` | done |
200
+ | message.sendSticker/Audio/Video/Live/Location/Transfer | `API#send_*_message` | POST `/messages` | done |
201
+ | message.sendText/Image/File/Post/Contact/AppCard/AppButton/Recall | `API#send_*_message` | POST `/messages` | done |
202
+ | **Code** |
203
+ | code.fetch | `API#read_code` | GET `/codes/:id` | done |
204
+ | code.schemes | `API#create_scheme` | POST `/schemes` | done |
205
+ | **Address** |
206
+ | address.fetchListOfChain | `API#safe_withdraw_addresses` | GET `/safe/addresses?chain=` | done |
207
+ | address.fetch/create/delete | `API#get_withdraw_address`, `#create_withdraw_address`, `#delete_withdraw_address` | `/addresses` | done |
208
+ | **External** |
209
+ | external.proxy | `API#external_proxy` | POST `/external/proxy` | done |
210
+ | external.deposits | `API#transactions` (legacy) | GET `/external/transactions` | done |
211
+ | external.checkAddress | `API#check_address` | GET `/external/addresses/check` | done |
212
+ | external.exchangeRates | `API#fiats` | GET `/external/fiats` | done |
213
+ | **Safe / UTXO / Transfer / Network / etc.** |
214
+ | safe.* / utxo.* / transfer.* / network.* | spread across existing `API` modules | same HTTP paths as TS | done |
215
+ | **Blaze** |
216
+ | blaze.loop | `API#blaze` | WebSocket | done |
217
+ | **WebView** |
218
+ | WebViewApi | — | browser bridge | n/a |
219
+
143
220
  Update this file when adding or changing API surfaces. Run `rake mixin_bot:api_coverage` to ensure no `missing` rows remain.
data/CHANGELOG.md CHANGED
@@ -7,26 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-05-24
11
+
10
12
  ### Added
11
13
 
12
- - **`mixinbot call METHOD`** and **`mixinbot list`** invoke any public `MixinBot::API` method from the CLI with JSON keyword arguments (`-d`).
13
- - **`mixinbot utils call`** / **`mixinbot utils list`** same for `MixinBot.utils` helpers.
14
+ - **`MixinBot::API#create_user` billing preflight** verifies app billing headroom (`credit > cost + next user fee`) via `app_billing` and `app_properties` before `POST /users`. Raises `InsufficientAppBillingError` by default; pass `force: true` to skip. `create_safe_user` forwards `force:` to `create_user`.
15
+ - **`MixinBot::InsufficientAppBillingError`** structured fields: `app_id`, `credit`, `cost`, `increment`.
16
+ - **CLI** — `mixinbot call create_user ... --force` skips billing preflight; billing failures map to structured error kind `billing`.
14
17
 
15
- ### Changed
18
+ ## [2.1.0] - 2026-05-24
16
19
 
17
- - **`mixinbot transfer`** — uses Safe API (`create_safe_transfer`) instead of legacy `POST /transfers`.
18
- - **`mixinbot api`** — routes through `MixinBot::Client` (supports JSON array POST bodies); keystore loading includes `spend_key` and `client_secret`.
19
- - **`mixinbot updatetip`** — uses `update_tip_pin` instead of misusing `update_pin`.
20
- - **`mixinbot safetransfer`** — delegates to `transfer` (no duplicated signing pipeline).
20
+ ### Added
21
21
 
22
- ### Deprecated
22
+ - **Node SDK REST parity** with [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client): Circle API (`API::Circle`), `external_proxy`, extended App CRUD/Safe registration, OAuth `authorizations` / `revoke_authorization`, user `blocking_users` / `rotate_user_code` / `user_logs`, conversation mute/disappear, HTTP message acknowledgements and additional send helpers, `create_scheme`, `safe_withdraw_addresses`, and query params on `pending_safe_deposits`.
23
+ - **API_COVERAGE.md** Node SDK section mapping TS symbols to Ruby methods.
23
24
 
24
- - **`mixinbot legacy-transfer`** — explicit legacy transfer command (replaces old default `transfer` behavior).
25
- - **`mixinbot safetransfer`** — use `transfer` instead.
25
+ ## [2.0.1] - 2026-05-24
26
26
 
27
27
  ### Fixed
28
28
 
29
- - **`mixinbot nftmemo`** — calls `MixinBot.utils.nft_memo` (was a broken `nft` alias).
29
+ - **`StringIO.new`** — use keyword `contents:` for Ruby 4 compatibility (`lib/mixin_bot/api/message.rb`).
30
+
31
+ ### Changed
32
+
33
+ - Release workflow creates a GitHub Release with notes from `CHANGELOG.md` when publishing version tags.
30
34
 
31
35
  ## [2.0.0] - 2026-05-16
32
36
 
data/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  Ruby SDK and CLI for [Mixin Network](https://developers.mixin.one/docs): authenticated REST calls, **Safe** UTXO transfers, Blaze messaging, network asset catalog, inscriptions, invoices and mix addresses, transaction encoding, and optional **MVM** (Mixin Virtual Machine) helpers.
6
6
 
7
- The gem aims for **parity with the official [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client)** Go 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.
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.0.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
9
+ Current gem version: **2.2.0** (see [CHANGELOG.md](CHANGELOG.md) for breaking changes and deprecations).
10
10
 
11
11
  ## Requirements
12
12
 
@@ -353,14 +353,14 @@ Publishing to [RubyGems.org](https://rubygems.org/gems/mixin_bot) is automated w
353
353
 
354
354
  1. Bump `MixinBot::VERSION` in `lib/mixin_bot/version.rb` and update `CHANGELOG.md`.
355
355
  2. Commit and push to `main`.
356
- 3. Create and push a tag matching the gem version (e.g. `v2.0.1` for version `2.0.1`):
356
+ 3. Create and push a tag matching the gem version (e.g. `v2.1.0` for version `2.1.0`):
357
357
 
358
358
  ```bash
359
- git tag v2.0.1
360
- git push origin v2.0.1
359
+ git tag v2.1.0
360
+ git push origin v2.1.0
361
361
  ```
362
362
 
363
- The [Release workflow](.github/workflows/release.yml) builds the gem and publishes to RubyGems.org via [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (GitHub OIDC; trusted publisher for workflow `release.yml` on `an-lee/mixin_bot`). To build without publishing, run the Release workflow manually with **dry run** enabled.
363
+ The [Release workflow](.github/workflows/release.yml) builds the gem, publishes to RubyGems.org via [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (GitHub OIDC; trusted publisher for workflow `release.yml` on `an-lee/mixin_bot`), and creates a GitHub Release with notes from `CHANGELOG.md` and the `.gem` attached. To build without publishing, run the Release workflow manually with **dry run** enabled.
364
364
 
365
365
  ## References
366
366
 
data/docs/agent/cli.md CHANGED
@@ -72,7 +72,7 @@ Error (stderr, exit 1):
72
72
  }
73
73
  ```
74
74
 
75
- Error kinds: `invalid_args`, `auth`, `not_found`, `api_error`, `unsupported`, `conflict`, `internal`.
75
+ Error kinds: `invalid_args`, `auth`, `not_found`, `api_error`, `billing`, `unsupported`, `conflict`, `internal`.
76
76
 
77
77
  ## Commands
78
78
 
@@ -106,8 +106,12 @@ List JSON shape:
106
106
  mixinbot call me -k keystore.json -o json
107
107
  mixinbot call safe_outputs -k keystore.json -d '{"asset":"965e5c6e-434c-3fa9-b780-c50f43cd955c","state":"unspent","limit":10}' -o json
108
108
  mixinbot call user USER_UUID -k keystore.json --data-only -o json
109
+ mixinbot call create_user "Bot User" -k keystore.json -o json
110
+ mixinbot call create_user "Bot User" -k keystore.json --force -o json
109
111
  ```
110
112
 
113
+ `create_user` performs a client-side app billing preflight by default. When credit lacks headroom for the next billed user, the CLI returns `"kind": "billing"`. Use `--force` to skip the preflight (or pass `"force": true` in `-d`; `-d` wins when both are set).
114
+
111
115
  ### Raw HTTP
112
116
 
113
117
  ```bash
@@ -3,17 +3,116 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module App
6
+ def app(app_id, access_token: nil)
7
+ path = format('/apps/%<id>s', id: app_id)
8
+ client.get path, access_token:
9
+ end
10
+ alias fetch_app app
11
+
12
+ def apps(access_token: nil)
13
+ client.get '/apps', access_token:
14
+ end
15
+ alias fetch_apps apps
16
+
17
+ def app_properties(access_token: nil)
18
+ client.get '/apps/property', access_token:
19
+ end
20
+ alias app_property app_properties
21
+
22
+ def app_billing(app_id, access_token: nil)
23
+ path = format('/safe/apps/%<id>s/billing', id: app_id)
24
+ client.get path, access_token:
25
+ end
26
+
27
+ ##
28
+ # Verifies the app has billing headroom before a billed operation (e.g.
29
+ # creating a network user). Skipped when +force+ is true.
30
+ #
31
+ # @param force [Boolean] skip the preflight and call the API anyway
32
+ # @raise [InsufficientAppBillingError] when +credit+ is not greater than
33
+ # total cost plus the next user fee from {app_properties}
34
+ #
35
+ def ensure_app_billing_credit!(force: false, access_token: nil)
36
+ return if force
37
+
38
+ app_id = config.app_id
39
+ billing = app_billing(app_id, access_token:)['data']
40
+ properties = app_properties(access_token:)['data']
41
+
42
+ credit = billing_decimal billing['credit']
43
+ cost_users = billing_decimal billing.dig('cost', 'users')
44
+ cost_resources = billing_decimal billing.dig('cost', 'resources')
45
+ cost = cost_users + cost_resources
46
+ increment = billing_decimal properties['price']
47
+
48
+ return if credit > cost + increment
49
+
50
+ raise InsufficientAppBillingError.new(
51
+ app_id:,
52
+ credit: credit.to_s('F'),
53
+ cost: cost.to_s('F'),
54
+ increment: increment.to_s('F')
55
+ )
56
+ end
57
+
58
+ def create_app(**kwargs)
59
+ payload = {
60
+ redirect_uri: kwargs[:redirect_uri],
61
+ home_uri: kwargs[:home_uri],
62
+ name: kwargs[:name],
63
+ description: kwargs[:description],
64
+ icon_base64: kwargs[:icon_base64],
65
+ category: kwargs[:category],
66
+ capabilities: kwargs[:capabilities],
67
+ resource_patterns: kwargs[:resource_patterns]
68
+ }.compact
69
+ client.post '/apps', **payload, access_token: kwargs[:access_token]
70
+ end
71
+
72
+ def update_app(app_id, **kwargs)
73
+ path = format('/apps/%<id>s', id: app_id)
74
+ payload = {
75
+ redirect_uri: kwargs[:redirect_uri],
76
+ home_uri: kwargs[:home_uri],
77
+ name: kwargs[:name],
78
+ description: kwargs[:description],
79
+ icon_base64: kwargs[:icon_base64],
80
+ category: kwargs[:category],
81
+ capabilities: kwargs[:capabilities],
82
+ resource_patterns: kwargs[:resource_patterns]
83
+ }.compact
84
+ client.post path, **payload, access_token: kwargs[:access_token]
85
+ end
86
+
87
+ def rotate_app_secret(app_id, access_token: nil)
88
+ path = format('/apps/%<id>s/secret', id: app_id)
89
+ client.post path, access_token:
90
+ end
91
+ alias update_app_secret rotate_app_secret
92
+
93
+ def update_app_safe_session(app_id, session_public_key:, access_token: nil)
94
+ path = format('/safe/apps/%<id>s/session', id: app_id)
95
+ client.post path, session_public_key:, access_token:
96
+ end
97
+
98
+ def register_app_safe(app_id, spend_public_key:, signature_base64:, access_token: nil)
99
+ path = format('/safe/apps/%<id>s/register', id: app_id)
100
+ client.post path, spend_public_key:, signature_base64:, access_token:
101
+ end
102
+
6
103
  def add_favorite_app(app_id, access_token: nil)
7
104
  path = format('/apps/%<id>s/favorite', id: app_id)
8
105
 
9
106
  client.post path, access_token:
10
107
  end
108
+ alias favorite_app add_favorite_app
11
109
 
12
110
  def remove_favorite_app(app_id, access_token: nil)
13
111
  path = format('/apps/%<id>s/unfavorite', id: app_id)
14
112
 
15
113
  client.post path, access_token:
16
114
  end
115
+ alias unfavorite_app remove_favorite_app
17
116
 
18
117
  def favorite_apps(user_id = nil, access_token: nil)
19
118
  path = format('/users/%<id>s/apps/favorite', id: user_id || config.app_id)
@@ -27,6 +126,12 @@ module MixinBot
27
126
  client.post path, user_id: receiver_user_id, pin_base64: tip[:pin_base64] || tip[:pin], access_token:
28
127
  end
29
128
  alias migrate transfer_app_ownership
129
+
130
+ private
131
+
132
+ def billing_decimal(value)
133
+ BigDecimal(value.to_s)
134
+ end
30
135
  end
31
136
  end
32
137
  end
@@ -56,6 +56,17 @@ module MixinBot
56
56
  client.post path, **payload, access_token: kwargs[:access_token]
57
57
  end
58
58
 
59
+ def authorizations(app_id: nil, access_token: nil)
60
+ params = {}
61
+ params[:app] = app_id if app_id
62
+ client.get '/authorizations', **params, access_token:
63
+ end
64
+
65
+ def revoke_authorization(client_id, access_token: nil)
66
+ client.post '/oauth/cancel', client_id:, access_token:
67
+ end
68
+ alias revoke_authorize revoke_authorization
69
+
59
70
  def authorization_data(app_id, scope = ['PROFILE:READ'])
60
71
  @_app_id = app_id
61
72
  @_scope = scope.join(' ')
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Circle
6
+ def circle(circle_id, access_token: nil)
7
+ path = format('/circles/%<circle_id>s', circle_id:)
8
+ client.get path, access_token:
9
+ end
10
+ alias fetch_circle circle
11
+
12
+ def circles(access_token: nil)
13
+ client.get '/circles', access_token:
14
+ end
15
+ alias fetch_circles circles
16
+
17
+ def circle_conversations(circle_id, **params)
18
+ path = format('/circles/%<circle_id>s/conversations', circle_id:)
19
+ client.get path, **params.compact, access_token: params[:access_token]
20
+ end
21
+
22
+ def create_circle(name:, access_token: nil)
23
+ client.post '/circles', name:, access_token:
24
+ end
25
+
26
+ def update_circle(circle_id, name:, access_token: nil)
27
+ path = format('/circles/%<circle_id>s', circle_id:)
28
+ client.post path, name:, access_token:
29
+ end
30
+
31
+ def delete_circle(circle_id, access_token: nil)
32
+ path = format('/circles/%<circle_id>s/delete', circle_id:)
33
+ client.post path, access_token:
34
+ end
35
+
36
+ def add_user_to_circle(user_id:, circle_id:, access_token: nil)
37
+ path = format('/users/%<user_id>s/circles', user_id:)
38
+ client.post path, circle_id:, action: 'ADD', access_token:
39
+ end
40
+
41
+ def remove_user_from_circle(user_id:, circle_id:, access_token: nil)
42
+ path = format('/users/%<user_id>s/circles', user_id:)
43
+ client.post path, circle_id:, action: 'REMOVE', access_token:
44
+ end
45
+
46
+ def add_conversation_to_circle(conversation_id:, circle_id:, access_token: nil)
47
+ path = format('/conversations/%<conversation_id>s/circles', conversation_id:)
48
+ client.post path, circle_id:, action: 'ADD', access_token:
49
+ end
50
+
51
+ def remove_conversation_from_circle(conversation_id:, circle_id:, access_token: nil)
52
+ path = format('/conversations/%<conversation_id>s/circles', conversation_id:)
53
+ client.post path, circle_id:, action: 'REMOVE', access_token:
54
+ end
55
+ end
56
+ end
57
+ end
@@ -11,6 +11,11 @@ module MixinBot
11
11
  def read_multisig_by_code(code_id, access_token: nil)
12
12
  read_code(code_id, access_token:)
13
13
  end
14
+
15
+ def create_scheme(target, access_token: nil)
16
+ client.post '/schemes', target:, access_token:
17
+ end
18
+ alias schemes create_scheme
14
19
  end
15
20
  end
16
21
  end
@@ -55,23 +55,40 @@ module MixinBot
55
55
  )
56
56
  end
57
57
 
58
- def update_group_conversation_name(name:, conversation_id:, access_token: nil)
58
+ def update_conversation(conversation_id:, **kwargs)
59
59
  path = format('/conversations/%<id>s', id: conversation_id)
60
60
  payload = {
61
- name:
62
- }
61
+ name: kwargs[:name],
62
+ announcement: kwargs[:announcement]
63
+ }.compact
64
+ client.post path, **payload, access_token: kwargs[:access_token]
65
+ end
66
+ alias update_group_info update_conversation
63
67
 
64
- client.post path, **payload, access_token:
68
+ def update_group_conversation_name(name:, conversation_id:, access_token: nil)
69
+ update_conversation(conversation_id:, name:, access_token:)
65
70
  end
66
71
 
67
72
  def update_group_conversation_announcement(announcement:, conversation_id:, access_token: nil)
68
- path = format('/conversations/%<id>s', id: conversation_id)
69
- payload = {
70
- announcement:
71
- }
73
+ update_conversation(conversation_id:, announcement:, access_token:)
74
+ end
75
+
76
+ def mute_conversation(conversation_id, duration:, access_token: nil)
77
+ path = format('/conversations/%<id>s/mute', id: conversation_id)
78
+ client.post path, duration:, access_token:
79
+ end
80
+ alias mute mute_conversation
81
+
82
+ def unmute_conversation(conversation_id, access_token: nil)
83
+ mute_conversation conversation_id, duration: 0, access_token:
84
+ end
85
+ alias unmute unmute_conversation
72
86
 
73
- client.post path, **payload, access_token:
87
+ def set_conversation_disappear_duration(conversation_id, duration:, access_token: nil)
88
+ path = format('/conversations/%<id>s/disappear', id: conversation_id)
89
+ client.post path, duration:, access_token:
74
90
  end
91
+ alias disappear_duration set_conversation_disappear_duration
75
92
 
76
93
  # participants = [{ user_id: "" }]
77
94
  def add_conversation_participants(conversation_id:, user_ids:, access_token: nil)
@@ -3,8 +3,15 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Deposit
6
- def pending_safe_deposits
7
- client.get '/safe/deposits', access_token: ''
6
+ def pending_safe_deposits(**params)
7
+ query = {
8
+ limit: params[:limit],
9
+ offset: params[:offset],
10
+ asset: params[:asset],
11
+ destination: params[:destination],
12
+ tag: params[:tag]
13
+ }.compact
14
+ client.get '/safe/deposits', **query, access_token: params[:access_token] || ''
8
15
  end
9
16
  alias fetch_pending_safe_deposits pending_safe_deposits
10
17
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module External
6
+ def external_proxy(method:, params: [], access_token: nil)
7
+ client.post '/external/proxy', method:, params:, access_token:
8
+ end
9
+ alias proxy external_proxy
10
+ end
11
+ end
12
+ end
@@ -126,6 +126,27 @@ module MixinBot
126
126
  path = '/relationships'
127
127
  client.post path, user_id:, action:, access_token:
128
128
  end
129
+ alias update_relationship relationship
130
+
131
+ def blocking_users(access_token: nil)
132
+ client.get '/blocking_users', access_token:
133
+ end
134
+ alias blockings blocking_users
135
+
136
+ def rotate_user_code(access_token: nil)
137
+ client.get '/me/code', access_token:
138
+ end
139
+ alias rotate_code rotate_user_code
140
+
141
+ def user_logs(**params)
142
+ query = {
143
+ limit: params[:limit],
144
+ offset: params[:offset],
145
+ category: params[:category]
146
+ }.compact
147
+ client.get '/logs', **query, access_token: params[:access_token]
148
+ end
149
+ alias logs user_logs
129
150
  end
130
151
  end
131
152
  end
@@ -48,6 +48,18 @@ module MixinBot
48
48
  base_message_params(options.merge(category: 'PLAIN_VIDEO'))
49
49
  end
50
50
 
51
+ def plain_live(options)
52
+ base_message_params(options.merge(category: 'PLAIN_LIVE'))
53
+ end
54
+
55
+ def plain_location(options)
56
+ base_message_params(options.merge(category: 'PLAIN_LOCATION'))
57
+ end
58
+
59
+ def transfer_message(options)
60
+ base_message_params(options.merge(category: 'SYSTEM_ACCOUNT_SNAPSHOT'))
61
+ end
62
+
51
63
  def app_card(options)
52
64
  base_message_params(options.merge(category: 'APP_CARD'))
53
65
  end
@@ -105,7 +117,7 @@ module MixinBot
105
117
  params:
106
118
  }.to_json
107
119
 
108
- io = StringIO.new 'wb'
120
+ io = StringIO.new
109
121
  gzip = Zlib::GzipWriter.new io
110
122
  gzip.write msg
111
123
  gzip.close
@@ -141,6 +153,30 @@ module MixinBot
141
153
  send_message app_button_group(options)
142
154
  end
143
155
 
156
+ def send_sticker_message(options)
157
+ send_message plain_sticker(options)
158
+ end
159
+
160
+ def send_audio_message(options)
161
+ send_message plain_audio(options)
162
+ end
163
+
164
+ def send_video_message(options)
165
+ send_message plain_video(options)
166
+ end
167
+
168
+ def send_live_message(options)
169
+ send_message plain_live(options)
170
+ end
171
+
172
+ def send_location_message(options)
173
+ send_message plain_location(options)
174
+ end
175
+
176
+ def send_transfer_message(options)
177
+ send_message transfer_message(options)
178
+ end
179
+
144
180
  def recall_message(message_id, options)
145
181
  send_message [recall_message_params(message_id, options)]
146
182
  end
@@ -149,6 +185,17 @@ module MixinBot
149
185
  send_message messages
150
186
  end
151
187
 
188
+ def acknowledge_message(message_id, status: 'READ', access_token: nil)
189
+ payload = { message_id:, status: }
190
+ client.post '/acknowledgements', payload, access_token:
191
+ end
192
+ alias send_acknowledgement acknowledge_message
193
+
194
+ def acknowledge_messages(messages, access_token: nil)
195
+ client.post '/acknowledgements', *messages, access_token:
196
+ end
197
+ alias send_acknowledgements acknowledge_messages
198
+
152
199
  # http post request
153
200
  def send_message(payload)
154
201
  path = '/messages'
@@ -37,9 +37,14 @@ module MixinBot
37
37
  #
38
38
  # @param full_name [String] display name for the new user
39
39
  # @param key [String, nil] optional 32-byte Ed25519 seed
40
+ # @param force [Boolean] when false (default), verify app billing credit
41
+ # headroom before calling the API; when true, skip the preflight
40
42
  # @return [Hash] Mixin response merged with the hex-encoded private key
43
+ # @raise [InsufficientAppBillingError] when billing credit lacks headroom
41
44
  #
42
- def create_user(full_name, key: nil)
45
+ def create_user(full_name, key: nil, force: false)
46
+ ensure_app_billing_credit!(force:)
47
+
43
48
  keypair = JOSE::JWA::Ed25519.keypair key
44
49
  session_secret = Base64.urlsafe_encode64 keypair[0], padding: false
45
50
  private_key = keypair[1].unpack1('H*')
@@ -85,19 +90,23 @@ module MixinBot
85
90
  # @param name [String] display name for the new user
86
91
  # @param private_key [String, nil] optional 32-byte session Ed25519 seed
87
92
  # @param spend_key [String, nil] optional 32-byte spend Ed25519 seed
93
+ # @param force [Boolean] forwarded to {#create_user}; see billing preflight
94
+ # there
88
95
  # @return [Hash] keystore with +:app_id+, +:session_id+,
89
96
  # +:session_private_key+, +:server_public_key+ and +:spend_key+
90
97
  # @raise [MixinBot::Error] when registration ultimately fails. Transient
91
98
  # PIN/response errors are retried up to {SAFE_REGISTER_MAX_RETRIES}
92
99
  # times; other errors bubble up immediately.
100
+ # @raise [InsufficientAppBillingError] when {#create_user} billing
101
+ # preflight fails
93
102
  #
94
- def create_safe_user(name, private_key: nil, spend_key: nil)
103
+ def create_safe_user(name, private_key: nil, spend_key: nil, force: false)
95
104
  session_keypair = JOSE::JWA::Ed25519.keypair private_key
96
105
  spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
97
106
 
98
107
  spend_key_hex = spend_keypair[1].unpack1('H*')
99
108
 
100
- user = create_user name, key: session_keypair[1][...32]
109
+ user = create_user name, key: session_keypair[1][...32], force: force
101
110
  data = user.fetch('data')
102
111
 
103
112
  keystore = {
@@ -70,6 +70,11 @@ module MixinBot
70
70
  end
71
71
  alias get_addresses_by_asset_id withdraw_addresses
72
72
 
73
+ def safe_withdraw_addresses(chain_id, access_token: nil)
74
+ client.get '/safe/addresses', chain: chain_id, access_token:
75
+ end
76
+ alias fetch_list_of_chain safe_withdraw_addresses
77
+
73
78
  def check_address(asset:, destination:, tag: nil)
74
79
  client.get '/external/addresses/check', asset:, destination:, tag:, access_token: ''
75
80
  end
data/lib/mixin_bot/api.rb CHANGED
@@ -10,10 +10,12 @@ require_relative 'api/auth'
10
10
  require_relative 'api/blaze'
11
11
  require_relative 'api/chain'
12
12
  require_relative 'api/code'
13
+ require_relative 'api/circle'
13
14
  require_relative 'api/computer_api'
14
15
  require_relative 'api/conversation'
15
16
  require_relative 'api/deposit'
16
17
  require_relative 'api/encrypted_message'
18
+ require_relative 'api/external'
17
19
  require_relative 'api/fiat'
18
20
  require_relative 'api/inscription'
19
21
  require_relative 'api/legacy_collectible'
@@ -338,10 +340,12 @@ module MixinBot
338
340
  include MixinBot::API::Blaze
339
341
  include MixinBot::API::Chain
340
342
  include MixinBot::API::Code
343
+ include MixinBot::API::Circle
341
344
  include MixinBot::API::ComputerApi
342
345
  include MixinBot::API::Conversation
343
346
  include MixinBot::API::Deposit
344
347
  include MixinBot::API::EncryptedMessage
348
+ include MixinBot::API::External
345
349
  include MixinBot::API::Fiat
346
350
  include MixinBot::API::Inscription
347
351
  include MixinBot::API::LegacyCollectible
@@ -15,11 +15,13 @@ module MixinBot
15
15
  LONGDESC
16
16
  option :keystore, type: :string, aliases: '-k', desc: 'keystore JSON file path or inline JSON'
17
17
  option :data, type: :string, aliases: '-d', default: '{}', desc: 'JSON object of keyword arguments'
18
+ option :force, type: :boolean, default: false, desc: 'Skip billing preflight for create_user (see -d force to override)'
18
19
  option :data_only, type: :boolean, default: false, desc: 'Print only the data field of API responses'
19
20
  def call(method_name, *positional)
20
21
  with_command_name('call') do
21
22
  setup_api_instance!
22
23
  kwargs = parse_json_data(options[:data])
24
+ kwargs = merge_call_force_kwargs(method_name, kwargs)
23
25
  result = invoke_api(method_name, kwargs:, positional:)
24
26
  print_result(result, data_only: options[:data_only], command: 'call')
25
27
  end
@@ -55,6 +57,14 @@ module MixinBot
55
57
 
56
58
  private
57
59
 
60
+ def merge_call_force_kwargs(method_name, kwargs)
61
+ return kwargs unless method_name.to_sym == :create_user
62
+ return kwargs if kwargs.key?(:force)
63
+ return kwargs unless options[:force]
64
+
65
+ kwargs.merge(force: true)
66
+ end
67
+
58
68
  def print_pretty_list(items, total, limit, offset)
59
69
  grouped = items.group_by { |item| item['owner'] }
60
70
  grouped.sort_by { |owner, _| owner }.each do |owner, names|
@@ -12,6 +12,7 @@ module MixinBot
12
12
  api_error: { retryable: false, description: 'Mixin API returned an error' },
13
13
  unsupported: { retryable: false, description: 'Operation is not supported in this context' },
14
14
  conflict: { retryable: false, description: 'Resource exists with incompatible configuration' },
15
+ billing: { retryable: false, description: 'App billing credit insufficient for the operation' },
15
16
  internal: { retryable: false, description: 'Unexpected internal error' }
16
17
  }.freeze
17
18
 
@@ -35,6 +36,8 @@ module MixinBot
35
36
  :auth
36
37
  when NotFoundError, UserNotFoundError
37
38
  :not_found
39
+ when InsufficientAppBillingError
40
+ :billing
38
41
  when ResponseError, RequestError, HttpError,
39
42
  InsufficientBalanceError, UtxoInsufficientError, InsufficientPoolError
40
43
  :api_error
@@ -51,6 +51,26 @@ module MixinBot
51
51
  #
52
52
  class InsufficientBalanceError < Error; end
53
53
 
54
+ ##
55
+ # Raised when app prepaid billing credit lacks headroom for a billed operation.
56
+ #
57
+ class InsufficientAppBillingError < Error
58
+ attr_reader :app_id, :credit, :cost, :increment
59
+
60
+ def initialize(app_id:, credit:, cost:, increment:)
61
+ @app_id = app_id
62
+ @credit = credit
63
+ @cost = cost
64
+ @increment = increment
65
+ super(
66
+ format(
67
+ 'app billing insufficient: credit %<credit>s <= cost %<cost>s + increment %<increment>s (app_id=%<app_id>s)',
68
+ credit:, cost:, increment:, app_id:
69
+ )
70
+ )
71
+ end
72
+ end
73
+
54
74
  ##
55
75
  # Raised when selected UTXOs cannot cover the requested amount (mirrors Go +UtxoInsufficientError+).
56
76
  #
@@ -11,5 +11,5 @@ module MixinBot
11
11
  #
12
12
  # @see https://semver.org/
13
13
  #
14
- VERSION = '2.0.0'
14
+ VERSION = '2.2.0'
15
15
  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.2. Gem version 2.0.0.
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.2.0.
4
4
 
5
5
  Important notes:
6
6
 
@@ -11,7 +11,7 @@ Important notes:
11
11
  ## Docs
12
12
 
13
13
  - [README](README.md): Install, config, API overview, CLI summary
14
- - [API coverage vs Go SDK](API_COVERAGE.md): Method parity matrix
14
+ - [API coverage vs Go/Node SDKs](API_COVERAGE.md): Method parity matrix
15
15
  - [CLI for agents](docs/agent/cli.md): Structured output, schema, examples
16
16
  - [Agent cookbook](docs/agent/cookbook.md): Common tasks (auth, transfer, messaging)
17
17
  - [Changelog](CHANGELOG.md): Breaking changes
@@ -25,5 +25,6 @@ Important notes:
25
25
  ## Optional
26
26
 
27
27
  - [Mixin official docs](https://developers.mixin.one/docs): External API reference
28
- - [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client): Parity reference
28
+ - [bot-api-go-client](https://github.com/MixinNetwork/bot-api-go-client): Go parity reference
29
+ - [bot-api-nodejs-client](https://github.com/MixinNetwork/bot-api-nodejs-client): Node parity reference
29
30
  - [Generated RDoc](doc/index.html): Full API reference (local)
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.0.0
4
+ version: 2.2.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-20 00:00:00.000000000 Z
11
+ date: 2026-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -282,11 +282,13 @@ files:
282
282
  - lib/mixin_bot/api/auth.rb
283
283
  - lib/mixin_bot/api/blaze.rb
284
284
  - lib/mixin_bot/api/chain.rb
285
+ - lib/mixin_bot/api/circle.rb
285
286
  - lib/mixin_bot/api/code.rb
286
287
  - lib/mixin_bot/api/computer_api.rb
287
288
  - lib/mixin_bot/api/conversation.rb
288
289
  - lib/mixin_bot/api/deposit.rb
289
290
  - lib/mixin_bot/api/encrypted_message.rb
291
+ - lib/mixin_bot/api/external.rb
290
292
  - lib/mixin_bot/api/fiat.rb
291
293
  - lib/mixin_bot/api/inscription.rb
292
294
  - lib/mixin_bot/api/legacy_collectible.rb