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
@@ -0,0 +1,505 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class Transaction
5
+ REFERENCES_TX_VERSION = 0x04
6
+ SAFE_TX_VERSION = 0x05
7
+ DEAULT_VERSION = 5
8
+ MAGIC = [0x77, 0x77].freeze
9
+ TX_VERSION = 2
10
+ MAX_ENCODE_INT = 0xFFFF
11
+ MAX_EXTRA_SIZE = 512
12
+ NULL_BYTES = [0x00, 0x00].freeze
13
+ AGGREGATED_SIGNATURE_PREFIX = 0xFF01
14
+ AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00].freeze
15
+ AGGREGATED_SIGNATURE_SPARSE_MASK = [0x01].freeze
16
+
17
+ attr_accessor :version, :asset, :inputs, :outputs, :extra, :signatures, :aggregated, :references, :hex, :hash
18
+
19
+ def initialize(**kwargs)
20
+ @version = kwargs[:version] || DEAULT_VERSION
21
+ @asset = kwargs[:asset]
22
+ @inputs = kwargs[:inputs]
23
+ @outputs = kwargs[:outputs]
24
+ @extra = kwargs[:extra].to_s
25
+ @hex = kwargs[:hex]
26
+ @signatures = kwargs[:signatures]
27
+ @aggregated = kwargs[:aggregated]
28
+ @references = kwargs[:references]
29
+ end
30
+
31
+ def encode
32
+ raise InvalidTransactionFormatError, 'asset is required' if asset.blank?
33
+ raise InvalidTransactionFormatError, 'inputs is required' if inputs.blank?
34
+ raise InvalidTransactionFormatError, 'outputs is required' if outputs.blank?
35
+
36
+ bytes = []
37
+
38
+ # magic number
39
+ bytes += MAGIC
40
+
41
+ # version
42
+ bytes += [0, version]
43
+
44
+ # asset
45
+ bytes += [asset].pack('H*').bytes
46
+
47
+ # inputs
48
+ bytes += encode_inputs
49
+
50
+ # output
51
+ bytes += encode_outputs
52
+
53
+ # placeholder for `references`
54
+ bytes += NULL_BYTES if version >= REFERENCES_TX_VERSION
55
+
56
+ # extra
57
+ extra_bytes = extra.bytes
58
+ raise InvalidTransactionFormatError, 'extra is too long' if extra_bytes.size > MAX_EXTRA_SIZE
59
+
60
+ bytes += MixinBot.utils.encode_uint_32 extra_bytes.size
61
+ bytes += extra_bytes
62
+
63
+ # aggregated
64
+ bytes += if aggregated.nil?
65
+ # signatures
66
+ encode_signatures
67
+ else
68
+ encode_aggregated_signature
69
+ end
70
+
71
+ @hash = SHA3::Digest::SHA256.hexdigest bytes.pack('C*')
72
+ @hex = bytes.pack('C*').unpack1('H*')
73
+
74
+ self
75
+ end
76
+
77
+ def decode
78
+ @bytes = [hex].pack('H*').bytes
79
+ @hash = SHA3::Digest::SHA256.hexdigest @bytes.pack('C*')
80
+
81
+ magic = @bytes.shift(2)
82
+ raise ArgumentError, 'Not valid raw' unless magic == MAGIC
83
+
84
+ _version = @bytes.shift(2)
85
+ @version = MixinBot.utils.decode_int _version
86
+
87
+ asset = @bytes.shift(32)
88
+ @asset = asset.pack('C*').unpack1('H*')
89
+
90
+ # read inputs
91
+ decode_inputs
92
+
93
+ # read outputs
94
+ decode_outputs
95
+
96
+ # TODO:
97
+ # read references
98
+ if version >= REFERENCES_TX_VERSION
99
+ references_size = @bytes.shift 2
100
+ raise ArgumentError, 'Not support references yet' unless references_size == NULL_BYTES
101
+ end
102
+
103
+ # read extra
104
+ # unsigned 32 endian for extra size
105
+ extra_size = MixinBot.utils.decode_uint_32 @bytes.shift(4)
106
+ @extra = @bytes.shift(extra_size).pack('C*')
107
+
108
+ num = MixinBot.utils.decode_uint_16 @bytes.shift(2)
109
+ if num == MAX_ENCODE_INT
110
+ # aggregated
111
+ @aggregated = {}
112
+
113
+ raise ArgumentError, 'invalid aggregated' unless MixinBot.utils.decode_uint_16(@bytes.shift(2)) == AGGREGATED_SIGNATURE_PREFIX
114
+
115
+ @aggregated['signature'] = @bytes.shift(64).pack('C*').unpack1('H*')
116
+
117
+ byte = @bytes.shift
118
+ case byte
119
+ when AGGREGATED_SIGNATURE_ORDINAY_MASK.first
120
+ @aggregated['signers'] = []
121
+ masks_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
122
+ masks = @bytes.shift(masks_size)
123
+ masks = Array(masks)
124
+
125
+ masks.each_with_index do |mask, i|
126
+ 8.times do |j|
127
+ k = 1 << j
128
+ aggregated['signers'].push((i * 8) + j) if mask & k == k
129
+ end
130
+ end
131
+ when AGGREGATED_SIGNATURE_SPARSE_MASK.first
132
+ signers_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
133
+ return if signers_size.zero?
134
+
135
+ aggregated['signers'] = []
136
+ signers_size.times do
137
+ aggregated['signers'].push MixinBot.utils.decode_uint_16(@bytes.shift(2))
138
+ end
139
+ end
140
+ elsif num.present? && num.positive? && @bytes.size.positive?
141
+ @signatures = []
142
+ num.times do
143
+ signature = {}
144
+
145
+ keys_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
146
+
147
+ keys_size.times do
148
+ index = MixinBot.utils.decode_uint_16 @bytes.shift(2)
149
+ signature[index] = @bytes.shift(64).pack('C*').unpack1('H*')
150
+ end
151
+
152
+ @signatures << signature
153
+ end
154
+ end
155
+
156
+ self
157
+ end
158
+
159
+ def to_h
160
+ {
161
+ version:,
162
+ asset:,
163
+ inputs:,
164
+ outputs:,
165
+ extra:,
166
+ signatures:,
167
+ aggregated:,
168
+ hash:,
169
+ references:
170
+ }.compact
171
+ end
172
+
173
+ private
174
+
175
+ def encode_inputs
176
+ bytes = []
177
+
178
+ bytes += MixinBot.utils.encode_uint_16(inputs.size)
179
+
180
+ inputs.each do |input|
181
+ bytes += [input['hash']].pack('H*').bytes
182
+ bytes += MixinBot.utils.encode_uint_16(input['index'])
183
+
184
+ # genesis
185
+ genesis = input['genesis'] || ''
186
+ if genesis.empty?
187
+ bytes += NULL_BYTES
188
+ else
189
+ genesis_bytes = [genesis].pack('H*').bytes
190
+ bytes += MixinBot.utils.encode_uint_16 genesis_bytes.size
191
+ bytes += genesis_bytes
192
+ end
193
+
194
+ # deposit
195
+ deposit = input['deposit']
196
+ if deposit.nil?
197
+ bytes += NULL_BYTES
198
+ else
199
+ bytes += MAGIC
200
+ bytes += [deposit['chain']].pack('H*').bytes
201
+
202
+ asset_bytes = [deposit['asset']].pack('H*')
203
+ bytes += MixinBot.utils.encode_uint_16 asset_bytes.size
204
+ bytes += asset_bytes
205
+
206
+ transaction_bytes = [deposit['transaction']].pack('H*')
207
+ bytes += MixinBot.utils.encode_uint_16 transaction_bytes.size
208
+ bytes += transaction_bytes
209
+
210
+ bytes += MixinBot.utils.encode_uint_64 deposit['index']
211
+
212
+ amount_bytes = MixinBot.utils.bytes_of deposit['amount']
213
+ bytes += MixinBot.utils.encode_uint_16 amount_bytes.size
214
+ bytes += amount_bytes
215
+ end
216
+
217
+ # mint
218
+ mint = input['mint']
219
+ if mint.nil?
220
+ bytes += NULL_BYTES
221
+ else
222
+ bytes += MAGIC
223
+
224
+ # group
225
+ group = mint['group'] || ''
226
+ if group.empty?
227
+ bytes += MixinBot.utils.encode_uint_16 NULL_BYTES
228
+ else
229
+ group_bytes = [group].pack('H*')
230
+ bytes += MixinBot.utils.encode_uint_16 group_bytes.size
231
+ bytes += group_bytes
232
+ end
233
+
234
+ bytes += MixinBot.utils.encode_uint_64 mint['batch']
235
+
236
+ amount_bytes = MixinBot.utils.encode_int mint['amount']
237
+ bytes += MixinBot.utils.encode_uint_16 amount_bytes.size
238
+ bytes += amount_bytes
239
+ end
240
+ end
241
+
242
+ bytes
243
+ end
244
+
245
+ def encode_outputs
246
+ bytes = []
247
+
248
+ bytes += MixinBot.utils.encode_uint_16 outputs.size
249
+
250
+ outputs.each do |output|
251
+ type = output['type'] || 0
252
+ bytes += [0x00, type]
253
+
254
+ # amount
255
+ amount_bytes = MixinBot.utils.encode_int (output['amount'].to_d * 1e8).round
256
+ bytes += MixinBot.utils.encode_uint_16 amount_bytes.size
257
+ bytes += amount_bytes
258
+
259
+ # keys
260
+ bytes += MixinBot.utils.encode_uint_16 output['keys'].size
261
+ output['keys'].each do |key|
262
+ bytes += [key].pack('H*').bytes
263
+ end
264
+
265
+ # mask
266
+ bytes += [output['mask']].pack('H*').bytes
267
+
268
+ # script
269
+ script_bytes = [output['script']].pack('H*').bytes
270
+ bytes += MixinBot.utils.encode_uint_16 script_bytes.size
271
+ bytes += script_bytes
272
+
273
+ # withdrawal
274
+ withdrawal = output['withdrawal']
275
+ if withdrawal.nil?
276
+ bytes += NULL_BYTES
277
+ else
278
+ bytes += MAGIC
279
+
280
+ # chain
281
+ bytes += [withdrawal['chain']].pack('H*').bytes
282
+
283
+ # asset
284
+ @asset_bytes = [withdrawal['asset']].pack('H*')
285
+ bytes += MixinBot.utils.encode_uint_16 asset_bytes.size
286
+ bytes += asset_bytes
287
+
288
+ # address
289
+ address = withdrawal['address'] || ''
290
+ if address.empty?
291
+ bytes += NULL_BYTES
292
+ else
293
+ address_bytes = [address].pack('H*').bytes
294
+ bytes += MixinBot.utils.encode_uint_16 address.size
295
+ bytes += address_bytes
296
+ end
297
+
298
+ # tag
299
+ tag = withdrawal['tag'] || ''
300
+ if tag.empty?
301
+ bytes += NULL_BYTES
302
+ else
303
+ address_bytes = [tag].pack('H*').bytes
304
+ bytes += MixinBot.utils.encode_uint_16 tag.size
305
+ bytes += address_bytes
306
+ end
307
+ end
308
+ end
309
+
310
+ bytes
311
+ end
312
+
313
+ def encode_aggregated_signature
314
+ bytes = []
315
+
316
+ bytes += MixinBot.utils.encode_uint_16 MAX_ENCODE_INT
317
+ bytes += MixinBot.utils.encode_uint_16 AGGREGATED_SIGNATURE_PREFIX
318
+ bytes += [aggregated['signature']].pack('H*').bytes
319
+
320
+ signers = aggregated['signers']
321
+ if signers.empty?
322
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
323
+ bytes += NULL_BYTES
324
+ else
325
+ signers.each do |sig, i|
326
+ raise ArgumentError, 'signers not sorted' if i.positive? && sig <= signers[i - 1]
327
+ raise ArgumentError, 'signers not sorted' if sig > MAX_ENCODE_INT
328
+ end
329
+
330
+ max = signers.last
331
+ if ((((max / 8) | 0) + 1) | 0) > aggregated['signature'].size * 2
332
+ bytes += AGGREGATED_SIGNATURE_SPARSE_MASK
333
+ bytes += MixinBot.utils.encode_uint_16 aggregated['signers'].size
334
+ signers.map(&->(signer) { bytes += MixinBot.utils.encode_uint_16(signer) })
335
+ end
336
+
337
+ masks_bytes = Array.new((max / 8) + 1, 0)
338
+ signers.each do |signer|
339
+ masks[signer / 8] = masks[signer / 8] ^ (1 << (signer % 8))
340
+ end
341
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
342
+ bytes += MixinBot.utils.encode_uint_16 masks_bytes.size
343
+ bytes += masks_bytes
344
+ end
345
+
346
+ bytes
347
+ end
348
+
349
+ def encode_signatures
350
+ bytes = []
351
+
352
+ sl =
353
+ if signatures.is_a? Array
354
+ signatures.size
355
+ else
356
+ 0
357
+ end
358
+
359
+ raise ArgumentError, 'signatures overflow' if sl == MAX_ENCODE_INT
360
+
361
+ bytes += MixinBot.utils.encode_uint_16 sl
362
+
363
+ if sl.positive?
364
+ signatures.each do |signature|
365
+ bytes += MixinBot.utils.encode_uint_16 signature.keys.size
366
+
367
+ signature.keys.sort.each do |key|
368
+ signature_bytes = [signature[key]].pack('H*').bytes
369
+ bytes += MixinBot.utils.encode_uint_16 key
370
+ bytes += signature_bytes
371
+ end
372
+ end
373
+ end
374
+
375
+ bytes
376
+ end
377
+
378
+ def decode_inputs
379
+ inputs_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
380
+ @inputs = []
381
+ inputs_size.times do
382
+ input = {}
383
+ hash = @bytes.shift(32)
384
+ input['hash'] = hash.pack('C*').unpack1('H*')
385
+
386
+ index = @bytes.shift(2)
387
+ input['index'] = MixinBot.utils.decode_uint_16 index
388
+
389
+ if @bytes[...2] == NULL_BYTES
390
+ @bytes.shift 2
391
+ else
392
+ genesis_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
393
+ genesis = @bytes.shift genesis_size
394
+ input['genesis'] = genesis.pack('C*').unpack1('H*')
395
+ end
396
+
397
+ if @bytes[...2] == NULL_BYTES
398
+ @bytes.shift 2
399
+ else
400
+ magic = @bytes.shift(2)
401
+ raise ArgumentError, 'Not valid input' unless magic == MAGIC
402
+
403
+ deposit = {}
404
+ deposit['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
405
+
406
+ asset_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
407
+ deposit['asset'] = @bytes.shift(asset_size).unpack1('H*')
408
+
409
+ transaction_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
410
+ deposit['transaction'] = @bytes.shift(transaction_size).unpack1('H*')
411
+
412
+ deposit['index'] = MixinBot.utils.decode_uint_64 @bytes.shift(8)
413
+
414
+ amount_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
415
+ deposit['amount'] = MixinBot.utils.decode_int @bytes.shift(amount_size)
416
+
417
+ input['deposit'] = deposit
418
+ end
419
+
420
+ if @bytes[...2] == NULL_BYTES
421
+ @bytes.shift 2
422
+ else
423
+ magic = @bytes.shift(2)
424
+ raise ArgumentError, 'Not valid input' unless magic == MAGIC
425
+
426
+ mint = {}
427
+ if bytes[...2] == NULL_BYTES
428
+ @bytes.shift 2
429
+ else
430
+ group_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
431
+ mint['group'] = @bytes.shift(group_size).unpack1('H*')
432
+ end
433
+
434
+ mint['batch'] = MixinBot.utils.decode_uint_64 @bytes.shift(8)
435
+ _amount_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
436
+ mint['amount'] = MixinBot.utils.decode_int bytes.shift(_amount_size)
437
+
438
+ input['mint'] = mint
439
+ end
440
+
441
+ @inputs.push input
442
+ end
443
+
444
+ self
445
+ end
446
+
447
+ def decode_outputs
448
+ outputs_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
449
+ @outputs = []
450
+ outputs_size.times do
451
+ output = {}
452
+
453
+ @bytes.shift
454
+ type = @bytes.shift
455
+ output['type'] = type
456
+
457
+ amount_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
458
+ output['amount'] = format('%.8f', MixinBot.utils.decode_int(@bytes.shift(amount_size)).to_f / 1e8).gsub(/\.?0+$/, '')
459
+
460
+ output['keys'] = []
461
+ keys_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
462
+ keys_size.times do
463
+ output['keys'].push @bytes.shift(32).pack('C*').unpack1('H*')
464
+ end
465
+
466
+ output['mask'] = @bytes.shift(32).pack('C*').unpack1('H*')
467
+
468
+ script_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
469
+ output['script'] = @bytes.shift(script_size).pack('C*').unpack1('H*')
470
+
471
+ if @bytes[...2] == NULL_BYTES
472
+ @bytes.shift 2
473
+ else
474
+ magic = @bytes.shift(2)
475
+ raise ArgumentError, 'Not valid output' unless magic == MAGIC
476
+
477
+ output['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
478
+
479
+ asset_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
480
+ output['asset'] = @bytes.shift(asset_size).unpack1('H*')
481
+
482
+ if @bytes[...2] == NULL_BYTES
483
+ @bytes.shift 2
484
+ else
485
+
486
+ adderss_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
487
+ output['adderss'] = @bytes.shift(adderss_size).pack('C*').unpack1('H*')
488
+ end
489
+
490
+ if @bytes[...2] == NULL_BYTES
491
+ @bytes.shift 2
492
+ else
493
+
494
+ tag_size = MixinBot.utils.decode_uint_16 @bytes.shift(2)
495
+ output['tag'] = @bytes.shift(tag_size).pack('C*').unpack1('H*')
496
+ end
497
+ end
498
+
499
+ @outputs.push output
500
+ end
501
+
502
+ self
503
+ end
504
+ end
505
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ module Utils
5
+ module Address
6
+ MAIN_ADDRESS_PREFIX = 'XIN'
7
+ MIX_ADDRESS_PREFIX = 'MIX'
8
+ MIX_ADDRESS_VERSION = 2
9
+
10
+ def build_main_address(public_key)
11
+ msg = MAIN_ADDRESS_PREFIX + public_key
12
+ checksum = SHA3::Digest::SHA256.digest msg
13
+ data = public_key + checksum[0...4]
14
+ base58 = Base58.binary_to_base58 data, :bitcoin
15
+ "#{MAIN_ADDRESS_PREFIX}#{base58}"
16
+ end
17
+
18
+ def parse_main_address(address)
19
+ raise ArgumentError, 'invalid address' unless address.start_with? MAIN_ADDRESS_PREFIX
20
+
21
+ data = address[MAIN_ADDRESS_PREFIX.length..]
22
+ data = Base58.base58_to_binary data, :bitcoin
23
+ raise ArgumentError, 'invalid address' unless data.length == 68
24
+
25
+ payload = data[...-4]
26
+
27
+ msg = MAIN_ADDRESS_PREFIX + payload
28
+ checksum = SHA3::Digest::SHA256.digest msg
29
+
30
+ raise ArgumentError, 'invalid address' unless checksum[0...4] == data[-4..]
31
+
32
+ payload
33
+ end
34
+
35
+ def build_mix_address(members, threshold)
36
+ raise ArgumentError, 'members should be an array' unless members.is_a? Array
37
+ raise ArgumentError, 'members should not be empty' if members.empty?
38
+ raise ArgumentError, 'members length should less than 256' if members.length > 255
39
+ raise ArgumentError, "invalid threshold: #{threshold}" if threshold > members.length
40
+
41
+ prefix = [MIX_ADDRESS_VERSION].pack('C*') + [threshold].pack('C*') + [members.length].pack('C*')
42
+
43
+ msg =
44
+ if members.all?(&->(member) { member.start_with? MAIN_ADDRESS_PREFIX })
45
+ members.map(&->(member) { parse_main_address(member) }).join
46
+ elsif members.none?(&->(member) { member.start_with? MAIN_ADDRESS_PREFIX })
47
+ members.map(&->(member) { MixinBot::UUID.new(hex: member).packed }).join
48
+ else
49
+ raise ArgumentError, 'invalid members'
50
+ end
51
+
52
+ checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + prefix + msg)
53
+
54
+ data = prefix + msg + checksum[0...4]
55
+ data = Base58.binary_to_base58 data, :bitcoin
56
+ "#{MIX_ADDRESS_PREFIX}#{data}"
57
+ end
58
+
59
+ def parse_mix_address(address)
60
+ raise ArgumentError, 'invalid address' unless address.start_with? MIX_ADDRESS_PREFIX
61
+
62
+ data = address[MIX_ADDRESS_PREFIX.length..]
63
+ data = Base58.base58_to_binary data, :bitcoin
64
+ raise ArgumentError, 'invalid address' if data.length < 3 + 16 + 4
65
+
66
+ msg = data[...-4]
67
+ checksum = SHA3::Digest::SHA256.digest((MIX_ADDRESS_PREFIX + msg))[0...4]
68
+
69
+ raise ArgumentError, 'invalid address' unless checksum[0...4] == data[-4..]
70
+
71
+ version = data[0].ord
72
+ raise ArgumentError, 'invalid address' unless version == MIX_ADDRESS_VERSION
73
+
74
+ threshold = data[1].ord
75
+ members_count = data[2].ord
76
+
77
+ if data[3..].length == members_count * 16
78
+ members = data[3..].scan(/.{16}/)
79
+ members = members.map(&->(member) { MixinBot::UUID.new(raw: member).unpacked })
80
+ else
81
+ members = data[3..].scan(/.{64}/)
82
+ members = members.map(&->(member) { build_main_address(member) })
83
+ end
84
+
85
+ {
86
+ members:,
87
+ threshold:
88
+ }
89
+ end
90
+
91
+ def build_safe_recipient(**kwargs)
92
+ members = kwargs[:members]
93
+ threshold = kwargs[:threshold]
94
+ amount = kwargs[:amount]
95
+
96
+ members = [members] if members.is_a? String
97
+ amount = format('%.8f', amount.to_d.to_r).gsub(/\.?0+$/, '')
98
+
99
+ {
100
+ members:,
101
+ threshold:,
102
+ amount:,
103
+ mix_address: build_mix_address(members, threshold)
104
+ }
105
+ end
106
+ end
107
+ end
108
+ end