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
@@ -6,24 +6,27 @@ module MixinBot
6
6
  NFT_ASSET_MIXIN_ID = '1700941284a95f31b25ec8c546008f208f88eee4419ccdcdbe6e3195e60128ca'
7
7
 
8
8
  def legacy_collectible(id, access_token: nil)
9
+ warn_legacy_mixin_api!('LegacyCollectible#legacy_collectible')
9
10
  path = "/collectibles/tokens/#{id}"
10
11
  client.get path, access_token:
11
12
  end
12
13
 
13
14
  def legacy_collection(id, access_token: nil)
15
+ warn_legacy_mixin_api!('LegacyCollectible#legacy_collection')
14
16
  path = "/collectibles/collections/#{id}"
15
17
  client.get path, access_token:
16
18
  end
17
19
 
18
20
  def legacy_collectibles(**kwargs)
21
+ warn_legacy_mixin_api!('LegacyCollectible#legacy_collectibles')
19
22
  limit = kwargs[:limit] || 100
20
23
  offset = kwargs[:offset] || ''
21
24
  state = kwargs[:state] || ''
22
25
 
23
- members = kwargs[:members] || [config.app_id]
24
- threshold = kwargs[:threshold] || members.length
26
+ member_ids = kwargs[:members] || [config.app_id]
27
+ threshold = kwargs[:threshold] || member_ids.length
25
28
 
26
- members = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
29
+ members_hash = MixinBot.utils.hash_members(member_ids)
27
30
 
28
31
  access_token = kwargs[:access_token]
29
32
 
@@ -32,7 +35,7 @@ module MixinBot
32
35
  limit:,
33
36
  offset:,
34
37
  state:,
35
- members:,
38
+ members: members_hash,
36
39
  threshold:
37
40
  }
38
41
 
@@ -41,7 +44,11 @@ module MixinBot
41
44
 
42
45
  COLLECTABLE_REQUEST_ACTIONS = %i[sign unlock].freeze
43
46
  def create_collectible_request(action, raw, access_token: nil)
44
- raise ArgumentError, "request action is limited in #{COLLECTABLE_REQUEST_ACTIONS.join(', ')}" unless COLLECTABLE_REQUEST_ACTIONS.include? action.to_sym
47
+ warn_legacy_mixin_api!('LegacyCollectible#create_collectible_request')
48
+ unless COLLECTABLE_REQUEST_ACTIONS.include? action.to_sym
49
+ raise ArgumentError,
50
+ "request action is limited in #{COLLECTABLE_REQUEST_ACTIONS.join(', ')}"
51
+ end
45
52
 
46
53
  path = '/collectibles/requests'
