mixin_bot 1.4.0 → 2.1.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +75 -0
  3. data/API_COVERAGE.md +220 -0
  4. data/CHANGELOG.md +108 -0
  5. data/README.md +375 -0
  6. data/docs/agent/cli.md +149 -0
  7. data/docs/agent/cookbook.md +152 -0
  8. data/examples/blaze.rb +43 -0
  9. data/examples/config.yml.example +21 -0
  10. data/lib/mixin_bot/address.rb +43 -3
  11. data/lib/mixin_bot/api/app.rb +75 -0
  12. data/lib/mixin_bot/api/asset.rb +114 -3
  13. data/lib/mixin_bot/api/auth.rb +29 -9
  14. data/lib/mixin_bot/api/blaze.rb +81 -0
  15. data/lib/mixin_bot/api/chain.rb +94 -0
  16. data/lib/mixin_bot/api/circle.rb +57 -0
  17. data/lib/mixin_bot/api/code.rb +21 -0
  18. data/lib/mixin_bot/api/computer_api.rb +60 -0
  19. data/lib/mixin_bot/api/conversation.rb +33 -10
  20. data/lib/mixin_bot/api/deposit.rb +19 -0
  21. data/lib/mixin_bot/api/encrypted_message.rb +1 -1
  22. data/lib/mixin_bot/api/external.rb +12 -0
  23. data/lib/mixin_bot/api/fiat.rb +12 -0
  24. data/lib/mixin_bot/api/inscription.rb +2 -2
  25. data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
  26. data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
  27. data/lib/mixin_bot/api/legacy_output.rb +10 -3
  28. data/lib/mixin_bot/api/legacy_payment.rb +2 -0
  29. data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
  30. data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
  31. data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
  32. data/lib/mixin_bot/api/legacy_user.rb +51 -0
  33. data/lib/mixin_bot/api/me.rb +120 -3
  34. data/lib/mixin_bot/api/message.rb +66 -28
  35. data/lib/mixin_bot/api/multisig.rb +19 -0
  36. data/lib/mixin_bot/api/network.rb +17 -0
  37. data/lib/mixin_bot/api/network_asset.rb +27 -0
  38. data/lib/mixin_bot/api/output.rb +1 -1
  39. data/lib/mixin_bot/api/pin.rb +16 -3
  40. data/lib/mixin_bot/api/pin_payload.rb +26 -0
  41. data/lib/mixin_bot/api/session.rb +14 -0
  42. data/lib/mixin_bot/api/snapshot.rb +6 -0
  43. data/lib/mixin_bot/api/tip.rb +74 -1
  44. data/lib/mixin_bot/api/transaction.rb +106 -17
  45. data/lib/mixin_bot/api/transfer.rb +141 -14
  46. data/lib/mixin_bot/api/turn.rb +12 -0
  47. data/lib/mixin_bot/api/user.rb +148 -45
  48. data/lib/mixin_bot/api/withdraw.rb +29 -23
  49. data/lib/mixin_bot/api.rb +252 -3
  50. data/lib/mixin_bot/bot_auth.rb +71 -0
  51. data/lib/mixin_bot/cli/api.rb +224 -143
  52. data/lib/mixin_bot/cli/base.rb +77 -0
  53. data/lib/mixin_bot/cli/call.rb +71 -0
  54. data/lib/mixin_bot/cli/errors.rb +56 -0
  55. data/lib/mixin_bot/cli/node.rb +9 -2
  56. data/lib/mixin_bot/cli/output.rb +196 -0
  57. data/lib/mixin_bot/cli/schema.rb +274 -0
  58. data/lib/mixin_bot/cli/schema_command.rb +21 -0
  59. data/lib/mixin_bot/cli/utils.rb +114 -18
  60. data/lib/mixin_bot/cli.rb +124 -48
  61. data/lib/mixin_bot/client/error_mapper.rb +40 -0
  62. data/lib/mixin_bot/client.rb +94 -64
  63. data/lib/mixin_bot/computer.rb +132 -0
  64. data/lib/mixin_bot/configuration.rb +108 -1
  65. data/lib/mixin_bot/errors.rb +102 -0
  66. data/lib/mixin_bot/models/address.rb +11 -0
  67. data/lib/mixin_bot/models/api_envelope.rb +67 -0
  68. data/lib/mixin_bot/models/asset.rb +11 -0
  69. data/lib/mixin_bot/models/ghost_keys.rb +14 -0
  70. data/lib/mixin_bot/models/output.rb +11 -0
  71. data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
  72. data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
  73. data/lib/mixin_bot/models/user.rb +11 -0
  74. data/lib/mixin_bot/models.rb +10 -0
  75. data/lib/mixin_bot/monitor.rb +77 -0
  76. data/lib/mixin_bot/transaction/buffer.rb +34 -0
  77. data/lib/mixin_bot/transaction/decoder.rb +227 -0
  78. data/lib/mixin_bot/transaction/encoder.rb +255 -0
  79. data/lib/mixin_bot/transaction.rb +6 -475
  80. data/lib/mixin_bot/url_scheme.rb +63 -0
  81. data/lib/mixin_bot/utils/address.rb +17 -80
  82. data/lib/mixin_bot/utils/crypto.rb +173 -1
  83. data/lib/mixin_bot/utils/decoder.rb +1 -1
  84. data/lib/mixin_bot/utils/encoder.rb +13 -0
  85. data/lib/mixin_bot/utils.rb +45 -0
  86. data/lib/mixin_bot/uuid.rb +78 -1
  87. data/lib/mixin_bot/version.rb +11 -1
  88. data/lib/mixin_bot.rb +172 -18
  89. data/lib/mvm/bridge.rb +46 -0
  90. data/lib/mvm/client.rb +60 -0
  91. data/lib/mvm/nft.rb +4 -2
  92. data/lib/mvm/registry.rb +2 -1
  93. data/lib/mvm.rb +93 -0
  94. data/lib/tasks/api_coverage.rake +20 -0
  95. data/llms.txt +30 -0
  96. metadata +79 -9
