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/lib/mixin_bot/cli.rb CHANGED
@@ -5,86 +5,162 @@ require 'cli/ui'
5
5
  require 'thor'
6
6
  require 'yaml'
7
7
  require 'json'
8
- require_relative 'cli/api'
9
- require_relative 'cli/node'
10
- require_relative 'cli/utils'
8
+
9
+ require_relative 'cli/base'
10
+ require_relative 'cli/errors'
11
+ require_relative 'cli/output'
12
+ require_relative 'cli/schema'
11
13
 
12
14
  module MixinBot
13
15
  class CLI < Thor
14
- # https://github.com/Shopify/cli-ui
16
+ include CLIHelpers
17
+ include CLIOutput
18
+
15
19
  UI = ::CLI::UI
16
20
 
17
21
  class_option :apihost, type: :string, aliases: '-a', default: 'api.mixin.one', desc: 'Specify mixin api host'
18
- class_option :pretty, type: :boolean, aliases: '-r', default: true, desc: 'Print output in pretty'
22
+ class_option :output, type: :string, aliases: '-o', enum: CLIOutput::OUTPUT_FORMATS,
23
+ desc: 'Output format: pretty, json, yaml (default: pretty in TTY, json when piped)'
24
+ class_option :pretty, type: :boolean, aliases: '-r', default: true,
25
+ desc: 'Pretty-print output (alias for --output pretty when set false → json)'
19
26
 
20
27
  attr_reader :keystore, :api_instance
21
28
 
22
- def initialize(*args)
23
- super
29
+ desc 'version', 'Display MixinBot version'
30
+ def version
31
+ with_command_name('version') do
32
+ emit_success({ 'version' => MixinBot::VERSION }, command: 'version')
33
+ end
34
+ end
35
+
36
+ def self.exit_on_failure?
37
+ true
38
+ end
39
+
40
+ private
41
+
42
+ def setup_api_instance!
43
+ MixinBot.config.api_host = options[:apihost] if options[:apihost].present?
44
+
24
45
  if options[:keystore].blank?
25
46
  @api_instance = MixinBot::API.new
26
- return
47
+ return @api_instance
27
48
  end
28
49
 
29
- keystore =
30
- if File.file? options[:keystore]
31
- File.read options[:keystore]
50
+ raw =
51
+ if File.file?(options[:keystore])
52
+ File.read(options[:keystore])
32
53
  else
33
54
  options[:keystore]
34
55
  end
35
56
 
36
57
  @keystore =
37
58
  begin
38
- JSON.parse keystore
59
+ JSON.parse(raw)
39
60
  rescue JSON::ParserError
