mixin_bot 1.4.0 → 2.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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +75 -0
  3. data/API_COVERAGE.md +143 -0
  4. data/CHANGELOG.md +112 -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 +7 -0
  12. data/lib/mixin_bot/api/asset.rb +114 -3
  13. data/lib/mixin_bot/api/auth.rb +19 -10
  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/code.rb +16 -0
  17. data/lib/mixin_bot/api/computer_api.rb +60 -0
  18. data/lib/mixin_bot/api/conversation.rb +7 -1
  19. data/lib/mixin_bot/api/deposit.rb +12 -0
  20. data/lib/mixin_bot/api/encrypted_message.rb +1 -1
  21. data/lib/mixin_bot/api/fiat.rb +12 -0
  22. data/lib/mixin_bot/api/inscription.rb +2 -2
  23. data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
  24. data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
  25. data/lib/mixin_bot/api/legacy_output.rb +10 -3
  26. data/lib/mixin_bot/api/legacy_payment.rb +2 -0
  27. data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
  28. data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
  29. data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
  30. data/lib/mixin_bot/api/legacy_user.rb +51 -0
  31. data/lib/mixin_bot/api/me.rb +99 -3
  32. data/lib/mixin_bot/api/message.rb +18 -27
  33. data/lib/mixin_bot/api/multisig.rb +19 -0
  34. data/lib/mixin_bot/api/network.rb +17 -0
  35. data/lib/mixin_bot/api/network_asset.rb +27 -0
  36. data/lib/mixin_bot/api/output.rb +1 -1
  37. data/lib/mixin_bot/api/pin.rb +16 -3
  38. data/lib/mixin_bot/api/pin_payload.rb +26 -0
  39. data/lib/mixin_bot/api/session.rb +14 -0
  40. data/lib/mixin_bot/api/snapshot.rb +6 -0
  41. data/lib/mixin_bot/api/tip.rb +74 -1
  42. data/lib/mixin_bot/api/transaction.rb +106 -17
  43. data/lib/mixin_bot/api/transfer.rb +141 -14
  44. data/lib/mixin_bot/api/turn.rb +12 -0
  45. data/lib/mixin_bot/api/user.rb +148 -45
  46. data/lib/mixin_bot/api/withdraw.rb +24 -23
  47. data/lib/mixin_bot/api.rb +248 -3
  48. data/lib/mixin_bot/bot_auth.rb +71 -0
  49. data/lib/mixin_bot/cli/api.rb +224 -143
  50. data/lib/mixin_bot/cli/base.rb +77 -0
  51. data/lib/mixin_bot/cli/call.rb +71 -0
  52. data/lib/mixin_bot/cli/errors.rb +56 -0
  53. data/lib/mixin_bot/cli/node.rb +9 -2
  54. data/lib/mixin_bot/cli/output.rb +196 -0
  55. data/lib/mixin_bot/cli/schema.rb +274 -0
  56. data/lib/mixin_bot/cli/schema_command.rb +21 -0
  57. data/lib/mixin_bot/cli/utils.rb +114 -18
  58. data/lib/mixin_bot/cli.rb +124 -48
  59. data/lib/mixin_bot/client/error_mapper.rb +40 -0
  60. data/lib/mixin_bot/client.rb +94 -64
  61. data/lib/mixin_bot/computer.rb +132 -0
  62. data/lib/mixin_bot/configuration.rb +108 -1
  63. data/lib/mixin_bot/errors.rb +102 -0
  64. data/lib/mixin_bot/models/address.rb +11 -0
  65. data/lib/mixin_bot/models/api_envelope.rb +67 -0
  66. data/lib/mixin_bot/models/asset.rb +11 -0
  67. data/lib/mixin_bot/models/ghost_keys.rb +14 -0
  68. data/lib/mixin_bot/models/output.rb +11 -0
  69. data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
  70. data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
  71. data/lib/mixin_bot/models/user.rb +11 -0
  72. data/lib/mixin_bot/models.rb +10 -0
  73. data/lib/mixin_bot/monitor.rb +77 -0
  74. data/lib/mixin_bot/transaction/buffer.rb +34 -0
  75. data/lib/mixin_bot/transaction/decoder.rb +227 -0
  76. data/lib/mixin_bot/transaction/encoder.rb +255 -0
  77. data/lib/mixin_bot/transaction.rb +6 -475
  78. data/lib/mixin_bot/url_scheme.rb +63 -0
  79. data/lib/mixin_bot/utils/address.rb +17 -80
  80. data/lib/mixin_bot/utils/crypto.rb +173 -1
  81. data/lib/mixin_bot/utils/decoder.rb +1 -1
  82. data/lib/mixin_bot/utils/encoder.rb +13 -0
  83. data/lib/mixin_bot/utils.rb +45 -0
  84. data/lib/mixin_bot/uuid.rb +78 -1
  85. data/lib/mixin_bot/version.rb +11 -1
  86. data/lib/mixin_bot.rb +172 -18
  87. data/lib/mvm/bridge.rb +46 -0
  88. data/lib/mvm/client.rb +60 -0
  89. data/lib/mvm/nft.rb +4 -2
  90. data/lib/mvm/registry.rb +2 -1
  91. data/lib/mvm.rb +93 -0
  92. data/lib/tasks/api_coverage.rake +20 -0
  93. data/llms.txt +29 -0
  94. metadata +77 -9
