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.
- checksums.yaml +4 -4
- data/AGENTS.md +75 -0
- data/API_COVERAGE.md +143 -0
- data/CHANGELOG.md +112 -0
- data/README.md +375 -0
- data/docs/agent/cli.md +149 -0
- data/docs/agent/cookbook.md +152 -0
- data/examples/blaze.rb +43 -0
- data/examples/config.yml.example +21 -0
- data/lib/mixin_bot/address.rb +43 -3
- data/lib/mixin_bot/api/app.rb +7 -0
- data/lib/mixin_bot/api/asset.rb +114 -3
- data/lib/mixin_bot/api/auth.rb +19 -10
- data/lib/mixin_bot/api/blaze.rb +81 -0
- data/lib/mixin_bot/api/chain.rb +94 -0
- data/lib/mixin_bot/api/code.rb +16 -0
- data/lib/mixin_bot/api/computer_api.rb +60 -0
- data/lib/mixin_bot/api/conversation.rb +7 -1
- data/lib/mixin_bot/api/deposit.rb +12 -0
- data/lib/mixin_bot/api/encrypted_message.rb +1 -1
- data/lib/mixin_bot/api/fiat.rb +12 -0
- data/lib/mixin_bot/api/inscription.rb +2 -2
- data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
- data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
- data/lib/mixin_bot/api/legacy_output.rb +10 -3
- data/lib/mixin_bot/api/legacy_payment.rb +2 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
- data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
- data/lib/mixin_bot/api/legacy_user.rb +51 -0
- data/lib/mixin_bot/api/me.rb +99 -3
- data/lib/mixin_bot/api/message.rb +18 -27
- data/lib/mixin_bot/api/multisig.rb +19 -0
- data/lib/mixin_bot/api/network.rb +17 -0
- data/lib/mixin_bot/api/network_asset.rb +27 -0
- data/lib/mixin_bot/api/output.rb +1 -1
- data/lib/mixin_bot/api/pin.rb +16 -3
- data/lib/mixin_bot/api/pin_payload.rb +26 -0
- data/lib/mixin_bot/api/session.rb +14 -0
- data/lib/mixin_bot/api/snapshot.rb +6 -0
- data/lib/mixin_bot/api/tip.rb +74 -1
- data/lib/mixin_bot/api/transaction.rb +106 -17
- data/lib/mixin_bot/api/transfer.rb +141 -14
- data/lib/mixin_bot/api/turn.rb +12 -0
- data/lib/mixin_bot/api/user.rb +148 -45
- data/lib/mixin_bot/api/withdraw.rb +24 -23
- data/lib/mixin_bot/api.rb +248 -3
- data/lib/mixin_bot/bot_auth.rb +71 -0
- data/lib/mixin_bot/cli/api.rb +224 -143
- data/lib/mixin_bot/cli/base.rb +77 -0
- data/lib/mixin_bot/cli/call.rb +71 -0
- data/lib/mixin_bot/cli/errors.rb +56 -0
- data/lib/mixin_bot/cli/node.rb +9 -2
- data/lib/mixin_bot/cli/output.rb +196 -0
- data/lib/mixin_bot/cli/schema.rb +274 -0
- data/lib/mixin_bot/cli/schema_command.rb +21 -0
- data/lib/mixin_bot/cli/utils.rb +114 -18
- data/lib/mixin_bot/cli.rb +124 -48
- data/lib/mixin_bot/client/error_mapper.rb +40 -0
- data/lib/mixin_bot/client.rb +94 -64
- data/lib/mixin_bot/computer.rb +132 -0
- data/lib/mixin_bot/configuration.rb +108 -1
- data/lib/mixin_bot/errors.rb +102 -0
- data/lib/mixin_bot/models/address.rb +11 -0
- data/lib/mixin_bot/models/api_envelope.rb +67 -0
- data/lib/mixin_bot/models/asset.rb +11 -0
- data/lib/mixin_bot/models/ghost_keys.rb +14 -0
- data/lib/mixin_bot/models/output.rb +11 -0
- data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
- data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
- data/lib/mixin_bot/models/user.rb +11 -0
- data/lib/mixin_bot/models.rb +10 -0
- data/lib/mixin_bot/monitor.rb +77 -0
- data/lib/mixin_bot/transaction/buffer.rb +34 -0
- data/lib/mixin_bot/transaction/decoder.rb +227 -0
- data/lib/mixin_bot/transaction/encoder.rb +255 -0
- data/lib/mixin_bot/transaction.rb +6 -475
- data/lib/mixin_bot/url_scheme.rb +63 -0
- data/lib/mixin_bot/utils/address.rb +17 -80
- data/lib/mixin_bot/utils/crypto.rb +173 -1
- data/lib/mixin_bot/utils/decoder.rb +1 -1
- data/lib/mixin_bot/utils/encoder.rb +13 -0
- data/lib/mixin_bot/utils.rb +45 -0
- data/lib/mixin_bot/uuid.rb +78 -1
- data/lib/mixin_bot/version.rb +11 -1
- data/lib/mixin_bot.rb +172 -18
- data/lib/mvm/bridge.rb +46 -0
- data/lib/mvm/client.rb +60 -0
- data/lib/mvm/nft.rb +4 -2
- data/lib/mvm/registry.rb +2 -1
- data/lib/mvm.rb +93 -0
- data/lib/tasks/api_coverage.rake +20 -0
- data/llms.txt +29 -0
- 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
|
-
|
|
9
|
-
require_relative 'cli/
|
|
10
|
-
require_relative 'cli/
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
if File.file?
|
|
31
|
-
File.read
|
|
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
|
|
59
|
+
JSON.parse(raw)
|
|
39
60
|
rescue JSON::ParserError
|
|
40
|
-
|
|
41
|
-
format(
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
89
|
+
def parse_json_data(json_string, label: 'data')
|
|
90
|
+
return {} if json_string.blank?
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
data/lib/mixin_bot/client.rb
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|