40
- log UI.fmt(
41
- format(
42
- '{{x}} falied to parse keystore.json: %<keystore>s',
43
- keystore: options[:keystore]
44
- )
61
+ abort_with_error(
62
+ format('failed to parse keystore JSON: %<path>s', path: options[:keystore]),
63
+ kind: :invalid_args,
64
+ hint: 'mixinbot call me -k keystore.json'
45
65
  )
46
66
  end
47
67
 
48
- return unless @keystore
68
+ @api_instance = build_api_from_keystore(@keystore)
69
+ rescue StandardError => e
70
+ abort_with_error(
71
+ format('failed to initialize API (check keystore): %<error>s', error: e.message),
72
+ kind: CLIErrors.kind_for_exception(e),
73
+ exception: e
74
+ )
75
+ end
49
76
 
50
- MixinBot.config.api_host = options[:apihost]
51
- @api_instance ||=
52
- begin
53
- MixinBot::API.new(
54
- app_id: @keystore['app_id'] || @keystore['client_id'],
55
- session_id: @keystore['session_id'],
56
- server_public_key: @keystore['server_public_key'] || @keystore['pin_token'],
57
- session_private_key: @keystore['session_private_key'] || @keystore['private_key']
58
- )
59
- rescue StandardError => e
60
- log UI.fmt '{{x}}: Failed to initialize api, maybe your keystore is incorrect: %<error>s', error: e.message
61
- end
77
+ def build_api_from_keystore(store)
78
+ MixinBot::API.new(
79
+ app_id: store['app_id'] || store['client_id'],
80
+ session_id: store['session_id'],
81
+ server_public_key: store['server_public_key'] || store['pin_token'],
82
+ session_private_key: store['session_private_key'] || store['private_key'],
83
+ spend_key: store['spend_key'],
84
+ client_secret: store['client_secret'],
85
+ pin: store['pin']
86
+ )
62
87
  end
63
88
 
64
- # desc 'node', 'mixin node commands helper'
65
- # subcommand 'node', MixinBot::NodeCLI
89
+ def parse_json_data(json_string, label: 'data')
90
+ return {} if json_string.blank?
66
91
 
67
- desc 'version', 'Distay MixinBot version'
68
- def version
69
- log MixinBot::VERSION
70
- end
92
+ parsed = JSON.parse(json_string)
93
+ abort_with_error("#{label} must be a JSON object", kind: :invalid_args) unless parsed.is_a?(Hash)
71
94
 
72
- def self.exit_on_failure?
73
- true
95
+ parsed.transform_keys(&:to_sym)
96
+ rescue JSON::ParserError => e
97
+ abort_with_error("invalid JSON for #{label}: #{e.message}", kind: :invalid_args)
74
98
  end
75
99
 
76
- private
100
+ def invoke_api(method_name, kwargs: {}, positional: [])
101
+ sym = method_name.to_sym
102
+ unless CLIHelpers.api_method_callable?(sym)
103
+ abort_with_error(
104
+ format('unknown or unsupported API method: %<method>s (run `mixinbot list`)', method: method_name),
105
+ kind: :unsupported,
106
+ hint: 'mixinbot list'
107
+ )
108
+ end
77
109
 
78
- def log(obj)
79
- if options[:pretty]
80
- if obj.is_a? String
81
- puts obj
82
- else
83
- ap obj
84
- end
85
- else
86
- puts obj.inspect
110
+ if CLIHelpers::INTERACTIVE_API_METHODS.include?(sym)
111
+ abort_with_error(
112
+ format('%<method>s is interactive and not supported from the CLI', method: method_name),
113
+ kind: :unsupported,
114
+ hint: 'Use the Ruby API for Blaze WebSocket (see examples/blaze.rb)'
115
+ )
87
116
  end
117
+
118
+ api_instance.public_send(sym, *positional, **kwargs)
119
+ rescue ::ArgumentError => e
120
+ abort_with_error(
121
+ format('invalid arguments for %<method>s: %<error>s', method: method_name, error: e.message),
122
+ kind: :invalid_args,
123
+ hint: format('mixinbot list %<method>s', method: method_name)
124
+ )
125
+ rescue MixinBot::Error => e
126
+ abort_with_error(e.message, exception: e)
127
+ end
128
+
129
+ def invoke_utils(method_name, kwargs: {}, positional: [])
130
+ sym = method_name.to_sym
131
+ unless CLIHelpers.utils_callable_methods.include?(sym)
132
+ abort_with_error(
133
+ format('unknown utils method: %<method>s', method: method_name),
134
+ kind: :unsupported,
135
+ hint: 'mixinbot utils list'
136
+ )
137
+ end
138
+
139
+ MixinBot.utils.public_send(sym, *positional, **kwargs)
140
+ rescue ::ArgumentError => e
141
+ abort_with_error(
142
+ format('invalid arguments for utils.%<method>s: %<error>s', method: method_name, error: e.message),
143
+ kind: :invalid_args
144
+ )
145
+ end
146
+
147
+ def warn_deprecated(message)
148
+ emit_info("warning: #{message}")
149
+ end
150
+ end
151
+ end
152
+
153
+ require_relative 'cli/call'
154
+ require_relative 'cli/api'
155
+ require_relative 'cli/node'
156
+ require_relative 'cli/utils'
157
+ require_relative 'cli/schema_command'
158
+
159
+ module MixinBot
160
+ class CLI
161
+ if system('which mixin > /dev/null 2>&1')
162
+ desc 'node SUBCOMMAND', 'Experimental mixin node CLI helpers (requires `mixin` binary)'
163
+ subcommand 'node', NodeCLI
88
164
  end
89
165
  end
90
166
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class Client
5
+ ##
6
+ # Maps Mixin API +error+ objects to Ruby exceptions.
7
+ #
8
+ module ErrorMapper
9
+ module_function
10
+
11
+ def raise_for!(verb:, path:, body:, response:, result:)
12
+ err = result['error'] || {}
13
+ code = err['code']
14
+ desc = err['description']
15
+ req_id = response&.headers&.[]('X-Request-Id')
16
+ srv_time = response&.headers&.[]('X-Server-Time')
17
+ errmsg = "#{verb.upcase} | #{path} | #{body}, errcode: #{code}, errmsg: #{desc}, request_id: #{req_id}, server_time: #{srv_time}"
18
+
19
+ case code
20
+ when 401, 20_121
21
+ raise UnauthorizedError, errmsg
22
+ when 403, 20_116, 10_002, 429
23
+ raise ForbiddenError, errmsg
24
+ when 404
25
+ raise NotFoundError, errmsg
26
+ when 20_117
27
+ raise InsufficientBalanceError, errmsg
28
+ when 20_118, 20_119
29
+ raise PinError, errmsg
30
+ when 30_103
31
+ raise InsufficientPoolError, errmsg
32
+ when 10_404
33
+ raise UserNotFoundError, errmsg
34
+ else
35
+ raise ResponseError, errmsg
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'client/error_mapper'
4
+
3
5
  module MixinBot
6
+ ##
7
+ # HTTP client for making requests to the Mixin Network API.
8
+ #
4
9
  class Client
5
10
  SERVER_SCHEME = 'https'
6
11
 
@@ -16,88 +21,113 @@ module MixinBot
16
21
  }