@@ -14,38 +14,30 @@ module MixinBot
14
14
  label: kwargs[:label]
15
15
  }
16
16
 
17
- if pin.length > 6
18
- payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:ADDRESS:ADD:', payload[:asset_id], payload[:destination], payload[:tag], payload[:label]
19
- else
20
- payload[:pin] = encrypt_pin pin
21
- end
17
+ payload.update(
18
+ tip_or_legacy_pin_payload(pin, 'TIP:ADDRESS:ADD:', payload[:asset_id], payload[:destination], payload[:tag], payload[:label])
19
+ )
22
20
 
23
21
  client.post path, **payload
24
22
  end
23
+ alias create_address create_withdraw_address
25
24
 
26
25
  def get_withdraw_address(address, access_token: nil)
27
26
  path = format('/addresses/%<address>s', address:)
28
27
 
29
28
  client.get path, access_token:
30
29
  end
30
+ alias read_address get_withdraw_address
31
31
 
32
32
  def delete_withdraw_address(address, **kwargs)
33
33
  pin = kwargs[:pin]
34
34
 
35
35
  path = format('/addresses/%<address>s/delete', address:)
36
- payload =
37
- if pin.length > 6
38
- {
39
- pin_base64: encrypt_tip_pin(pin, 'TIP:ADDRESS:REMOVE:', address)
40
- }
41
- else
42
- {
43
- pin: encrypt_pin(pin)
44
- }
45
- end
36
+ payload = tip_or_legacy_pin_payload(pin, 'TIP:ADDRESS:REMOVE:', address)
46
37
 
47
38
  client.post path, **payload
48
39
  end
40
+ alias delete_address delete_withdraw_address
49
41
 
50
42
  def withdrawals(**kwargs)
51
43
  address_id = kwargs[:address_id]
@@ -53,7 +45,7 @@ module MixinBot
53
45
  amount = format('%.8f', kwargs[:amount].to_d.to_r)
54
46
  trace_id = kwargs[:trace_id]
55
47
  memo = kwargs[:memo]
56
- kwargs[:access_token]
48
+ access_token = kwargs[:access_token]
57
49
 
58
50
  path = '/withdrawals'
59
51
  payload = {
@@ -63,14 +55,28 @@ module MixinBot
63
55
  memo:
64
56
  }
65
57
 
66
- if pin.length > 6
67
- fee = '0'
68
- payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:WITHDRAW:', address_id, amount, fee, trace_id, memo
69
- else
70
- payload[:pin] = encrypt_pin pin
71
- end
58
+ fee = '0'
59
+ payload.update(
60
+ tip_or_legacy_pin_payload(pin, 'TIP:WITHDRAWAL:CREATE:', address_id, amount, fee, trace_id, memo)
61
+ )
72
62
 
