mixin_bot 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mixin_bot/api/address.rb +21 -0
  3. data/lib/mixin_bot/api/app.rb +5 -11
  4. data/lib/mixin_bot/api/asset.rb +9 -16
  5. data/lib/mixin_bot/api/attachment.rb +27 -22
  6. data/lib/mixin_bot/api/auth.rb +31 -53
  7. data/lib/mixin_bot/api/blaze.rb +4 -3
  8. data/lib/mixin_bot/api/collectible.rb +60 -58
  9. data/lib/mixin_bot/api/conversation.rb +29 -49
  10. data/lib/mixin_bot/api/encrypted_message.rb +17 -17
  11. data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
  12. data/lib/mixin_bot/api/legacy_output.rb +50 -0
  13. data/lib/mixin_bot/api/legacy_payment.rb +31 -0
  14. data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
  15. data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
  16. data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
  17. data/lib/mixin_bot/api/me.rb +13 -17
  18. data/lib/mixin_bot/api/message.rb +13 -10
  19. data/lib/mixin_bot/api/multisig.rb +16 -221
  20. data/lib/mixin_bot/api/output.rb +46 -0
  21. data/lib/mixin_bot/api/payment.rb +9 -20
  22. data/lib/mixin_bot/api/pin.rb +57 -65
  23. data/lib/mixin_bot/api/rpc.rb +9 -11
  24. data/lib/mixin_bot/api/snapshot.rb +15 -29
  25. data/lib/mixin_bot/api/tip.rb +43 -0
  26. data/lib/mixin_bot/api/transaction.rb +184 -60
  27. data/lib/mixin_bot/api/transfer.rb +64 -32
  28. data/lib/mixin_bot/api/user.rb +83 -53
  29. data/lib/mixin_bot/api/withdraw.rb +52 -53
  30. data/lib/mixin_bot/api.rb +78 -45
  31. data/lib/mixin_bot/cli/api.rb +151 -8
  32. data/lib/mixin_bot/cli/utils.rb +14 -4
  33. data/lib/mixin_bot/cli.rb +13 -10
  34. data/lib/mixin_bot/client.rb +76 -127
  35. data/lib/mixin_bot/configuration.rb +98 -0
  36. data/lib/mixin_bot/nfo.rb +174 -0
  37. data/lib/mixin_bot/transaction.rb +505 -0
  38. data/lib/mixin_bot/utils/address.rb +108 -0
  39. data/lib/mixin_bot/utils/crypto.rb +182 -0
  40. data/lib/mixin_bot/utils/decoder.rb +58 -0
  41. data/lib/mixin_bot/utils/encoder.rb +63 -0
  42. data/lib/mixin_bot/utils.rb +8 -109
  43. data/lib/mixin_bot/uuid.rb +41 -0
  44. data/lib/mixin_bot/version.rb +1 -1
  45. data/lib/mixin_bot.rb +39 -14
  46. data/lib/mvm/bridge.rb +2 -19
  47. data/lib/mvm/client.rb +11 -33
  48. data/lib/mvm/nft.rb +4 -4
  49. data/lib/mvm/registry.rb +9 -9
  50. data/lib/mvm/scan.rb +3 -5
  51. data/lib/mvm.rb +5 -6
  52. metadata +101 -44
  53. data/lib/mixin_bot/utils/nfo.rb +0 -176
  54. data/lib/mixin_bot/utils/transaction.rb +0 -478
  55. data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -3,45 +3,31 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Snapshot
6
- def network_snapshots(**options)
7
- path = format(
8
- '/network/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&order=%<order>s',
9
- limit: options[:limit],
10
- offset: options[:offset],
11
- asset: options[:asset],
12
- order: options[:order]
13
- )
14
-
15
- access_token = options[:access_token] || access_token('GET', path)
16
- authorization = format('Bearer %<access_token>s', access_token: access_token)
17
- client.get(path, headers: { 'Authorization': authorization })
18
- end
19
- alias read_network_snapshots network_snapshots
20
-
21
- def snapshots(**options)
22
- path = format(
23
- '/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&opponent=%<opponent>s&order=%<order>s',
6
+ def safe_snapshots(**options)
7
+ path = '/safe/snapshots'
8
+ params = {
24
9
  limit: options[:limit],
25
10
  offset: options[:offset],
26
11
  asset: options[:asset],
27
12
  opponent: options[:opponent],
13
+ app: options[:app_id],
28
14
  order: options[:order]
29
- )
15
+ }
30
16
 
