mixin_bot 0.12.1 → 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 +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