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
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  module MixinBot
4
4
  class API
@@ -17,65 +17,56 @@ module MixinBot
17
17
  end
18
18
 
19
19
  def plain_text(options)
20
- options.merge!(category: 'PLAIN_TEXT')
21
- base_message_params(options)
20
+ base_message_params(options.merge(category: 'PLAIN_TEXT'))
22
21
  end
23
22
 
24
23
  def plain_post(options)
25
- options.merge!(category: 'PLAIN_POST')
26
- base_message_params(options)
24
+ base_message_params(options.merge(category: 'PLAIN_POST'))
27
25
  end
28
26
 
29
27
  def plain_image(options)
30
- options.merge!(category: 'PLAIN_IMAGE')
31
- base_message_params(options)
28
+ base_message_params(options.merge(category: 'PLAIN_IMAGE'))
32
29
  end
33
30
 
34
31
  def plain_data(options)
35
- options.merge!(category: 'PLAIN_DATA')
36
- base_message_params(options)
32
+ base_message_params(options.merge(category: 'PLAIN_DATA'))
37
33
  end
38
34
 
39
35
  def plain_sticker(options)
40
- options.merge!(category: 'PLAIN_STICKER')
41
- base_message_params(options)
36
+ base_message_params(options.merge(category: 'PLAIN_STICKER'))
42
37
  end
43
38
 
44
39
  def plain_contact(options)
45
- options.merge!(category: 'PLAIN_CONTACT')
46
- base_message_params(options)
40
+ base_message_params(options.merge(category: 'PLAIN_CONTACT'))
47
41
  end
48
42
 
49
43
  def plain_audio(options)
50
- options.merge!(category: 'PLAIN_AUDIO')
51
- base_message_params(options)
44
+ base_message_params(options.merge(category: 'PLAIN_AUDIO'))
52
45
  end
53
46
 
54
47
  def plain_video(options)
55
- options.merge!(category: 'PLAIN_VIDEO')
56
- base_message_params(options)
48
+ base_message_params(options.merge(category: 'PLAIN_VIDEO'))
57
49
  end
58
50
 
59
51
  def app_card(options)
60
- options.merge!(category: 'APP_CARD')
61
- base_message_params(options)
52
+ base_message_params(options.merge(category: 'APP_CARD'))
62
53
  end
63
54
 
64
55
  def app_button_group(options)
65
- options.merge!(category: 'APP_BUTTON_GROUP')
66
- base_message_params(options)
56
+ base_message_params(options.merge(category: 'APP_BUTTON_GROUP'))
67
57
  end
68
58
 
69
59
  def recall_message_params(message_id, options)
70
60
  raise 'recipient_id is required!' if options[:recipient_id].nil?
71
61
 
