mixin_bot 0.12.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 +34 -56
  7. data/lib/mixin_bot/api/blaze.rb +4 -3
  8. data/lib/mixin_bot/api/conversation.rb +29 -49
  9. data/lib/mixin_bot/api/encrypted_message.rb +19 -19
  10. data/lib/mixin_bot/api/inscription.rb +71 -0
  11. data/lib/mixin_bot/api/legacy_collectible.rb +140 -0
  12. data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
  13. data/lib/mixin_bot/api/legacy_output.rb +50 -0
  14. data/lib/mixin_bot/api/legacy_payment.rb +31 -0
  15. data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
  16. data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
  17. data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
  18. data/lib/mixin_bot/api/me.rb +13 -17
  19. data/lib/mixin_bot/api/message.rb +13 -10
  20. data/lib/mixin_bot/api/multisig.rb +17 -222
  21. data/lib/mixin_bot/api/output.rb +48 -0
  22. data/lib/mixin_bot/api/payment.rb +9 -20
  23. data/lib/mixin_bot/api/pin.rb +57 -65
  24. data/lib/mixin_bot/api/rpc.rb +12 -14
  25. data/lib/mixin_bot/api/snapshot.rb +15 -29
  26. data/lib/mixin_bot/api/tip.rb +43 -0
  27. data/lib/mixin_bot/api/transaction.rb +295 -60
  28. data/lib/mixin_bot/api/transfer.rb +69 -31
  29. data/lib/mixin_bot/api/user.rb +88 -53
  30. data/lib/mixin_bot/api/withdraw.rb +52 -53
  31. data/lib/mixin_bot/api.rb +81 -46
  32. data/lib/mixin_bot/cli/api.rb +149 -5
  33. data/lib/mixin_bot/cli/utils.rb +14 -4
  34. data/lib/mixin_bot/cli.rb +13 -10
  35. data/lib/mixin_bot/client.rb +74 -127
  36. data/lib/mixin_bot/configuration.rb +98 -0
  37. data/lib/mixin_bot/nfo.rb +174 -0
  38. data/lib/mixin_bot/transaction.rb +524 -0
  39. data/lib/mixin_bot/utils/address.rb +121 -0
  40. data/lib/mixin_bot/utils/crypto.rb +218 -0
  41. data/lib/mixin_bot/utils/decoder.rb +56 -0
  42. data/lib/mixin_bot/utils/encoder.rb +63 -0
  43. data/lib/mixin_bot/utils.rb +8 -109
  44. data/lib/mixin_bot/uuid.rb +41 -0
  45. data/lib/mixin_bot/version.rb +1 -1
  46. data/lib/mixin_bot.rb +39 -14
  47. data/lib/mvm/bridge.rb +2 -19
  48. data/lib/mvm/client.rb +11 -33
  49. data/lib/mvm/nft.rb +4 -4
  50. data/lib/mvm/registry.rb +9 -9
  51. data/lib/mvm/scan.rb +3 -5
  52. data/lib/mvm.rb +5 -6
  53. metadata +77 -103
  54. data/lib/mixin_bot/api/collectible.rb +0 -138
  55. data/lib/mixin_bot/utils/nfo.rb +0 -176
  56. data/lib/mixin_bot/utils/transaction.rb +0 -478
  57. data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -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,310 @@
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
+ XIN_ASSET_ID = 'c94ac88f-4671-3976-b60a-09064f1811e8'
10
+ EXTRA_SIZE_STORAGE_CAPACITY = 1024 * 1024 * 4
11
+ EXTRA_STORAGE_PRICE_STEP = 0.0001
12
+
13
+ def create_safe_keys(*payload, access_token: nil)
14
+ raise ArgumentError, 'payload should be an array' unless payload.is_a? Array
15
+ raise ArgumentError, 'payload should not be empty' unless payload.size.positive?
16
+ raise ArgumentError, 'invalid payload' unless payload.all?(&lambda { |param|
17
+ param.key?(:receivers) && param.key?(:index)
18
+ })
19
+
20
+ payload.each do |param|
21
+ param[:hint] ||= SecureRandom.uuid
22
+ end
23
+
24
+ path = '/safe/keys'
25
+
26
+ client.post path, *payload, access_token:
27
+ end
28
+ alias create_ghost_keys create_safe_keys
29
+
30
+ def generate_safe_keys(recipients)
31
+ raise ArgumentError, 'recipients should be an array' unless recipients.is_a? Array
32
+
33
+ ghost_keys = []
34
+ uuid_recipients = []
30
35
 
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)
36
+ recipients.each_with_index do |recipient, index|
37
+ next if recipient[:mix_address].blank?
38
+
39
+ if recipient[:members].all?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
40
+ key = JOSE::JWA::Ed25519.keypair
41
+ gk = {
42
+ mask: key[0].unpack1('H*'),
43
+ keys: []
44
+ }
45
+ recipient[:members].each do |member|
46
+ payload = MixinBot.utils.parse_main_address member
47
+ spend_key = payload[0...32]
48
+ view_key = payload[-32..]
49
+
50
+ ghost_public_key = MixinBot.utils.derive_ghost_public_key(key[1], view_key, spend_key, index)
51
+
52
+ gk[:keys] << ghost_public_key.unpack1('H*')
53
+ end
54
+
55
+ ghost_keys[index] = gk.with_indifferent_access
56
+
57
+ elsif recipient[:members].none?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
58
+ uuid_recipients.push(
59
+ {
60
+ receivers: recipient[:members],
61
+ index:,
62
+ hint: SecureRandom.uuid
63
+ }.with_indifferent_access
64
+ )
65
+ end
66
+ end
67
+
68
+ if uuid_recipients.present?
69
+ keys = create_safe_keys(*uuid_recipients)['data']
70
+ keys.each_with_index do |key, index|
71
+ ghost_keys[uuid_recipients[index][:index]] = key
72
+ end
73
+ end
74
+
75
+ ghost_keys
35
76
  end