data/examples/blaze.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/mixin_bot'
4
+ require 'base64'
5
+ require 'yaml'
6
+
7
+ CONFIG = YAML.load_file("#{File.dirname __FILE__}/config.yml")
8
+ MixinBot.configure do
9
+ self.app_id = CONFIG['app_id']
10
+ self.client_secret = CONFIG['client_secret']
11
+ self.session_id = CONFIG['session_id']
12
+ self.server_public_key = CONFIG['server_public_key']
13
+ self.session_private_key = CONFIG['session_private_key']
14
+ end
15
+
16
+ # default connect
17
+ # EM.run {
18
+ # MixinBot.api.start_blaze_connect
19
+ # }
20
+
21
+ EM.run do
22
+ MixinBot.api.start_blaze_connect do
23
+ def on_open(blaze, _event)
24
+ p [Time.now.to_s, :on_open]
25
+ blaze.send list_pending_message
26
+ end
27
+
28
+ def on_message(blaze, event)
29
+ raw = JSON.parse ws_message(event.data)
30
+ p [Time.now.to_s, :on_message, raw&.[]('action')]
31
+
32
+ blaze.send acknowledge_message_receipt(raw['data']['message_id']) unless raw&.[]('data')&.[]('message_id').nil?
33
+ end
34
+
35
+ # def on_error(blaze, event)
36
+ # p [Time.now.to_s, :on_error]
37
+ # end
38
+
39
+ # def on_close(blaze, event)
40
+ # p [Time.now.to_s, :on_close, event.code, event.reason]
41
+ # end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ app_id: 0508a116-1239-4e28-b150-85a8e3e6b400
2
+ client_secret: d9dc58107bacde6713c12734663de6bbb2b83e917d5fe62167bda13ca6665b15
3
+ session_id: 25696f85-b7b4-4509-8c3f-2684a8fc4a2a
4
+ pin_code: "431005"
5
+ server_public_key: b0pjBUKI0Vp9K+NspaLCKKeV1IzuO5o2Zgzp2uriXYn6bKcKowhZ7nsjNT1mxtq82dsPzGficOM9H9n1HvNyilrKrpW5ndu7/dMsw/r3JvbexDvLZfAGmuAHcZOy+AwbMSJ/XurLJg1lS9w9PVkDltqWWy2TOunUlbe1lWYy/1M=
6
+ session_private_key: |
7
+ -----BEGIN RSA PRIVATE KEY-----
8
+ MIICXAIBAAKBgQDQYjiR/Te6Bh/1bk8gWRbQkrX0AIGPja1DLUQHu5Uw9M4P53O3
9
+ f4pDCGoN3R5+LYjODtquOwmEjcMhbhp6XarrnJVXH8WGmJcpjVwGtwIjPTeRMu4Z
10
+ 8XWo314fO2pLeiqWbJ4knlGb0oiXxBghwF4KglUSElQ/FXjdKaabHCw0zQIDAQAB
11
+ AoGALgqFlTvtZByWUxPcR7lnYQ4JRbAW8DDNZ1pI/axkejyciscIujJjygvB4u5I
12
+ HnjRETYW+wfwQmlQA8Lf9slbSe1FlsajNWl/zc+bwoYTO3yfB81GWwk/e20dTGfs
13
+ 2d8ITe+LlYUMlqZgBGr2Y2pHeXDvuzC6ZsKZQKoS3B2RscECQQD0RTXp0tNSbWE7
14
+ TzhXwZu3jkIRhZsajeDv+7VYMZ94TO1PGeQSi25jBT8JcDfbyXO8eZTtjARfTEmE
15
+ Ln6ejveHAkEA2mPb+TyZl0FqIluwo1KbCv3PVlaHDL+HyHoBsq+6yhn3VRMDbyqs
16
+ ZKViJxaLrzUx6+IQOr9T8LqFcaQhSrxeCwJANqMgewuoLwC+RejjXmW08erFBmxP
17
+ FDJ2BNfVaUO4Os1iK1ZMOIWtjEKJJhBOvj+iPp8nW7b852AF9aX8tnSeEwJAacH8
18
+ D7l6A5aJCDRw2NazAGKjGoNyiQjjf4Ed+2NASIjEjq1Td20p6N9yJc20PVe8Yieq
19
+ hliLFMOuxbae7KtFuwJBAI9xMuLBLWixwvezyFCAJpj93ClOgSIlM7qgEfUxLvjZ
20
+ YxyVoIU3haXJPYPgaVE9nEwwiNPRS5DJ2fCGT+oGfrY=
21
+ -----END RSA PRIVATE KEY-----
@@ -10,6 +10,14 @@ module MixinBot
10
10
  class MixAddress