73
- client.post path, **payload
63
+ client.post path, **payload, access_token:
64
+ end
65
+ alias send_withdrawal withdrawals
66
+
67
+ def withdraw_addresses(asset_id, access_token: nil)
68
+ path = format('/assets/%<asset_id>s/addresses', asset_id:)
69
+ client.get path, access_token:
70
+ end
71
+ alias get_addresses_by_asset_id withdraw_addresses
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
+
78
+ def check_address(asset:, destination:, tag: nil)
79
+ client.get '/external/addresses/check', asset:, destination:, tag:, access_token: ''
74
80
  end
75
81
  end
76
82
  end
data/lib/mixin_bot/api.rb CHANGED
@@ -8,10 +8,18 @@ require_relative 'api/asset'
8
8
  require_relative 'api/attachment'
9
9
  require_relative 'api/auth'
10
10
  require_relative 'api/blaze'
11
+ require_relative 'api/chain'
12
+ require_relative 'api/code'
13
+ require_relative 'api/circle'
14
+ require_relative 'api/computer_api'
11
15
  require_relative 'api/conversation'
16
+ require_relative 'api/deposit'
12
17
  require_relative 'api/encrypted_message'
18
+ require_relative 'api/external'
19
+ require_relative 'api/fiat'
13
20
  require_relative 'api/inscription'
14
21
  require_relative 'api/legacy_collectible'
22
+ require_relative 'api/legacy_user'
15
23
  require_relative 'api/legacy_multisig'
16
24
  require_relative 'api/legacy_output'
17
25
  require_relative 'api/legacy_payment'
@@ -21,21 +29,155 @@ require_relative 'api/legacy_transfer'
21
29
  require_relative 'api/me'
22
30
  require_relative 'api/message'
23
31
  require_relative 'api/multisig'
32
+ require_relative 'api/network'
33
+ require_relative 'api/network_asset'
24
34
  require_relative 'api/output'
25
35
  require_relative 'api/payment'
36
+ require_relative 'api/pin_payload'
26
37
  require_relative 'api/pin'
27
38
  require_relative 'api/rpc'
39
+ require_relative 'api/session'
28
40
  require_relative 'api/snapshot'
29
41
  require_relative 'api/tip'
30
42
  require_relative 'api/transaction'
31
43
  require_relative 'api/transfer'
44
+ require_relative 'api/turn'
32
45
  require_relative 'api/user'
33
46
  require_relative 'api/withdraw'
34
47
 
35
48
  module MixinBot