72
- options.merge!(
73
- category: 'MESSAGE_RECALL',
74
- data: {
75
- message_id:
76
- }
62
+ base_message_params(
63
+ options.merge(
64
+ category: 'MESSAGE_RECALL',
65
+ data: {
66
+ message_id:
67
+ }
68
+ )
77
69
  )
78
- base_message_params(options)
79
70
  end
80
71
 
81
72
  # base format of message params
@@ -34,6 +34,25 @@ module MixinBot
34
34
 
35
35
  client.get path, access_token:
36
36
  end
37
+ alias fetch_safe_multisig_request safe_multisig_request
38
+
39
+ def create_multisig_raw_tx(_asset_id:, senders:, receivers:, threshold:, inputs:, amount:, trace_id:,
40
+ extra: '')
41
+ out_hint = MixinBot.utils.unique_object_id(trace_id, 'OUTPUT', '0')
42
+ change_hint = MixinBot.utils.unique_object_id(trace_id, 'OUTPUT', '1')
43
+ keys = create_safe_keys(
44
+ { receivers:, index: 0, hint: out_hint },
45
+ { receivers: senders, index: 1, hint: change_hint }
46
+ )['data']
47
+
48
+ receivers_list = [
49
+ { members: receivers, threshold: 1, amount: amount.to_s, ghosts: [keys[0]] },
50
+ { members: senders, threshold:, amount: nil, ghosts: [keys[1]] }
51
+ ].compact
52
+
53
+ tx = build_safe_transaction(utxos: inputs, receivers: receivers_list, extra:)
54
+ MixinBot.utils.encode_raw_transaction(tx)
55
+ end
37
56
  end
38
57
  end
39
58
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Network
6
+ def network_assets
7
+ client.get '/network', access_token: ''
8
+ end
9
+ alias read_network_assets network_assets
10
+
11
+ def network_assets_top
12
+ client.get '/network/assets/top', access_token: ''
13
+ end
14
+ alias read_network_assets_top network_assets_top
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module MixinBot
6
+ class API
7
+ module NetworkAsset
8
+ def network_asset(asset_id)
9
+ path = format('/network/assets/%<asset_id>s', asset_id:)
10
+ client.get path, access_token: ''
11
+ end
12
+ alias read_asset network_asset
13
+
14
+ def network_ticker(asset_id, offset: nil, access_token: nil)
15
+ params = { asset: asset_id, offset: }.compact
16
+ client.get '/network/ticker', **params, access_token: access_token || ''
17
+ end
18
+ alias read_asset_ticker network_ticker
19
+
20
+ def network_asset_search(name)
21
+ path = "/network/assets/search/#{CGI.escape(name.to_s)}"
22
+ client.get path, access_token: ''
23
+ end
24
+ alias asset_search network_asset_search
25
+ end
26
+ end
27
+ end
@@ -21,7 +21,7 @@ module MixinBot
21
21
  members = kwargs[:members].presence || [config.app_id]
22
22
  threshold = kwargs[:threshold] || members.length
23
23
 
24
- members_hash = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
24
+ members_hash = MixinBot.utils.hash_members(members)
25
25
 
26
26
  path = '/safe/outputs'
27
27
  params = {
@@ -21,12 +21,13 @@ module MixinBot
21
21
  }
22
22
  else
23
23
  {
24
- pin: MixinBot.utils.encrypt_pin(pin)
24
+ pin: encrypt_pin(pin)
25
25
  }
26
26
  end
27
27
 
28
28
  client.post path, **payload
29
29
  end
30
+ alias verify_pin_tip verify_pin
30
31
 
31
32
  # https://developers.mixin.one/api/alpha-mixin-network/create-pin/
32
33
  def update_pin(pin:, old_pin: nil)
@@ -45,6 +46,17 @@ module MixinBot
45
46
  client.post path, **payload
46
47
  end
47
48
 
49
+ def update_tip_pin(pin, pub_tip)
50
+ old_encrypted = encrypt_pin(pin, iterator: (Time.now.utc.to_f * 1e9).to_i)
51
+ pub_buf = [pub_tip].pack('H*')
52
+ raise ArgumentError, 'invalid public key' unless pub_buf.bytesize == 32
53
+
54
+ pub_tip_buf = pub_buf + [1].pack('Q>')
55
+ encrypted_pin = encrypt_pin(pub_tip_buf.unpack1('H*'), iterator: (Time.now.utc.to_f * 1e9).to_i + 1)
56
+ path = '/pin/update'
57
+ client.post path, old_pin_base64: old_encrypted, pin_base64: encrypted_pin
58
+ end
59
+
48
60
  def prepare_tip_key(counter = 0)
49
61
  ed25519_key = JOSE::JWA::Ed25519.keypair
50
62
 
@@ -57,8 +69,9 @@ module MixinBot
57
69
  }
58
70
  end
59
71
 
60
- def encrypt_pin(pin, iterator: nil)
61
- MixinBot.utils.encrypt_pin(pin, iterator:, shared_key: generate_shared_key_with_server)
72
+ def encrypt_pin(pin, iterator: nil, shared_key: nil)
73
+ sk = shared_key.presence || generate_shared_key_with_server
74
+ MixinBot.utils.encrypt_pin(pin, iterator:, shared_key: sk)
62
75
  end