11
11
  attr_accessor :version, :uuid_members, :xin_members, :threshold, :address, :payload
12
12
 
13
+ def self.parse(string)
14
+ new(address: string)
15
+ end
16
+
17
+ def self.from_members(members:, threshold:)
18
+ new(members:, threshold:)
19
+ end
20
+
13
21
  def initialize(**args)
14
22
  args = args.with_indifferent_access
15
23
 
@@ -23,8 +31,9 @@ module MixinBot
23
31
  @version = args[:version] || MIX_ADDRESS_VERSION
24
32
 
25
33
  if args[:members].present?
26
- @uuid_members = args[:members].reject { |member| member.start_with?(MAIN_ADDRESS_PREFIX) }
27
- @xin_members = args[:members].select { |member| member.start_with? MAIN_ADDRESS_PREFIX }
34
+ @xin_members, @uuid_members = args[:members].partition do |member|
35
+ member.start_with?(MAIN_ADDRESS_PREFIX)
36
+ end
28
37
  else
29
38
  @uuid_members = args[:uuid_members] || []
30
39
  @xin_members = args[:xin_members] || []
@@ -53,11 +62,42 @@ module MixinBot
53
62
  }
54
63
  end
55
64
 
65
+ def request_or_generate_ghost_keys(output_index = 0, api: MixinBot.api)
66
+ if xin_members.present?
67
+ key = JOSE::JWA::Ed25519.keypair
68
+ gk = { 'mask' => key[0].unpack1('H*'), 'keys' => [] }
69
+ xin_members.each do |member|
70
+ payload = MixinBot.utils.parse_main_address(member)
71
+ spend_key = payload[0...32]
72
+ view_key = payload[-32..]
73
+ ghost = MixinBot.utils.derive_ghost_public_key(key[1], view_key, spend_key, output_index)
74
+ gk['keys'] << ghost.unpack1('H*')
75
+ end
76
+ gk
77
+ else
78
+ hint = SecureRandom.uuid
79
+ api.create_safe_keys(
80
+ { receivers: (uuid_members + xin_members).sort, index: output_index, hint: }
81
+ )['data'].first
82
+ end
83
+ end
84
+
56
85
  def encode
57
86
  raise ArgumentError, 'members should be an array' unless uuid_members.is_a?(Array) || xin_members.is_a?(Array)
58
87
  raise ArgumentError, 'members should not be empty' if uuid_members.empty? && xin_members.empty?
59
88
  raise ArgumentError, 'members length should less than 256' if uuid_members.length + xin_members.length > 255
60
- raise ArgumentError, "invalid threshold: #{threshold}" if threshold > (uuid_members.length + xin_members.length)
89
+
90
+ member_count = uuid_members.length + xin_members.length
91
+ # UUID mix (Go NewUUIDMixAddress): threshold must be in 1..len(members).
92
+ # XIN-only mix (Go NewMainnetMixAddress): threshold may exceed member count (sparse), e.g. storage 1-of-64 style.
93
+ if uuid_members.present? && xin_members.empty?
94
+ raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive? && threshold <= member_count
95
+ elsif xin_members.present? && uuid_members.empty?
96
+ raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive?
97
+ raise ArgumentError, 'too many XIN members' if member_count > 64
98
+ elsif threshold > member_count
99
+ raise ArgumentError, "invalid threshold: #{threshold}"
100
+ end
61
101
 
62
102
  prefix =
63
103
  [version].pack('C*') +
@@ -20,6 +20,13 @@ module MixinBot
20
20
 
21
21
  client.get path, access_token:
22
22
  end
23
+
24
+ def transfer_app_ownership(receiver_user_id:, pin:, access_token: nil)
25
+ path = format('/apps/%<app_id>s/transfer', app_id: config.app_id)
26
+ tip = tip_or_legacy_pin_payload(pin, 'TIP:APP:OWNERSHIP:TRANSFER:', receiver_user_id)
27
+ client.post path, user_id: receiver_user_id, pin_base64: tip[:pin_base64] || tip[:pin], access_token:
28
+ end
29
+ alias migrate transfer_app_ownership
23
30
  end