31
- access_token = options[:access_token] || access_token('GET', path)
32
- authorization = format('Bearer %<access_token>s', access_token: access_token)
33
- client.get(path, headers: { 'Authorization': authorization })
17
+ client.get path, **params
34
18
  end
35
- alias read_snapshots snapshots
36
19
 
37
- def network_snapshot(snapshot_id, **options)
38
- path = format('/network/snapshots/%<snapshot_id>s', snapshot_id: snapshot_id)
20
+ def create_safe_snapshot_notification(**kwargs)
21
+ path = '/safe/snapshots/notifications'
22
+
23
+ payload = {
24
+ transaction_hash: kwargs[:transaction_hash],
25
+ output_index: kwargs[:output_index],
26
+ receiver_id: kwargs[:receiver_id]
27
+ }
39
28
 
40
- access_token = options[:access_token] || access_token('GET', path)
41
- authorization = format('Bearer %<access_token>s', access_token: access_token)
42
- client.get(path, headers: { 'Authorization': authorization })
29
+ client.post path, **payload, access_token: kwargs[:access_token]
43
30
  end
44
- alias read_network_snapshot network_snapshot
45
31
  end
46
32
  end
47
33
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Tip
6
+ TIP_ACTIONS = %w[
7
+ TIP:VERIFY:
8
+ TIP:ADDRESS:ADD:
9
+ TIP:ADDRESS:REMOVE:
10
+ TIP:USER:DEACTIVATE:
11
+ TIP:EMERGENCY:CONTACT:CREATE:
12
+ TIP:EMERGENCY:CONTACT:READ:
13
+ TIP:EMERGENCY:CONTACT:REMOVE:
14
+ TIP:PHONE:NUMBER:UPDATE:
15
+ TIP:MULTISIG:REQUEST:SIGN:
16
+ TIP:MULTISIG:REQUEST:UNLOCK:
17
+ TIP:COLLECTIBLE:REQUEST:SIGN:
18
+ TIP:COLLECTIBLE:REQUEST:UNLOCK:
19
+ TIP:TRANSFER:CREATE:
20
+ TIP:WITHDRAWAL:CREATE:
21
+ TIP:TRANSACTION:CREATE:
22
+ TIP:OAUTH:APPROVE:
23
+ TIP:PROVISIONING:UPDATE:
24
+ TIP:APP:OWNERSHIP:TRANSFER:
25
+ SEQUENCER:REGISTER:
26
+ ].freeze
27
+
28
+ def encrypt_tip_pin(pin, action, *params)
29
+ raise ArgumentError, 'invalid action' unless TIP_ACTIONS.include? action
30
+
31
+ pin_key = MixinBot.utils.decode_key pin
32
+
33
+ msg = action + params.flatten.map(&:to_s).join
34
+
35
+ msg = Digest::SHA256.digest(msg) unless action == 'TIP:VERIFY:'
36
+
37
+ signature = JOSE::JWA::Ed25519.sign msg, pin_key
38
+
39
+ encrypt_pin signature
40
+ end
41
+ end
42
+ end
43
+ end
@@ -3,75 +3,199 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Transaction
6
- MULTISIG_TRANSACTION_ARGUMENTS = %i[asset_id receivers threshold amount].freeze
7
- def create_multisig_transaction(pin, options = {})
8
- raise ArgumentError, "#{MULTISIG_TRANSACTION_ARGUMENTS.join(', ')} are needed for create multisig transaction" unless MULTISIG_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
9
-
10
- asset_id = options[:asset_id]
11
- receivers = options[:receivers]
12
- threshold = options[:threshold]
13
- amount = options[:amount].to_d
14
- memo = options[:memo]
15
- trace_id = options[:trace_id] || SecureRandom.uuid
16
- encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
17
-
18
- path = '/transactions'
19
- payload = {
20
- asset_id: asset_id,
21
- opponent_multisig: {
22
- receivers: receivers,
23
- threshold: threshold
24
- },
25
- pin: encrypted_pin,
26
- amount: format('%.8f', amount.to_r),
27
- trace_id: trace_id,
28
- memo: memo
29
- }
6
+ SAFE_TX_VERSION = 0x05
7
+ OUTPUT_TYPE_SCRIPT = 0x00
8
+ OUTPUT_TYPE_WITHDRAW_SUBMIT = 0xa1
9
+
10
+ # ghost keys
11
+ def create_safe_keys(*payload, access_token: nil)
12
+ raise ArgumentError, 'payload should be an array' unless payload.is_a? Array
13
+ raise ArgumentError, 'payload should not be empty' unless payload.size.positive?
14
+ raise ArgumentError, 'invalid payload' unless payload.all?(&lambda { |param|
15
+ param.key?(:receivers) && param.key?(:index)
16
+ })
17
+
18
+ payload.each do |param|
19
+ param[:hint] ||= SecureRandom.uuid
20
+ end
21
+
22
+ path = '/safe/keys'
30
23
 