17
22
  ) do |f|
18
23
  f.request :json
19
- f.request :retry
24
+ f.request :retry, max: 2, interval: 0.5, interval_randomness: 0.5, backoff_factor: 2,
25
+ exceptions: [Faraday::ConnectionFailed, Faraday::TimeoutError]
20
26
  f.response :json
21
27
  f.response :logger if config.debug
22
28
  end
23
29
  end
24
30
 
25
- def get(path, *, **)
26
- request(:get, path, *, **)
27
- end
31
+ ##
32
+ # GET request. Remaining keyword arguments are treated as query-string parameters.
33
+ #
34
+ # @return [MixinBot::Models::ApiEnvelope]
35
+ #
36
+ def get(path, **kwargs)
37
+ access_token = kwargs.delete(:access_token)
38
+ exp_in = kwargs.delete(:exp_in) || 600
39
+ scp = kwargs.delete(:scp) || 'FULL'
28
40
 
29
- def post(path, *, **)
30
- request(:post, path, *, **)
31
- end
41
+ kwargs.compact!
42
+ body = ''
43
+ full_path = kwargs.empty? ? path : "#{path}?#{URI.encode_www_form(kwargs.sort_by { |k, _| k.to_s })}"
32
44
 
33
- private
45
+ token = access_token.presence || sign_token('GET', full_path, body, exp_in:, scp:)
46
+ response = @conn.get(full_path, nil, authorization_headers(token))
47
+ parse_response!(verb: 'GET', path: full_path, body:, response:)
48
+ end
34
49
 
35
- def request(verb, path, *args, **kwargs)
36
- access_token = kwargs.delete :access_token
50
+ ##
51
+ # POST with a Hash body (+**kwargs+ merged into JSON object) or an Array body (+*args+).
52
+ #
53
+ # @return [MixinBot::Models::ApiEnvelope]
54
+ #
55
+ def post(path, *args, **kwargs)
56
+ access_token = kwargs.delete(:access_token)
37
57
  exp_in = kwargs.delete(:exp_in) || 600
38
58
  scp = kwargs.delete(:scp) || 'FULL'
39
59
 
40
- kwargs.compact!
41
60
  body =
42
- if verb == :post
43
- if args.present?
44
- args.to_json
45
- else
46
- kwargs.to_json
47
- end
61
+ if args.present?
62
+ args.to_json
48
63
  else
49
- ''
64
+ kwargs.compact.to_json
50
65
  end
51
66
 
52
- path = "#{path}?#{URI.encode_www_form(kwargs.sort_by { |k, _v| k })}" if verb == :get && kwargs.present?
53
- access_token ||=
54
- MixinBot.utils.access_token(
55
- verb.to_s.upcase,
56
- path,
57
- body,
58
- exp_in:,
59
- scp:,
60
- app_id: config.app_id,
61
- session_id: config.session_id,
62
- private_key: config.session_private_key
63
- )
64
- authorization = format('Bearer %<access_token>s', access_token:)
65
-
66
- response =
67
- case verb
68
- when :get
69
- @conn.get path, nil, { Authorization: authorization }
70
- when :post
71
- @conn.post path, body, { Authorization: authorization }
72
- end
67
+ token = access_token.presence || sign_token('POST', path, body, exp_in:, scp:)
68
+ response = @conn.post(path, body, authorization_headers(token))
69
+ parse_response!(verb: 'POST', path:, body:, response:)
70
+ end
73
71
 