63
76
 
64
77
  def decrypt_pin(msg)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ ##
6
+ # Shared helpers for legacy 6-digit PIN vs TIP (+pin_base64+) payloads.
7
+ #
8
+ module PinPayload
9
+ private
10
+
11
+ ##
12
+ # @return [Hash] either +{ pin: ... }+ or +{ pin_base64: ... }+
13
+ #
14
+ def tip_or_legacy_pin_payload(pin, tip_action, *tip_params)
15
+ p = pin
16
+ raise ArgumentError, 'pin is required' if p.blank?
17
+
18
+ if p.to_s.length > 6
19
+ { pin_base64: encrypt_tip_pin(p, tip_action, *tip_params) }
20
+ else
21
+ { pin: encrypt_pin(p) }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Session
6
+ def fetch_user_sessions(user_ids, access_token: nil)
7
+ raise ArgumentError, 'user_ids required' if user_ids.blank?
8
+
9
+ client.fetch_post_array '/sessions/fetch', Array(user_ids), access_token:
10
+ end
11
+ alias fetch_user_session fetch_user_sessions
12
+ end
13
+ end
14
+ end
@@ -17,6 +17,12 @@ module MixinBot
17
17
  client.get path, **params
18
18
  end
19
19
 
20
+ def safe_snapshot(snapshot_id, access_token: nil)
21
+ path = format('/safe/snapshots/%<snapshot_id>s', snapshot_id:)
22
+ client.get path, access_token:
23
+ end
24
+ alias safe_snapshot_by_id safe_snapshot
25
+
20
26
  def create_safe_snapshot_notification(**kwargs)
21
27
  path = '/safe/snapshots/notifications'
22
28
 
@@ -17,6 +17,7 @@ module MixinBot
17
17
  TIP:COLLECTIBLE:REQUEST:SIGN:
18
18
  TIP:COLLECTIBLE:REQUEST:UNLOCK:
19
19
  TIP:TRANSFER:CREATE:
20
+ TIP:WITHDRAW:
20
21
  TIP:WITHDRAWAL:CREATE:
21
22
  TIP:TRANSACTION:CREATE:
22
23
  TIP:OAUTH:APPROVE:
@@ -30,7 +31,7 @@ module MixinBot
30
31
 
31
32
  pin_key = MixinBot.utils.decode_key pin
32
33
 
33
- msg = action + params.flatten.map(&:to_s).join
34
+ msg = action + params.join
34
35
 
35
36
  msg = Digest::SHA256.digest(msg) unless action == 'TIP:VERIFY:'
36
37
 
@@ -38,6 +39,78 @@ module MixinBot
38
39
 
39
40
  encrypt_pin signature
40
41
  end