31
- access_token = options[:access_token]
32
- access_token ||= access_token('POST', path, payload.to_json)
33
- authorization = format('Bearer %<access_token>s', access_token: access_token)
34
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
24
+ client.post path, *payload
35
25
  end
26
+ alias create_ghost_keys create_safe_keys
27
+
28
+ # kwargs:
29
+ # {
30
+ # utxos: [ utxo ],
31
+ # receivers: [ {
32
+ # members: [ uuid ],
33
+ # threshold: integer,
34
+ # amount: string,
35
+ # } ],
36
+ # ghosts: [ ghost ],
37
+ # extra: string,
38
+ # }
39
+ SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
40
+ def build_safe_transaction(**kwargs)
41
+ raise ArgumentError, "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction" unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
42
+
43
+ utxos = kwargs[:utxos].map(&:with_indifferent_access)
44
+ receivers = kwargs[:receivers].map(&:with_indifferent_access)
45
+
46
+ senders = utxos.map { |utxo| utxo['receivers'] }.uniq
47
+ raise ArgumentError, 'utxos should have same senders' if senders.size > 1
48
+
49
+ senders_threshold = utxos.map { |utxo| utxo['receivers_threshold'] }.uniq
50
+ raise ArgumentError, 'utxos should have same senders_threshold' if senders_threshold.size > 1
51
+
52
+ raise ArgumentError, 'utxos should not be empty' if utxos.empty?
53
+ raise ArgumentError, 'utxos too many' if utxos.size > 256
54
+
55
+ recipients = receivers.map do |receiver|
56
+ MixinBot.utils.build_safe_recipient(
57
+ members: receiver[:members],
58
+ threshold: receiver[:threshold],
59
+ amount: receiver[:amount]
60
+ ).with_indifferent_access
61
+ end
62
+
63
+ inputs_sum = utxos.sum(&->(utxo) { utxo['amount'].to_d })
64
+ outputs_sum = recipients.sum(&->(recipient) { recipient['amount'].to_d })
65
+ change = inputs_sum - outputs_sum
66
+ raise InsufficientBalanceError, "inputs sum: #{inputs_sum}" if change.negative?
67
+
68
+ if change.positive?
69
+ recipients << MixinBot.utils.build_safe_recipient(
70
+ members: utxos[0]['receivers'],
71
+ threshold: utxos[0]['receivers_threshold'],
72
+ amount: change
73
+ ).with_indifferent_access
74
+ end
75
+ raise ArgumentError, 'recipients too many' if recipients.size > 256
76
+
77
+ asset = utxos[0]['asset']
78
+ inputs = []
79
+ utxos.each do |utxo|
80
+ raise ArgumentError, 'utxo asset not match' unless utxo['asset'] == asset
81
+
82
+ inputs << {
83
+ hash: utxo['transaction_hash'],
84
+ index: utxo['output_index']
85
+ }
86
+ end
87
+
88
+ ghost_payload = recipients.map.with_index do |r, index|
89
+ {
90
+ receivers: r[:members],
91
+ index:,
92
+ hint: SecureRandom.uuid
93
+ }
94
+ end
95
+ ghosts = create_safe_keys(*ghost_payload)['data']
96
+
97
+ outputs = []
98
+ recipients.each_with_index do |recipient, index|
99
+ outputs << if recipient['destination']
100
+ {
101
+ type: OUTPUT_TYPE_WITHDRAW_SUBMIT,
102
+ amount: recipient['amount'],
103
+ withdrawal: {
104
+ address: recipient['destination'],
105
+ tag: recipient['tag'] || ''
106
+ }
107
+ }
108
+ else
109
+ {
110
+ type: OUTPUT_TYPE_SCRIPT,
111
+ amount: recipient['amount'],
112
+ keys: ghosts[index]['keys'],
113
+ mask: ghosts[index]['mask'],
114
+ script: build_threshold_script(recipient['threshold'])
115
+ }
116
+ end
117
+ end
36
118
 