74
- result = response.body
72
+ ##
73
+ # Explicit query-string GET (preferred for new code).
74
+ #
75
+ def fetch_get(path, query: nil, access_token: nil, exp_in: 600, scp: 'FULL')
76
+ q = (query || {}).dup
77
+ q.compact!
78
+ body = ''
79
+ full_path = q.empty? ? path : "#{path}?#{URI.encode_www_form(q.sort_by { |k, _| k.to_s })}"
80
+ token = access_token.presence || sign_token('GET', full_path, body, exp_in:, scp:)
81
+ response = @conn.get(full_path, nil, authorization_headers(token))
82
+ parse_response!(verb: 'GET', path: full_path, body:, response:)
83
+ end
75
84
 
76
- if result['error'].blank?
77
- result.merge! result['data'] if result['data'].is_a? Hash
78
- return result
79
- end
85
+ ##
86
+ # Explicit JSON-object POST (preferred for new code).
87
+ #
88
+ def fetch_post(path, body:, access_token: nil, exp_in: 600, scp: 'FULL')
89
+ payload = body.is_a?(String) ? body : body.compact.to_json
90
+ token = access_token.presence || sign_token('POST', path, payload, exp_in:, scp:)
91
+ response = @conn.post(path, payload, authorization_headers(token))
92
+ parse_response!(verb: 'POST', path:, body: payload, response:)
93
+ end
80
94
 
81
- errmsg = "#{verb.upcase} | #{path} | #{body}, errcode: #{result['error']['code']}, errmsg: #{result['error']['description']}, request_id: #{response&.[]('X-Request-Id')}, server_time: #{response&.[]('X-Server-Time')}'"
82
-
83
- case result['error']['code']
84
- when 401, 20121
85
- raise UnauthorizedError, errmsg
86
- when 403, 20116, 10002, 429
87
- raise ForbiddenError, errmsg
88
- when 404
89
- raise NotFoundError, errmsg
90
- when 20117
91
- raise InsufficientBalanceError, errmsg
92
- when 20118, 20119
93
- raise PinError, errmsg
94
- when 30103
95
- raise InsufficientPoolError, errmsg
96
- when 10404
97
- raise UserNotFoundError, errmsg
98
- else
99
- raise ResponseError, errmsg
100
- end
95
+ ##
96
+ # Explicit JSON-array POST (e.g. +/users/fetch+, +/safe/keys+).
97
+ #
98
+ def fetch_post_array(path, array_body, access_token: nil, exp_in: 600, scp: 'FULL')
99
+ payload = array_body.to_json
100
+ token = access_token.presence || sign_token('POST', path, payload, exp_in:, scp:)
101
+ response = @conn.post(path, payload, authorization_headers(token))
102
+ parse_response!(verb: 'POST', path:, body: payload, response:)
103
+ end
104
+
105
+ private
106
+
107
+ def authorization_headers(token)
108
+ return {} if token.blank?
109
+
110
+ { Authorization: format('Bearer %<access_token>s', access_token: token) }
111
+ end
112
+
113
+ def sign_token(method, uri, body, exp_in:, scp:)
114
+ MixinBot.utils.access_token(
115
+ method,
116
+ uri,
117
+ body,
118
+ exp_in:,
119
+ scp:,
120
+ app_id: config.app_id,
121
+ session_id: config.session_id,
122
+ private_key: config.session_private_key
123
+ )
124
+ end
125
+
126
+ def parse_response!(verb:, path:, body:, response:)
127
+ result = response.body
128
+ return MixinBot::Models::ApiEnvelope.new(result) if result['error'].blank?
129
+
130
+ ErrorMapper.raise_for!(verb:, path:, body:, response:, result:)
101
131
  end
102
132
  end
103
133
  end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module MixinBot