42
+
43
+ def get_tip_node(path, request_id: nil, access_token: nil)
44
+ url = format('/external/tip/%<path>s', path:)
45
+ if request_id.present?
46
+ client.fetch_get url, access_token: access_token || ''
47
+ else
48
+ client.get url, access_token: access_token || ''
49
+ end
50
+ end
51
+ alias get_tip_node_by_path get_tip_node
52
+
53
+ def tip_migrate_body(pub_hex)
54
+ "TIP:MIGRATE:#{pub_hex}"
55
+ end
56
+
57
+ def tip_body_for_verify(timestamp = (Time.now.to_f * 1e9).to_i)
58
+ format('TIP:VERIFY:%032d', timestamp)
59
+ end
60
+
61
+ def tip_body_for_raw_transaction_create(asset_id, opponent_key, opponent_receivers, opponent_threshold, amount,
62
+ trace_id, memo)
63
+ receivers = Array(opponent_receivers).join
64
+ format(
65
+ 'TIP:TRANSACTION:CREATE:%<asset_id>s%<opponent_key>s%<receivers>s%<opponent_threshold>s%<amount>s%<trace_id>s%<memo>s',
66
+ asset_id:, opponent_key:, receivers:, opponent_threshold:, amount:, trace_id:, memo:
67
+ )
68
+ end
69
+
70
+ def tip_body_for_withdrawal_create(address_id, amount, fee, trace_id, memo)
71
+ format(
72
+ 'TIP:WITHDRAWAL:CREATE:%<address_id>s%<amount>s%<fee>s%<trace_id>s%<memo>s',
73
+ address_id:, amount:, fee:, trace_id:, memo:
74
+ )
75
+ end
76
+
77
+ def tip_body_for_transfer(asset_id, counter_user_id, amount, trace_id, memo)
78
+ format(
79
+ 'TIP:TRANSFER:CREATE:%<asset_id>s%<counter_user_id>s%<amount>s%<trace_id>s%<memo>s',
80
+ asset_id:, counter_user_id:, amount:, trace_id:, memo:
81
+ )
82
+ end
83
+
84
+ def tip_body_for_phone_number_update(verification_id, code)
85
+ format('TIP:PHONE:NUMBER:UPDATE:%<verification_id>s%<code>s', verification_id:, code:)
86
+ end
87
+
88
+ def tip_body_for_emergency_contact_create(verification_id, code)
89
+ format('TIP:EMERGENCY:CONTACT:CREATE:%<verification_id>s%<code>s', verification_id:, code:)
90
+ end
91
+
92
+ def tip_body_for_address_add(asset_id, public_key, key_tag, name)
93
+ format(
94
+ 'TIP:ADDRESS:ADD:%<asset_id>s%<public_key>s%<key_tag>s%<name>s',
95
+ asset_id:, public_key:, key_tag:, name:
96
+ )
97
+ end
98
+
99
+ def tip_body_for_provisioning_update(device_id, secret)
100
+ format('TIP:PROVISIONING:UPDATE:%<device_id>s%<secret>s', device_id:, secret:)
101
+ end
102
+
103
+ def tip_body_for_ownership_transfer(user_id)
104
+ format('TIP:APP:OWNERSHIP:TRANSFER:%<user_id>s', user_id:)
105
+ end
106
+
107
+ def tip_body_for_sequencer_register(user_id, public_key)
108
+ format('SEQUENCER:REGISTER:%<user_id>s%<public_key>s', user_id:, public_key:)
109
+ end
110
+
111
+ def tip_body(str)
112
+ str.to_s.b
113
+ end
41
114
  end
42
115
  end
43
116
  end
@@ -36,7 +36,7 @@ module MixinBot
36
36
  recipients.each_with_index do |recipient, index|
37
37
  next if recipient[:mix_address].blank?
38
38
 
39
- if recipient[:members].all?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
39
+ if recipient[:members].all?(&->(m) { m.start_with? MixinBot::MAIN_ADDRESS_PREFIX })
40
40
  key = JOSE::JWA::Ed25519.keypair