49
+ ##
50
+ # Main API interface for interacting with Mixin Network.
51
+ #
52
+ # The API class provides access to all Mixin Network endpoints including:
53
+ # - User and bot profile management
54
+ # - Asset management (read assets, check balances)
55
+ # - Transfers and payments (Safe API and legacy)
56
+ # - Messaging (send/receive messages via Blaze)
57
+ # - Conversations and encrypted messages
58
+ # - Multisig operations
59
+ # - NFT and collectible operations
60
+ # - Transaction building and signing
61
+ # - Withdrawal operations
62
+ #
63
+ # == Usage
64
+ #
65
+ # === Using Global Configuration
66
+ #
67
+ # MixinBot.configure do
68
+ # self.app_id = 'your-app-id'
69
+ # self.session_id = 'your-session-id'
70
+ # self.session_private_key = 'your-private-key'
71
+ # self.server_public_key = 'server-public-key'
72
+ # end
73
+ #
74
+ # # Access via global instance
75
+ # MixinBot.api.me
76
+ # MixinBot.api.assets
77
+ #
78
+ # === Creating Dedicated Instances
79
+ #
80
+ # api = MixinBot::API.new(
81
+ # app_id: 'your-app-id',
82
+ # session_id: 'your-session-id',
83
+ # session_private_key: 'your-private-key',
84
+ # server_public_key: 'server-public-key'
85
+ # )
86
+ #
87
+ # api.me
88
+ # api.assets
89
+ #
90
+ # == API Categories
91
+ #
92
+ # === Profile & Users
93
+ # - me, safe_me, update_me - Bot profile operations
94
+ # - read_user, read_users, search_user - User lookup
95
+ # - friends - List bot friends
96
+ #
97
+ # === Assets & Balance
98
+ # - assets, asset - Read asset information
99
+ # - ticker - Get asset ticker data
100
+ # - safe_assets - Read Safe API assets
101
+ #
102
+ # === Transfers & Payments
103
+ # - create_transfer, create_safe_transfer - Send payments
104
+ # - build_safe_transaction - Build raw transactions
105
+ # - sign_safe_transaction - Sign transactions
106
+ # - send_safe_transaction - Submit signed transactions
107
+ #
108
+ # === Messaging
109
+ # - start_blaze_connect - Connect to Blaze WebSocket
110
+ # - send_message - Send text/data messages
111
+ # - send_encrypted_messages - Send encrypted messages
112
+ #
113
+ # === Multisig & UTXOs
114
+ # - safe_outputs - Read unspent outputs
115
+ # - multisig_payments - Multisig payment operations
116
+ # - safe_ghost_keys - Generate ghost keys
117
+ #
118
+ # === NFT & Collectibles
119
+ # - create_collectible_request - Create NFT requests
120
+ # - read_collectibles - Read collectible tokens
121
+ # - inscriptions - Inscription operations
122
+ #
123
+ # == Examples
124
+ #
125
+ # Get bot information:
126
+ #
127
+ # profile = MixinBot.api.me
128
+ # puts profile['full_name']
129
+ #
130
+ # Read assets:
131
+ #
132
+ # assets = MixinBot.api.assets
133
+ # assets.each do |asset|
134
+ # puts "#{asset['symbol']}: #{asset['balance']}"
135
+ # end
136
+ #
137
+ # Send a transfer:
138
+ #
139
+ # result = MixinBot.api.create_transfer(
140
+ # members: ['recipient-user-id'],
141
+ # threshold: 1,
142
+ # asset_id: 'asset-uuid',
143
+ # amount: '0.01',
144
+ # memo: 'Payment for services',
145
+ # trace_id: SecureRandom.uuid
146
+ # )
147
+ #
36
148
  class API
37
- attr_reader :config, :client
149
+ ##
150
+ # @return [MixinBot::Configuration] the configuration for this API instance
151
+ attr_reader :config
38
152
 
153
+ ##
154
+ # @return [MixinBot::Client] the HTTP client for making API requests
155
+ attr_reader :client
156
+
157
+ ##
158
+ # Initializes a new API instance.
159
+ #
160
+ # If no parameters are provided, uses the global MixinBot configuration.
161
+ # Otherwise, creates a new configuration with the provided parameters.
162
+ #
163
+ # @param kwargs [Hash] configuration options (see Configuration#initialize)
164
+ # @option kwargs [String] :app_id the application ID
165
+ # @option kwargs [String] :session_id the session ID
166
+ # @option kwargs [String] :session_private_key the session private key
167
+ # @option kwargs [String] :server_public_key the server public key
168
+ # @option kwargs [String] :spend_key the spend private key (for Safe API)
169
+ #
170
+ # @example Using global configuration
171
+ # api = MixinBot::API.new
172
+ #
173
+ # @example With custom configuration
174
+ # api = MixinBot::API.new(
175
+ # app_id: 'your-app-id',
176
+ # session_id: 'your-session-id',
177
+ # session_private_key: 'your-private-key',
178
+ # server_public_key: 'server-public-key'
179
+ # )
180
+ #
39
181
  def initialize(**kwargs)
40
182
  @config =
41
183
  if kwargs.present?
@@ -47,14 +189,42 @@ module MixinBot
47
189
  @client = Client.new(@config)
48
190
  end
49
191
 
192
+ ##
193
+ # Provides access to utility methods.
194
+ #
195
+ # @return [Module] the Utils module
196
+ #
50
197
  def utils
51
198
  MixinBot::Utils
52
199
  end
53
200
 
201
+ ##
202
+ # Returns the client ID (same as app_id).
203
+ #
204
+ # @return [String] the client ID
205
+ #
54
206
  def client_id
55
207
  config.app_id
56
208
  end
57
209
 