24
31
  end
25
32
  end
@@ -2,20 +2,101 @@
2
2
 
3
3
  module MixinBot
4
4
  class API
5
+ ##
6
+ # API methods for asset management.
7
+ #
8
+ # Provides methods to:
9
+ # - List all assets in bot's wallet
10
+ # - Read specific asset information
11
+ # - Get asset ticker/price data
12
+ #
5
13
  module Asset
6
- # https://developers.mixin.one/api/alpha-mixin-network/read-assets/
14
+ ##
15
+ # Retrieves all assets in the bot's wallet.
16
+ #
17
+ # Returns an array of asset objects, each containing:
18
+ # - asset_id: the asset UUID
19
+ # - symbol: the asset symbol (e.g., "BTC", "ETH")
20
+ # - name: the full asset name
21
+ # - icon_url: URL to the asset icon
22
+ # - balance: current balance
23
+ # - destination: deposit address
24
+ # - tag: deposit memo/tag (if applicable)
25
+ # - price_btc: price in BTC
26
+ # - price_usd: price in USD
27
+ # - chain_id: the blockchain UUID
28
+ # - change_btc: 24h price change in BTC
29
+ # - change_usd: 24h price change in USD
30
+ # - confirmations: required confirmations
31
+ # - asset_key: the asset key for deposit
32
+ #
33
+ # @param access_token [String, nil] optional access token
34
+ # @return [Array<Hash>] array of asset objects
35
+ #
36
+ # @example
37
+ # assets = api.assets
38
+ # assets.each do |asset|
39
+ # puts "#{asset['symbol']}: #{asset['balance']}"
40
+ # end
41
+ #
42
+ # @see https://developers.mixin.one/docs/api/assets/assets
43
+ #
7
44
  def assets(access_token: nil)
8
45
  path = '/assets'
9
46
  client.get path, access_token:
10
47
  end
11
48
 
12
- # https://developers.mixin.one/api/alpha-mixin-network/read-asset/
49
+ ##
50
+ # Retrieves information for a specific asset.
51
+ #
52
+ # Returns detailed information about a single asset including:
53
+ # - Current balance
54
+ # - Price information
55
+ # - Deposit address
56
+ # - Network details
57
+ #
58
+ # @param asset_id [String] the asset UUID
59
+ # @param access_token [String, nil] optional access token
60
+ # @return [Hash] the asset information
61
+ #
62
+ # @example
63
+ # # Get Bitcoin information
64
+ # btc = api.asset('c6d0c728-2624-429b-8e0d-d9d19b6592fa')
65
+ # puts "BTC Balance: #{btc['balance']}"
66
+ # puts "BTC Price: $#{btc['price_usd']}"
67
+ #
68
+ # @see https://developers.mixin.one/docs/api/assets/asset
69
+ #
13
70
  def asset(asset_id, access_token: nil)
14
71
  path = format('/assets/%<asset_id>s', asset_id:)
15
72
  client.get path, access_token:
16
73
  end
17
74
 
18
- # https://developers.mixin.one/document/wallet/api/ticker
75
+ ##
76
+ # Retrieves ticker/price data for an asset.
77
+ #
78
+ # Returns historical price and volume data for an asset,
79
+ # useful for charts and price tracking.
80
+ #
81
+ # @param asset_id [String] the asset UUID
82
+ # @param kwargs [Hash] query options
83
+ # @option kwargs [String, DateTime, Time] :offset the time offset for historical data
84
+ # @option kwargs [String] :access_token optional access token
85
+ # @return [Hash] ticker data including price and volume
86
+ #
87
+ # @example
88
+ # # Get current ticker
89
+ # ticker = api.ticker('c6d0c728-2624-429b-8e0d-d9d19b6592fa')
90
+ # puts "Price: $#{ticker['price_usd']}"
91
+ #
92
+ # # Get historical ticker
93
+ # ticker = api.ticker(
94
+ # 'c6d0c728-2624-429b-8e0d-d9d19b6592fa',
95
+ # offset: 1.day.ago
96
+ # )
97
+ #
98
+ # @see https://developers.mixin.one/docs/api/assets/ticker
99
+ #
19
100
  def ticker(asset_id, **kwargs)
20
101
  offset = kwargs[:offset]
21
102
  offset = DateTime.rfc3339(offset) if offset.is_a? String
@@ -24,6 +105,36 @@ module MixinBot
24
105
  path = '/ticker'
25
106
  client.get path, asset_id:, offset:, access_token: kwargs[:access_token]
26
107
  end