41
41
  gk = {
42
42
  mask: key[0].unpack1('H*'),
@@ -54,7 +54,7 @@ module MixinBot
54
54
 
55
55
  ghost_keys[index] = gk.with_indifferent_access
56
56
 
57
- elsif recipient[:members].none?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
57
+ elsif recipient[:members].none?(&->(m) { m.start_with? MixinBot::MAIN_ADDRESS_PREFIX })
58
58
  uuid_recipients.push(
59
59
  {
60
60
  receivers: recipient[:members],
@@ -88,7 +88,12 @@ module MixinBot
88
88
  # }
89
89
  SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
90
90
  def build_safe_transaction(**kwargs)
91
- raise ArgumentError, "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction" unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
91
+ unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? do |param|
92
+ kwargs.keys.include? param
93
+ end
94
+ raise ArgumentError,
95
+ "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction"
96
+ end
92
97
  raise ArgumentError, 'receivers should be an array' unless kwargs[:receivers].is_a? Array
93
98
  raise ArgumentError, 'utxos should be an array' unless kwargs[:utxos].is_a? Array
94
99
 
@@ -126,10 +131,20 @@ module MixinBot
126
131
  end
127
132
  raise ArgumentError, 'recipients too many' if recipients.size > 256
128
133
 
129
- asset = utxos[0]['asset']
134
+ mixin_asset_for = lambda do |u|
135
+ h = u.with_indifferent_access
136
+ next h[:asset] if h[:asset].present?
137
+
138
+ aid = h[:asset_id]
139
+ raise ArgumentError, 'utxo asset_id or asset is required' if aid.blank?
140
+
141
+ SHA3::Digest::SHA256.hexdigest(aid)
142
+ end
143
+
144
+ asset = mixin_asset_for.call(utxos[0])
130
145
  inputs = []
131
146
  utxos.each do |utxo|
132
- raise ArgumentError, 'utxo asset not match' unless utxo['asset'] == asset
147
+ raise ArgumentError, 'utxo asset not match' unless mixin_asset_for.call(utxo) == asset
133
148
 
134
149
  inputs << {
135
150
  hash: utxo['transaction_hash'],
@@ -171,6 +186,17 @@ module MixinBot
171
186
  }
172
187
  end
173
188
 
189
+ def verify_raw_transaction(requests)
190
+ requests = Array(requests).map do |r|
191
+ r = r.with_indifferent_access
192
+ { request_id: r[:request_id], raw: r[:raw] }
193
+ end
194
+ create_safe_transaction_request(requests.first[:request_id], requests.first[:raw]) if requests.one?
195
+
196
+ path = '/safe/transaction/requests'
197
+ client.post path, *requests
198
+ end
199
+
174
200
  def create_safe_transaction_request(request_id, raw)
175
201
  path = '/safe/transaction/requests'
176
202
  payload = [{
@@ -181,25 +207,73 @@ module MixinBot
181
207
  client.post path, *payload
182
208
  end
183
209
 
184
- def send_safe_transaction(request_id, raw)
210
+ def send_safe_transaction(request_id, raw = nil, requests: nil)
185
211
  path = '/safe/transactions'
186
- payload = [{
187
- request_id:,
188
- raw:
189
- }]
212
+ payload =
213
+ if requests.present?
214
+ Array(requests).map { |r| r.with_indifferent_access.slice(:request_id, :raw) }
215
+ else
216
+ [{ request_id:, raw: }]
217
+ end
190
218
 
191
219
  client.post path, *payload
192
220
  end
221
+ alias send_raw_transaction send_safe_transaction
193
222
 
194
223
  def safe_transaction(request_id, access_token: nil)
195
224
  path = format('/safe/transactions/%<request_id>s', request_id:)
196
225
 
197
226
  client.get path, access_token:
198
227
  end
228
+ alias get_transaction_by_id safe_transaction
229
+ alias get_transaction_by_id_with_safe_user safe_transaction
230
+
231
+ def estimate_storage_cost(extra)
232
+ step = BigDecimal(EXTRA_STORAGE_PRICE_STEP)
233
+ len = extra.to_s.bytesize
234
+ steps = (len / 1024) + 1
235
+ step * steps
236
+ end
237
+
238
+ def storage_recipient
239
+ MixinBot::MixAddress.from_members(
240
+ members: [MixinBot.utils.burning_address],
241
+ threshold: 64
242
+ )
243
+ end
244
+
245
+ def create_object_storage_transaction(extra:, trace_id:, _references: nil, limit: nil, utxos: nil, **transfer_opts)
246
+ amount = estimate_storage_cost(extra)
247
+ amount = [amount, BigDecimal(limit.to_s)].max if limit.present?
248
+ kwargs = {
249
+ asset_id: XIN_ASSET_ID,
250
+ amount: amount.to_s('F'),
251
+ trace_id:,
252
+ memo: extra.to_s,
253
+ **transfer_opts
254
+ }
255
+ kwargs[:utxos] = utxos if utxos.present?
256
+ kwargs[:members] = [config.app_id] unless kwargs.key?(:members)
257
+ create_safe_transfer(**kwargs)
258
+ end
259
+
260
+ def request_ghost_recipients_with_trace_id(recipients, _trace_id)
261
+ generate_safe_keys(
262
+ recipients.map do |r|
263
+ r = r.with_indifferent_access
264
+ { members: r[:members], threshold: r[:threshold], mix_address: r[:mix_address] }
265
+ end
266
+ )
267
+ end
199
268
 
200
269
  SIGN_SAFE_TRANSACTION_ARGUMENTS = %i[raw utxos request spend_key].freeze
201
270
  def sign_safe_transaction(**kwargs)
202
- raise ArgumentError, "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction" unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
271
+ unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? do |param|
272
+ kwargs.keys.include? param
273
+ end
274
+ raise ArgumentError,
275
+ "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction"
276
+ end
203
277
 
204
278
  raw = kwargs[:raw]
205
279
  tx = MixinBot.utils.decode_raw_transaction raw
@@ -265,7 +339,12 @@ module MixinBot
265
339
 
266
340
  INSCRIBE_TRANSACTION_ARGUMENTS = %i[content collection_hash].freeze
267
341
  def build_inscribe_transaction(**kwargs)
268
- raise ArgumentError, "#{INSCRIBE_TRANSACTION_ARGUMENTS.join(', ')} are needed for inscribe transaction" unless INSCRIBE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
342
+ unless INSCRIBE_TRANSACTION_ARGUMENTS.all? do |param|
343
+ kwargs.keys.include? param
344
+ end
345
+ raise ArgumentError,
346
+ "#{INSCRIBE_TRANSACTION_ARGUMENTS.join(', ')} are needed for inscribe transaction"
347
+ end
269
348
 
270
349
  receivers = kwargs[:receivers].presence || [config.app_id]
271
350
  receivers_threshold = kwargs[:receivers_threshold] || receivers.length
@@ -283,14 +362,20 @@ module MixinBot
283
362
  MixinBot.api.build_object_transaction data.to_json, references: [collection_hash]
284
363
  end
285
364
 
286
- OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS = %i[amount inscription_hash utxos].freeze
365
+ OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS = %i[amount inscription_hash sequence utxos].freeze
287
366
  def build_occupy_transaction(**kwargs)
288
- raise ArgumentError, "#{OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.join(', ')} are needed for occupy NFT transaction" unless OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
367
+ unless OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.all? do |param|
368
+ kwargs.keys.include? param
369
+ end
370
+ raise ArgumentError,
371
+ "#{OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.join(', ')} are needed for occupy NFT transaction"
372
+ end
289
373
 
290
374
  members = kwargs[:members].presence || [config.app_id]
291
375
  threshold = kwargs[:threshold] || members.length
292
376
  amount = kwargs[:amount]
293
377
  inscription_hash = kwargs[:inscription_hash]
378
+ sequence = kwargs[:sequence]
294
379
 
295
380
  receivers = [
296
381
  {
@@ -302,11 +387,15 @@ module MixinBot
302
387
 
303
388
  extra = {
304
389
  operation: 'occupy',
305
- recipient:,
306
- content:
390
+ sequence:
307
391
  }.to_json
308
392
 
309
- MixinBot.api.build_safe_transaction(utxos:, receivers:, extra:, references: [inscription_hash])
393
+ build_safe_transaction(utxos: kwargs[:utxos], receivers:, extra:, references: [inscription_hash])
394
+ end
395
+
396
+ def send_kernel_transaction_from_account(**_kwargs)
397
+ raise NotImplementedError,
398
+ 'send_kernel_transaction_from_account requires kernel UTXO signing; use native mixin CLI or build manually'
310
399
  end
311
400
  end
312
401
  end