210
+ ##
211
+ # Generates an access token for API authentication.
212
+ #
213
+ # Creates a JWT token signed with the bot's private key for authenticating
214
+ # API requests. The token includes request details and has a limited lifetime.
215
+ #
216
+ # @param method [String] the HTTP method (GET, POST, etc.)
217
+ # @param uri [String] the request URI path
218
+ # @param body [String] the request body
219
+ # @param kwargs [Hash] additional options
220
+ # @option kwargs [Integer] :exp_in (600) token expiration time in seconds
221
+ # @option kwargs [String] :scp ('FULL') token scope
222
+ #
223
+ # @return [String] the JWT access token
224
+ #
225
+ # @example
226
+ # token = api.access_token('GET', '/me', '')
227
+ #
58
228
  def access_token(method, uri, body, **kwargs)
59
229
  utils.access_token(
60
230
  method,
@@ -67,20 +237,71 @@ module MixinBot
67
237
  private_key: config.session_private_key
68
238
  )
69
239
  end
240
+ alias sign_authentication_token access_token
241
+ alias sign_authentication_token_without_body access_token
242
+ alias sign_authentication_token_with_request_id access_token
70
243
 
244
+ ##
245
+ # Encodes a transaction hash to raw transaction format.
246
+ #
247
+ # @param txn [Hash] the transaction hash with keys: version, asset, inputs, outputs, extra
248
+ # @return [String] the hex-encoded raw transaction
249
+ #
250
+ # @example
251
+ # raw = api.encode_raw_transaction(
252
+ # version: 5,
253
+ # asset: 'asset-id',
254
+ # inputs: [...],
255
+ # outputs: [...],
256
+ # extra: 'memo'
257
+ # )
258
+ #
71
259
  def encode_raw_transaction(txn)
72
260
  utils.encode_raw_transaction txn
73
261
  end
74
262
 
263
+ ##
264
+ # Decodes a raw transaction to a hash.
265
+ #
266
+ # @param raw [String] the hex-encoded raw transaction
267
+ # @return [Hash] the decoded transaction
268
+ #
269
+ # @example
270
+ # txn = api.decode_raw_transaction(raw_hex)
271
+ # puts txn['asset']
272
+ # puts txn['inputs']
273
+ #
75
274
  def decode_raw_transaction(raw)
76
275
  utils.decode_raw_transaction raw
77
276
  end
78
277
 
278
+ ##
279
+ # Generates a trace ID from a transaction hash.
280
+ #
281
+ # Creates a deterministic UUID trace ID from a transaction hash,
282
+ # useful for tracking outputs from a transaction.
283
+ #
284
+ # @param hash [String] the transaction hash
285
+ # @param output_index [Integer] the output index (default: 0)
286
+ # @return [String] the generated trace UUID
287
+ #
288
+ # @example
289
+ # trace_id = api.generate_trace_from_hash(tx_hash, 0)
290
+ #
79
291
  def generate_trace_from_hash(hash, output_index = 0)
80
292
  utils.generate_trace_from_hash hash, output_index
81
293
  end
82
294
 
83
- # Use a mixin software to implement transaction build
295
+ ##
296
+ # Encodes a raw transaction using native mixin command-line tool.
297
+ #
298
+ # Requires the 'mixin' command to be installed and available in PATH.
299
+ # This is an alternative to the Ruby implementation.
300
+ #
301
+ # @param json [String] the transaction JSON
302
+ # @return [String] the encoded raw transaction
303
+ # @raise [RuntimeError] if mixin command is not available
304
+ #
84
305
  def encode_raw_transaction_native(json)
85
306
  ensure_mixin_command_exist
86
307
  command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
@@ -91,7 +312,16 @@ module MixinBot
91
312
  output.chomp
92
313
  end
93
314
 
94
- # Use a mixin software to implement transaction build
315
+ ##
316
+ # Decodes a raw transaction using native mixin command-line tool.
317
+ #
318
+ # Requires the 'mixin' command to be installed and available in PATH.
319
+ # This is an alternative to the Ruby implementation.
320
+ #
321
+ # @param raw [String] the hex-encoded raw transaction
322
+ # @return [Hash] the decoded transaction
323
+ # @raise [RuntimeError] if mixin command is not available
324
+ #
95
325
  def decode_raw_transaction_native(raw)
96
326
  ensure_mixin_command_exist
97
327
  command = format("mixin decoderawtransaction --raw '%<arg>s'", arg: raw)
@@ -108,10 +338,18 @@ module MixinBot
108
338
  include MixinBot::API::Attachment
109
339
  include MixinBot::API::Auth
110
340
  include MixinBot::API::Blaze