108
+
109
+ def fetch_assets(asset_ids, access_token: nil)
110
+ client.fetch_post_array '/safe/assets/fetch', Array(asset_ids), access_token:
111
+ end
112
+
113
+ def asset_fee(asset_id, destination:, access_token: nil)
114
+ path = format('/safe/assets/%<asset_id>s/fees', asset_id:)
115
+ client.get path, destination:, access_token:
116
+ end
117
+ alias read_asset_fee asset_fee
118
+
119
+ def asset_balance(asset_id)
120
+ outputs = safe_outputs(asset: asset_id, state: 'unspent')
121
+ Array(outputs['data']).sum { |o| o['amount'].to_d }
122
+ end
123
+ alias asset_balance_with_safe_user asset_balance
124
+
125
+ def user_asset_balance(user_id, asset_id, access_token: nil)
126
+ members_hash = MixinBot.utils.hash_members([user_id])
127
+ path = '/safe/outputs'
128
+ response = client.get(
129
+ path,
130
+ members: members_hash,
131
+ threshold: 1,
132
+ asset: asset_id,
133
+ state: 'unspent',
134
+ access_token:
135
+ )
136
+ Array(response['data']).sum { |o| o['amount'].to_d }
137
+ end
27
138
  end
28
139
  end
29
140
  end
@@ -3,8 +3,22 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Auth
6
+ def sign_oauth_access_token(_authorization_id:, method:, uri:, body:, scope:, request_id: nil, **kwargs)
7
+ MixinBot.utils.access_token(
8
+ method,
9
+ uri,
10
+ body,
11
+ exp_in: kwargs[:exp_in] || 600,
12
+ scp: scope,
13
+ app_id: kwargs[:app_id] || config.app_id,
14
+ session_id: kwargs[:session_id] || config.session_id,
15
+ private_key: kwargs[:private_key] || config.session_private_key,
16
+ request_id:
17
+ )
18
+ end
19
+
6
20
  def oauth_token(code)
