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
@@ -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