37
- MAINNET_TRANSACTION_ARGUMENTS = %i[asset_id opponent_key amount].freeze
38
- def create_mainnet_transaction(pin, options = {})
39
- raise ArgumentError, "#{MAINNET_TRANSACTION_ARGUMENTS.join(', ')} are needed for create main net transactions" unless MAINNET_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
40
-
41
- asset_id = options[:asset_id]
42
- opponent_key = options[:opponent_key]
43
- amount = options[:amount].to_d
44
- memo = options[:memo]
45
- trace_id = options[:trace_id] || SecureRandom.uuid
46
- encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
47
-
48
- path = '/transactions'
49
- payload = {
50
- asset_id: asset_id,
51
- opponent_key: opponent_key,
52
- pin: encrypted_pin,
53
- amount: format('%.8f', amount.to_r),
54
- trace_id: trace_id,
55
- memo: memo
119
+ {
120
+ version: SAFE_TX_VERSION,
121
+ asset:,
122
+ inputs:,
123
+ outputs:,
124
+ extra: kwargs[:extra] || ''
56
125
  }
126
+ end
127
+
128
+ def create_safe_transaction_request(request_id, raw)
129
+ path = '/safe/transaction/requests'
130
+ payload = [{
131
+ request_id:,
132
+ raw:
133
+ }]
134
+
135
+ client.post path, *payload
136
+ end
137
+
138
+ def send_safe_transaction(request_id, raw)
139
+ path = '/safe/transactions'
140
+ payload = [{
141
+ request_id:,
142
+ raw:
143
+ }]
144
+
145
+ client.post path, *payload
146
+ end
147
+
148
+ def safe_transaction(request_id, access_token: nil)
149
+ path = format('/safe/transactions/%<request_id>s', request_id:)
57
150
 
58
- access_token = options[:access_token]
59
- access_token ||= access_token('POST', path, payload.to_json)
60
- authorization = format('Bearer %<access_token>s', access_token: access_token)
61
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
151
+ client.get path, access_token:
62
152
  end
63
153
 
64
- def transactions(**options)
65
- path = format(
66
- '/external/transactions?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&destination=%<destination>s&tag=%<tag>s',
67
- limit: options[:limit],
68
- offset: options[:offset],
69
- asset: options[:asset],
70
- destination: options[:destination],
71
- tag: options[:tag]
154
+ SIGN_SAFE_TRANSACTION_ARGUMENTS = %i[raw utxos request spend_key].freeze
155
+ def sign_safe_transaction(**kwargs)
156
+ raise ArgumentError, "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction" unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
157
+
158
+ raw = kwargs[:raw]
159
+ tx = MixinBot.utils.decode_raw_transaction raw
160
+ utxos = kwargs[:utxos]
161
+ request = kwargs[:request]
162
+ spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
163
+ spend_key = Digest::SHA512.digest spend_key[...32]
164
+
165
+ msg = [raw].pack('H*')
166
+
167
+ y_point = JOSE::JWA::FieldElement.new(
168
+ JOSE::JWA::X25519.clamp_scalar(spend_key[...32]).x,
169
+ JOSE::JWA::Edwards25519Point::L
72
170
  )
73
171
 
74
- client.get path
172
+ tx[:signatures] = []
173
+ tx[:inputs].each_with_index do |input, index|
174
+ utxo = utxos[index]
175
+ raise ArgumentError, 'utxo not match' unless input['hash'] == utxo['transaction_hash'] && input['index'] == utxo['output_index']
176
+
177
+ view = [request['views'][index]].pack('H*')
178
+ x_point = JOSE::JWA::FieldElement.new(
179
+ # https://github.com/potatosalad/ruby-jose/blob/e1be589b889f1e59ac233a5d19a3fa13f1e4b8a0/lib/jose/jwa/x25519.rb#L122C14-L122C48
180
+ OpenSSL::BN.new(view.reverse, 2),
181
+ JOSE::JWA::Edwards25519Point::L
182
+ )
183
+
184
+ t_point = x_point + y_point
185
+ key = t_point.to_bytes(JOSE::JWA::Edwards25519Point::B)
186
+
187
+ pub = MixinBot.utils.generate_public_key key
188
+ key_index = utxo['keys'].index pub.unpack1('H*')
189
+ raise ArgumentError, 'cannot find valid key' unless key_index.is_a? Integer
190
+
191
+ signature = MixinBot.utils.sign(msg, key:)
192
+ signature = signature.unpack1('H*')
193
+ sig = {}
194
+ sig[key_index] = signature
195
+ tx[:signatures] << sig
196
+ end
197
+
198
+ MixinBot.utils.encode_raw_transaction tx
75
199
  end
76
200
  end
77
201
  end
@@ -3,40 +3,72 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Transfer
6
- TRANSFER_ARGUMENTS = %i[asset_id opponent_id amount].freeze
7
- def create_transfer(pin, options = {})
8
- raise ArgumentError, "#{TRANSFER_ARGUMENTS.join(', ')} are needed for create transfer" unless TRANSFER_ARGUMENTS.all? { |param| options.keys.include? param }
9
-
10
- asset_id = options[:asset_id]
11
- opponent_id = options[:opponent_id]
12
- amount = options[:amount].to_d
13
- memo = options[:memo]
14
- trace_id = options[:trace_id] || SecureRandom.uuid
15
- encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
16
-
17
- path = '/transfers'
18
- payload = {
19
- asset_id: asset_id,
20
- opponent_id: opponent_id,
21
- pin: encrypted_pin,
22
- amount: format('%.8f', amount.to_r),
23
- trace_id: trace_id,
24
- memo: memo
25
- }
26
-
27
- access_token = options[:access_token]
28
- access_token ||= access_token('POST', path, payload.to_json)
29
- authorization = format('Bearer %<access_token>s', access_token: access_token)
30
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
31
- end
6
+ # kwargs:
7
+ # {
8
+ # members: uuid | [ uuid ],
9
+ # threshold: integer / nil,
10
+ # asset_id: uuid,
11
+ # amount: string / float,
12
+ # trace_id: uuid / nil,
13
+ # request_id: uuid / nil,
14
+ # memo: string,
15
+ # spend_key: string / nil,
16
+ # }
17
+ def create_safe_transfer(**kwargs)
18
+ asset_id = kwargs[:asset_id]
19
+ raise ArgumentError, 'asset_id required' if asset_id.blank?
20
+
21
+ amount = kwargs[:amount]&.to_d
22
+ raise ArgumentError, 'amount required' if amount.blank?
23
+
24
+ members = [kwargs[:members]].flatten.compact
25
+ raise ArgumentError, 'members required' if members.blank?
26
+
27
+ threshold = kwargs[:threshold] || members.length
28
+ request_id = kwargs[:request_id] || kwargs[:trace_id] || SecureRandom.uuid
29
+ memo = kwargs[:memo] || ''
30
+
31
+ # step 1: select inputs
32
+ outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
33
+
34
+ utxos = []
35
+ outputs.each do |output|
36
+ break if utxos.sum { |o| o['amount'].to_d } >= amount
37
+
38
+ utxos.shift if utxos.size >= 256
39
+ utxos << output
40
+ end
41
+
42
+ # step 2: build transaction
43
+ tx = build_safe_transaction(
44
+ utxos:,
45
+ receivers: [{
46
+ members:,
47
+ threshold:,
48
+ amount:
49
+ }],
50
+ extra: memo
51
+ )
52
+ raw = MixinBot.utils.encode_raw_transaction tx
53
+
54
+ # step 3: verify transaction
55
+ request = create_safe_transaction_request(request_id, raw)['data']
56
+
57
+ # step 4: sign transaction
58
+ spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
59
+ signed_raw = sign_safe_transaction(
60
+ raw:,
61
+ utxos:,
62
+ request: request[0],
63
+ spend_key:
64
+ )
32
65
 
33
- def transfer(trace_id, access_token: nil)
34
- path = format('/transfers/trace/%<trace_id>s', trace_id: trace_id)
35
- access_token ||= access_token('GET', path, '')
36
- authorization = format('Bearer %<access_token>s', access_token: access_token)
37
- client.get(path, headers: { 'Authorization': authorization })
66
+ # step 5: submit transaction
67
+ send_safe_transaction(
68
+ request_id,
69
+ signed_raw
70
+ )
38
71
  end
39
- alias read_transfer transfer
40
72
  end
41
73
  end
42
74
  end
@@ -3,76 +3,106 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module User
6
- # https://developers.mixin.one/api/beta-mixin-message/read-user/
7
- def read_user(user_id)
8
- # user_id: Mixin User UUID
9
- path = format('/users/%<user_id>s', user_id: user_id)
10
- access_token = access_token('GET', path, '')
11
- authorization = format('Bearer %<access_token>s', access_token: access_token)
12
- client.get(path, headers: { 'Authorization': authorization })
6
+ def user(user_id, access_token: nil)
7
+ path = format('/users/%<user_id>s', user_id:)
8
+ client.get path, access_token:
13
9
  end
14
10
 
15
- # https://developers.mixin.one/api/alpha-mixin-network/app-user/
16
- # Create a new Mixin Network user (like a normal Mixin Messenger user). You should keep PrivateKey which is used to sign an AuthenticationToken and encrypted PIN for the user.
17
- def create_user(full_name, key_type: 'RSA', rsa_key: nil, ed25519_key: nil)
18
- case key_type
19
- when 'RSA'
20
- rsa_key ||= generate_rsa_key
21
- session_secret = rsa_key[:public_key].gsub(/^-----.*PUBLIC KEY-----$/, '').strip
22
- when 'Ed25519'
23
- ed25519_key ||= generate_ed25519_key
24
- session_secret = ed25519_key[:public_key]
25
- else
26
- raise 'Only RSA and Ed25519 are supported'
27
- end
11
+ def create_user(full_name, key: nil)
12
+ keypair = JOSE::JWA::Ed25519.keypair key
13
+ session_secret = Base64.urlsafe_encode64 keypair[0], padding: false
14
+ private_key = keypair[1].unpack1('H*')
28
15
 
16
+ path = '/users'
29
17
  payload = {
30
- full_name: full_name,
31
- session_secret: session_secret
18
+ full_name:,
19
+ session_secret:
32
20
  }
33
- access_token = access_token('POST', '/users', payload.to_json)
34
- authorization = format('Bearer %<access_token>s', access_token: access_token)
35
- res = client.post('/users', headers: { 'Authorization': authorization }, json: payload)
36
21
 
37
- res.merge(rsa_key: rsa_key, ed25519_key: ed25519_key)
22
+ res = client.post path, **payload
23
+ res.merge(private_key:).with_indifferent_access
38
24
  end
39
25
 
40
- def generate_rsa_key
41
- rsa_key = OpenSSL::PKey::RSA.new 1024
42
- {
43
- private_key: rsa_key.to_pem,
44
- public_key: rsa_key.public_key.to_pem
45
- }
26
+ def search_user(query, access_token: nil)
27
+ path = format('/search/%<query>s', query:)
28
+
29
+ client.get path, access_token:
46
30
  end
47
31
 
48
- def generate_ed25519_key
49
- ed25519_key = JOSE::JWA::Ed25519.keypair
50
- {
51
- private_key: Base64.strict_encode64(ed25519_key[1]),
52
- public_key: Base64.strict_encode64(ed25519_key[0])
32
+ def fetch_users(user_ids)
33
+ path = '/users/fetch'
34
+ user_ids = [user_ids] if user_ids.is_a? String
35
+ payload = user_ids
36
+
37
+ client.post path, *payload
38
+ end
39
+
40
+ def create_safe_user(name, private_key: nil, spend_key: nil)
41
+ private_keypair = JOSE::JWA::Ed25519.keypair private_key
42
+ private_key = private_keypair[1].unpack1('H*')
43
+
44
+ spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
45
+ spend_key = spend_keypair[1].unpack1('H*')
46
+
47
+ user = create_user name, key: private_keypair[1][...32]
48
+
49
+ keystore = {
50
+ app_id: user['data']['user_id'],
51
+ session_id: user['data']['session_id'],
52
+ private_key:,
53
+ pin_token: user['data']['pin_token_base64'],
54
+ spend_key: spend_keypair[1].unpack1('H*')
53
55
  }
56
+ user_api = MixinBot::API.new(**keystore)
57
+
58
+ user_api.update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: user['data']['tip_counter'])
59
+
60
+ # wait for tip pin update in server
61
+ sleep 1
62
+
63
+ user_api.safe_register spend_key
64
+
65
+ keystore
54
66
  end
55
67
 
56
- # https://developers.mixin.one/api/beta-mixin-message/search-user/
57
- # search by Mixin Id or Phone Number
58
- def search_user(query)
59
- path = format('/search/%<query>s', query: query)
68
+ def safe_register(pin, spend_key: nil)
69
+ path = '/safe/users'
70
+
71
+ spend_key ||= MixinBot.utils.decode_key pin
72
+ key = JOSE::JWA::Ed25519.keypair spend_key[...32]
73
+ public_key = key[0].unpack1('H*')
74
+
75
+ hex = SHA3::Digest::SHA256.hexdigest config.app_id
76
+ signature = Base64.urlsafe_encode64 JOSE::JWA::Ed25519.sign([hex].pack('H*'), key[1]), padding: false
77
+
78
+ pin_base64 = encrypt_tip_pin pin, 'SEQUENCER:REGISTER:', config.app_id, public_key
60
79
 
61
- access_token = access_token('GET', path, '')
62
- authorization = format('Bearer %<access_token>s', access_token: access_token)
63
- client.get(path, headers: { 'Authorization': authorization })
80
+ payload = {
81
+ public_key:,
82
+ signature:,
83
+ pin_base64:
84
+ }
85
+
86
+ client.post path, **payload
64
87
  end
65
88
 
66
- # https://developers.mixin.one/api/beta-mixin-message/read-users/
67
- def fetch_users(user_ids)
68
- # user_ids: a array of user_ids
69
- path = '/users/fetch'
70
- user_ids = [user_ids] if user_ids.is_a? String
71
- payload = user_ids
89
+ def migrate_to_safe(spend_key:, old_pin: nil)
90
+ profile = me['data']
91
+ return true if profile['has_safe']
92
+
93
+ spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
94
+ spend_key = spend_keypair[1].unpack1('H*')
72
95
 
73
- access_token = access_token('POST', path, payload.to_json)
74
- authorization = format('Bearer %<access_token>s', access_token: access_token)
75
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
96
+ update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: profile['tip_counter']) if profile['tip_key_base64'].blank?
97
+
98
+ # wait for tip pin update in server
99
+ sleep 1
100
+
101
+ safe_register spend_key
102
+
103
+ {
104
+ spend_key:
105
+ }.with_indifferent_access
76
106
  end
77
107
  end
78
108
  end