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/node.rb
CHANGED
|
@@ -34,7 +34,7 @@ module MixinBot
|
|
|
34
34
|
'-c',
|
|
35
35
|
c.to_s
|
|
36
36
|
)
|
|
37
|
-
distributions =
|
|
37
|
+
distributions = parse_mixin_cli_output(o)
|
|
38
38
|
spinner.update_title "#{distributions.size} mint distributions listed"
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -54,7 +54,7 @@ module MixinBot
|
|
|
54
54
|
'-x',
|
|
55
55
|
tx
|
|
56
56
|
)
|
|
57
|
-
tx =
|
|
57
|
+
tx = parse_mixin_cli_output(o)
|
|
58
58
|
spinner.update_title "#{tx[:outputs].size} transaction outputs found"
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -92,6 +92,13 @@ module MixinBot
|
|
|
92
92
|
$CHILD_STATUS.success?
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
def parse_mixin_cli_output(output)
|
|
96
|
+
JSON.parse(output, symbolize_names: true)
|
|
97
|
+
rescue JSON::ParserError
|
|
98
|
+
# Mixin node CLI historically prints Ruby hash literals.
|
|
99
|
+
eval(output)
|
|
100
|
+
end
|
|
101
|
+
|
|
95
102
|
def log(obj)
|
|
96
103
|
if options[:pretty]
|
|
97
104
|
if obj.is_a? String
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module MixinBot
|
|
7
|
+
##
|
|
8
|
+
# Structured and human-friendly stdout/stderr formatting for mixinbot.
|
|
9
|
+
#
|
|
10
|
+
module CLIOutput
|
|
11
|
+
OUTPUT_FORMATS = %w[pretty json yaml].freeze
|
|
12
|
+
|
|
13
|
+
def cli_options
|
|
14
|
+
merged = options.dup
|
|
15
|
+
merged = parent_options.merge(merged) if respond_to?(:parent_options) && parent_options.present?
|
|
16
|
+
merged
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def output_format
|
|
20
|
+
opts = cli_options
|
|
21
|
+
explicit = opts[:output].to_s.downcase if opts[:output].present?
|
|
22
|
+
return explicit if OUTPUT_FORMATS.include?(explicit)
|
|
23
|
+
|
|
24
|
+
return 'json' if opts.key?(:pretty) && opts[:pretty] == false
|
|
25
|
+
|
|
26
|
+
$stdout.tty? ? 'pretty' : 'json'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def structured_output?
|
|
30
|
+
%w[json yaml].include?(output_format)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def current_command_name
|
|
34
|
+
@current_command_name || self.class.name.split('::').last.sub(/CLI\z/, '').downcase
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_command_name(name)
|
|
38
|
+
previous = @current_command_name
|
|
39
|
+
@current_command_name = name
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
@current_command_name = previous
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def emit_success(data, command: nil)
|
|
46
|
+
command_name = command || current_command_name
|
|
47
|
+
if structured_output?
|
|
48
|
+
write_stdout(encode_output(envelope('ok', command_name, data)))
|
|
49
|
+
else
|
|
50
|
+
write_pretty(data)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def emit_list(items:, total:, limit:, offset:, command: 'list')
|
|
55
|
+
payload = {
|
|
56
|
+
'items' => items,
|
|
57
|
+
'total' => total,
|
|
58
|
+
'limit' => limit,
|
|
59
|
+
'offset' => offset
|
|
60
|
+
}
|
|
61
|
+
emit_success(payload, command:)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emit_info(message)
|
|
65
|
+
return if message.blank?
|
|
66
|
+
|
|
67
|
+
if structured_output?
|
|
68
|
+
warn(message)
|
|
69
|
+
else
|
|
70
|
+
warn(format_info(message))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def abort_with_error(message, kind: nil, hint: nil, exception: nil)
|
|
75
|
+
resolved_kind = kind || (exception && CLIErrors.kind_for_exception(exception)) || CLIErrors.kind_for_message(message)
|
|
76
|
+
hint ||= default_error_hint
|
|
77
|
+
|
|
78
|
+
if structured_output?
|
|
79
|
+
error_body = {
|
|
80
|
+
'status' => 'error',
|
|
81
|
+
'error' => {
|
|
82
|
+
'kind' => resolved_kind.to_s,
|
|
83
|
+
'message' => message.to_s
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
error_body['error']['hint'] = hint if hint.present?
|
|
87
|
+
warn(JSON.generate(error_body))
|
|
88
|
+
else
|
|
89
|
+
warn(format_error(message))
|
|
90
|
+
end
|
|
91
|
+
exit(1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def print_result(obj, data_only: false, command: nil)
|
|
95
|
+
out =
|
|
96
|
+
case obj
|
|
97
|
+
when MixinBot::Models::ApiEnvelope
|
|
98
|
+
data_only ? (obj['data'] || obj.to_h) : obj.to_h
|
|
99
|
+
when Hash
|
|
100
|
+
data_only ? (obj['data'] || obj) : obj
|
|
101
|
+
else
|
|
102
|
+
obj
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
emit_success(out, command:)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def log(obj)
|
|
109
|
+
emit_success(obj)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def paginate_items(items, limit:, offset:)
|
|
113
|
+
limit = limit.to_i
|
|
114
|
+
offset = offset.to_i
|
|
115
|
+
limit = 100 if limit <= 0
|
|
116
|
+
offset = 0 if offset.negative?
|
|
117
|
+
|
|
118
|
+
total = items.size
|
|
119
|
+
slice = items.drop(offset).first(limit)
|
|
120
|
+
[slice, total, limit, offset]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def select_fields(items, fields)
|
|
124
|
+
return items if fields.blank?
|
|
125
|
+
|
|
126
|
+
keys = fields.split(',').map(&:strip).reject(&:empty?)
|
|
127
|
+
return items if keys.empty?
|
|
128
|
+
|
|
129
|
+
items.map do |item|
|
|
130
|
+
item.slice(*keys)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def envelope(status, command, data)
|
|
137
|
+
{
|
|
138
|
+
'status' => status,
|
|
139
|
+
'command' => command,
|
|
140
|
+
'data' => normalize_data(data)
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def normalize_data(data)
|
|
145
|
+
case data
|
|
146
|
+
when MixinBot::Models::ApiEnvelope
|
|
147
|
+
data.to_h
|
|
148
|
+
when Hash
|
|
149
|
+
data.transform_keys(&:to_s)
|
|
150
|
+
else
|
|
151
|
+
data
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def encode_output(payload)
|
|
156
|
+
case output_format
|
|
157
|
+
when 'yaml'
|
|
158
|
+
payload.to_yaml
|
|
159
|
+
else
|
|
160
|
+
JSON.generate(payload)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def write_stdout(text)
|
|
165
|
+
puts text
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def write_pretty(obj)
|
|
169
|
+
if obj.is_a?(String)
|
|
170
|
+
puts obj
|
|
171
|
+
else
|
|
172
|
+
ap obj
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def format_info(message)
|
|
177
|
+
if defined?(UI) && UI.respond_to?(:fmt)
|
|
178
|
+
UI.fmt(message.to_s)
|
|
179
|
+
else
|
|
180
|
+
message.to_s
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def format_error(message)
|
|
185
|
+
if defined?(UI) && UI.respond_to?(:fmt)
|
|
186
|
+
UI.fmt("{{x}} #{message}")
|
|
187
|
+
else
|
|
188
|
+
"Error: #{message}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def default_error_hint
|
|
193
|
+
'Run `mixinbot help` or `mixinbot schema -o json` for usage'
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
##
|
|
5
|
+
# Builds clispec-shaped schema documents for mixinbot.
|
|
6
|
+
#
|
|
7
|
+
module CLISchema
|
|
8
|
+
MUTATING_COMMANDS = %w[
|
|
9
|
+
transfer legacy-transfer safetransfer authcode updatetip saferegister pay
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
MUTATING_API_METHOD_PREFIXES = %w[
|
|
13
|
+
create_ update_ delete_ send_ post_ authorize_ safe_register migrate_
|
|
14
|
+
build_inscribe transfer_app upload_
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
COMMAND_DEFINITIONS = [
|
|
18
|
+
{
|
|
19
|
+
'name' => 'version',
|
|
20
|
+
'description' => 'Display MixinBot gem version',
|
|
21
|
+
'mutating' => false,
|
|
22
|
+
'args' => [],
|
|
23
|
+
'output_fields' => [
|
|
24
|
+
{ 'name' => 'version', 'type' => 'string' }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
'name' => 'schema',
|
|
29
|
+
'description' => 'Emit machine-readable CLI schema (clispec-shaped)',
|
|
30
|
+
'mutating' => false,
|
|
31
|
+
'args' => [
|
|
32
|
+
{ 'name' => '--output', 'type' => 'string', 'required' => false,
|
|
33
|
+
'enum' => %w[pretty json yaml], 'default' => nil }
|
|
34
|
+
],
|
|
35
|
+
'output_fields' => [
|
|
36
|
+
{ 'name' => 'name', 'type' => 'string' },
|
|
37
|
+
{ 'name' => 'version', 'type' => 'string' },
|
|
38
|
+
{ 'name' => 'commands', 'type' => 'array' },
|
|
39
|
+
{ 'name' => 'errors', 'type' => 'array' }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
'name' => 'list',
|
|
44
|
+
'description' => 'List callable MixinBot::API methods',
|
|
45
|
+
'mutating' => false,
|
|
46
|
+
'args' => [
|
|
47
|
+
{ 'name' => 'FILTER', 'type' => 'string', 'required' => false },
|
|
48
|
+
{ 'name' => '--limit', 'type' => 'integer', 'required' => false, 'default' => 100 },
|
|
49
|
+
{ 'name' => '--offset', 'type' => 'integer', 'required' => false, 'default' => 0 },
|
|
50
|
+
{ 'name' => '--fields', 'type' => 'string', 'required' => false,
|
|
51
|
+
'description' => 'Comma-separated fields: name,owner' }
|
|
52
|
+
],
|
|
53
|
+
'output_fields' => [
|
|
54
|
+
{ 'name' => 'items', 'type' => 'array' },
|
|
55
|
+
{ 'name' => 'total', 'type' => 'integer' },
|
|
56
|
+
{ 'name' => 'limit', 'type' => 'integer' },
|
|
57
|
+
{ 'name' => 'offset', 'type' => 'integer' }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
'name' => 'call',
|
|
62
|
+
'description' => 'Invoke a MixinBot::API method with JSON keyword arguments',
|
|
63
|
+
'mutating' => 'conditional',
|
|
64
|
+
'mutating_note' => 'Mutating when METHOD matches write operations; use `mixinbot list` to inspect',
|
|
65
|
+
'args' => [
|
|
66
|
+
{ 'name' => 'METHOD', 'type' => 'string', 'required' => true },
|
|
67
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => false, 'aliases' => ['-k'] },
|
|
68
|
+
{ 'name' => '--data', 'type' => 'string', 'required' => false, 'default' => '{}', 'aliases' => ['-d'] },
|
|
69
|
+
{ 'name' => '--data-only', 'type' => 'boolean', 'required' => false, 'default' => false }
|
|
70
|
+
],
|
|
71
|
+
'output_fields' => [
|
|
72
|
+
{ 'name' => 'data', 'type' => 'object | array | string' }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
'name' => 'api',
|
|
77
|
+
'description' => 'Signed GET/POST request to a Mixin API path',
|
|
78
|
+
'mutating' => 'conditional',
|
|
79
|
+
'mutating_note' => 'Mutating when --method POST',
|
|
80
|
+
'args' => [
|
|
81
|
+
{ 'name' => 'PATH', 'type' => 'string', 'required' => true },
|
|
82
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] },
|
|
83
|
+
{ 'name' => '--method', 'type' => 'string', 'required' => false, 'default' => 'GET', 'aliases' => ['-m'] },
|
|
84
|
+
{ 'name' => '--data', 'type' => 'string', 'required' => false, 'default' => '{}', 'aliases' => ['-d'] },
|
|
85
|
+
{ 'name' => '--data-only', 'type' => 'boolean', 'required' => false, 'default' => true }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
'name' => 'transfer',
|
|
90
|
+
'description' => 'Safe transfer to USER_ID',
|
|
91
|
+
'mutating' => true,
|
|
92
|
+
'args' => [
|
|
93
|
+
{ 'name' => 'USER_ID', 'type' => 'string', 'required' => true },
|
|
94
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] },
|
|
95
|
+
{ 'name' => '--asset', 'type' => 'string', 'required' => true },
|
|
96
|
+
{ 'name' => '--amount', 'type' => 'string', 'required' => true },
|
|
97
|
+
{ 'name' => '--memo', 'type' => 'string', 'required' => false },
|
|
98
|
+
{ 'name' => '--trace', 'type' => 'string', 'required' => false },
|
|
99
|
+
{ 'name' => '--spend-key', 'type' => 'string', 'required' => false },
|
|
100
|
+
{ 'name' => '--dry-run', 'type' => 'boolean', 'required' => false, 'default' => false }
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
'name' => 'legacy-transfer',
|
|
105
|
+
'description' => 'Legacy POST /transfers (deprecated)',
|
|
106
|
+
'mutating' => true,
|
|
107
|
+
'args' => [
|
|
108
|
+
{ 'name' => 'USER_ID', 'type' => 'string', 'required' => true },
|
|
109
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] },
|
|
110
|
+
{ 'name' => '--asset', 'type' => 'string', 'required' => true },
|
|
111
|
+
{ 'name' => '--amount', 'type' => 'number', 'required' => true }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
'name' => 'safetransfer',
|
|
116
|
+
'description' => 'Alias for transfer (deprecated)',
|
|
117
|
+
'mutating' => true,
|
|
118
|
+
'args' => []
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
'name' => 'authcode',
|
|
122
|
+
'description' => 'OAuth authorization code',
|
|
123
|
+
'mutating' => true,
|
|
124
|
+
'args' => [
|
|
125
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] },
|
|
126
|
+
{ 'name' => '--app-id', 'type' => 'string', 'required' => true, 'aliases' => ['-c'] }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
'name' => 'saferegister',
|
|
131
|
+
'description' => 'Register on SAFE network',
|
|
132
|
+
'mutating' => true,
|
|
133
|
+
'args' => [
|
|
134
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] },
|
|
135
|
+
{ 'name' => '--spend-key', 'type' => 'string', 'required' => true }
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
'name' => 'pay',
|
|
140
|
+
'description' => 'Generate Safe payment URL',
|
|
141
|
+
'mutating' => false,
|
|
142
|
+
'args' => [
|
|
143
|
+
{ 'name' => '--members', 'type' => 'array', 'required' => true },
|
|
144
|
+
{ 'name' => '--asset', 'type' => 'string', 'required' => true },
|
|
145
|
+
{ 'name' => '--amount', 'type' => 'string', 'required' => true }
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
'name' => 'encrypt',
|
|
150
|
+
'description' => 'Encrypt PIN using session keys',
|
|
151
|
+
'mutating' => false,
|
|
152
|
+
'args' => [
|
|
153
|
+
{ 'name' => 'PIN', 'type' => 'string', 'required' => true },
|
|
154
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] }
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
'name' => 'verifypin',
|
|
159
|
+
'description' => 'Verify PIN',
|
|
160
|
+
'mutating' => false,
|
|
161
|
+
'args' => [
|
|
162
|
+
{ 'name' => 'PIN', 'type' => 'string', 'required' => true },
|
|
163
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] }
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
'name' => 'updatetip',
|
|
168
|
+
'description' => 'Update TIP PIN',
|
|
169
|
+
'mutating' => true,
|
|
170
|
+
'args' => [
|
|
171
|
+
{ 'name' => 'PIN', 'type' => 'string', 'required' => true },
|
|
172
|
+
{ 'name' => '--keystore', 'type' => 'string', 'required' => true, 'aliases' => ['-k'] }
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
'name' => 'utils',
|
|
177
|
+
'description' => 'Utils subcommands (call, list)',
|
|
178
|
+
'mutating' => false,
|
|
179
|
+
'args' => []
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
'name' => 'utils call',
|
|
183
|
+
'description' => 'Invoke a MixinBot.utils method',
|
|
184
|
+
'mutating' => false,
|
|
185
|
+
'args' => [
|
|
186
|
+
{ 'name' => 'METHOD', 'type' => 'string', 'required' => true },
|
|
187
|
+
{ 'name' => '--data', 'type' => 'string', 'required' => false, 'default' => '{}', 'aliases' => ['-d'] }
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
'name' => 'utils list',
|
|
192
|
+
'description' => 'List callable MixinBot.utils methods',
|
|
193
|
+
'mutating' => false,
|
|
194
|
+
'args' => [
|
|
195
|
+
{ 'name' => 'FILTER', 'type' => 'string', 'required' => false },
|
|
196
|
+
{ 'name' => '--limit', 'type' => 'integer', 'required' => false, 'default' => 100 },
|
|
197
|
+
{ 'name' => '--offset', 'type' => 'integer', 'required' => false, 'default' => 0 },
|
|
198
|
+
{ 'name' => '--fields', 'type' => 'string', 'required' => false, 'default' => 'name' }
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
'name' => 'unique',
|
|
203
|
+
'description' => 'Deterministic UUID from two or more UUIDs',
|
|
204
|
+
'mutating' => false,
|
|
205
|
+
'args' => [{ 'name' => 'UUIDS', 'type' => 'array', 'required' => true }]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
'name' => 'generatetrace',
|
|
209
|
+
'description' => 'Trace UUID from transaction hash',
|
|
210
|
+
'mutating' => false,
|
|
211
|
+
'args' => [{ 'name' => 'HASH', 'type' => 'string', 'required' => true }]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
'name' => 'decodetx',
|
|
215
|
+
'description' => 'Decode raw transaction hex',
|
|
216
|
+
'mutating' => false,
|
|
217
|
+
'args' => [{ 'name' => 'TRANSACTION', 'type' => 'string', 'required' => true }]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
'name' => 'nftmemo',
|
|
221
|
+
'description' => 'NFT mint memo',
|
|
222
|
+
'mutating' => false,
|
|
223
|
+
'args' => []
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
'name' => 'rsa',
|
|
227
|
+
'description' => 'Generate RSA key pair',
|
|
228
|
+
'mutating' => false,
|
|
229
|
+
'args' => []
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
'name' => 'ed25519',
|
|
233
|
+
'description' => 'Generate Ed25519 key pair',
|
|
234
|
+
'mutating' => false,
|
|
235
|
+
'args' => []
|
|
236
|
+
}
|
|
237
|
+
].freeze
|
|
238
|
+
|
|
239
|
+
module_function
|
|
240
|
+
|
|
241
|
+
def build
|
|
242
|
+
{
|
|
243
|
+
'name' => 'mixinbot',
|
|
244
|
+
'version' => MixinBot::VERSION,
|
|
245
|
+
'license' => 'MIT',
|
|
246
|
+
'commands' => commands_with_globals,
|
|
247
|
+
'errors' => CLIErrors.schema_errors,
|
|
248
|
+
'api_method_count' => CLIHelpers.api_callable_methods.size,
|
|
249
|
+
'api_method_registry' => 'mixinbot list -o json'
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def commands_with_globals
|
|
254
|
+
global_args = [
|
|
255
|
+
{ 'name' => '--apihost', 'type' => 'string', 'required' => false, 'default' => 'api.mixin.one',
|
|
256
|
+
'aliases' => ['-a'] },
|
|
257
|
+
{ 'name' => '--output', 'type' => 'string', 'required' => false,
|
|
258
|
+
'enum' => CLIOutput::OUTPUT_FORMATS, 'aliases' => ['-o'],
|
|
259
|
+
'description' => 'Output format; defaults to pretty in TTY, json when piped' },
|
|
260
|
+
{ 'name' => '--pretty', 'type' => 'boolean', 'required' => false, 'default' => true,
|
|
261
|
+
'aliases' => ['-r'], 'description' => 'Alias for --output pretty' }
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
COMMAND_DEFINITIONS.map do |cmd|
|
|
265
|
+
cmd.merge('global_args' => global_args)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def mutating_api_method?(method_name)
|
|
270
|
+
name = method_name.to_s
|
|
271
|
+
MUTATING_API_METHOD_PREFIXES.any? { |prefix| name.start_with?(prefix) }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
class CLI
|
|
5
|
+
desc 'schema', 'Emit machine-readable CLI schema (clispec-shaped)'
|
|
6
|
+
long_desc <<-LONGDESC
|
|
7
|
+
Discover mixinbot commands, arguments, and error kinds without parsing --help.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
|
|
11
|
+
$ mixinbot schema -o json
|
|
12
|
+
$ mixinbot schema -o json | jq '.commands[].name'
|
|
13
|
+
$ mixinbot schema -o yaml
|
|
14
|
+
LONGDESC
|
|
15
|
+
def schema
|
|
16
|
+
with_command_name('schema') do
|
|
17
|
+
emit_success(CLISchema.build, command: 'schema')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/mixin_bot/cli/utils.rb
CHANGED
|
@@ -1,45 +1,141 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MixinBot
|
|
4
|
-
class
|
|
5
|
-
|
|
4
|
+
class UtilsCLI < Thor
|
|
5
|
+
include CLIOutput
|
|
6
|
+
include CLIHelpers
|
|
7
|
+
|
|
8
|
+
desc 'call METHOD', 'Invoke a MixinBot.utils method'
|
|
9
|
+
option :data, type: :string, aliases: '-d', default: '{}', desc: 'JSON object of keyword arguments'
|
|
10
|
+
def call(method_name, *positional)
|
|
11
|
+
with_command_name('utils call') do
|
|
12
|
+
kwargs = parse_utils_json_data(options[:data])
|
|
13
|
+
result = invoke_utils_method(method_name, kwargs:, positional:)
|
|
14
|
+
emit_success(result, command: 'utils call')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc 'list [FILTER]', 'List callable MixinBot.utils methods'
|
|
19
|
+
option :limit, type: :numeric, default: 100, desc: 'Maximum items to return'
|
|
20
|
+
option :offset, type: :numeric, default: 0, desc: 'Number of items to skip'
|
|
21
|
+
option :fields, type: :string, default: 'name', desc: 'Comma-separated fields for JSON output'
|
|
22
|
+
def list(filter = nil)
|
|
23
|
+
with_command_name('utils list') do
|
|
24
|
+
methods = CLIHelpers.utils_callable_methods
|
|
25
|
+
if filter.present?
|
|
26
|
+
needle = filter.downcase
|
|
27
|
+
methods = methods.select { |m| m.to_s.downcase.include?(needle) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
items = methods.map { |name| { 'name' => name.to_s } }
|
|
31
|
+
page, total, limit, offset = paginate_items(items, limit: options[:limit], offset: options[:offset])
|
|
32
|
+
page = select_fields(page, options[:fields])
|
|
33
|
+
|
|
34
|
+
if structured_output?
|
|
35
|
+
emit_list(items: page, total:, limit:, offset:, command: 'utils list')
|
|
36
|
+
else
|
|
37
|
+
page.each { |item| puts item['name'] }
|
|
38
|
+
emit_info("Showing #{page.size} of #{total} (limit=#{limit}, offset=#{offset})") if total > limit || offset.positive?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def parse_utils_json_data(json_string, label: 'data')
|
|
46
|
+
return {} if json_string.blank?
|
|
47
|
+
|
|
48
|
+
parsed = JSON.parse(json_string)
|
|
49
|
+
abort_with_error("#{label} must be a JSON object", kind: :invalid_args) unless parsed.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
parsed.transform_keys(&:to_sym)
|
|
52
|
+
rescue JSON::ParserError => e
|
|
53
|
+
abort_with_error("invalid JSON for #{label}: #{e.message}", kind: :invalid_args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def invoke_utils_method(method_name, kwargs: {}, positional: [])
|
|
57
|
+
sym = method_name.to_sym
|
|
58
|
+
unless CLIHelpers.utils_callable_methods.include?(sym)
|
|
59
|
+
abort_with_error(
|
|
60
|
+
format('unknown utils method: %<method>s', method: method_name),
|
|
61
|
+
kind: :unsupported,
|
|
62
|
+
hint: 'mixinbot utils list'
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
MixinBot.utils.public_send(sym, *positional, **kwargs)
|
|
67
|
+
rescue ::ArgumentError => e
|
|
68
|
+
abort_with_error(
|
|
69
|
+
format('invalid arguments for utils.%<method>s: %<error>s', method: method_name, error: e.message),
|
|
70
|
+
kind: :invalid_args
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class CLI
|
|
76
|
+
desc 'utils', 'Utils dispatcher (call, list)'
|
|
77
|
+
subcommand 'utils', UtilsCLI
|
|
78
|
+
|
|
79
|
+
desc 'encrypt PIN', 'Encrypt PIN using session keys'
|
|
6
80
|
option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
|
|
7
81
|
option :iterator, type: :string, aliases: '-i', desc: 'Iterator'
|
|
8
82
|
def encrypt(pin)
|
|
9
|
-
|
|
83
|
+
with_command_name('encrypt') do
|
|
84
|
+
setup_api_instance!
|
|
85
|
+
emit_success(api_instance.encrypt_pin(pin.to_s, iterator: options[:iterator]), command: 'encrypt')
|
|
86
|
+
end
|
|
10
87
|
end
|
|
11
88
|
|
|
12
|
-
desc 'unique UUIDS', '
|
|
89
|
+
desc 'unique UUIDS', 'Deterministic UUID from two or more UUIDs'
|
|
13
90
|
def unique(*uuids)
|
|
14
|
-
|
|
91
|
+
with_command_name('unique') do
|
|
92
|
+
emit_success(MixinBot.utils.unique_uuid(*uuids), command: 'unique')
|
|
93
|
+
end
|
|
15
94
|
end
|
|
16
95
|
|
|
17
|
-
desc 'generatetrace HASH', '
|
|
96
|
+
desc 'generatetrace HASH', 'Trace UUID from transaction hash'
|
|
97
|
+
option :index, type: :numeric, default: 0, desc: 'Output index'
|
|
18
98
|
def generatetrace(hash)
|
|
19
|
-
|
|
99
|
+
with_command_name('generatetrace') do
|
|
100
|
+
emit_success(
|
|
101
|
+
MixinBot.utils.generate_trace_from_hash(hash, options[:index]),
|
|
102
|
+
command: 'generatetrace'
|
|
103
|
+
)
|
|
104
|
+
end
|
|
20
105
|
end
|
|
21
106
|
|
|
22
|
-
desc 'decodetx TRANSACTION', '
|
|
107
|
+
desc 'decodetx TRANSACTION', 'Decode raw transaction hex'
|
|
23
108
|
def decodetx(transaction)
|
|
24
|
-
|
|
109
|
+
with_command_name('decodetx') do
|
|
110
|
+
emit_success(MixinBot.utils.decode_raw_transaction(transaction), command: 'decodetx')
|
|
111
|
+
end
|
|
25
112
|
end
|
|
26
113
|
|
|
27
|
-
desc 'nftmemo', '
|
|
28
|
-
option :collection, type: :string, required: true, aliases: '-c', desc: 'Collection ID
|
|
29
|
-
option :token, type: :numeric, required: true, aliases: '-t', desc: 'Token ID
|
|
30
|
-
option :hash, type: :string, required: true, aliases: '-h', desc: '
|
|
114
|
+
desc 'nftmemo', 'NFT mint memo'
|
|
115
|
+
option :collection, type: :string, required: true, aliases: '-c', desc: 'Collection ID (UUID)'
|
|
116
|
+
option :token, type: :numeric, required: true, aliases: '-t', desc: 'Token ID'
|
|
117
|
+
option :hash, type: :string, required: true, aliases: '-h', desc: 'Metadata hash (256-bit hex)'
|
|
31
118
|
def nftmemo
|
|
32
|
-
|
|
119
|
+
with_command_name('nftmemo') do
|
|
120
|
+
emit_success(
|
|
121
|
+
MixinBot.utils.nft_memo(options[:collection], options[:token], options[:hash]),
|
|
122
|
+
command: 'nftmemo'
|
|
123
|
+
)
|
|
124
|
+
end
|
|
33
125
|
end
|
|
34
126
|
|
|
35
|
-
desc 'rsa', '
|
|
127
|
+
desc 'rsa', 'Generate RSA key pair'
|
|
36
128
|
def rsa
|
|
37
|
-
|
|
129
|
+
with_command_name('rsa') do
|
|
130
|
+
emit_success(MixinBot.utils.generate_rsa_key, command: 'rsa')
|
|
131
|
+
end
|
|
38
132
|
end
|
|
39
133
|
|
|
40
|
-
desc 'ed25519', '
|
|
134
|
+
desc 'ed25519', 'Generate Ed25519 key pair'
|
|
41
135
|
def ed25519
|
|
42
|
-
|
|
136
|
+
with_command_name('ed25519') do
|
|
137
|
+
emit_success(MixinBot.utils.generate_ed25519_key, command: 'ed25519')
|
|
138
|
+
end
|
|
43
139
|
end
|
|
44
140
|
end
|
|
45
141
|
end
|