36
77
 
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
78
+ # kwargs:
79
+ # {
80
+ # utxos: [ utxo ],
81
+ # receivers: [ {
82
+ # members: [ uuid ],
83
+ # threshold: integer,
84
+ # amount: string,
85
+ # } ],
86
+ # ghosts: [ ghost ],
87
+ # extra: string,
88
+ # }
89
+ SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
90
+ def build_safe_transaction(**kwargs)
91
+ raise ArgumentError, "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction" unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
92
+ raise ArgumentError, 'receivers should be an array' unless kwargs[:receivers].is_a? Array
93
+ raise ArgumentError, 'utxos should be an array' unless kwargs[:utxos].is_a? Array
94
+
95
+ utxos = kwargs[:utxos].map(&:with_indifferent_access)
96
+ receivers = kwargs[:receivers].map(&:with_indifferent_access)
97
+
98
+ senders = utxos.map { |utxo| utxo['receivers'] }.uniq
99
+ raise ArgumentError, 'utxos should have same senders' if senders.size > 1
100
+
101
+ senders_threshold = utxos.map { |utxo| utxo['receivers_threshold'] }.uniq
102
+ raise ArgumentError, 'utxos should have same senders_threshold' if senders_threshold.size > 1
103
+
104
+ raise ArgumentError, 'utxos should not be empty' if utxos.empty?
105
+ raise ArgumentError, 'utxos too many' if utxos.size > 256
106
+
107
+ recipients = receivers.map do |receiver|
108
+ MixinBot.utils.build_safe_recipient(
109
+ members: receiver[:members],
110
+ threshold: receiver[:threshold],
111
+ amount: receiver[:amount]
112
+ ).with_indifferent_access
113
+ end
114
+
115
+ inputs_sum = utxos.sum(&->(utxo) { utxo['amount'].to_d })
116
+ outputs_sum = recipients.sum(&->(recipient) { recipient['amount'].to_d })
117
+ change = inputs_sum - outputs_sum
118
+ raise InsufficientBalanceError, "inputs sum #{inputs_sum} < outputs sum #{outputs_sum}" if change.negative?
119
+
120
+ if change.positive?
121
+ recipients << MixinBot.utils.build_safe_recipient(
122
+ members: utxos.first['receivers'],
123
+ threshold: utxos.first['receivers_threshold'],
124
+ amount: change
125
+ ).with_indifferent_access
126
+ end
127
+ raise ArgumentError, 'recipients too many' if recipients.size > 256
128
+
129
+ asset = utxos[0]['asset']
130
+ inputs = []
131
+ utxos.each do |utxo|
132
+ raise ArgumentError, 'utxo asset not match' unless utxo['asset'] == asset
133
+
134
+ inputs << {
135
+ hash: utxo['transaction_hash'],
136
+ index: utxo['output_index']
137
+ }
138
+ end
139
+
140
+ ghosts = generate_safe_keys(recipients)
141
+
142
+ outputs = []
143
+ recipients.each_with_index do |recipient, index|
144
+ outputs << if recipient['destination']
145
+ {
146
+ type: OUTPUT_TYPE_WITHDRAW_SUBMIT,
147
+ amount: recipient['amount'],
148
+ withdrawal: {
149
+ address: recipient['destination'],
150
+ tag: recipient['tag'] || ''
151
+ }
152
+ }
153
+ else
154
+ {
155
+ type: OUTPUT_TYPE_SCRIPT,
156
+ amount: recipient['amount'],
157
+ keys: ghosts[index]['keys'],
158
+ mask: ghosts[index]['mask'],
159
+ script: build_threshold_script(recipient['threshold'])
160
+ }
161
+ end
162
+ end
163
+
164
+ {
165
+ version: SAFE_TX_VERSION,
166
+ asset:,
167
+ inputs:,
168
+ outputs:,
169
+ extra: kwargs[:extra] || '',
170
+ references: kwargs[:references] || []
56
171
  }