7
- path = 'oauth/token'
21
+ path = '/oauth/token'
8
22
  payload = {
9
23
  client_id: config.app_id,
10
24
  client_secret: config.client_secret,
@@ -30,20 +44,15 @@ module MixinBot
30
44
 
31
45
  path = '/oauth/authorize'
32
46
  pin = kwargs[:pin] || config.pin
47
+ raise ArgumentError, 'pin is required' if pin.blank?
48
+
49
+ tip = tip_or_legacy_pin_payload(pin, 'TIP:OAUTH:APPROVE:', data['scopes'], data['authorization_id'])
33
50
  payload = {
34
51
  authorization_id: data['authorization_id'],
35
52
  scopes: data['scopes'],
36
- pin_base64: encrypt_pin(kwargs[:pin])
53
+ pin_base64: tip[:pin_base64] || tip[:pin]
37
54
  }
38
55
 
39
- raise ArgumentError, 'pin is required' if pin.blank?
40
-
41
- payload[:pin_base64] = if pin.size > 6
42
- encrypt_tip_pin(pin, 'TIP:OAUTH:APPROVE:', data['scopes'], data['authorization_id'])
43
- else
44
- encrypt_pin(pin)
45
- end
46
-
47
56
  client.post path, **payload, access_token: kwargs[:access_token]
48
57
  end
49
58
 
@@ -58,6 +58,87 @@ module MixinBot
58
58
  start_blaze_connect(&_block) if reconnect
59
59
  end
60
60
  end
61
+
62
+ def blaze_send_plain_text(socket, conversation_id:, recipient_id:, content:)
63
+ socket.send write_ws_message(
64
+ params: {
65
+ conversation_id:,
66
+ recipient_id:,
67
+ message_id: SecureRandom.uuid,
68
+ category: 'PLAIN_TEXT',
69
+ data_base64: Base64.urlsafe_encode64(content.to_s, padding: false)
70
+ }
71
+ )
72
+ end
73
+
74
+ def blaze_send_recall_message(socket, conversation_id:, recipient_id:, message_id:)
75
+ data = { message_id: }.to_json
76
+ socket.send write_ws_message(
77
+ params: {
78
+ conversation_id:,
79
+ recipient_id:,
80
+ message_id: SecureRandom.uuid,
81
+ category: 'MESSAGE_RECALL',
82
+ data_base64: Base64.urlsafe_encode64(data, padding: false)
83
+ }
84
+ )
85
+ end
86
+
87
+ def blaze_send_post(socket, conversation_id:, recipient_id:, content:)
88
+ blaze_send_plain_text(socket, conversation_id:, recipient_id:, content:)
89
+ end
90
+
91
+ def blaze_send_contact(socket, conversation_id:, recipient_id:, contact_id:)
92
+ data = { user_id: contact_id }.to_json
93
+ socket.send write_ws_message(
94
+ params: {
95
+ conversation_id:,
96
+ recipient_id:,
97
+ message_id: SecureRandom.uuid,
98
+ category: 'PLAIN_CONTACT',
99
+ data_base64: Base64.urlsafe_encode64(data, padding: false)
100
+ }
101
+ )
102
+ end
103
+
104
+ def blaze_send_app_card(socket, conversation_id:, recipient_id:, title:, description:, action:, icon_url:)
105
+ data = { title:, description:, action:, icon_url: }.to_json
106
+ socket.send write_ws_message(
107
+ params: {
108
+ conversation_id:,
109
+ recipient_id:,
110
+ message_id: SecureRandom.uuid,
111
+ category: 'APP_CARD',
112
+ data_base64: Base64.urlsafe_encode64(data, padding: false)
113
+ }
114
+ )
115
+ end
116
+
117
+ def blaze_send_app_button(socket, conversation_id:, recipient_id:, label:, action:, color:)
118
+ data = [{ label:, action:, color: }].to_json
119
+ socket.send write_ws_message(
120
+ params: {
121
+ conversation_id:,
122
+ recipient_id:,
123
+ message_id: SecureRandom.uuid,
124
+ category: 'APP_BUTTON_GROUP',
125
+ data_base64: Base64.urlsafe_encode64(data, padding: false)
126
+ }
127
+ )
128
+ end
129
+
130
+ def blaze_send_group_app_button(socket, conversation_id:, recipient_id:, buttons:)
131
+ data = buttons.to_json
132
+ socket.send write_ws_message(
133
+ params: {
134
+ conversation_id:,
135
+ recipient_id:,
136
+ message_id: SecureRandom.uuid,
137
+ category: 'APP_BUTTON_GROUP',
138
+ data_base64: Base64.urlsafe_encode64(data, padding: false)
139
+ }
140
+ )
141
+ end
61
142
  end
62
143
  end
63
144
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Chain
6
+ CHAIN_NAMES = {
7
+ '59c09123-95cc-3ffd-a659-0f9169074cee' => 'Lightning',
8
+ 'c6d0c728-2624-429b-8e0d-d9d19b6592fa' => 'Bitcoin',
9
+ 'fd11b6e3-0b87-41f1-a41f-f0e9b49e5bf0' => 'Bitcoin Cash',
10
+ '574388fd-b93f-4034-a682-01c2bc095d17' => 'Bitcoin SV',
11
+ '76c802a2-7c88-447f-a93e-c29c9e5dd9c8' => 'Litecoin',
12
+ '43d61dcd-e413-450d-80b8-101d5e903357' => 'Ethereum',
13
+ '2204c1ee-0ea2-4add-bb9a-b3719cfff93a' => 'Ethereum Classic',
14
+ '1949e683-6a08-49e2-b087-d6b72398588f' => 'BNB Smart Chain',
15
+ 'b7938396-3f94-4e0a-9179-d3440718156f' => 'Polygon',
16
+ '3fb612c5-6844-3979-ae4a-5a84e79da870' => 'Base',
17
+ '60360611-370c-3b69-9826-b13db93f6aba' => 'OP Mainnet',
18
+ '8c590110-1abc-3697-84f2-05214e6516aa' => 'Arbitrum One',
19
+ 'a0ffd769-5850-4b48-9651-d2ae44a3e64d' => 'Mixin Virtual Machine',
20
+ '8f5caf2a-283d-4c85-832a-91e83bbf290b' => 'Decred',
21
+ '23dfb5a5-5d7b-48b6-905f-3970e3176e27' => 'Ripple',
22
+ '990c4c29-57e9-48f6-9819-7d986ea44985' => 'Siacoin',
23
+ '6cfe566e-4aad-470b-8c9a-2fd35b49c68d' => 'EOS',
24
+ '6770a1e5-6086-44d5-b60f-545f9d9e8ffd' => 'Dogecoin',
25
+ '6472e7e3-75fd-48b6-b1dc-28d294ee1476' => 'Dash',
26
+ 'c996abc9-d94e-4494-b1cf-2a3fd3ac5714' => 'Zcash',
27
+ '27921032-f73e-434e-955f-43d55672ee31' => 'NEM',
28
+ '882eb041-64ea-465f-a4da-817bd3020f52' => 'Arweave',
29
+ 'a2c5d22b-62a2-4c13-b3f0-013290dbac60' => 'Horizen',
30
+ '25dabac5-056a-48ff-b9f9-f67395dc407c' => 'TRON',
31
+ '56e63c06-b506-4ec5-885a-4a5ac17b83c1' => 'Stellar',
32
+ 'b207bce9-c248-4b8e-b6e3-e357146f3f4c' => 'MassGrid',
33
+ '443e1ef5-bc9b-47d3-be77-07f328876c50' => 'Bytom',
34
+ '71a0e8b5-a289-4845-b661-2b70ff9968aa' => 'Bytom',
35
+ '7397e9f1-4e42-4dc8-8a3b-171daaadd436' => 'Cosmos',
36
+ '9c612618-ca59-4583-af34-be9482f5002d' => 'Akash',
37
+ '17f78d7c-ed96-40ff-980c-5dc62fecbc85' => 'BNB Beacon Chain',
38
+ '05c5ac01-31f9-4a69-aa8a-ab796de1d041' => 'Monero',
39
+ 'c99a3779-93df-404d-945d-eddc440aa0b2' => 'Starcoin',
40
+ '05891083-63d2-4f3d-bfbe-d14d7fb9b25a' => 'Bitshares',
41
+ '6877d485-6b64-4225-8d7e-7333393cb243' => 'Ravencoin',
42
+ '1351e6bd-66cf-40c1-8105-8a8fe518a222' => 'Grin',
43
+ 'c3b9153a-7fab-4138-a3a4-99849cadc073' => 'VCash',
44
+ '13036886-6b83-4ced-8d44-9f69151587bf' => 'Handshake',
45
+ 'd243386e-6d84-42e6-be03-175be17bf275' => 'Nervos',
46
+ '5649ca42-eb5f-4c0e-ae28-d9a4e77eded3' => 'Tezos',
47
+ 'f8b77dc0-46fd-4ea1-9821-587342475869' => 'Namecoin',
48
+ '64692c23-8971-4cf4-84a7-4dd1271dd887' => 'Solana',
49
+ 'd6ac94f7-c932-4e11-97dd-617867f0669e' => 'NEAR',
50
+ '08285081-e1d8-4be6-9edc-e203afa932da' => 'Filecoin',
51
+ 'eea900a8-b327-488c-8d8d-1428702fe240' => 'MobileCoin',
52
+ '54c61a72-b982-4034-a556-0d99e3c21e39' => 'Polkadot',
53
+ '9d29e4f6-d67c-4c4b-9525-604b04afbe9f' => 'Kusama',
54
+ '706b6f84-3333-4e55-8e89-275e71ce9803' => 'Algorand',
55
+ 'cbc77539-0a20-4666-8c8a-4ded62b36f0a' => 'Avalanche X-Chain',
56
+ '1f67ac58-87ba-3571-9781-e9413c046f34' => 'Avalanche C-Chain',
57
+ '163a2142-398d-3483-aee3-d47db8da4d10' => 'MarsChain',
58
+ 'b12bb04a-1cea-401c-a086-0be61f544889' => 'XDC Network',
59
+ 'd2c1c7e1-a1a9-4f88-b282-d93b0a08b42b' => 'Aptos',
60
+ '2bd97283-2582-33a8-bcba-f4b8ed189572' => 'Sui',
61
+ 'ef660437-d915-4e27-ad3f-632bfb6ba0ee' => 'TON'
62
+ }.freeze
63
+
64
+ XIN_ASSET_ID = 'c94ac88f-4671-3976-b60a-09064f1811e8'
65
+ VAULTA_ASSET_ID = 'ac2b79f3-ec9c-3d87-b4ca-3e825228dda5'
66
+
67
+ def network_chain(chain_id)
68
+ path = format('/network/chains/%<chain_id>s', chain_id:)
69
+ client.get path, access_token: ''
70
+ end
71
+ alias read_network_chain_by_id network_chain
72
+
73
+ def network_chains
74
+ client.get '/network/chains', access_token: ''
75
+ end
76
+ alias read_network_chains network_chains
77
+
78
+ def chain_name(chain_id)
79
+ CHAIN_NAMES[chain_id] || 'Not Supported Chain'
80
+ end
81
+ alias get_chain_name chain_name
82
+
83
+ def chain_id?(chain_id)
84
+ CHAIN_NAMES.key?(chain_id)
85
+ end
86
+ alias is_chain_id chain_id?
87
+
88
+ def full_chains
89
+ CHAIN_NAMES.transform_values { true }.dup
90
+ end
91
+ alias get_full_chains full_chains
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Code
6
+ def read_code(code_id, access_token: nil)
7
+ path = format('/codes/%<code_id>s', code_id:)
8
+ client.get path, access_token: access_token || ''
9
+ end
10
+
11
+ def read_multisig_by_code(code_id, access_token: nil)
12
+ read_code(code_id, access_token:)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ # Delegates to {MixinBot::Computer} for API surface parity with Go SDK.
6
+ module ComputerApi
7
+ def get_computer_info
8
+ MixinBot::Computer.info
9
+ end
10
+
11
+ def get_computer_user(addr)
12
+ MixinBot::Computer.user(addr)
13
+ end
14
+
15
+ def get_computer_deployed_assets
16
+ MixinBot::Computer.deployed_assets
17
+ end
18
+
19
+ def get_computer_system_call(id)
20
+ MixinBot::Computer.system_call(id)
21
+ end
22
+
23
+ def computer_deploy_external_asset(assets)
24
+ MixinBot::Computer.deploy_external_assets(assets)
25
+ end
26
+
27
+ def lock_computer_nonce_account(mix)
28
+ MixinBot::Computer.lock_nonce_account(mix)
29
+ end
30
+
31
+ def get_fee_on_xin_based_on_sol(sol_amount)
32
+ MixinBot::Computer.fee_on_xin_from_sol(sol_amount)
33
+ end
34
+
35
+ def register_computer
36
+ MixinBot::Computer.register_computer(self)
37
+ end
38
+
39
+ def computer_user_id_to_bytes(id)
40
+ MixinBot::Computer.user_id_to_bytes(id)
41
+ end
42
+
43
+ def build_system_call_extra(uid, cid, skip_process: false, fid: nil)
44
+ MixinBot::Computer.build_system_call_extra(uid, cid, skip_process:, fid:)
45
+ end
46
+
47
+ def encode_operation_memo(operation, extra = +'')
48
+ MixinBot::Computer.encode_operation_memo(operation, extra)
49
+ end
50
+
51
+ def encode_mtg_extra(app_id, extra)
52
+ MixinBot::Computer.encode_mtg_extra(app_id, extra)
53
+ end
54
+
55
+ def decode_computer_extra_base64(extra)
56
+ MixinBot::Computer.decode_computer_extra_base64(extra)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -29,7 +29,8 @@ module MixinBot
29
29
 
30
30
  def create_group_conversation(user_ids:, name:, **kwargs)
31
31
  random_id = kwargs[:random_id] || SecureRandom.uuid
32
- conversation_id = kwargs[:conversation_id] || MixinBot.utils.generate_group_conversation_id(user_ids:, name:, owner_id: config.app_id, random_id:)
32
+ conversation_id = kwargs[:conversation_id] || MixinBot.utils.generate_group_conversation_id(user_ids:, name:,
33
+ owner_id: config.app_id, random_id:)
33
34
  create_conversation(
34
35
  announcement: kwargs[:announcement],
35
36
  category: 'GROUP',
@@ -94,6 +95,11 @@ module MixinBot
94
95
  client.post path, access_token:
95
96
  end
96
97
 
98
+ def join_conversation(conversation_id, access_token: nil)
99
+ path = format('/conversations/%<conversation_id>s/join', conversation_id:)
100
+ client.post path, access_token:
101
+ end
102
+
97
103
  def rotate_conversation(conversation_id, access_token: nil)
98
104
  path = format('/conversations/%<id>s/rotate', id: conversation_id)
99
105
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Deposit
6
+ def pending_safe_deposits
7
+ client.get '/safe/deposits', access_token: ''
8
+ end
9
+ alias fetch_pending_safe_deposits pending_safe_deposits
10
+ end
11
+ end
12
+ end
@@ -194,7 +194,7 @@ module MixinBot
194
194
  decrypter.key = key
195
195
  decrypter.iv = bytes[prefix_size...(prefix_size + 12)].pack('C*')
196
196
  decrypter.auth_tag = bytes.last(16).pack('C*')
197
- decrypted = decrypter.update(bytes[(prefix_size + 12)...(bytes.size - 16)].pack('C*'))
197
+ decrypted = decrypter.update(bytes[(prefix_size + 12)...-16].pack('C*'))
198
198
  decrypter.final
199
199
 
200
200
  Base64.urlsafe_encode64 decrypted
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Fiat
6
+ def fiats(access_token: nil)
7
+ client.get '/external/fiats', access_token: access_token || ''
8
+ end
9
+ alias get_fiats fiats
10
+ end
11
+ end
12
+ end
@@ -21,8 +21,8 @@ module MixinBot
21
21
  client.get path, offset:
22
22
  end
23
23
 
24
- def collectibles(members: [], access_token: nil)
25
- unspent_outputs = safe_outputs(state: :unspent, members:, access_token:)['data']
24
+ def collectibles(members: [], access_token: nil, state: :unspent)
25
+ unspent_outputs = safe_outputs(state:, members:, access_token:)['data']
26
26
  unspent_outputs.select { |output| output['inscription_hash'].present? }
27
27
  end
28
28