mixin_bot 0.7.13 → 0.8.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/utils/nfo.rb +154 -0
- data/lib/mixin_bot/utils/transaction.rb +478 -0
- data/lib/mixin_bot/utils/uuid.rb +43 -0
- data/lib/mixin_bot/utils.rb +21 -521
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +4 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a76c8b7d2c5d8cebc963a56d350e3fd5e4bbc1e18ad3e037b65cfb6e1d77b0d1
|
4
|
+
data.tar.gz: 8c9813a57cb33f256c695a88e42f53feffb33a45979f5fbf788916f332a52d26
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 999325d7c1b73c54b80a6a7a2e432e83aaff341997db01211fb43cddffa32448451aaf5c1d92852dfcda872d92b4724d938bacde83621650c20594aaa034ff32
|
7
|
+
data.tar.gz: 7b49bda1bb1026823ca51301a7f61380db7714f9d9441c466ff0a0c2bb50be52e199900e13b0d7c5e8e6782f938c57f38562e9d56439346c62b7abb364d928e5
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
module Utils
|
5
|
+
class Nfo
|
6
|
+
NFT_MEMO_PREFIX = 'NFO'
|
7
|
+
NFT_MEMO_VERSION = 0x00
|
8
|
+
NFT_MEMO_DEFAULT_CHAIN = '43d61dcd-e413-450d-80b8-101d5e903357'
|
9
|
+
NFT_MEMO_DEFAULT_CLASS = '3c8c161a18ae2c8b14fda1216fff7da88c419b5d'
|
10
|
+
NULL_UUID = '00000000-0000-0000-0000-000000000000'
|
11
|
+
|
12
|
+
attr_reader :prefix, :version, :raw
|
13
|
+
attr_accessor :mask, :chain, :nm_class, :collection, :token, :extra, :memo, :hex
|
14
|
+
|
15
|
+
def initialize(**kwargs)
|
16
|
+
@prefix = NFT_MEMO_PREFIX
|
17
|
+
@version = NFT_MEMO_VERSION
|
18
|
+
@mask = kwargs[:mask] || 0
|
19
|
+
@chain = kwargs[:chain]
|
20
|
+
@nm_class = kwargs[:nm_class]
|
21
|
+
@collection = kwargs[:collection]
|
22
|
+
@token = kwargs[:token]
|
23
|
+
@extra = kwargs[:extra]
|
24
|
+
@memo = kwargs[:memo]
|
25
|
+
@hex = kwargs[:hex]
|
26
|
+
end
|
27
|
+
|
28
|
+
def mint_memo
|
29
|
+
raise MixinBot::InvalidNfoFormatError, 'token is required' if token.blank?
|
30
|
+
raise MixinBot::InvalidNfoFormatError, 'extra must be 256-bit string' if extra.blank? || extra.size != 64
|
31
|
+
|
32
|
+
@collection = NULL_UUID if collection.blank?
|
33
|
+
@chain = NFT_MEMO_DEFAULT_CHAIN
|
34
|
+
@nm_class= NFT_MEMO_DEFAULT_CLASS
|
35
|
+
mark 0
|
36
|
+
encode
|
37
|
+
|
38
|
+
memo
|
39
|
+
end
|
40
|
+
|
41
|
+
def mark(*indexes)
|
42
|
+
indexes.map do |index|
|
43
|
+
if index >= 64 || index < 0
|
44
|
+
raise ArgumentError, "invalid NFO memo index #{index}"
|
45
|
+
end
|
46
|
+
@mask = mask ^ (1 << index)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def encode
|
51
|
+
bytes = []
|
52
|
+
|
53
|
+
bytes += prefix.bytes
|
54
|
+
bytes += [version]
|
55
|
+
|
56
|
+
if mask != 0
|
57
|
+
bytes += [1]
|
58
|
+
bytes += MixinBot::Utils.encode_unit_64 mask
|
59
|
+
bytes += MixinBot::Utils::UUID.new(hex: chain).packed.bytes
|
60
|
+
|
61
|
+
class_bytes = [nm_class].pack('H*').bytes
|
62
|
+
bytes += MixinBot::Utils.bytes_of class_bytes.size
|
63
|
+
bytes += class_bytes
|
64
|
+
|
65
|
+
collection_bytes = collection.split('-').pack('H* H* H* H* H*').bytes
|
66
|
+
bytes += MixinBot::Utils.bytes_of collection_bytes.size
|
67
|
+
bytes += collection_bytes
|
68
|
+
|
69
|
+
# token_bytes = memo[:token].split('-').pack('H* H* H* H* H*').bytes
|
70
|
+
token_bytes = MixinBot::Utils.bytes_of token
|
71
|
+
bytes += MixinBot::Utils.bytes_of token_bytes.size
|
72
|
+
bytes += token_bytes
|
73
|
+
end
|
74
|
+
|
75
|
+
extra_bytes = [extra].pack('H*').bytes
|
76
|
+
bytes += MixinBot::Utils.bytes_of extra_bytes.size
|
77
|
+
bytes += extra_bytes
|
78
|
+
|
79
|
+
@raw = bytes.pack('C*')
|
80
|
+
@hex = raw.unpack1('H*')
|
81
|
+
@memo = Base64.urlsafe_encode64 raw, padding: false
|
82
|
+
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def decode
|
87
|
+
@raw =
|
88
|
+
if memo.present?
|
89
|
+
Base64.urlsafe_decode64 memo
|
90
|
+
elsif hex.present?
|
91
|
+
[hex].pack('H*')
|
92
|
+
else
|
93
|
+
raise InvalidNfoFormatError, 'memo or hex is required'
|
94
|
+
end
|
95
|
+
|
96
|
+
@hex = raw.unpack1('H*') if hex.blank?
|
97
|
+
@memo = Base64.urlsafe_encode64 raw, padding: false if memo.blank?
|
98
|
+
|
99
|
+
decode_bytes
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def decode_bytes
|
104
|
+
bytes = raw.bytes
|
105
|
+
|
106
|
+
_prefix = bytes.shift(3).pack('C*')
|
107
|
+
raise MixinBot::InvalidNfoFormatError, "NFO prefix #{_prefix}" if _prefix != prefix
|
108
|
+
|
109
|
+
_version = bytes.shift
|
110
|
+
raise MixinBot::InvalidNfoFormatError, "NFO version #{prefix}" if _version != version
|
111
|
+
|
112
|
+
hint = bytes.shift
|
113
|
+
if hint == 1
|
114
|
+
@mask = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
115
|
+
|
116
|
+
@chain = MixinBot::Utils::UUID.new(hex: bytes.shift(16).pack('C*').unpack1('H*')).unpacked
|
117
|
+
|
118
|
+
class_length = bytes.shift
|
119
|
+
@nm_class = bytes.shift(class_length).pack('C*').unpack1('H*')
|
120
|
+
|
121
|
+
collection_length = bytes.shift
|
122
|
+
@collection = MixinBot::Utils::UUID.new(hex: bytes.shift(collection_length).pack('C*').unpack1('H*')).unpacked
|
123
|
+
|
124
|
+
token_length = bytes.shift
|
125
|
+
@token = MixinBot::Utils.bytes_to_int bytes.shift(token_length)
|
126
|
+
end
|
127
|
+
|
128
|
+
extra_length = bytes.shift
|
129
|
+
@extra = bytes.shift(extra_length).pack('C*').unpack1('H*')
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_h
|
133
|
+
hash = {
|
134
|
+
prefix: prefix,
|
135
|
+
version: version,
|
136
|
+
mask: mask,
|
137
|
+
chain: chain,
|
138
|
+
class: nm_class,
|
139
|
+
collection: collection,
|
140
|
+
token: token,
|
141
|
+
extra: extra,
|
142
|
+
memo: memo,
|
143
|
+
hex: hex
|
144
|
+
}
|
145
|
+
|
146
|
+
hash.each do |key, value|
|
147
|
+
hash.delete key if value.blank?
|
148
|
+
end
|
149
|
+
|
150
|
+
hash
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,478 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
module Utils
|
5
|
+
class Transaction
|
6
|
+
DEAULT_VERSION = 2
|
7
|
+
MAGIC = [0x77, 0x77]
|
8
|
+
TX_VERSION = 2
|
9
|
+
MAX_ENCODE_INT = 0xFFFF
|
10
|
+
NULL_BYTES = [0x00, 0x00]
|
11
|
+
AGGREGATED_SIGNATURE_PREFIX = 0xFF01
|
12
|
+
AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00]
|
13
|
+
AGGREGATED_SIGNATURE_SPARSE_MASK = [0x01]
|
14
|
+
|
15
|
+
attr_accessor :version, :asset, :inputs, :outputs, :extra, :signatures, :aggregated, :hex, :hash
|
16
|
+
|
17
|
+
def initialize(**kwargs)
|
18
|
+
@version = kwargs[:version] || DEAULT_VERSION
|
19
|
+
@asset = kwargs[:asset]
|
20
|
+
@inputs = kwargs[:inputs]
|
21
|
+
@outputs = kwargs[:outputs]
|
22
|
+
@extra = kwargs[:extra]
|
23
|
+
@hex = kwargs[:hex]
|
24
|
+
@signatures = kwargs[:signatures]
|
25
|
+
@aggregated = kwargs[:aggregated]
|
26
|
+
end
|
27
|
+
|
28
|
+
def encode
|
29
|
+
raise InvalidTransactionFormatError, 'asset is required' if asset.blank?
|
30
|
+
raise InvalidTransactionFormatError, 'inputs is required' if inputs.blank?
|
31
|
+
raise InvalidTransactionFormatError, 'outputs is required' if outputs.blank?
|
32
|
+
|
33
|
+
bytes = []
|
34
|
+
|
35
|
+
# magic number
|
36
|
+
bytes += MAGIC
|
37
|
+
|
38
|
+
# version
|
39
|
+
bytes += [0, version]
|
40
|
+
|
41
|
+
# asset
|
42
|
+
bytes += [asset].pack('H*').bytes
|
43
|
+
|
44
|
+
# inputs
|
45
|
+
bytes += encode_inputs
|
46
|
+
|
47
|
+
# output
|
48
|
+
bytes += encode_outputs
|
49
|
+
|
50
|
+
# extra
|
51
|
+
extra_bytes = [extra].pack('H*').bytes
|
52
|
+
bytes += MixinBot::Utils.encode_int extra_bytes.size
|
53
|
+
bytes += extra_bytes
|
54
|
+
|
55
|
+
# aggregated
|
56
|
+
if aggregated.nil?
|
57
|
+
# signatures
|
58
|
+
bytes += encode_signatures
|
59
|
+
else
|
60
|
+
bytes += encode_aggregated_signature
|
61
|
+
end
|
62
|
+
|
63
|
+
@hash = SHA3::Digest::SHA256.hexdigest bytes.pack('C*')
|
64
|
+
@hex = bytes.pack('C*').unpack1('H*')
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def decode
|
70
|
+
@bytes = [hex].pack('H*').bytes
|
71
|
+
@hash = SHA3::Digest::SHA256.hexdigest @bytes.pack('C*')
|
72
|
+
|
73
|
+
magic = @bytes.shift(2)
|
74
|
+
raise ArgumentError, 'Not valid raw' unless magic == MAGIC
|
75
|
+
|
76
|
+
version = @bytes.shift(2)
|
77
|
+
@version = MixinBot::Utils.bytes_to_int version
|
78
|
+
|
79
|
+
asset = @bytes.shift(32)
|
80
|
+
@asset = asset.pack('C*').unpack1('H*')
|
81
|
+
|
82
|
+
# read inputs
|
83
|
+
decode_inputs
|
84
|
+
|
85
|
+
# read outputs
|
86
|
+
decode_outputs
|
87
|
+
|
88
|
+
extra_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
89
|
+
@extra = @bytes.shift(extra_size).pack('C*').unpack1('H*')
|
90
|
+
|
91
|
+
num = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
92
|
+
if num == MAX_ENCODE_INT
|
93
|
+
# aggregated
|
94
|
+
@aggregated = {}
|
95
|
+
|
96
|
+
raise ArgumentError, 'invalid aggregated' unless @bytes.shift(2).reverse.pack('C*').unpack1('S*') == AGGREGATED_SIGNATURE_PREFIX
|
97
|
+
|
98
|
+
@aggregated['signature'] = @bytes.shift(64).pack('C*').unpack1('H*')
|
99
|
+
|
100
|
+
byte = @bytes.shift
|
101
|
+
case byte
|
102
|
+
when AGGREGATED_SIGNATURE_ORDINAY_MASK.first
|
103
|
+
@aggregated['signers'] = []
|
104
|
+
masks_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
105
|
+
masks = @bytes.shift(masks_size)
|
106
|
+
masks = [masks] unless masks.is_a? Array
|
107
|
+
|
108
|
+
masks.each_with_index do |mask, i|
|
109
|
+
8.times do |j|
|
110
|
+
k = 1 << j
|
111
|
+
aggregated['signers'].push(i * 8 + j) if mask & k == k
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when AGGREGATED_SIGNATURE_SPARSE_MASK.first
|
115
|
+
signers_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
116
|
+
return if signers_size == 0
|
117
|
+
|
118
|
+
aggregated['signers'] = []
|
119
|
+
signers_size.times do
|
120
|
+
aggregated['signers'].push @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
else
|
124
|
+
if !@bytes.empty? && @bytes[...2] != NULL_BYTES
|
125
|
+
signatures_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
126
|
+
@signatures = @bytes.shift(signatures_size).pack('C*').unpack1('H*')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_h
|
134
|
+
{
|
135
|
+
version: version,
|
136
|
+
asset: asset,
|
137
|
+
inputs: inputs,
|
138
|
+
outputs: outputs,
|
139
|
+
extra: extra,
|
140
|
+
signatures: signatures,
|
141
|
+
aggregated: aggregated,
|
142
|
+
hash: hash
|
143
|
+
}.compact
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def encode_inputs
|
149
|
+
bytes = []
|
150
|
+
|
151
|
+
bytes += MixinBot::Utils.encode_int(inputs.size)
|
152
|
+
|
153
|
+
inputs.each do |input|
|
154
|
+
bytes += [input['hash']].pack('H*').bytes
|
155
|
+
bytes += MixinBot::Utils.encode_int(input['index'])
|
156
|
+
|
157
|
+
# genesis
|
158
|
+
genesis = input['genesis'] || ''
|
159
|
+
if genesis.empty?
|
160
|
+
bytes += NULL_BYTES
|
161
|
+
else
|
162
|
+
genesis_bytes = [genesis].pack('H*').bytes
|
163
|
+
bytes += MixinBot::Utils.encode_int genesis_bytes.size
|
164
|
+
bytes += genesis_bytes
|
165
|
+
end
|
166
|
+
|
167
|
+
# deposit
|
168
|
+
deposit = input['deposit']
|
169
|
+
if deposit.nil?
|
170
|
+
bytes += NULL_BYTES
|
171
|
+
else
|
172
|
+
bytes += MAGIC
|
173
|
+
bytes += [deposit['chain']].pack('H*').bytes
|
174
|
+
|
175
|
+
asset_bytes = [deposit['asset']].pack('H*')
|
176
|
+
bytes += MixinBot::Utils.encode_int asset_bytes.size
|
177
|
+
bytes += asset_bytes
|
178
|
+
|
179
|
+
transaction_bytes = [deposit['transaction']].pack('H*')
|
180
|
+
bytes += MixinBot::Utils.encode_int transaction_bytes.size
|
181
|
+
bytes += transaction_bytes
|
182
|
+
|
183
|
+
bytes += MixinBot::Utils.encode_unit_64 deposit['index']
|
184
|
+
|
185
|
+
amount_bytes = MixinBot::Utils.bytes_of deposit['amount']
|
186
|
+
bytes += MixinBot::Utils.encode_int amount_bytes.size
|
187
|
+
bytes += amount_bytes
|
188
|
+
end
|
189
|
+
|
190
|
+
# mint
|
191
|
+
mint = input['mint']
|
192
|
+
if mint.nil?
|
193
|
+
bytes += NULL_BYTES
|
194
|
+
else
|
195
|
+
bytes += MAGIC
|
196
|
+
|
197
|
+
# group
|
198
|
+
group = mint['group'] || ''
|
199
|
+
if group.empty?
|
200
|
+
bytes += MixinBot::Utils.encode_int NULL_BYTES
|
201
|
+
else
|
202
|
+
group_bytes = [group].pack('H*')
|
203
|
+
bytes += MixinBot::Utils.encode_int group_bytes.size
|
204
|
+
bytes += group_bytes
|
205
|
+
end
|
206
|
+
|
207
|
+
bytes += MixinBot::Utils.encode_unit_64 mint['batch']
|
208
|
+
|
209
|
+
amount_bytes = MixinBot::Utils.bytes_of mint['amount']
|
210
|
+
bytes += MixinBot::Utils.encode_int amount_bytes.size
|
211
|
+
bytes += amount_bytes
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
bytes
|
216
|
+
end
|
217
|
+
|
218
|
+
def encode_outputs
|
219
|
+
bytes = []
|
220
|
+
|
221
|
+
bytes += MixinBot::Utils.encode_int outputs.size
|
222
|
+
|
223
|
+
outputs.each do |output|
|
224
|
+
type = output['type'] || 0
|
225
|
+
bytes += [0x00, type]
|
226
|
+
|
227
|
+
# amount
|
228
|
+
amount_bytes = MixinBot::Utils.bytes_of (output['amount'].to_d * 1e8).round
|
229
|
+
bytes += MixinBot::Utils.encode_int amount_bytes.size
|
230
|
+
bytes += amount_bytes
|
231
|
+
|
232
|
+
# keys
|
233
|
+
bytes += MixinBot::Utils.encode_int output['keys'].size
|
234
|
+
output['keys'].each do |key|
|
235
|
+
bytes += [key].pack('H*').bytes
|
236
|
+
end
|
237
|
+
|
238
|
+
# mask
|
239
|
+
bytes += [output['mask']].pack('H*').bytes
|
240
|
+
|
241
|
+
# script
|
242
|
+
script_bytes = [output['script']].pack('H*').bytes
|
243
|
+
bytes += MixinBot::Utils.encode_int script_bytes.size
|
244
|
+
bytes += script_bytes
|
245
|
+
|
246
|
+
# withdrawal
|
247
|
+
withdrawal = output['withdrawal']
|
248
|
+
if withdrawal.nil?
|
249
|
+
bytes += NULL_BYTES
|
250
|
+
else
|
251
|
+
bytes += MAGIC
|
252
|
+
|
253
|
+
# chain
|
254
|
+
bytes += [withdrawal['chain']].pack('H*').bytes
|
255
|
+
|
256
|
+
# asset
|
257
|
+
@asset_bytes = [withdrawal['asset']].pack('H*')
|
258
|
+
bytes += MixinBot::Utils.encode_int asset_bytes.size
|
259
|
+
bytes += asset_bytes
|
260
|
+
|
261
|
+
# address
|
262
|
+
address = withdrawal['address'] || ''
|
263
|
+
if address.empty?
|
264
|
+
bytes += NULL_BYTES
|
265
|
+
else
|
266
|
+
address_bytes = [address].pack('H*').bytes
|
267
|
+
bytes += MixinBot::Utils.encode_int address.size
|
268
|
+
bytes += address_bytes
|
269
|
+
end
|
270
|
+
|
271
|
+
# tag
|
272
|
+
tag = withdrawal['tag'] || ''
|
273
|
+
if tag.empty?
|
274
|
+
bytes += NULL_BYTES
|
275
|
+
else
|
276
|
+
address_bytes = [tag].pack('H*').bytes
|
277
|
+
bytes += MixinBot::Utils.encode_int tag.size
|
278
|
+
bytes += address_bytes
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
bytes
|
284
|
+
end
|
285
|
+
|
286
|
+
def encode_aggregated_signature
|
287
|
+
bytes = []
|
288
|
+
|
289
|
+
bytes += MixinBot::Utils.encode_int MAX_ENCODE_INT
|
290
|
+
bytes += MixinBot::Utils.encode_int AGGREGATED_SIGNATURE_PREFIX
|
291
|
+
bytes += [aggregated['signature']].pack('H*').bytes
|
292
|
+
|
293
|
+
signers = aggregated['signers']
|
294
|
+
if signers.size == 0
|
295
|
+
bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
|
296
|
+
bytes += NULL_BYTES
|
297
|
+
else
|
298
|
+
signers.each do |sig, i|
|
299
|
+
raise ArgumentError, 'signers not sorted' if i > 0 && sig <= signers[i - 1]
|
300
|
+
raise ArgumentError, 'signers not sorted' if sig > MAX_ENCODE_INT
|
301
|
+
end
|
302
|
+
|
303
|
+
max = signers.last
|
304
|
+
if (((max / 8 | 0) + 1 | 0) > aggregated['signature'].size * 2)
|
305
|
+
bytes += AGGREGATED_SIGNATURE_SPARSE_MASK
|
306
|
+
bytes += MixinBot::Utils.encode_int aggregated['signers'].size
|
307
|
+
signers.map(&->(signer) { bytes += MixinBot::Utils.encode_int(signer) })
|
308
|
+
end
|
309
|
+
|
310
|
+
masks_bytes = Array.new(max / 8 + 1, 0)
|
311
|
+
signers.each do |signer|
|
312
|
+
masks[signer/8] = masks[signer/8] ^ (1 << (signer % 8))
|
313
|
+
end
|
314
|
+
bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
|
315
|
+
bytes += MixinBot::Utils.encode_int masks_bytes.size
|
316
|
+
bytes += masks_bytes
|
317
|
+
end
|
318
|
+
|
319
|
+
bytes
|
320
|
+
end
|
321
|
+
|
322
|
+
def encode_signatures
|
323
|
+
bytes = []
|
324
|
+
|
325
|
+
sl =
|
326
|
+
if signatures.is_a? Hash
|
327
|
+
signatures.keys.size
|
328
|
+
else
|
329
|
+
0
|
330
|
+
end
|
331
|
+
|
332
|
+
raise ArgumentError, 'signatures overflow' if sl == MAX_ENCODE_INT
|
333
|
+
bytes += MixinBot::Utils.encode_int sl
|
334
|
+
|
335
|
+
if sl > 0
|
336
|
+
bytes += MixinBot::Utils.encode_int signatures.keys.size
|
337
|
+
signatures.keys.sort.each do |key|
|
338
|
+
bytes += MixinBot::Utils.encode_int key
|
339
|
+
bytes += [signatures[key]].pack('H*').bytes
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
bytes
|
344
|
+
end
|
345
|
+
|
346
|
+
def decode_inputs
|
347
|
+
inputs_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
348
|
+
@inputs = []
|
349
|
+
inputs_size.times do
|
350
|
+
input = {}
|
351
|
+
hash = @bytes.shift(32)
|
352
|
+
input['hash'] = hash.pack('C*').unpack1('H*')
|
353
|
+
|
354
|
+
index = @bytes.shift(2)
|
355
|
+
input['index'] = index.reverse.pack('C*').unpack1('S*')
|
356
|
+
|
357
|
+
if @bytes[...2] != NULL_BYTES
|
358
|
+
genesis_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
359
|
+
genesis = @bytes.shift(genesis_size)
|
360
|
+
input['genesis'] = genesis.pack('C*').unpack1('H*')
|
361
|
+
else
|
362
|
+
@bytes.shift 2
|
363
|
+
end
|
364
|
+
|
365
|
+
if @bytes[...2] != NULL_BYTES
|
366
|
+
magic = @bytes.shift(2)
|
367
|
+
raise ArgumentError, 'Not valid input' unless magic == MAGIC
|
368
|
+
|
369
|
+
deposit = {}
|
370
|
+
deposit['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
|
371
|
+
|
372
|
+
asset_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
373
|
+
deposit['asset'] = @bytes.shift(asset_size).unpack1('H*')
|
374
|
+
|
375
|
+
transaction_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
376
|
+
deposit['transaction'] = @bytes.shift(transaction_size).unpack1('H*')
|
377
|
+
|
378
|
+
deposit['index'] = @bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
379
|
+
|
380
|
+
amount_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
381
|
+
deposit['amount'] = MixinBot::Utils.bytes_to_int @bytes.shift(amount_size)
|
382
|
+
|
383
|
+
input['deposit'] = deposit
|
384
|
+
else
|
385
|
+
@bytes.shift 2
|
386
|
+
end
|
387
|
+
|
388
|
+
if @bytes[...2] != NULL_BYTES
|
389
|
+
magic = @bytes.shift(2)
|
390
|
+
raise ArgumentError, 'Not valid input' unless magic == MAGIC
|
391
|
+
|
392
|
+
mint = {}
|
393
|
+
if bytes[...2] != NULL_BYTES
|
394
|
+
group_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
395
|
+
mint['group'] = @bytes.shift(group_size).unpack1('H*')
|
396
|
+
else
|
397
|
+
@bytes.shift 2
|
398
|
+
end
|
399
|
+
|
400
|
+
mint['batch'] = @bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
401
|
+
_amount_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
402
|
+
mint['amount'] = MixinBot::Utils.bytes_to_int bytes.shift(_amount_size)
|
403
|
+
|
404
|
+
input['mint'] = mint
|
405
|
+
else
|
406
|
+
@bytes.shift 2
|
407
|
+
end
|
408
|
+
|
409
|
+
@inputs.push input
|
410
|
+
end
|
411
|
+
|
412
|
+
self
|
413
|
+
end
|
414
|
+
|
415
|
+
def decode_outputs
|
416
|
+
outputs_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
417
|
+
@outputs = []
|
418
|
+
outputs_size.times do
|
419
|
+
output = {}
|
420
|
+
|
421
|
+
@bytes.shift
|
422
|
+
type = @bytes.shift
|
423
|
+
output['type'] = type
|
424
|
+
|
425
|
+
amount_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
426
|
+
output['amount'] = format('%.8f', MixinBot::Utils.bytes_to_int(@bytes.shift(amount_size)).to_f / 1e8)
|
427
|
+
|
428
|
+
output['keys'] = []
|
429
|
+
keys_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
430
|
+
keys_size.times do
|
431
|
+
output['keys'].push @bytes.shift(32).pack('C*').unpack1('H*')
|
432
|
+
end
|
433
|
+
|
434
|
+
output['mask'] = @bytes.shift(32).pack('C*').unpack1('H*')
|
435
|
+
|
436
|
+
script_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
437
|
+
output['script'] = @bytes.shift(script_size).pack('C*').unpack1('H*')
|
438
|
+
|
439
|
+
if @bytes[...2] != NULL_BYTES
|
440
|
+
magic = @bytes.shift(2)
|
441
|
+
raise ArgumentError, 'Not valid output' unless magic == MAGIC
|
442
|
+
|
443
|
+
withdraw = {}
|
444
|
+
|
445
|
+
output['chain'] = @bytes.shift(32).pack('C*').unpack1('H*')
|
446
|
+
|
447
|
+
asset_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
448
|
+
output['asset'] = @bytes.shift(asset_size).unpack1('H*')
|
449
|
+
|
450
|
+
if @bytes[...2] != NULL_BYTES
|
451
|
+
address = {}
|
452
|
+
|
453
|
+
adderss_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
454
|
+
output['adderss'] = @bytes.shift(adderss_size).pack('C*').unpack1('H*')
|
455
|
+
else
|
456
|
+
@bytes.shift 2
|
457
|
+
end
|
458
|
+
|
459
|
+
if @bytes[...2] != NULL_BYTES
|
460
|
+
tag = {}
|
461
|
+
|
462
|
+
tag_size = @bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
463
|
+
output['tag'] = @bytes.shift(tag_size).pack('C*').unpack1('H*')
|
464
|
+
else
|
465
|
+
@bytes.shift 2
|
466
|
+
end
|
467
|
+
else
|
468
|
+
@bytes.shift 2
|
469
|
+
end
|
470
|
+
|
471
|
+
@outputs.push output
|
472
|
+
end
|
473
|
+
|
474
|
+
self
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
module Utils
|
5
|
+
class UUID
|
6
|
+
attr_accessor :hex, :raw
|
7
|
+
|
8
|
+
def initialize(**args)
|
9
|
+
@hex = args[:hex]
|
10
|
+
@raw = args[:raw]
|
11
|
+
|
12
|
+
raise MixinBot::InvalidUuidFormatError if raw.present? && raw.size != 16
|
13
|
+
raise MixinBot::InvalidUuidFormatError if hex.present? && hex.gsub('-', '').size != 32
|
14
|
+
end
|
15
|
+
|
16
|
+
def packed
|
17
|
+
if raw.present?
|
18
|
+
raw
|
19
|
+
elsif hex.present?
|
20
|
+
[hex.gsub('-', '')].pack('H*')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def unpacked
|
25
|
+
_hex =
|
26
|
+
if hex.present?
|
27
|
+
hex.gsub('-', '')
|
28
|
+
elsif raw.present?
|
29
|
+
_hex = raw.unpack1('H*')
|
30
|
+
end
|
31
|
+
|
32
|
+
format(
|
33
|
+
'%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
|
34
|
+
first: _hex[0..7],
|
35
|
+
second: _hex[8..11],
|
36
|
+
third: _hex[12..15],
|
37
|
+
forth: _hex[16..19],
|
38
|
+
fifth: _hex[20..]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/mixin_bot/utils.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './utils/nfo'
|
4
|
+
require_relative './utils/uuid'
|
5
|
+
require_relative './utils/transaction'
|
6
|
+
|
3
7
|
module MixinBot
|
4
8
|
module Utils
|
5
9
|
class << self
|
@@ -10,12 +14,6 @@ module MixinBot
|
|
10
14
|
AGGREGATED_SIGNATURE_PREFIX = 0xFF01
|
11
15
|
AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00]
|
12
16
|
AGGREGATED_SIGNATURE_SPARSE_MASK = [0x01]
|
13
|
-
NFT_MEMO_PREFIX = 'NFO'
|
14
|
-
NFT_MEMO_VERSION = 0x00
|
15
|
-
NFT_MEMO_DEFAULT_CHAIN = '43d61dcd-e413-450d-80b8-101d5e903357'
|
16
|
-
NFT_MEMO_DEFAULT_CLASS = '3c8c161a18ae2c8b14fda1216fff7da88c419b5d'
|
17
|
-
NFT_MASK = 0x00
|
18
|
-
NULL_UUID = '00000000-0000-0000-0000-000000000000'
|
19
17
|
|
20
18
|
def generate_unique_uuid(uuid_1, uuid_2)
|
21
19
|
md5 = Digest::MD5.new
|
@@ -25,16 +23,8 @@ module MixinBot
|
|
25
23
|
digest6 = (digest[6].ord & 0x0f | 0x30).chr
|
26
24
|
digest8 = (digest[8].ord & 0x3f | 0x80).chr
|
27
25
|
cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
|
28
|
-
hex = cipher.unpack1('H*')
|
29
26
|
|
30
|
-
|
31
|
-
'%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
|
32
|
-
first: hex[0..7],
|
33
|
-
second: hex[8..11],
|
34
|
-
third: hex[12..15],
|
35
|
-
forth: hex[16..19],
|
36
|
-
fifth: hex[20..]
|
37
|
-
)
|
27
|
+
UUID.new(raw: cipher).unpacked
|
38
28
|
end
|
39
29
|
|
40
30
|
def unique_uuid(*uuids)
|
@@ -54,13 +44,12 @@ module MixinBot
|
|
54
44
|
digest = md5.digest
|
55
45
|
digest[6] = ((digest[6].ord & 0x0f) | 0x30).chr
|
56
46
|
digest[8] = ((digest[8].ord & 0x3f) | 0x80).chr
|
57
|
-
hex = digest.unpack1('H*')
|
58
47
|
|
59
|
-
|
48
|
+
UUID.new(raw: digest).unpacked
|
60
49
|
end
|
61
50
|
|
62
51
|
def hex_to_uuid(hex)
|
63
|
-
|
52
|
+
UUID.new(hex: hex).unpacked
|
64
53
|
end
|
65
54
|
|
66
55
|
def sign_raw_transaction(tx)
|
@@ -70,197 +59,27 @@ module MixinBot
|
|
70
59
|
raise ArgumentError, "#{tx} is not a valid json" unless tx.is_a? Hash
|
71
60
|
|
72
61
|
tx = tx.with_indifferent_access
|
73
|
-
bytes = []
|
74
|
-
|
75
|
-
# magic number
|
76
|
-
bytes += MAGIC
|
77
|
-
|
78
|
-
# version
|
79
|
-
bytes += [0, tx['version']]
|
80
|
-
|
81
|
-
# asset
|
82
|
-
bytes += [tx['asset']].pack('H*').bytes
|
83
|
-
|
84
|
-
# inputs
|
85
|
-
bytes += encode_inputs tx['inputs']
|
86
|
-
|
87
|
-
# output
|
88
|
-
bytes += encode_outputs tx['outputs']
|
89
|
-
|
90
|
-
# extra
|
91
|
-
extra_bytes = [tx['extra']].pack('H*').bytes
|
92
|
-
bytes += encode_int extra_bytes.size
|
93
|
-
bytes += extra_bytes
|
94
|
-
|
95
|
-
# aggregated
|
96
|
-
aggregated = tx['aggregated']
|
97
|
-
if aggregated.nil?
|
98
|
-
# signatures
|
99
|
-
bytes += encode_signatures tx['signatures']
|
100
|
-
else
|
101
|
-
bytes += encode_aggregated_signature aggregated
|
102
|
-
end
|
103
|
-
|
104
|
-
bytes.pack('C*').unpack1('H*')
|
105
|
-
end
|
106
|
-
|
107
|
-
def decode_raw_transaction(raw)
|
108
|
-
bytes = [raw].pack('H*').bytes
|
109
|
-
tx = {}
|
110
|
-
|
111
|
-
magic = bytes.shift(2)
|
112
|
-
raise ArgumentError, 'Not valid raw' unless magic == MAGIC
|
113
|
-
|
114
|
-
version = bytes.shift(2)
|
115
|
-
tx['version'] = bytes_to_int version
|
116
|
-
|
117
|
-
asset = bytes.shift(32)
|
118
|
-
tx['asset'] = asset.pack('C*').unpack1('H*')
|
119
|
-
|
120
|
-
# read inputs
|
121
|
-
bytes, tx = decode_inputs bytes, tx
|
122
|
-
|
123
|
-
# read outputs
|
124
|
-
bytes, tx = decode_outputs bytes, tx
|
125
|
-
|
126
|
-
extra_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
127
|
-
tx['extra'] = bytes.shift(extra_size).pack('C*').unpack1('H*')
|
128
|
-
|
129
|
-
num = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
130
|
-
if num == MAX_ENCODE_INT
|
131
|
-
# aggregated
|
132
|
-
aggregated = {}
|
133
|
-
|
134
|
-
raise ArgumentError, 'invalid aggregated' unless bytes.shift(2).reverse.pack('C*').unpack1('S*') == AGGREGATED_SIGNATURE_PREFIX
|
135
|
-
|
136
|
-
aggregated['signature'] = bytes.shift(64).pack('C*').unpack1('H*')
|
137
|
-
|
138
|
-
byte = bytes.shift
|
139
|
-
case byte
|
140
|
-
when AGGREGATED_SIGNATURE_ORDINAY_MASK.first
|
141
|
-
aggregated['signers'] = []
|
142
|
-
masks_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
143
|
-
masks = bytes.shift(masks_size)
|
144
|
-
masks = [masks] unless masks.is_a? Array
|
145
|
-
|
146
|
-
masks.each_with_index do |mask, i|
|
147
|
-
8.times do |j|
|
148
|
-
k = 1 << j
|
149
|
-
aggregated['signers'].push(i * 8 + j) if mask & k == k
|
150
|
-
end
|
151
|
-
end
|
152
|
-
when AGGREGATED_SIGNATURE_SPARSE_MASK.first
|
153
|
-
signers_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
154
|
-
return if signers_size == 0
|
155
|
-
|
156
|
-
aggregated['signers'] = []
|
157
|
-
signers_size.times do
|
158
|
-
aggregated['signers'].push bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
tx['aggregated'] = aggregated
|
163
|
-
else
|
164
|
-
if !bytes.empty? && bytes[...2] != NULL_BYTES
|
165
|
-
signatures_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
166
|
-
tx['signatures'] = bytes.shift(signatures_size).pack('C*').unpack1('H*')
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
tx
|
171
|
-
end
|
172
|
-
|
173
|
-
def nft_memo_hash(collection, token_id, hash)
|
174
|
-
collection = NULL_UUID if collection.empty?
|
175
|
-
raise ArgumentError, 'hash must be 256-bit string' unless hash.is_a?(String) && hash.size == 64
|
176
|
-
|
177
|
-
memo = {
|
178
|
-
prefix: NFT_MEMO_PREFIX,
|
179
|
-
version: NFT_MEMO_VERSION,
|
180
|
-
mask: 0,
|
181
|
-
chain: NFT_MEMO_DEFAULT_CHAIN,
|
182
|
-
class: NFT_MEMO_DEFAULT_CLASS,
|
183
|
-
collection: collection,
|
184
|
-
token: token_id.to_i,
|
185
|
-
extra: hash
|
186
|
-
}
|
187
|
-
|
188
|
-
mark = [0]
|
189
|
-
mark.map do |index|
|
190
|
-
if index >= 64
|
191
|
-
raise ArgumentError, "invalid NFO memo index #{index}"
|
192
|
-
end
|
193
|
-
memo[:mask] = memo[:mask] ^ (1 << index)
|
194
|
-
end
|
195
62
|
|
196
|
-
|
63
|
+
Transaction.new(
|
64
|
+
asset: tx[:asset],
|
65
|
+
inputs: tx[:inputs],
|
66
|
+
outputs: tx[:outputs],
|
67
|
+
extra: tx[:extra],
|
68
|
+
).encode.hex
|
197
69
|
end
|
198
70
|
|
199
|
-
def
|
200
|
-
|
71
|
+
def decode_raw_transaction(hex)
|
72
|
+
Transaction.new(hex: hex).decode.to_h
|
201
73
|
end
|
202
74
|
|
203
|
-
def
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
if memo[:mask] != 0
|
210
|
-
bytes += [1]
|
211
|
-
bytes += encode_unit_64 memo[:mask]
|
212
|
-
bytes += memo[:chain].split('-').pack('H* H* H* H* H*').bytes
|
213
|
-
|
214
|
-
class_bytes = [memo[:class]].pack('H*').bytes
|
215
|
-
bytes += bytes_of class_bytes.size
|
216
|
-
bytes += class_bytes
|
217
|
-
|
218
|
-
collection_bytes = memo[:collection].split('-').pack('H* H* H* H* H*').bytes
|
219
|
-
bytes += bytes_of collection_bytes.size
|
220
|
-
bytes += collection_bytes
|
221
|
-
|
222
|
-
# token_bytes = memo[:token].split('-').pack('H* H* H* H* H*').bytes
|
223
|
-
token_bytes = bytes_of memo[:token]
|
224
|
-
bytes += bytes_of token_bytes.size
|
225
|
-
bytes += token_bytes
|
226
|
-
end
|
227
|
-
|
228
|
-
extra_bytes = [memo[:extra]].pack('H*').bytes
|
229
|
-
bytes += bytes_of extra_bytes.size
|
230
|
-
bytes += extra_bytes
|
231
|
-
|
232
|
-
Base64.urlsafe_encode64 bytes.pack('C*'), padding: false
|
233
|
-
end
|
234
|
-
|
235
|
-
def decode_nft_memo(encoded)
|
236
|
-
bytes = Base64.urlsafe_decode64(encoded).bytes
|
237
|
-
memo = {}
|
238
|
-
memo[:prefix] = bytes.shift(3).pack('C*')
|
239
|
-
memo[:version] = bytes.shift
|
240
|
-
|
241
|
-
hint = bytes.shift
|
242
|
-
if hint == 1
|
243
|
-
memo[:mask] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
244
|
-
memo[:chain] = hex_to_uuid bytes.shift(16).pack('C*').unpack1('H*')
|
245
|
-
|
246
|
-
class_length = bytes.shift
|
247
|
-
memo[:class] = bytes.shift(class_length).pack('C*').unpack1('H*')
|
248
|
-
|
249
|
-
collection_length = bytes.shift
|
250
|
-
memo[:collection] = hex_to_uuid bytes.shift(collection_length).pack('C*').unpack1('H*')
|
251
|
-
|
252
|
-
token_length = bytes.shift
|
253
|
-
memo[:token] = bytes_to_int bytes.shift(token_length)
|
254
|
-
end
|
255
|
-
|
256
|
-
extra_length = bytes.shift
|
257
|
-
memo[:extra] = bytes.shift(extra_length).pack('C*').unpack1('H*')
|
258
|
-
|
259
|
-
memo
|
75
|
+
def nft_memo(collection, token, extra)
|
76
|
+
MixinBot::Utils::Nfo.new(
|
77
|
+
collection: collection,
|
78
|
+
token: token,
|
79
|
+
extra: extra
|
80
|
+
).mint_memo
|
260
81
|
end
|
261
82
|
|
262
|
-
private
|
263
|
-
|
264
83
|
def encode_int(int)
|
265
84
|
raise ArgumentError, "only support int #{int}" unless int.is_a?(Integer)
|
266
85
|
raise ArgumentError,"int #{int} is larger than MAX_ENCODE_INT #{MAX_ENCODE_INT}" if int > MAX_ENCODE_INT
|
@@ -293,325 +112,6 @@ module MixinBot
|
|
293
112
|
|
294
113
|
int
|
295
114
|
end
|
296
|
-
|
297
|
-
def encode_inputs(inputs, bytes = [])
|
298
|
-
bytes += encode_int(inputs.size)
|
299
|
-
inputs.each do |input|
|
300
|
-
bytes += [input['hash']].pack('H*').bytes
|
301
|
-
bytes += encode_int(input['index'])
|
302
|
-
|
303
|
-
# genesis
|
304
|
-
genesis = input['genesis'] || ''
|
305
|
-
if genesis.empty?
|
306
|
-
bytes += NULL_BYTES
|
307
|
-
else
|
308
|
-
genesis_bytes = [genesis].pack('H*').bytes
|
309
|
-
bytes += encode_int genesis_bytes.size
|
310
|
-
bytes += genesis_bytes
|
311
|
-
end
|
312
|
-
|
313
|
-
# deposit
|
314
|
-
deposit = input['deposit']
|
315
|
-
if deposit.nil?
|
316
|
-
bytes += NULL_BYTES
|
317
|
-
else
|
318
|
-
bytes += MAGIC
|
319
|
-
bytes += [deposit['chain']].pack('H*').bytes
|
320
|
-
|
321
|
-
asset_bytes = [deposit['asset']].pack('H*')
|
322
|
-
bytes += encode_int asset_bytes.size
|
323
|
-
bytes += asset_bytes
|
324
|
-
|
325
|
-
transaction_bytes = [deposit['transaction']].pack('H*')
|
326
|
-
bytes += encode_int transaction_bytes.size
|
327
|
-
bytes += transaction_bytes
|
328
|
-
|
329
|
-
bytes += encode_unit_64 deposit['index']
|
330
|
-
|
331
|
-
amount_bytes = bytes_of deposit['amount']
|
332
|
-
bytes += encode_int amount_bytes.size
|
333
|
-
bytes += amount_bytes
|
334
|
-
end
|
335
|
-
|
336
|
-
# mint
|
337
|
-
mint = input['mint']
|
338
|
-
if mint.nil?
|
339
|
-
bytes += NULL_BYTES
|
340
|
-
else
|
341
|
-
bytes += MAGIC
|
342
|
-
|
343
|
-
# group
|
344
|
-
group = mint['group'] || ''
|
345
|
-
if group.empty?
|
346
|
-
bytes += encode_int NULL_BYTES
|
347
|
-
else
|
348
|
-
group_bytes = [group].pack('H*')
|
349
|
-
bytes += encode_int group_bytes.size
|
350
|
-
bytes += group_bytes
|
351
|
-
end
|
352
|
-
|
353
|
-
bytes += encode_unit_64 mint['batch']
|
354
|
-
|
355
|
-
amount_bytes = bytes_of mint['amount']
|
356
|
-
bytes += encode_int amount_bytes.size
|
357
|
-
bytes += amount_bytes
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
bytes
|
362
|
-
end
|
363
|
-
|
364
|
-
def encode_outputs(outputs, bytes = [])
|
365
|
-
bytes += encode_int(outputs.size)
|
366
|
-
outputs.each do |output|
|
367
|
-
type = output['type'] || 0
|
368
|
-
bytes += [0x00, type]
|
369
|
-
|
370
|
-
# amount
|
371
|
-
amount_bytes = bytes_of (output['amount'].to_f * 1e8).round
|
372
|
-
bytes += encode_int amount_bytes.size
|
373
|
-
bytes += amount_bytes
|
374
|
-
|
375
|
-
# keys
|
376
|
-
bytes += encode_int output['keys'].size
|
377
|
-
output['keys'].each do |key|
|
378
|
-
bytes += [key].pack('H*').bytes
|
379
|
-
end
|
380
|
-
|
381
|
-
# mask
|
382
|
-
bytes += [output['mask']].pack('H*').bytes
|
383
|
-
|
384
|
-
# script
|
385
|
-
script_bytes = [output['script']].pack('H*').bytes
|
386
|
-
bytes += encode_int script_bytes.size
|
387
|
-
bytes += script_bytes
|
388
|
-
|
389
|
-
# withdrawal
|
390
|
-
withdrawal = output['withdrawal']
|
391
|
-
if withdrawal.nil?
|
392
|
-
bytes += NULL_BYTES
|
393
|
-
else
|
394
|
-
bytes += MAGIC
|
395
|
-
|
396
|
-
# chain
|
397
|
-
bytes += [withdrawal['chain']].pack('H*').bytes
|
398
|
-
|
399
|
-
# asset
|
400
|
-
asset_bytes = [withdrawal['asset']].pack('H*')
|
401
|
-
bytes += encode_int asset_bytes.size
|
402
|
-
bytes += asset_bytes
|
403
|
-
|
404
|
-
# address
|
405
|
-
address = withdrawal['address'] || ''
|
406
|
-
if address.empty?
|
407
|
-
bytes += NULL_BYTES
|
408
|
-
else
|
409
|
-
address_bytes = [address].pack('H*').bytes
|
410
|
-
bytes += encode_int address.size
|
411
|
-
bytes += address_bytes
|
412
|
-
end
|
413
|
-
|
414
|
-
# tag
|
415
|
-
tag = withdrawal['tag'] || ''
|
416
|
-
if tag.empty?
|
417
|
-
bytes += NULL_BYTES
|
418
|
-
else
|
419
|
-
address_bytes = [tag].pack('H*').bytes
|
420
|
-
bytes += encode_int tag.size
|
421
|
-
bytes += address_bytes
|
422
|
-
end
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
bytes
|
427
|
-
end
|
428
|
-
|
429
|
-
def encode_aggregated_signature(aggregated, bytes = [])
|
430
|
-
bytes += encode_int MAX_ENCODE_INT
|
431
|
-
bytes += encode_int AGGREGATED_SIGNATURE_PREFIX
|
432
|
-
bytes += [aggregated['signature']].pack('H*').bytes
|
433
|
-
|
434
|
-
signers = aggregated['signers']
|
435
|
-
if signers.size == 0
|
436
|
-
bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
|
437
|
-
bytes += NULL_BYTES
|
438
|
-
else
|
439
|
-
signers.each do |sig, i|
|
440
|
-
raise ArgumentError, 'signers not sorted' if i > 0 && sig <= signers[i - 1]
|
441
|
-
raise ArgumentError, 'signers not sorted' if sig > MAX_ENCODE_INT
|
442
|
-
end
|
443
|
-
|
444
|
-
max = signers.last
|
445
|
-
if (((max / 8 | 0) + 1 | 0) > aggregated['signature'].size * 2)
|
446
|
-
bytes += AGGREGATED_SIGNATURE_SPARSE_MASK
|
447
|
-
bytes += encode_int aggregated['signers'].size
|
448
|
-
signers.map(&->(signer) { bytes += encode_int(signer) })
|
449
|
-
end
|
450
|
-
|
451
|
-
masks_bytes = Array.new(max / 8 + 1, 0)
|
452
|
-
signers.each do |signer|
|
453
|
-
masks[signer/8] = masks[signer/8] ^ (1 << (signer % 8))
|
454
|
-
end
|
455
|
-
bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
|
456
|
-
bytes += encode_int masks_bytes.size
|
457
|
-
bytes += masks_bytes
|
458
|
-
end
|
459
|
-
|
460
|
-
bytes
|
461
|
-
end
|
462
|
-
|
463
|
-
def encode_signatures(signatures, bytes = [])
|
464
|
-
sl =
|
465
|
-
if signatures.is_a? Hash
|
466
|
-
signatures.keys.size
|
467
|
-
else
|
468
|
-
0
|
469
|
-
end
|
470
|
-
|
471
|
-
raise ArgumentError, 'signatures overflow' if sl == MAX_ENCODE_INT
|
472
|
-
bytes += encode_int sl
|
473
|
-
|
474
|
-
if sl > 0
|
475
|
-
bytes += encode_int signatures.keys.size
|
476
|
-
signatures.keys.sort.each do |key|
|
477
|
-
bytes += encode_int key
|
478
|
-
bytes += [signatures[key]].pack('H*').bytes
|
479
|
-
end
|
480
|
-
end
|
481
|
-
|
482
|
-
bytes
|
483
|
-
end
|
484
|
-
|
485
|
-
def decode_inputs(bytes, tx)
|
486
|
-
inputs_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
487
|
-
tx['inputs'] = []
|
488
|
-
inputs_size.times do
|
489
|
-
input = {}
|
490
|
-
hash = bytes.shift(32)
|
491
|
-
input['hash'] = hash.pack('C*').unpack1('H*')
|
492
|
-
|
493
|
-
index = bytes.shift(2)
|
494
|
-
input['index'] = index.reverse.pack('C*').unpack1('S*')
|
495
|
-
|
496
|
-
if bytes[...2] != NULL_BYTES
|
497
|
-
genesis_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
498
|
-
genesis = bytes.shift(genesis_size)
|
499
|
-
input['genesis'] = genesis.pack('C*').unpack1('H*')
|
500
|
-
else
|
501
|
-
bytes.shift 2
|
502
|
-
end
|
503
|
-
|
504
|
-
if bytes[...2] != NULL_BYTES
|
505
|
-
magic = bytes.shift(2)
|
506
|
-
raise ArgumentError, 'Not valid input' unless magic == MAGIC
|
507
|
-
|
508
|
-
deposit = {}
|
509
|
-
deposit['chain'] = bytes.shift(32).pack('C*').unpack1('H*')
|
510
|
-
|
511
|
-
asset_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
512
|
-
deposit['asset'] = bytes.shift(asset_size).unpack1('H*')
|
513
|
-
|
514
|
-
transaction_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
515
|
-
deposit['transaction'] = bytes.shift(transaction_size).unpack1('H*')
|
516
|
-
|
517
|
-
deposit['index'] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
518
|
-
|
519
|
-
amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
520
|
-
deposit['amount'] = bytes_to_int bytes.shift(amount_size)
|
521
|
-
|
522
|
-
input['deposit'] = deposit
|
523
|
-
else
|
524
|
-
bytes.shift 2
|
525
|
-
end
|
526
|
-
|
527
|
-
if bytes[...2] != NULL_BYTES
|
528
|
-
magic = bytes.shift(2)
|
529
|
-
raise ArgumentError, 'Not valid input' unless magic == MAGIC
|
530
|
-
|
531
|
-
mint = {}
|
532
|
-
if bytes[...2] != NULL_BYTES
|
533
|
-
group_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
534
|
-
mint['group'] = bytes.shift(group_size).unpack1('H*')
|
535
|
-
else
|
536
|
-
bytes.shift 2
|
537
|
-
end
|
538
|
-
|
539
|
-
mint['batch'] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
540
|
-
_amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
541
|
-
mint['amount'] = bytes_to_int bytes.shift(_amount_size)
|
542
|
-
|
543
|
-
input['mint'] = mint
|
544
|
-
else
|
545
|
-
bytes.shift 2
|
546
|
-
end
|
547
|
-
|
548
|
-
tx['inputs'].push input
|
549
|
-
end
|
550
|
-
|
551
|
-
[bytes, tx]
|
552
|
-
end
|
553
|
-
|
554
|
-
def decode_outputs(bytes, tx)
|
555
|
-
outputs_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
556
|
-
tx['outputs'] = []
|
557
|
-
outputs_size.times do
|
558
|
-
output = {}
|
559
|
-
|
560
|
-
bytes.shift
|
561
|
-
type = bytes.shift
|
562
|
-
output['type'] = type
|
563
|
-
|
564
|
-
amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
565
|
-
output['amount'] = format('%.8f', bytes_to_int(bytes.shift(amount_size)).to_f / 1e8)
|
566
|
-
|
567
|
-
output['keys'] = []
|
568
|
-
keys_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
569
|
-
keys_size.times do
|
570
|
-
output['keys'].push bytes.shift(32).pack('C*').unpack1('H*')
|
571
|
-
end
|
572
|
-
|
573
|
-
output['mask'] = bytes.shift(32).pack('C*').unpack1('H*')
|
574
|
-
|
575
|
-
script_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
576
|
-
output['script'] = bytes.shift(script_size).pack('C*').unpack1('H*')
|
577
|
-
|
578
|
-
if bytes[...2] != NULL_BYTES
|
579
|
-
magic = bytes.shift(2)
|
580
|
-
raise ArgumentError, 'Not valid output' unless magic == MAGIC
|
581
|
-
|
582
|
-
withdraw = {}
|
583
|
-
|
584
|
-
output['chain'] = bytes.shift(32).pack('C*').unpack1('H*')
|
585
|
-
|
586
|
-
asset_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
587
|
-
output['asset'] = bytes.shift(asset_size).unpack1('H*')
|
588
|
-
|
589
|
-
if bytes[...2] != NULL_BYTES
|
590
|
-
address = {}
|
591
|
-
|
592
|
-
adderss_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
593
|
-
output['adderss'] = bytes.shift(adderss_size).pack('C*').unpack1('H*')
|
594
|
-
else
|
595
|
-
bytes.shift 2
|
596
|
-
end
|
597
|
-
|
598
|
-
if bytes[...2] != NULL_BYTES
|
599
|
-
tag = {}
|
600
|
-
|
601
|
-
tag_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
|
602
|
-
output['tag'] = bytes.shift(tag_size).pack('C*').unpack1('H*')
|
603
|
-
else
|
604
|
-
bytes.shift 2
|
605
|
-
end
|
606
|
-
else
|
607
|
-
bytes.shift 2
|
608
|
-
end
|
609
|
-
|
610
|
-
tx['outputs'].push output
|
611
|
-
end
|
612
|
-
|
613
|
-
[bytes, tx]
|
614
|
-
end
|
615
115
|
end
|
616
116
|
end
|
617
117
|
end
|
data/lib/mixin_bot/version.rb
CHANGED
data/lib/mixin_bot.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# third-party dependencies
|
4
4
|
require 'English'
|
5
|
-
require 'active_support'
|
5
|
+
require 'active_support/all'
|
6
6
|
require 'base64'
|
7
7
|
require 'bigdecimal'
|
8
8
|
require 'bigdecimal/util'
|
@@ -41,4 +41,7 @@ module MixinBot
|
|
41
41
|
class InsufficientBalanceError < Error; end
|
42
42
|
class InsufficientPoolError < Error; end
|
43
43
|
class PinError < Error; end
|
44
|
+
class InvalidNfoFormatError < MixinBot::Error; end
|
45
|
+
class InvalidUuidFormatError < MixinBot::Error; end
|
46
|
+
class InvalidTransactionFormatError < MixinBot::Error; end
|
44
47
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mixin_bot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- an-lee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -298,6 +298,9 @@ files:
|
|
298
298
|
- lib/mixin_bot/cli/utils.rb
|
299
299
|
- lib/mixin_bot/client.rb
|
300
300
|
- lib/mixin_bot/utils.rb
|
301
|
+
- lib/mixin_bot/utils/nfo.rb
|
302
|
+
- lib/mixin_bot/utils/transaction.rb
|
303
|
+
- lib/mixin_bot/utils/uuid.rb
|
301
304
|
- lib/mixin_bot/version.rb
|
302
305
|
homepage: https://github.com/an-lee/mixin_bot
|
303
306
|
licenses:
|