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,524 @@
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 += encode_references 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_uint32 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
+ # read references
97
+ decode_references if version >= REFERENCES_TX_VERSION
98
+
99
+ # read extra
100
+ # unsigned 32 endian for extra size
101
+ extra_size = MixinBot.utils.decode_uint32 @bytes.shift(4)
102
+ @extra = @bytes.shift(extra_size).pack('C*')
103
+
104
+ num = MixinBot.utils.decode_uint16 @bytes.shift(2)
105
+ if num == MAX_ENCODE_INT
106
+ # aggregated
107
+ @aggregated = {}
108
+
109
+ raise ArgumentError, 'invalid aggregated' unless MixinBot.utils.decode_uint16(@bytes.shift(2)) == AGGREGATED_SIGNATURE_PREFIX
110
+
111
+ @aggregated['signature'] = @bytes.shift(64).pack('C*').unpack1('H*')
112
+
113
+ byte = @bytes.shift
114
+ case byte
115
+ when AGGREGATED_SIGNATURE_ORDINAY_MASK.first
116
+ @aggregated['signers'] = []
117
+ masks_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
118
+ masks = @bytes.shift(masks_size)
119
+ masks = Array(masks)
120
+
121
+ masks.each_with_index do |mask, i|
122
+ 8.times do |j|
123
+ k = 1 << j
124
+ aggregated['signers'].push((i * 8) + j) if mask & k == k
125
+ end
126
+ end
127
+ when AGGREGATED_SIGNATURE_SPARSE_MASK.first
128
+ signers_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
129
+ return if signers_size.zero?
130
+
131
+ aggregated['signers'] = []
132
+ signers_size.times do
133
+ aggregated['signers'].push MixinBot.utils.decode_uint16(@bytes.shift(2))
134
+ end
135
+ end
136
+ elsif num.present? && num.positive? && @bytes.size.positive?
137
+ @signatures = []
138
+ num.times do
139
+ signature = {}
140
+
141
+ keys_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
142
+
143
+ keys_size.times do
144
+ index = MixinBot.utils.decode_uint16 @bytes.shift(2)
145
+ signature[index] = @bytes.shift(64).pack('C*').unpack1('H*')
146
+ end
147
+
148
+ @signatures << signature
149
+ end
150
+ end
151
+
152
+ self
153
+ end
154
+
155
+ def to_h
156
+ {
157
+ version:,
158
+ asset:,
159
+ inputs:,
160
+ outputs:,
161
+ extra:,
162
+ signatures:,
163
+ aggregated:,
164
+ hash:,
165
+ references:
166
+ }.compact
167
+ end
168
+
169
+ private
170
+
171
+ def encode_inputs
172
+ bytes = []
173
+
174
+ bytes += MixinBot.utils.encode_uint16(inputs.size)
175
+
176
+ inputs.each do |input|
177
+ bytes += [input['hash']].pack('H*').bytes
178
+ bytes += MixinBot.utils.encode_uint16(input['index'])
179
+
180
+ # genesis
181
+ genesis = input['genesis'] || ''
182
+ if genesis.empty?
183
+ bytes += NULL_BYTES
184
+ else
185
+ genesis_bytes = [genesis].pack('H*').bytes
186
+ bytes += MixinBot.utils.encode_uint16 genesis_bytes.size
187
+ bytes += genesis_bytes
188
+ end
189
+
190
+ # deposit
191
+ deposit = input['deposit']
192
+ if deposit.nil?
193
+ bytes += NULL_BYTES
194
+ else
195
+ bytes += MAGIC
196
+ bytes += [deposit['chain']].pack('H*').bytes
197
+
198
+ asset_bytes = [deposit['asset']].pack('H*')
199
+ bytes += MixinBot.utils.encode_uint16 asset_bytes.size
200
+ bytes += asset_bytes
201
+
202
+ transaction_bytes = [deposit['transaction']].pack('H*')
203
+ bytes += MixinBot.utils.encode_uint16 transaction_bytes.size
204
+ bytes += transaction_bytes
205
+
206
+ bytes += MixinBot.utils.encode_uint64 deposit['index']
207
+
208
+ amount_bytes = MixinBot.utils.bytes_of deposit['amount']
209
+ bytes += MixinBot.utils.encode_uint16 amount_bytes.size
210
+ bytes += amount_bytes
211
+ end
212
+
213
+ # mint
214
+ mint = input['mint']
215
+ if mint.nil?
216
+ bytes += NULL_BYTES
217
+ else
218
+ bytes += MAGIC
219
+
220
+ # group
221
+ group = mint['group'] || ''
222
+ if group.empty?
223
+ bytes += MixinBot.utils.encode_uint16 NULL_BYTES
224
+ else
225
+ group_bytes = [group].pack('H*')
226
+ bytes += MixinBot.utils.encode_uint16 group_bytes.size
227
+ bytes += group_bytes
228
+ end
229
+
230
+ bytes += MixinBot.utils.encode_uint64 mint['batch']
231
+
232
+ amount_bytes = MixinBot.utils.encode_int mint['amount']
233
+ bytes += MixinBot.utils.encode_uint16 amount_bytes.size
234
+ bytes += amount_bytes
235
+ end
236
+ end
237
+
238
+ bytes
239
+ end
240
+
241
+ def encode_outputs
242
+ bytes = []
243
+
244
+ bytes += MixinBot.utils.encode_uint16 outputs.size
245
+
246
+ outputs.each do |output|
247
+ type = output['type'] || 0
248
+ bytes += [0x00, type]
249
+
250
+ # amount
251
+ amount_bytes = MixinBot.utils.encode_int (output['amount'].to_d * 1e8).round
252
+ bytes += MixinBot.utils.encode_uint16 amount_bytes.size
253
+ bytes += amount_bytes
254
+
255
+ # keys
256
+ bytes += MixinBot.utils.encode_uint16 output['keys'].size
257
+ output['keys'].each do |key|
258
+ bytes += [key].pack('H*').bytes
259
+ end
260
+
261
+ # mask
262
+ bytes += [output['mask']].pack('H*').bytes
263
+
264
+ # script
265
+ script_bytes = [output['script']].pack('H*').bytes
266
+ bytes += MixinBot.utils.encode_uint16 script_bytes.size
267
+ bytes += script_bytes
268
+
269
+ # withdrawal
270
+ withdrawal = output['withdrawal']
271
+ if withdrawal.nil?
272
+ bytes += NULL_BYTES
273
+ else
274
+ bytes += MAGIC
275
+
276
+ # chain
277
+ bytes += [withdrawal['chain']].pack('H*').bytes
278
+
279
+ # asset
280
+ @asset_bytes = [withdrawal['asset']].pack('H*')
281
+ bytes += MixinBot.utils.encode_uint16 asset_bytes.size
282
+ bytes += asset_bytes
283
+
284
+ # address
285
+ address = withdrawal['address'] || ''
286
+ if address.empty?
287
+ bytes += NULL_BYTES
288
+ else
289
+ address_bytes = [address].pack('H*').bytes
290
+ bytes += MixinBot.utils.encode_uint16 address.size
291
+ bytes += address_bytes
292
+ end
293
+
294
+ # tag
295
+ tag = withdrawal['tag'] || ''
296
+ if tag.empty?
297
+ bytes += NULL_BYTES
298
+ else
299
+ address_bytes = [tag].pack('H*').bytes
300
+ bytes += MixinBot.utils.encode_uint16 tag.size
301
+ bytes += address_bytes
302
+ end
303
+ end
304
+ end
305
+
306
+ bytes
307
+ end
308
+
309
+ def encode_references
310
+ bytes = []
311
+
312
+ bytes += MixinBot.utils.encode_uint16 references.size
313
+
314
+ references.each do |reference|
315
+ bytes += [reference].pack('H*').bytes
316
+ end
317
+
318
+ bytes
319
+ end
320
+
321
+ def encode_aggregated_signature
322
+ bytes = []
323
+
324
+ bytes += MixinBot.utils.encode_uint16 MAX_ENCODE_INT
325
+ bytes += MixinBot.utils.encode_uint16 AGGREGATED_SIGNATURE_PREFIX
326
+ bytes += [aggregated['signature']].pack('H*').bytes
327
+
328
+ signers = aggregated['signers']
329
+ if signers.empty?
330
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
331
+ bytes += NULL_BYTES
332
+ else
333
+ signers.each do |sig, i|
334
+ raise ArgumentError, 'signers not sorted' if i.positive? && sig <= signers[i - 1]
335
+ raise ArgumentError, 'signers not sorted' if sig > MAX_ENCODE_INT
336
+ end
337
+
338
+ max = signers.last
339
+ if ((((max / 8) | 0) + 1) | 0) > aggregated['signature'].size * 2
340
+ bytes += AGGREGATED_SIGNATURE_SPARSE_MASK
341
+ bytes += MixinBot.utils.encode_uint16 aggregated['signers'].size
342
+ signers.map(&->(signer) { bytes += MixinBot.utils.encode_uint16(signer) })
343
+ end
344
+
345
+ masks_bytes = Array.new((max / 8) + 1, 0)
346
+ signers.each do |signer|
347
+ masks[signer / 8] = masks[signer / 8] ^ (1 << (signer % 8))
348
+ end
349
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
350
+ bytes += MixinBot.utils.encode_uint16 masks_bytes.size
351
+ bytes += masks_bytes
352
+ end
353
+
354
+ bytes
355
+ end
356
+
357
+ def encode_signatures
358
+ bytes = []
359
+
360
+ sl =
361
+ if signatures.is_a? Array
362
+ signatures.size
363
+ else
364
+ 0
365
+ end
366
+
367
+ raise ArgumentError, 'signatures overflow' if sl == MAX_ENCODE_INT
368
+
369
+ bytes += MixinBot.utils.encode_uint16 sl
370
+
371
+ if sl.positive?
372
+ signatures.each do |signature|
373
+ bytes += MixinBot.utils.encode_uint16 signature.keys.size
374
+
375
+ signature.keys.sort.each do |key|
376
+ signature_bytes = [signature[key]].pack('H*').bytes
377
+ bytes += MixinBot.utils.encode_uint16 key
378
+ bytes += signature_bytes
379
+ end
380
+ end
381
+ end
382
+
383
+ bytes
384
+ end
385
+
386
+ def decode_inputs
387
+ inputs_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
388
+ @inputs = []
389
+ inputs_size.times do
390
+ input = {}
391
+ hash = @bytes.shift(32)
392
+ input['hash'] = hash.pack('C*').unpack1('H*')
393
+
394
+ index = @bytes.shift(2)
395
+ input['index'] = MixinBot.utils.decode_uint16 index
396
+
397
+ if @bytes[...2] == NULL_BYTES
398
+ @bytes.shift 2
399
+ else
400
+ genesis_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
401
+ genesis = @bytes.shift genesis_size
402
+ input['genesis'] = genesis.pack('C*').unpack1('H*')
403
+ end
404
+
405
+ if @bytes[...2] == NULL_BYTES
406
+ @bytes.shift 2
407
+ else
408
+ magic = @bytes.shift(2)
409
+ raise ArgumentError, 'Not valid input' unless magic == MAGIC
410
+
411
+ deposit = {}
412
+ deposit['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
413
+
414
+ asset_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
415
+ deposit['asset'] = @bytes.shift(asset_size).unpack1('H*')
416
+
417
+ transaction_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
418
+ deposit['transaction'] = @bytes.shift(transaction_size).unpack1('H*')
419
+
420
+ deposit['index'] = MixinBot.utils.decode_uint64 @bytes.shift(8)
421
+
422
+ amount_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
423
+ deposit['amount'] = MixinBot.utils.decode_int @bytes.shift(amount_size)
424
+
425
+ input['deposit'] = deposit
426
+ end
427
+
428
+ if @bytes[...2] == NULL_BYTES
429
+ @bytes.shift 2
430
+ else
431
+ magic = @bytes.shift(2)
432
+ raise ArgumentError, 'Not valid input' unless magic == MAGIC
433
+
434
+ mint = {}
435
+ if bytes[...2] == NULL_BYTES
436
+ @bytes.shift 2
437
+ else
438
+ group_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
439
+ mint['group'] = @bytes.shift(group_size).unpack1('H*')
440
+ end
441
+
442
+ mint['batch'] = MixinBot.utils.decode_uint64 @bytes.shift(8)
443
+ _amount_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
444
+ mint['amount'] = MixinBot.utils.decode_int bytes.shift(_amount_size)
445
+
446
+ input['mint'] = mint
447
+ end
448
+
449
+ @inputs.push input
450
+ end
451
+
452
+ self
453
+ end
454
+
455
+ def decode_outputs
456
+ outputs_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
457
+ @outputs = []
458
+ outputs_size.times do
459
+ output = {}
460
+
461
+ @bytes.shift
462
+ type = @bytes.shift
463
+ output['type'] = type
464
+
465
+ amount_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
466
+ output['amount'] = format('%.8f', MixinBot.utils.decode_int(@bytes.shift(amount_size)).to_f / 1e8).gsub(/\.?0+$/, '')
467
+
468
+ output['keys'] = []
469
+ keys_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
470
+ keys_size.times do
471
+ output['keys'].push @bytes.shift(32).pack('C*').unpack1('H*')
472
+ end
473
+
474
+ output['mask'] = @bytes.shift(32).pack('C*').unpack1('H*')
475
+
476
+ script_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
477
+ output['script'] = @bytes.shift(script_size).pack('C*').unpack1('H*')
478
+
479
+ if @bytes[...2] == NULL_BYTES
480
+ @bytes.shift 2
481
+ else
482
+ magic = @bytes.shift(2)
483
+ raise ArgumentError, 'Not valid output' unless magic == MAGIC
484
+
485
+ output['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
486
+
487
+ asset_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
488
+ output['asset'] = @bytes.shift(asset_size).unpack1('H*')
489
+
490
+ if @bytes[...2] == NULL_BYTES
491
+ @bytes.shift 2
492
+ else
493
+
494
+ adderss_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
495
+ output['adderss'] = @bytes.shift(adderss_size).pack('C*').unpack1('H*')
496
+ end
497
+
498
+ if @bytes[...2] == NULL_BYTES
499
+ @bytes.shift 2
500
+ else
501
+
502
+ tag_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
503
+ output['tag'] = @bytes.shift(tag_size).pack('C*').unpack1('H*')
504
+ end
505
+ end
506
+
507
+ @outputs.push output
508
+ end
509
+
510
+ self
511
+ end
512
+
513
+ def decode_references
514
+ references_size = MixinBot.utils.decode_uint16 @bytes.shift(2)
515
+ @references = []
516
+
517
+ references_size.times do
518
+ @references.push @bytes.shift(32).pack('C*').unpack1('H*')
519
+ end
520
+
521
+ self
522
+ end
523
+ end
524
+ end
@@ -0,0 +1,121 @@
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
+
24
+ payload = data[...-4]
25
+
26
+ msg = MAIN_ADDRESS_PREFIX + payload
27
+ checksum = SHA3::Digest::SHA256.digest msg
28
+
29
+ raise ArgumentError, 'invalid address' unless checksum[0...4] == data[-4..]
30
+
31
+ payload
32
+ end
33
+
34
+ def build_mix_address(members, threshold)
35
+ raise ArgumentError, 'members should be an array' unless members.is_a? Array
36
+ raise ArgumentError, 'members should not be empty' if members.empty?
37
+ raise ArgumentError, 'members length should less than 256' if members.length > 255
38
+
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
+
107
+ def burning_address
108
+ seed = "\0" * 64
109
+
110
+ digest1 = SHA3::Digest::SHA256.digest seed
111
+ digest2 = SHA3::Digest::SHA256.digest digest1
112
+ src = digest1 + digest2
113
+
114
+ spend_key = MixinBot::Utils.shared_public_key(seed)
115
+ view_key = MixinBot::Utils.shared_public_key(src)
116
+
117
+ MixinBot::Utils.build_main_address spend_key + view_key
118
+ end
119
+ end
120
+ end
121
+ end