mixin_bot 0.12.1 → 1.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 (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 +29 -51
  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 +149 -5
  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,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
@@ -3,75 +3,74 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Withdraw
6
- # https://developers.mixin.one/api/alpha-mixin-network/create-address/
7
- def create_withdraw_address(options, access_token: nil)
6
+ def create_withdraw_address(**kwargs)
8
7
  path = '/addresses'
9
- encrypted_pin = encrypt_pin(options[:pin])
8
+ pin = kwargs[:pin]
10
9
  payload =
11
- # for EOS withdraw, account_name & account_tag must be valid
12
- if options[:public_key].nil?
13
- {
14
- asset_id: options[:asset_id],
15
- account_name: options[:account_name],
16
- account_tag: options[:account_tag],
17
- label: options[:label],
18
- pin: encrypted_pin
19
- }
20
- # for other withdraw
21
- else
22
- {
23
- asset_id: options[:asset_id],
24
- public_key: options[:public_key],
25
- label: options[:label],
26
- pin: encrypted_pin
27
- }
28
- end
10
+ {
11
+ asset_id: kwargs[:asset_id],
12
+ destination: kwargs[:destination],
13
+ tag: kwargs[:tag],
14
+ label: kwargs[:label]
15
+ }
16
+
17
+ if pin.length > 6
18
+ payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:ADDRESS:ADD:', payload[:asset_id], payload[:destination], payload[:tag], payload[:label]
19
+ else
20
+ payload[:pin] = encrypt_pin pin
21
+ end
29
22
 
30
- access_token ||= access_token('POST', path, payload.to_json)
31
- authorization = format('Bearer %<access_token>s', access_token: access_token)
32
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
23
+ client.post path, **payload
33
24
  end
34
25
 
35
- # https://developers.mixin.one/api/alpha-mixin-network/read-address/
36
26
  def get_withdraw_address(address, access_token: nil)
37
- path = format('/addresses/%<address>s', address: address)
38
- access_token ||= access_token('GET', path, '')
39
- authorization = format('Bearer %<access_token>s', access_token: access_token)
40
- client.get(path, headers: { 'Authorization': authorization })
27
+ path = format('/addresses/%<address>s', address:)
28
+
29
+ client.get path, access_token:
41
30
  end
42
31
 
43
- # https://developers.mixin.one/api/alpha-mixin-network/delete-address/
44
- def delete_withdraw_address(address, pin, access_token: nil)
45
- path = format('/addresses/%<address>s/delete', address: address)
46
- payload = {
47
- pin: encrypt_pin(pin)
48
- }
32
+ def delete_withdraw_address(address, **kwargs)
33
+ pin = kwargs[:pin]
49
34
 
50
- access_token ||= access_token('POST', path, payload.to_json)
51
- authorization = format('Bearer %<access_token>s', access_token: access_token)
52
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
35
+ path = format('/addresses/%<address>s/delete', address:)
36
+ payload =
37
+ if pin.length > 6
38
+ {
39
+ pin_base64: encrypt_tip_pin(pin, 'TIP:ADDRESS:REMOVE:', address)
40
+ }
41
+ else
42
+ {
43
+ pin: encrypt_pin(pin)
44
+ }
45
+ end
46
+
47
+ client.post path, **payload
53
48
  end
54
49
 
55
- # https://developers.mixin.one/api/alpha-mixin-network/withdrawal-addresses/
56
- def withdrawals(options, access_token: nil)
57
- address_id = options[:address_id]
58
- pin = options[:pin]
59
- amount = options[:amount]
60
- trace_id = options[:trace_id]
61
- memo = options[:memo]
50
+ def withdrawals(**kwargs)
51
+ address_id = kwargs[:address_id]
52
+ pin = kwargs[:pin]
53
+ amount = format('%.8f', kwargs[:amount].to_d.to_r)
54
+ trace_id = kwargs[:trace_id]
55
+ memo = kwargs[:memo]
56
+ kwargs[:access_token]
62
57
 
63
58
  path = '/withdrawals'
64
59
  payload = {
65
- address_id: address_id,
66
- amount: amount,
67
- trace_id: trace_id,
68
- memo: memo,
69
- pin: encrypt_pin(pin)
60
+ address_id:,
61
+ amount:,
62
+ trace_id:,
63
+ memo:
70
64
  }
71
65
 
72
- access_token ||= access_token('POST', path, payload.to_json)
73
- authorization = format('Bearer %<access_token>s', access_token: access_token)
74
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
66
+ if pin.length > 6
67
+ fee = '0'
68
+ payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:WITHDRAW:', address_id, amount, fee, trace_id, memo
69
+ else
70
+ payload[:pin] = encrypt_pin pin
71
+ end
72
+
73
+ client.post path, **payload
75
74
  end
76
75
  end
77
76
  end