mixin_bot 0.12.1 → 1.1.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.
- checksums.yaml +4 -4
- data/lib/mixin_bot/api/address.rb +21 -0
- data/lib/mixin_bot/api/app.rb +5 -11
- data/lib/mixin_bot/api/asset.rb +9 -16
- data/lib/mixin_bot/api/attachment.rb +27 -22
- data/lib/mixin_bot/api/auth.rb +34 -56
- data/lib/mixin_bot/api/blaze.rb +4 -3
- data/lib/mixin_bot/api/conversation.rb +29 -49
- data/lib/mixin_bot/api/encrypted_message.rb +19 -19
- data/lib/mixin_bot/api/inscription.rb +71 -0
- data/lib/mixin_bot/api/legacy_collectible.rb +140 -0
- data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
- data/lib/mixin_bot/api/legacy_output.rb +50 -0
- data/lib/mixin_bot/api/legacy_payment.rb +31 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
- data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
- data/lib/mixin_bot/api/me.rb +13 -17
- data/lib/mixin_bot/api/message.rb +13 -10
- data/lib/mixin_bot/api/multisig.rb +17 -222
- data/lib/mixin_bot/api/output.rb +48 -0
- data/lib/mixin_bot/api/payment.rb +9 -20
- data/lib/mixin_bot/api/pin.rb +57 -65
- data/lib/mixin_bot/api/rpc.rb +12 -14
- data/lib/mixin_bot/api/snapshot.rb +15 -29
- data/lib/mixin_bot/api/tip.rb +43 -0
- data/lib/mixin_bot/api/transaction.rb +295 -60
- data/lib/mixin_bot/api/transfer.rb +69 -31
- data/lib/mixin_bot/api/user.rb +88 -53
- data/lib/mixin_bot/api/withdraw.rb +52 -53
- data/lib/mixin_bot/api.rb +81 -46
- data/lib/mixin_bot/cli/api.rb +149 -5
- data/lib/mixin_bot/cli/utils.rb +14 -4
- data/lib/mixin_bot/cli.rb +13 -10
- data/lib/mixin_bot/client.rb +74 -127
- data/lib/mixin_bot/configuration.rb +98 -0
- data/lib/mixin_bot/nfo.rb +174 -0
- data/lib/mixin_bot/transaction.rb +524 -0
- data/lib/mixin_bot/utils/address.rb +121 -0
- data/lib/mixin_bot/utils/crypto.rb +218 -0
- data/lib/mixin_bot/utils/decoder.rb +56 -0
- data/lib/mixin_bot/utils/encoder.rb +63 -0
- data/lib/mixin_bot/utils.rb +8 -109
- data/lib/mixin_bot/uuid.rb +41 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +39 -14
- data/lib/mvm/bridge.rb +2 -19
- data/lib/mvm/client.rb +11 -33
- data/lib/mvm/nft.rb +4 -4
- data/lib/mvm/registry.rb +9 -9
- data/lib/mvm/scan.rb +3 -5
- data/lib/mvm.rb +5 -6
- metadata +77 -103
- data/lib/mixin_bot/api/collectible.rb +0 -138
- data/lib/mixin_bot/utils/nfo.rb +0 -176
- data/lib/mixin_bot/utils/transaction.rb +0 -478
- 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
|