341
+ include MixinBot::API::Chain
342
+ include MixinBot::API::Code
343
+ include MixinBot::API::Circle
344
+ include MixinBot::API::ComputerApi
111
345
  include MixinBot::API::Conversation
346
+ include MixinBot::API::Deposit
112
347
  include MixinBot::API::EncryptedMessage
348
+ include MixinBot::API::External
349
+ include MixinBot::API::Fiat
113
350
  include MixinBot::API::Inscription
114
351
  include MixinBot::API::LegacyCollectible
352
+ include MixinBot::API::LegacyUser
115
353
  include MixinBot::API::LegacyMultisig
116
354
  include MixinBot::API::LegacyOutput
117
355
  include MixinBot::API::LegacyPayment
@@ -121,19 +359,30 @@ module MixinBot
121
359
  include MixinBot::API::Me
122
360
  include MixinBot::API::Message
123
361
  include MixinBot::API::Multisig
362
+ include MixinBot::API::Network
363
+ include MixinBot::API::NetworkAsset
124
364
  include MixinBot::API::Output
125
365
  include MixinBot::API::Payment
126
366
  include MixinBot::API::Pin
127
367
  include MixinBot::API::Rpc
368
+ include MixinBot::API::Session
128
369
  include MixinBot::API::Snapshot
129
370
  include MixinBot::API::Tip
371
+ include MixinBot::API::PinPayload
130
372
  include MixinBot::API::Transaction
131
373
  include MixinBot::API::Transfer
374
+ include MixinBot::API::Turn
132
375
  include MixinBot::API::User
133
376
  include MixinBot::API::Withdraw
134
377
 
135
378
  private
136
379
 
380
+ def warn_legacy_mixin_api!(api_label)
381
+ MixinBot.deprecator.warn(
382
+ "MixinBot legacy API #{api_label} is deprecated; migrate to the Safe API. See CHANGELOG for 2.0.0."
383
+ )
384
+ end
385
+
137
386
  def ensure_mixin_command_exist
138
387
  return if command?('mixin')
139
388
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ # Bot platform request signing (parity with Go BotAuthClient).
5
+ class BotAuth
6
+ class MapCache
7
+ def initialize
8
+ @store = {}
9
+ end
10
+
11
+ def get(key)
12
+ @store[key]
13
+ end
14
+
15
+ def put(key, value)
16
+ @store[key] = value
17
+ end
18
+
19
+ def delete(key)
20
+ @store.delete(key)
21
+ end
22
+ end
23
+
24
+ class Client
25
+ PLATFORM_PREFIX = 'up_'
26
+
27
+ def initialize(api, cache: MapCache.new)
28
+ @api = api
29
+ @cache = cache
30
+ end
31
+
32
+ def sign_request(timestamp, bot_user_id, method, uri, body = nil)
33
+ shared_key = shared_key_for(bot_user_id)
34
+ data = "#{timestamp}#{method}#{uri}"
35
+ data += body.to_s if body.present?
36
+ digest = OpenSSL::HMAC.digest('SHA256', shared_key, data)
37
+ Base64.urlsafe_encode64(@api.config.app_id.b + digest, padding: false)
38
+ end
39
+
40
+ private
41
+
42
+ def shared_key_for(user_id)
43
+ cached = @cache.get(user_id)
44
+ return cached if cached.present? && cached.bytesize >= 32
45
+
46
+ sessions = @api.fetch_user_sessions([user_id])['data']
47
+ session = Array(sessions).first
48
+ raise MixinBot::NotFoundError, "no session for #{user_id}" if session.nil?
49
+
50
+ u_pk = Base64.urlsafe_decode64(session['public_key'])
51
+ sk = @api.config.session_private_key_curve25519
52
+ shared = JOSE::JWA::X25519.x25519(sk, u_pk[0, 32])
53
+ @cache.put(user_id, shared)
54
+ @cache.put("#{PLATFORM_PREFIX}#{user_id}", session['platform'].to_s.b) if session['platform']
55
+ shared
56
+ end
57
+ end
58
+
59
+ def self.new_map_cache
60
+ MapCache.new
61
+ end
62
+
63
+ def self.new_client(api, cache: MapCache.new)
64
+ Client.new(api, cache:)
65
+ end
66
+
67
+ def self.new_default_client(api, cache: MapCache.new)
68
+ new_client(api, cache:)
69
+ end
70
+ end
71
+ end