6
+ # Mixin Computer API client (https://computer.mixin.one).
7
+ class Computer
8
+ BASE_URI = 'https://computer.mixin.one'
9
+ OPERATION_TYPE_ADD_USER = 1
10
+ OPERATION_TYPE_SYSTEM_CALL = 2
11
+ OPERATION_TYPE_USER_DEPOSIT = 3
12
+ SOLANA_CHAIN_ID = '64692c23-8971-4cf4-84a7-4dd1271dd887'
13
+
14
+ class << self
15
+ def connection
16
+ @connection ||= Faraday.new(url: BASE_URI) do |f|
17
+ f.request :json
18
+ f.response :json
19
+ f.adapter Faraday.default_adapter
20
+ end
21
+ end
22
+
23
+ def request(method, path, body = nil)
24
+ response =
25
+ case method.to_s.upcase
26
+ when 'GET'
27
+ connection.get(path)
28
+ when 'POST'
29
+ connection.post(path, body)
30
+ else
31
+ raise ArgumentError, "unsupported method #{method}"
32
+ end
33
+ raise MixinBot::ServerError, "computer HTTP #{response.status}" if response.status >= 500
34
+
35
+ response.body
36
+ end
37
+
38
+ def info
39
+ request 'GET', '/'
40
+ end
41
+ alias get_computer_info info
42
+
43
+ def user(addr)
44
+ request 'GET', "/users/#{addr}"
45
+ end
46
+ alias get_computer_user user
47
+
48
+ def deployed_assets
49
+ request 'GET', '/deployed_assets'
50
+ end
51
+ alias get_computer_deployed_assets deployed_assets
52
+
53
+ def system_call(id)
54
+ request 'GET', "/system_calls/#{id}"
55
+ end
56
+ alias get_computer_system_call system_call
57
+
58
+ def deploy_external_assets(assets)
59
+ assets = Array(assets)
60
+ raise ArgumentError, "cannot deploy asset from Solana: #{SOLANA_CHAIN_ID}" if assets.include?(SOLANA_CHAIN_ID)
61
+
62
+ request 'POST', '/deployed_assets', assets
63
+ end
64
+ alias computer_deploy_external_asset deploy_external_assets
65
+
66
+ def lock_nonce_account(mix)
67
+ request 'POST', '/nonce_accounts', { mix: }
68
+ end
69
+ alias lock_computer_nonce_account lock_nonce_account
70
+
71
+ def fee_on_xin_from_sol(sol_amount)
72
+ request 'POST', '/fee', { sol_amount: sol_amount.to_s }
73
+ end
74
+ alias get_fee_on_xin_based_on_sol fee_on_xin_from_sol
75
+
76
+ def register_computer(api)
77
+ info_data = info
78
+ mix = MixinBot::MixAddress.from_members(members: [api.config.app_id], threshold: 1).address
79
+ memo = encode_mtg_extra(
80
+ info_data.dig('members', 'app_id'),
81
+ encode_operation_memo(OPERATION_TYPE_ADD_USER, mix)
82
+ )
83
+ trace = MixinBot.utils.unique_object_id(mix, 'computer_register')
84
+ api.create_safe_transfer(
85
+ members: info_data.dig('members', 'members'),
86
+ threshold: info_data.dig('members', 'threshold'),
87
+ asset_id: info_data.dig('params', 'operation', 'asset'),
88
+ amount: info_data.dig('params', 'operation', 'price'),
89
+ trace_id: trace,
90
+ memo:
91
+ )
92
+ end
93
+
94
+ def user_id_to_bytes(id)
95
+ n = Integer(id, 10)
96
+ raise ArgumentError, "invalid user id: #{id}" if n.negative?
97
+
98
+ [n].pack('Q>')
99
+ end
100
+ alias computer_user_id_to_bytes user_id_to_bytes
101
+
102
+ def build_system_call_extra(uid, cid, skip_process: false, fid: nil)
103
+ extra = user_id_to_bytes(uid)
104
+ extra += MixinBot::UUID.new(hex: cid).packed
105
+ extra += [skip_process ? 1 : 0].pack('C')
106
+ extra += MixinBot::UUID.new(hex: fid).packed if fid.present?
107
+ extra
108
+ end
109
+
110
+ def encode_operation_memo(operation, extra = +'')
111
+ [operation].pack('C') + extra.to_s
112
+ end
113
+
114
+ def encode_mtg_extra(app_id, extra)
115
+ data = MixinBot::UUID.new(hex: app_id).packed + extra.to_s
116
+ Base64.urlsafe_encode64(data, padding: false)
117
+ end
118
+
119
+ def decode_computer_extra_base64(extra)
120
+ data = Base64.urlsafe_decode64(extra)
121
+ return ['', nil] if data.length < 16
122
+
123
+ app_id = MixinBot::UUID.new(raw: data[0, 16]).unpacked
124
+ [app_id, data[16..]]
125
+ end
126
+
127
+ def solana_asset_id_for(deployed)
128
+ MixinBot.utils.unique_object_id(SOLANA_CHAIN_ID, deployed['address'])
129
+ end
130
+ end
131
+ end
132
+ end