47
54
  payload = {
@@ -52,47 +59,33 @@ module MixinBot
52
59
  end
53
60
 
54
61
  def create_sign_collectible_request(raw, access_token: nil)
62
+ warn_legacy_mixin_api!('LegacyCollectible#create_sign_collectible_request')
55
63
  create_collectible_request 'sign', raw, access_token:
56
64
  end
57
65
 
58
66
  def create_unlock_collectible_request(raw, access_token: nil)
67
+ warn_legacy_mixin_api!('LegacyCollectible#create_unlock_collectible_request')
59
68
  create_collectible_request 'unlock', raw, access_token:
60
69
  end
61
70
 
62
71
  def sign_collectible_request(request_id, pin = nil)
72
+ warn_legacy_mixin_api!('LegacyCollectible#sign_collectible_request')
63
73
  pin ||= config.pin
64
74
  raise ArgumentError, 'pin is needed for sign collectible request' if pin.blank?
65
75
 
66
76
  path = format('/collectibles/requests/%<request_id>s/sign', request_id:)
67
- payload =
68
- if pin.length > 6
69
- pin_base64 = encrypt_tip_pin(pin, 'TIP:COLLECTIBLE:REQUEST:SIGN:', request_id)
70
- {
71
- pin_base64:
72
- }
73
- else
74
- {
75
- pin: encrypt_pin(pin)
76
- }
77
- end
77
+ payload = tip_or_legacy_pin_payload(pin, 'TIP:COLLECTIBLE:REQUEST:SIGN:', request_id)
78
+
78
79
  client.post path, **payload
79
80
  end
80
81
 
81
82
  def unlock_collectible_request(request_id, pin = nil)
83
+ warn_legacy_mixin_api!('LegacyCollectible#unlock_collectible_request')
82
84
  pin ||= config.pin
83
85
  raise ArgumentError, 'pin is needed for sign collectible request' if pin.blank?
84
86
 
85
87
  path = format('/collectibles/requests/%<request_id>s/unlock', request_id:)
86
- payload =
87
- if pin.length > 6
88
- {
89
- pin_base64: encrypt_tip_pin(pin, 'TIP:COLLECTIBLE:REQUEST:UNLOCK:', request_id)
90
- }
91
- else
92
- {
93
- pin: encrypt_pin(pin)
94
- }
95
- end
88
+ payload = tip_or_legacy_pin_payload(pin, 'TIP:COLLECTIBLE:REQUEST:UNLOCK:', request_id)
96
89
 
97
90
  client.post path, **payload
98
91
  end
@@ -114,7 +107,13 @@ module MixinBot
114
107
  # }
115
108
  COLLECTIBLE_TRANSACTION_ARGUMENTS = %i[collectible nfo receivers receivers_threshold].freeze
116
109
  def build_collectible_transaction(**kwargs)
117
- raise ArgumentError, "#{COLLECTIBLE_TRANSACTION_ARGUMENTS.join(', ')} are needed for build collectible transaction" unless COLLECTIBLE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
110
+ warn_legacy_mixin_api!('LegacyCollectible#build_collectible_transaction')
111
+ unless COLLECTIBLE_TRANSACTION_ARGUMENTS.all? do |param|
112
+ kwargs.keys.include? param
113
+ end
114
+ raise ArgumentError,
115
+ "#{COLLECTIBLE_TRANSACTION_ARGUMENTS.join(', ')} are needed for build collectible transaction"
116
+ end
118
117
 
119
118
  kwargs = kwargs.with_indifferent_access
120
119
  collectible = kwargs['collectible']
@@ -5,7 +5,11 @@ module MixinBot
5
5
  module LegacyMultisig
6
6
  MULTISIG_REQUEST_ACTIONS = %i[sign unlock].freeze
7
7
  def create_multisig_request(action, raw, access_token: nil)
8
- raise ArgumentError, "request action is limited in #{MULTISIG_REQUEST_ACTIONS.join(', ')}" unless MULTISIG_REQUEST_ACTIONS.include? action.to_sym
8
+ warn_legacy_mixin_api!('LegacyMultisig#create_multisig_request')
9
+ unless MULTISIG_REQUEST_ACTIONS.include? action.to_sym
10
+ raise ArgumentError,
11
+ "request action is limited in #{MULTISIG_REQUEST_ACTIONS.join(', ')}"
12
+ end
9
13
 
10
14
  path = '/multisigs/requests'
11
15
  payload = {
@@ -17,46 +21,39 @@ module MixinBot
17
21
 
18
22
  # transfer from the multisig address
19
23
  def create_sign_multisig_request(raw, access_token: nil)
24
+ warn_legacy_mixin_api!('LegacyMultisig#create_sign_multisig_request')
20
25
  create_multisig_request 'sign', raw, access_token:
21
26
  end
22
27
 
23
28
  # transfer from the multisig address
24
29
  # create a request for unlock a multi-sign
25
30
  def create_unlock_multisig_request(raw, access_token: nil)
31
+ warn_legacy_mixin_api!('LegacyMultisig#create_unlock_multisig_request')
26
32
  create_multisig_request 'unlock', raw, access_token:
27
33
  end
28
34
 
29
35
  def sign_multisig_request(request_id, pin = nil)
36
+ warn_legacy_mixin_api!('LegacyMultisig#sign_multisig_request')
30
37
  pin ||= config.pin
31
38
  path = format('/multisigs/requests/%<request_id>s/sign', request_id:)
32
- payload =
33
- if pin.length > 6
34
- {
35
- pin_base64: encrypt_tip_pin(pin, 'TIP:MULTISIG:REQUEST:SIGN:', request_id)
36
- }
37
- else
38
- {
39
- pin: encrypt_pin(pin)
40
- }
41
- end
39
+ payload = tip_or_legacy_pin_payload(pin, 'TIP:MULTISIG:REQUEST:SIGN:', request_id)
42
40
 
43
41
  client.post path, **payload
44
42
  end
45
43
 
44
+ def cancel_multisig_request(request_id, access_token: nil)
45
+ warn_legacy_mixin_api!('LegacyMultisig#cancel_multisig_request')
46
+ path = format('/multisigs/requests/%<request_id>s/cancel', request_id:)
47
+ client.post path, access_token:
48
+ end
49
+ alias cancel_multisig cancel_multisig_request
50
+
46
51
  def unlock_multisig_request(request_id, pin = nil)
52
+ warn_legacy_mixin_api!('LegacyMultisig#unlock_multisig_request')
47
53
  pin ||= config.pin
48
54
 
49
55
  path = format('/multisigs/requests/%<request_id>s/unlock', request_id:)
50
- payload =
51
- if pin.length > 6
52
- {
53
- pin_base64: encrypt_tip_pin(pin, 'TIP:MULTISIG:REQUEST:UNLOCK:', request_id)
54
- }
55
- else
56
- {
57
- pin: encrypt_pin(pin)
58
- }
59
- end
56
+ payload = tip_or_legacy_pin_payload(pin, 'TIP:MULTISIG:REQUEST:UNLOCK:', request_id)
60
57
 
61
58
  client.post path, **payload
62
59
  end
@@ -64,6 +61,7 @@ module MixinBot
64
61
  # pay to the multisig address
65
62
  # used for create multisig payment code_id
66
63
  def create_multisig_payment(**kwargs)
64
+ warn_legacy_mixin_api!('LegacyMultisig#create_multisig_payment')
67
65
  path = '/payments'
68
66
  payload = {
69
67
  asset_id: kwargs[:asset_id],
@@ -79,6 +77,7 @@ module MixinBot
79
77
  end
80
78
 
81
79
  def verify_multisig(code_id, access_token: nil)
80
+ warn_legacy_mixin_api!('LegacyMultisig#verify_multisig')
82
81
  path = format('/codes/%<code_id>s', code_id:)
83
82
  client.get path, access_token:
84
83
  end
@@ -3,21 +3,26 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module LegacyOutput
6
+ def read_multisigs(**)
7
+ warn_legacy_mixin_api!('LegacyOutput#read_multisigs')
8
+ legacy_outputs(**)
9
+ end
10
+
6
11
  def legacy_outputs(**kwargs)
12
+ warn_legacy_mixin_api!('LegacyOutput#legacy_outputs')
7
13
  limit = kwargs[:limit] || 100
8
14
  offset = kwargs[:offset] || ''
9
15
  state = kwargs[:state] || ''
10
- members = kwargs[:members] || []
16
+ members_hash = MixinBot.utils.hash_members(kwargs[:members] || [])
11
17
  threshold = kwargs[:threshold] || ''
12
18
  access_token = kwargs[:access_token]
13
- members = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
14
19
 
15
20
  path = '/multisigs/outputs'
16
21
  params = {
17
22
  limit:,
18
23
  offset:,
19
24
  state:,
20
- members:,
25
+ members: members_hash,
21
26
  threshold:
22
27
  }.compact_blank
23
28
 
@@ -27,6 +32,7 @@ module MixinBot
27
32
  alias multisig_outputs legacy_outputs
28
33
 
29
34
  def create_output(receivers:, index:, hint: nil, access_token: nil)
35
+ warn_legacy_mixin_api!('LegacyOutput#create_output')
30
36
  path = '/outputs'
31
37
  payload = {
32
38
  receivers:,
@@ -37,6 +43,7 @@ module MixinBot
37
43
  end
38
44
 
39
45
  def build_output(receivers:, index:, amount:, threshold:, hint: nil)
46
+ warn_legacy_mixin_api!('LegacyOutput#build_output')
40
47
  _output = create_output(receivers:, index:, hint:)
41
48
  {
42
49
  amount: format('%.8f', amount.to_d.to_r),
@@ -4,6 +4,7 @@ module MixinBot
4
4
  class API
5
5
  module LegacyPayment
6
6
  def pay_url(**kwargs)
7
+ warn_legacy_mixin_api!('LegacyPayment#pay_url')
7
8
  format(
8
9
  'https://mixin.one/pay?recipient=%<recipient_id>s&asset=%<asset>s&amount=%<amount>s&trace=%<trace>s&memo=%<memo>s',
9
10
  recipient_id: kwargs[:recipient_id],
@@ -16,6 +17,7 @@ module MixinBot
16
17
 
17
18
  # https://developers.mixin.one/api/alpha-mixin-network/verify-payment/
18
19
  def verify_payment(**kwargs)
20
+ warn_legacy_mixin_api!('LegacyPayment#verify_payment')
19
21
  path = '/payments'
20
22
  payload = {
21
23
  asset_id: kwargs[:asset_id],
@@ -4,6 +4,7 @@ module MixinBot
4
4
  class API
5
5
  module LegacySnapshot
6
6
  def network_snapshots(**kwargs)
7
+ warn_legacy_mixin_api!('LegacySnapshot#network_snapshots')
7
8
  path = '/network/snapshots'
8
9
  params = {
9
10
  limit: kwargs[:limit],
@@ -16,6 +17,7 @@ module MixinBot
16
17
  end
17
18
 
18
19
  def snapshots(**kwargs)
20
+ warn_legacy_mixin_api!('LegacySnapshot#snapshots')
19
21
  path = '/snapshots'
20
22
 
21
23
  params = {
@@ -30,10 +32,24 @@ module MixinBot
30
32
  end
31
33
 
32
34
  def network_snapshot(snapshot_id, **kwargs)
35
+ warn_legacy_mixin_api!('LegacySnapshot#network_snapshot')
33
36
  path = format('/network/snapshots/%<snapshot_id>s', snapshot_id:)
34
37
 
35
38
  client.get path, access_token: kwargs[:access_token]
36
39
  end
40
+
41
+ def snapshot(snapshot_id, access_token: nil)
42
+ warn_legacy_mixin_api!('LegacySnapshot#snapshot')
43
+ path = format('/snapshots/%<snapshot_id>s', snapshot_id:)
44
+ client.get path, access_token:
45
+ end
46
+ alias snapshot_by_id snapshot
47
+
48
+ def snapshot_by_trace_id(trace_id, access_token: nil)
49
+ warn_legacy_mixin_api!('LegacySnapshot#snapshot_by_trace_id')
50
+ path = format('/snapshots/trace/%<trace_id>s', trace_id:)
51
+ client.get path, access_token:
52
+ end
37
53
  end
38
54
  end
39
55
  end
@@ -18,7 +18,13 @@ module MixinBot
18
18
  # }
19
19
  RAW_TRANSACTION_ARGUMENTS = %i[utxos senders senders_threshold receivers receivers_threshold amount].freeze
20
20
  def build_raw_transaction(**kwargs)
21
- raise ArgumentError, "#{RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build raw transaction" unless RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
21
+ warn_legacy_mixin_api!('LegacyTransaction#build_raw_transaction')
22
+ unless RAW_TRANSACTION_ARGUMENTS.all? do |param|
23
+ kwargs.keys.include? param
24
+ end
25
+ raise ArgumentError,
26
+ "#{RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build raw transaction"
27
+ end
22
28
 
23
29
  senders = kwargs[:senders]
24
30
  senders_threshold = kwargs[:senders_threshold]
@@ -96,7 +102,13 @@ module MixinBot
96
102
  # use safe transaction protocol instead
97
103
  MULTISIG_TRANSACTION_ARGUMENTS = %i[asset_id receivers threshold amount].freeze
98
104
  def create_multisig_transaction(pin, **options)
99
- raise ArgumentError, "#{MULTISIG_TRANSACTION_ARGUMENTS.join(', ')} are needed for create multisig transaction" unless MULTISIG_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
105
+ warn_legacy_mixin_api!('LegacyTransaction#create_multisig_transaction')
106
+ unless MULTISIG_TRANSACTION_ARGUMENTS.all? do |param|
107
+ options.keys.include? param
108
+ end
109
+ raise ArgumentError,
110
+ "#{MULTISIG_TRANSACTION_ARGUMENTS.join(', ')} are needed for create multisig transaction"
111
+ end
100
112
 
101
113
  asset_id = options[:asset_id]
102
114
  receivers = options[:receivers].sort
@@ -117,11 +129,9 @@ module MixinBot
117
129
  memo:
118
130
  }
119
131
 
120
- if pin.length > 6
121
- payload[:pin_base64] = encrypt_tip_pin(pin, 'TIP:TRANSACTION:CREATE:', asset_id, receivers.join, threshold, amount, trace_id, memo)
122
- else
123
- payload[:pin] = encrypt_pin(pin)
124
- end
132
+ payload.update(
133
+ tip_or_legacy_pin_payload(pin, 'TIP:TRANSACTION:CREATE:', asset_id, receivers.join, threshold, amount, trace_id, memo)
134
+ )
125
135
 
126
136
  client.post path, **payload
127
137
  end
@@ -129,7 +139,13 @@ module MixinBot
129
139
  # use safe transaction protocol instead
130
140
  MAINNET_TRANSACTION_ARGUMENTS = %i[asset_id opponent_id amount].freeze
131
141
  def create_mainnet_transaction(pin, **options)
132
- raise ArgumentError, "#{MAINNET_TRANSACTION_ARGUMENTS.join(', ')} are needed for create main net transactions" unless MAINNET_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
142
+ warn_legacy_mixin_api!('LegacyTransaction#create_mainnet_transaction')
143
+ unless MAINNET_TRANSACTION_ARGUMENTS.all? do |param|
144
+ options.keys.include? param
145
+ end
146
+ raise ArgumentError,
147
+ "#{MAINNET_TRANSACTION_ARGUMENTS.join(', ')} are needed for create main net transactions"
148
+ end
133
149
 
134
150
  asset_id = options[:asset_id]
135
151
  opponent_id = options[:opponent_id]
@@ -146,17 +162,16 @@ module MixinBot
146
162
  memo:
147
163
  }
148
164
 
149
- if pin.length > 6
150
- payload[:pin_base64] = encrypt_tip_pin(pin, 'TIP:TRANSACTION:CREATE:', asset_id, opponent_id, amount, trace_id, memo)
151
- else
152
- payload[:pin] = encrypt_pin(pin)
153
- end
165
+ payload.update(
166
+ tip_or_legacy_pin_payload(pin, 'TIP:TRANSACTION:CREATE:', asset_id, opponent_id, amount, trace_id, memo)
167
+ )
154
168
 
155
169
  client.post path, **payload
156
170
  end
157
171
 
158
172
  # use safe transaction protocol instead
159
173
  def transactions(**options)
174
+ warn_legacy_mixin_api!('LegacyTransaction#transactions')
160
175
  path = '/external/transactions'
161
176
  params = {
162
177
  limit: options[:limit],
@@ -5,7 +5,10 @@ module MixinBot
5
5
  module LegacyTransfer
6
6
  TRANSFER_ARGUMENTS = %i[asset_id opponent_id amount].freeze
7
7
  def create_legacy_transfer(pin, **kwargs)
8
- raise ArgumentError, "#{TRANSFER_ARGUMENTS.join(', ')} are needed for create transfer" unless TRANSFER_ARGUMENTS.all? { |param| kwargs.keys.include? param }
8
+ warn_legacy_mixin_api!('LegacyTransfer#create_legacy_transfer')
9
+ raise ArgumentError, "#{TRANSFER_ARGUMENTS.join(', ')} are needed for create transfer" unless TRANSFER_ARGUMENTS.all? do |param|
10
+ kwargs.keys.include? param
11
+ end
9
12
 
10
13
  asset_id = kwargs[:asset_id]
11
14
  opponent_id = kwargs[:opponent_id]
@@ -21,22 +24,22 @@ module MixinBot
21
24
  memo:
22
25
  }
23
26
 
24
- if pin.length > 6
25
- pin_base64 = encrypt_tip_pin pin, 'TIP:TRANSFER:CREATE:', asset_id, opponent_id, amount, trace_id, memo
26
- payload[:pin_base64] = pin_base64
27
- else
28
- encrypted_pin = encrypt_pin pin
29
- payload[:pin] = encrypted_pin
30
- end
27
+ payload.update(
28
+ tip_or_legacy_pin_payload(pin, 'TIP:TRANSFER:CREATE:', asset_id, opponent_id, amount, trace_id, memo)
29
+ )
31
30
 
32
31
  path = '/transfers'
33
32
  client.post path, **payload
34
33
  end
35
34
 
36
35
  def legacy_transfer(trace_id, access_token: nil)
36
+ warn_legacy_mixin_api!('LegacyTransfer#legacy_transfer')
37
37
  path = format('/transfers/trace/%<trace_id>s', trace_id:)
38
38
  client.get path, access_token:
39
39
  end
40
+
41
+ alias transfer legacy_transfer
42
+ alias create_transfer create_legacy_transfer
40
43
  end
41
44
  end
42
45
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyUser
6
+ # Upgrades a legacy RSA keystore user to Ed25519 session keys.
7
+ #
8
+ # @param keystore [Hash] :pin, :session_id, :pin_token (base64), :private_key (PEM)
9
+ def upgrade_legacy_user(keystore)
10
+ kl = keystore.with_indifferent_access
11
+ priv = OpenSSL::PKey::RSA.new(kl[:private_key])
12
+ token = Base64.decode64(kl[:pin_token])
13
+ key_bytes = priv.decrypt(
14
+ token,
15
+ rsa_padding_mode: 'oaep',
16
+ rsa_oaep_md: 'sha256',
17
+ rsa_mgf1_md: 'sha1',
18
+ oaep_label: kl[:session_id]
19
+ )
20
+
21
+ pin_byte = kl[:pin].to_s.b
22
+ pin_byte += [Time.now.to_i].pack('Q<')
23
+ pin_byte += [0].pack('Q<')
24
+ padding = 16 - (pin_byte.length % 16)
25
+ pin_byte += ([padding].pack('C') * padding)
26
+
27
+ cipher = OpenSSL::Cipher.new('AES-256-CBC')
28
+ cipher.encrypt
29
+ iv = cipher.random_iv
30
+ cipher.key = key_bytes
31
+ ciphertext = iv + cipher.update(pin_byte) + cipher.final
32
+
33
+ pub_bytes = priv.public_key.to_der
34
+ seed = Digest::SHA512.digest(priv.to_der)[0, 32]
35
+ pub_ed25519 = RbNaCl::Signatures::Ed25519::SigningKey.new(seed).verify_key.to_bytes
36
+
37
+ payload = {
38
+ session_secret_legacy: Base64.urlsafe_encode64(pub_bytes, padding: false),
39
+ session_secret: Base64.urlsafe_encode64(pub_ed25519, padding: false),
40
+ session_id: kl[:session_id],
41
+ pin: Base64.urlsafe_encode64(ciphertext, padding: false)
42
+ }
43
+
44
+ result = client.post '/legacy/users', **payload, access_token: ''
45
+ data = result['data'] || result.data
46
+ result['data'] = data.merge('session_private_key' => seed.unpack1('H*')) if data.is_a?(Hash)
47
+ result
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,14 +2,60 @@
2
2
 
3
3
  module MixinBot
4
4
  class API
5
+ ##
6
+ # API methods for bot profile management.
7
+ #
8
+ # Provides methods to:
9
+ # - Retrieve bot profile information
10
+ # - Update bot profile (name, avatar)
11
+ # - List friends
12
+ # - Access Safe API profile
13
+ #
5
14
  module Me
15
+ ##
16
+ # Retrieves the bot's profile information.
17
+ #
18
+ # Returns detailed information about the bot including:
19
+ # - user_id
20
+ # - full_name
21
+ # - avatar_url
22
+ # - identity_number
23
+ # - phone
24
+ # - biography
25
+ # - created_at
26
+ #
27
+ # @param access_token [String, nil] optional access token for authentication
28
+ # @return [Hash] the bot profile
29
+ #
30
+ # @example
31
+ # profile = api.me
32
+ # puts profile['full_name']
33
+ # puts profile['identity_number']
34
+ #
35
+ # @see https://developers.mixin.one/docs/api/users/me
36
+ #
6
37
  def me(access_token: nil)
7
38
  path = '/me'
8
39
  client.get path, access_token:
9
40
  end
10
41
 
11
- # avatar_base64:
12
- # String: Base64 of image, supports format png, jpeg and gif, base64 image size > 1024.
42
+ ##
43
+ # Updates the bot's profile.
44
+ #
45
+ # Allows updating:
46
+ # - full_name: the bot's display name
47
+ # - avatar_base64: Base64-encoded avatar image (PNG, JPEG, or GIF, size > 1024 bytes)
48
+ #
49
+ # @param kwargs [Hash] update options
50
+ # @option kwargs [String] :full_name the new name for the bot
51
+ # @option kwargs [String] :avatar_base64 Base64-encoded image data
52
+ # @option kwargs [String] :access_token optional access token
53
+ # @return [Hash] the updated profile
54
+ #
55
+ # @example
56
+ # api.update_me(full_name: 'My Awesome Bot')
57
+ # api.update_me(avatar_base64: Base64.strict_encode64(File.read('avatar.png')))
58
+ #
13
59
  def update_me(**kwargs)
14
60
  path = '/me'
15
61
  payload = {
@@ -20,16 +66,66 @@ module MixinBot
20
66
  client.post path, **payload
21
67
  end
22
68
 
23
- # https://developers.mixin.one/api/beta-mixin-message/friends/
69
+ ##
70
+ # Retrieves the bot's friend list.
71
+ #
72
+ # Returns an array of users who have interacted with the bot
73
+ # and are in the bot's contact list.
74
+ #
75
+ # @param access_token [String, nil] optional access token
76
+ # @return [Array<Hash>] array of user profiles
77
+ #
78
+ # @example
79
+ # friends = api.friends
80
+ # friends.each do |friend|
81
+ # puts "#{friend['full_name']} (#{friend['user_id']})"
82
+ # end
83
+ #
84
+ # @see https://developers.mixin.one/docs/api/users/friends
85
+ #
24
86
  def friends(access_token: nil)
25
87
  path = '/friends'
26
88
  client.get path, access_token:
27
89
  end
28
90
 
91
+ ##
92
+ # Retrieves the bot's Safe API profile.
93
+ #
94
+ # Returns Safe API-specific profile information including:
95
+ # - Safe wallet address
96
+ # - TIP signing key
97
+ # - Safe network status
98
+ #
99
+ # @param access_token [String, nil] optional access token
100
+ # @return [Hash] the Safe API profile
101
+ #
102
+ # @example
103
+ # safe_profile = api.safe_me
104
+ # puts safe_profile['user_id']
105
+ #
29
106
  def safe_me(access_token: nil)
30
107
  path = '/safe/me'
31
108
  client.get path, access_token:
32
109
  end
110
+ alias request_user_me safe_me
111
+
112
+ def update_preferences(message_source: nil, conversation_source: nil, currency: nil, threshold: nil,
113
+ access_token: nil)
114
+ path = '/me/preferences'
115
+ payload = {
116
+ receive_message_source: message_source,
117
+ accept_conversation_source: conversation_source,
118
+ fiat_currency: currency,
119
+ transfer_notification_threshold: threshold
120
+ }.compact
121
+ client.post path, **payload, access_token:
122
+ end
123
+ alias update_preference update_preferences
124
+
125
+ def relationship(user_id:, action:, access_token: nil)
126
+ path = '/relationships'
127
+ client.post path, user_id:, action:, access_token:
128
+ end
33
129
  end
34
130
  end
35
131
  end