172
+ end
173
+
174
+ def create_safe_transaction_request(request_id, raw)
175
+ path = '/safe/transaction/requests'
176
+ payload = [{
177
+ request_id:,
178
+ raw:
179
+ }]
180
+
181
+ client.post path, *payload
182
+ end
57
183
 
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)
184
+ def send_safe_transaction(request_id, raw)
185
+ path = '/safe/transactions'
186
+ payload = [{
187
+ request_id:,
188
+ raw:
189
+ }]
190
+
191
+ client.post path, *payload
192
+ end
193
+
194
+ def safe_transaction(request_id, access_token: nil)
195
+ path = format('/safe/transactions/%<request_id>s', request_id:)
196
+
197
+ client.get path, access_token:
62
198
  end
63
199
 
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]
200
+ SIGN_SAFE_TRANSACTION_ARGUMENTS = %i[raw utxos request spend_key].freeze
201
+ def sign_safe_transaction(**kwargs)
202
+ raise ArgumentError, "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction" unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
203
+
204
+ raw = kwargs[:raw]
205
+ tx = MixinBot.utils.decode_raw_transaction raw
206
+ utxos = kwargs[:utxos]
207
+ request = kwargs[:request]
208
+ spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
209
+ spend_key = Digest::SHA512.digest spend_key[...32]
210
+
211
+ msg = [raw].pack('H*')
212
+
213
+ y_scalar = JOSE::JWA::FieldElement.new(
214
+ JOSE::JWA::X25519.clamp_scalar(spend_key[...32]).x,
215
+ JOSE::JWA::Edwards25519Point::L
72
216
  )
73
217
 
