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.
- 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 +29 -51
- data/lib/mixin_bot/api/blaze.rb +4 -3
- data/lib/mixin_bot/api/collectible.rb +60 -58
- data/lib/mixin_bot/api/conversation.rb +29 -49
- data/lib/mixin_bot/api/encrypted_message.rb +17 -17
- 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 +16 -221
- data/lib/mixin_bot/api/output.rb +46 -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 +9 -11
- 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 +184 -60
- data/lib/mixin_bot/api/transfer.rb +64 -32
- data/lib/mixin_bot/api/user.rb +83 -53
- data/lib/mixin_bot/api/withdraw.rb +52 -53
- data/lib/mixin_bot/api.rb +78 -45
- 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 +76 -127
- data/lib/mixin_bot/configuration.rb +98 -0
- data/lib/mixin_bot/nfo.rb +174 -0
- data/lib/mixin_bot/transaction.rb +505 -0
- data/lib/mixin_bot/utils/address.rb +108 -0
- data/lib/mixin_bot/utils/crypto.rb +182 -0
- data/lib/mixin_bot/utils/decoder.rb +58 -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 +101 -44
- 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,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
|