74
- client.get path
218
+ tx[:signatures] = []
219
+ tx[:inputs].each_with_index do |input, index|
220
+ utxo = utxos[index]
221
+ raise ArgumentError, 'utxo not match' unless input['hash'] == utxo['transaction_hash'] && input['index'] == utxo['output_index']
222
+
223
+ view = [request['views'][index]].pack('H*')
224
+ x_scalar = MixinBot.utils.scalar_from_bytes(view)
225
+
226
+ t_scalar = x_scalar + y_scalar
227
+ key = t_scalar.to_bytes(JOSE::JWA::Edwards25519Point::B)
228
+
229
+ pub = MixinBot.utils.shared_public_key key
230
+ key_index = utxo['keys'].index pub.unpack1('H*')
231
+ raise ArgumentError, 'cannot find valid key' unless key_index.is_a? Integer
232
+
233
+ signature = MixinBot.utils.sign(msg, key:)
234
+ signature = signature.unpack1('H*')
235
+ sig = {}
236
+ sig[key_index] = signature
237
+ tx[:signatures] << sig
238
+ end
239
+
240
+ MixinBot.utils.encode_raw_transaction tx
241
+ end
242
+
243
+ def build_object_transaction(extra, **)
244
+ extra = extra.to_s
245
+ raise ArgumentError, 'Extra too large' if extra.bytesize > EXTRA_SIZE_STORAGE_CAPACITY
246
+
247
+ # calculate fee base on extra length
248
+ amount = EXTRA_STORAGE_PRICE_STEP * ((extra.bytesize / 1024) + 1)
249
+
250
+ # burning address
251
+ receivers = [
252
+ {
253
+ members: [MixinBot.utils.burning_address],
254
+ threshold: 64,
255
+ amount:
256
+ }
257
+ ]
258
+
259
+ # find XIN utxos
260
+ utxos = build_utxos(asset_id: XIN_ASSET_ID, amount:)
261
+
262
+ # build transaction
263
+ build_safe_transaction utxos:, receivers:, extra:, **
264
+ end
265
+
266
+ INSCRIBE_TRANSACTION_ARGUMENTS = %i[content collection_hash].freeze
267
+ def build_inscribe_transaction(**kwargs)
268
+ raise ArgumentError, "#{INSCRIBE_TRANSACTION_ARGUMENTS.join(', ')} are needed for inscribe transaction" unless INSCRIBE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
269
+
270
+ receivers = kwargs[:receivers].presence || [config.app_id]
271
+ receivers_threshold = kwargs[:receivers_threshold] || receivers.length
272
+ recipient = MixinBot.utils.build_mix_address(receivers, receivers_threshold)
273
+
274
+ content = kwargs[:content]
275
+ collection_hash = kwargs[:collection_hash]
276
+
277
+ data = {
278
+ operation: 'inscribe',
279
+ recipient:,
280
+ content:
281
+ }
282
+
283
+ MixinBot.api.build_object_transaction data.to_json, references: [collection_hash]
284
+ end
285
+
286
+ OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS = %i[amount inscription_hash utxos].freeze
287
+ def build_occupy_transaction(**kwargs)
288
+ raise ArgumentError, "#{OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.join(', ')} are needed for occupy NFT transaction" unless OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
289
+
290
+ members = kwargs[:members].presence || [config.app_id]
291
+ threshold = kwargs[:threshold] || members.length
292
+ amount = kwargs[:amount]
293
+ inscription_hash = kwargs[:inscription_hash]
294
+
295
+ receivers = [
296
+ {
297
+ members:,
298
+ threshold:,
299
+ amount:
300
+ }
301
+ ]
302
+
303
+ extra = {
304
+ operation: 'occupy',
305
+ recipient:,
306
+ content:
307
+ }.to_json
308
+
309
+ MixinBot.api.build_safe_transaction(utxos:, receivers:, extra:, references: [inscription_hash])
75
310
  end
76
311
  end
77
312
  end
@@ -3,40 +3,78 @@
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)
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
+ utxos = build_utxos(asset_id:, amount:)
33
+
34
+ # step 2: build transaction
35
+ tx = build_safe_transaction(
36
+ utxos:,
37
+ receivers: [{
38
+ members:,
39
+ threshold:,
40
+ amount:
41
+ }],
42
+ extra: memo
43
+ )
44
+ raw = MixinBot.utils.encode_raw_transaction tx
45
+
46
+ # step 3: verify transaction
47
+ request = create_safe_transaction_request(request_id, raw)['data']
48
+
49
+ # step 4: sign transaction
50
+ spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
51
+ signed_raw = MixinBot.api.sign_safe_transaction(
52
+ raw:,
53
+ utxos:,
54
+ request: request[0],
55
+ spend_key:
56
+ )
57
+
58
+ # step 5: submit transaction
59
+ send_safe_transaction(
60
+ request_id,
61
+ signed_raw
62
+ )
31
63
  end
32
64
 
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 })
65
+ def build_utxos(asset_id:, amount:)
66
+ outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
67
+
68
+ utxos = []
69
+ outputs.each do |output|
70
+ break if utxos.sum { |o| o['amount'].to_d } >= amount
71
+
72
+ utxos.shift if utxos.size >= 256
73
+ utxos << output
74
+ end
75
+
76
+ utxos
38
77
  end
39
- alias read_transfer transfer
40
78
  end
41
79
  end
42
80
  end