mixin_bot 0.5.6 → 0.6.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f65a69b3d924aecec8bde921fcc600f117937c25ad656dc0779db1db512b2570
4
- data.tar.gz: 8cae0fb36dd2244cd97892f9410fdda8d86c775451496105bfeac65bfac2cfc8
3
+ metadata.gz: '0482fa8ed14e5435679aba1bd40e8f9d13a4db3c4f96f2b7fe97f2639d7d55d1'
4
+ data.tar.gz: 506032e530c347644b878d1f1ba7a520208ca9c5ee1450351657291af46c9153
5
5
  SHA512:
6
- metadata.gz: 2217346a169bbb546e2a63d4c9b3c3116eddcfb2563ad90d294dc945cc8d9e5617b20d20d0fc8a50b0d95981e9a9d06817afd6f4788ceb06ee7459808ef7de41
7
- data.tar.gz: e566c88279c8337ccc1c03f92ea6a3c41e9cdc1a6f854c76ef64432d65c11cc78a2c3f3fcba32a5d2f70f952d56538b90150de917ddcd11c478091f427eb098a
6
+ metadata.gz: b855af1cda16ae26a7b224593adabadc5f9636f364ad36bccbcba4117ad631a38ff70abc35c3cac05df81a6bf14235d923029699df8256c8f05b59a0c697d46a
7
+ data.tar.gz: 7cd57b5eabe7726b5fe4d751e85ad1a4a36754b8b547a0a27baad26ccffff16a7731dc0dd3151be011e872b79a34444a6bd74b22ec550d4e567231a0b85a8552
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Collectible
6
+ NFT_ASSET_MIXIN_ID = '1700941284a95f31b25ec8c546008f208f88eee4419ccdcdbe6e3195e60128ca'
7
+
8
+ def collectible(id, access_token: nil)
9
+ path = "/collectibles/tokens/#{id}"
10
+ access_token ||= access_token('GET', path, '')
11
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
12
+ client.get(path, headers: { 'Authorization': authorization })
13
+ end
14
+
15
+ def collectible_outputs(**kwargs)
16
+ limit = kwargs[:limit] || 100
17
+ offset = kwargs[:offset] || ''
18
+ state = kwargs[:state] || ''
19
+ members = kwargs[:members] || [client_id]
20
+ threshold = kwargs[:threshold] || 1
21
+ access_token = kwargs[:access_token]
22
+ members = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
23
+
24
+ path = format(
25
+ '/collectibles/outputs?limit=%<limit>s&offset=%<offset>s&state=%<state>s&members=%<members>s&threshold=%<threshold>s',
26
+ limit: limit,
27
+ offset: offset,
28
+ state: state,
29
+ members: members,
30
+ threshold: threshold
31
+ )
32
+ access_token ||= access_token('GET', path, '')
33
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
34
+ client.get(path, headers: { 'Authorization': authorization })
35
+ end
36
+ alias collectibles collectible_outputs
37
+
38
+ COLLECTABLE_REQUEST_ACTIONS = %i[sign unlock].freeze
39
+ def create_collectible_request(action, raw, access_token: nil)
40
+ raise ArgumentError, "request action is limited in #{COLLECTABLE_REQUEST_ACTIONS.join(', ')}" unless COLLECTABLE_REQUEST_ACTIONS.include? action.to_sym
41
+ path = '/collectibles/requests'
42
+ payload = {
43
+ action: action,
44
+ raw: raw
45
+ }
46
+ access_token ||= access_token('POST', path, payload.to_json)
47
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
48
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
49
+ end
50
+
51
+ def create_sign_collectible_request(raw, access_token: nil)
52
+ create_collectible_request 'sign', raw, access_token: access_token
53
+ end
54
+
55
+ def create_unlock_collectible_request(raw, access_token: nil)
56
+ create_collectible_request 'unlock', raw, access_token: access_token
57
+ end
58
+
59
+ def sign_collectible_request(request_id, pin)
60
+ path = format('/collectibles/requests/%<request_id>s/sign', request_id: request_id)
61
+ payload = {
62
+ pin: encrypt_pin(pin)
63
+ }
64
+ access_token ||= access_token('POST', path, payload.to_json)
65
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
66
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
67
+ end
68
+
69
+ def unlock_collectible_request(request_id, pin)
70
+ path = format('/collectibles/requests/%<request_id>s/unlock', request_id: request_id)
71
+ payload = {
72
+ pin: encrypt_pin(pin)
73
+ }
74
+ access_token ||= access_token('POST', path, payload.to_json)
75
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
76
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
77
+ end
78
+
79
+ def cancel_collectible_request(request_id, pin)
80
+ path = format('/collectibles/requests/%<request_id>s/cancel', request_id: request_id)
81
+ payload = {
82
+ pin: encrypt_pin(pin)
83
+ }
84
+ access_token ||= access_token('POST', path, payload.to_json)
85
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
86
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
87
+ end
88
+ end
89
+
90
+ # collectible = {
91
+ # type: 'non_fungible_output',
92
+ # user_id: '',
93
+ # output_id: '',
94
+ # token_id: '',
95
+ # transaction_hash: '',
96
+ # output_index: '',
97
+ # amount: 1,
98
+ # senders: [],
99
+ # sender_threshold: 1,
100
+ # receivers: [],
101
+ # receivers_threshold: 1,
102
+ # state: 'unspent'
103
+ # }
104
+ COLLECTIBLE_TRANSACTION_ARGUMENTS = %i[collectible nfo receivers threshold].freeze
105
+ def build_collectible_transaction(**kwargs)
106
+ raise ArgumentError, "#{COLLECTIBLE_TRANSACTION_ARGUMENTS.join(', ')} are needed for build collectible transaction" unless COLLECTIBLE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
107
+
108
+ kwargs = kwargs.with_indifferent_access
109
+ collectible = kwargs['collectible']
110
+ raise "collectible is #{collectible['state']}" unless collectible['state'] == 'unspent'
111
+
112
+ build_raw_transaction(
113
+ utxos: [collectible],
114
+ senders: collectible['receivers'],
115
+ receivers: kwargs['receivers'],
116
+ threshold: kwargs['threshold'],
117
+ extra: kwargs["nfo"],
118
+ amount: 1,
119
+ asset_mixin_id: NFT_ASSET_MIXIN_ID
120
+ )
121
+ end
122
+
123
+ def nft_memo(collection, token_id, meta)
124
+ MixinBot::Utils.nft_memo collection, token_id, meta
125
+ end
126
+ end
127
+ end
@@ -123,24 +123,7 @@ module MixinBot
123
123
  end
124
124
 
125
125
  def unique_uuid(user_id, opponent_id = nil)
126
- opponent_id ||= client_id
127
- md5 = Digest::MD5.new
128
- md5 << [user_id, opponent_id].min
129
- md5 << [user_id, opponent_id].max
130
- digest = md5.digest
131
- digest6 = (digest[6].ord & 0x0f | 0x30).chr
132
- digest8 = (digest[8].ord & 0x3f | 0x80).chr
133
- cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
134
- hex = cipher.unpack1('H*')
135
-
136
- format(
137
- '%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
138
- first: hex[0..7],
139
- second: hex[8..11],
140
- third: hex[12..15],
141
- forth: hex[16..19],
142
- fifth: hex[20..]
143
- )
126
+ MixinBot::Utils.unique_uuid user_id, opponent_id
144
127
  end
145
128
  alias unique_conversation_id unique_uuid
146
129
  end
@@ -3,31 +3,6 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Multisig
6
- # https://w3c.group/c/1574309272319630
7
-
8
- # {"data":[
9
- # {
10
- # "type":"multisig_utxo",
11
- # "user_id":"514ae2ff-c24e-4379-a482-e2c0f798ebb1",
12
- # "utxo_id":"94711ac9-5981-4fe3-8c0e-19622219ea72",
13
- # "asset_id":"965e5c6e-434c-3fa9-b780-c50f43cd955c",
14
- # "transaction_hash":"2e67f3e36ee4b3c13effcc8a9aaafeb8122cad98f72d9ccc04d65a5ada2aa39d",
15
- # "output_index":0,
16
- # "amount":"0.123456",
17
- # "threshold":2,
18
- # "members":[
19
- # "514ae2ff-c24e-4379-a482-e2c0f798ebb1",
20
- # "13ce6c86-307a-5187-98b0-76424cbc0fbf",
21
- # "2b9df368-8e3e-46ce-ac57-e6111e8ff50e",
22
- # "3cb87491-4fa0-4c2f-b387-262b63cbc412"
23
- # ],
24
- # "memo":"难道你是女生",
25
- # "state":"unspent",
26
- # "created_at":"2019-11-03T13:30:43.922655Z",
27
- # "signed_by":"",
28
- # "signed_tx":""
29
- # }
30
- # ]}
31
6
  def outputs(**kwargs)
32
7
  limit = kwargs[:limit] || 100
33
8
  offset = kwargs[:offset] || ''
@@ -50,25 +25,27 @@ module MixinBot
50
25
  client.get(path, headers: { 'Authorization': authorization })
51
26
  end
52
27
  alias multisigs outputs
28
+ alias multisig_outputs outputs
53
29
 
54
- def create_output(receivers:, index:, access_token: nil)
30
+ def create_output(receivers:, index:, hint: nil, access_token: nil)
55
31
  path = '/outputs'
56
32
  payload = {
57
33
  receivers: receivers,
58
- index: index
34
+ index: index,
35
+ hint: hint
59
36
  }
60
37
  access_token ||= access_token('POST', path, payload.to_json)
61
38
  authorization = format('Bearer %<access_token>s', access_token: access_token)
62
39
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
63
40
  end
64
41
 
65
- # transfer from the multisig address
66
- # create a request for multi sign
67
- # for now, raw(RAW-TRANSACTION-HEX) can only be generated by Mixin SDK of Golang or Javascript
68
- def create_sign_multisig_request(raw, access_token: nil)
42
+ MULTISIG_REQUEST_ACTIONS = %i[sign unlock].freeze
43
+ def create_multisig_request(action, raw, access_token: nil)
44
+ raise ArgumentError, "request action is limited in #{MULTISIG_REQUEST_ACTIONS.join(', ')}" unless MULTISIG_REQUEST_ACTIONS.include? action.to_sym
45
+
69
46
  path = '/multisigs/requests'
70
47
  payload = {
71
- action: 'sign',
48
+ action: action,
72
49
  raw: raw
73
50
  }
74
51
  access_token ||= access_token('POST', path, payload.to_json)
@@ -76,17 +53,15 @@ module MixinBot
76
53
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
77
54
  end
78
55
 
56
+ # transfer from the multisig address
57
+ def create_sign_multisig_request(raw, access_token: nil)
58
+ create_multisig_request 'sign', raw, access_token: access_token
59
+ end
60
+
79
61
  # transfer from the multisig address
80
62
  # create a request for unlock a multi-sign
81
63
  def create_unlock_multisig_request(raw, access_token: nil)
82
- path = '/multisigs/requests'
83
- payload = {
84
- action: 'unlock',
85
- raw: raw
86
- }
87
- access_token ||= access_token('POST', path, payload.to_json)
88
- authorization = format('Bearer %<access_token>s', access_token: access_token)
89
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
64
+ create_multisig_request 'unlock', raw, access_token: access_token
90
65
  end
91
66
 
92
67
  def sign_multisig_request(request_id, pin)
@@ -121,7 +96,7 @@ module MixinBot
121
96
 
122
97
  # pay to the multisig address
123
98
  # used for create multisig payment code_id
124
- def create_multisig_payment(**kwargs)
99
+ def create_payment(**kwargs)
125
100
  path = '/payments'
126
101
  payload = {
127
102
  asset_id: kwargs[:asset_id],
@@ -138,6 +113,7 @@ module MixinBot
138
113
  authorization = format('Bearer %<access_token>s', access_token: access_token)
139
114
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
140
115
  end
116
+ alias create_multisig_payment create_payment
141
117
 
142
118
  def verify_multisig(code_id, access_token: nil)
143
119
  path = format('/codes/%<code_id>s', code_id: code_id)
@@ -176,33 +152,27 @@ module MixinBot
176
152
  # amount: string / float,
177
153
  # memo: string,
178
154
  # }
179
- RAW_TRANSACTION_ARGUMENTS = %i[senders receivers amount threshold asset_id].freeze
155
+ RAW_TRANSACTION_ARGUMENTS = %i[utxos senders receivers amount threshold].freeze
180
156
  def build_raw_transaction(**kwargs)
181
157
  raise ArgumentError, "#{RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build raw transaction" unless RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
182
158
 
183
- senders = kwargs[:senders]
184
- receivers = kwargs[:receivers]
185
- amount = kwargs[:amount]
186
- threshold = kwargs[:threshold]
187
- asset_id = kwargs[:asset_id]
188
- utxos = kwargs[:utxos]
189
- memo = kwargs[:memo]
190
- access_token = kwargs[:access_token]
159
+ senders = kwargs[:senders]
160
+ threshold = kwargs[:threshold]
161
+ receivers = kwargs[:receivers]
162
+ receivers_threshold = kwargs[:receivers_threshold]
163
+ amount = kwargs[:amount]
164
+ threshold = kwargs[:threshold]
165
+ asset_id = kwargs[:asset_id]
166
+ asset_mixin_id = kwargs[:asset_mixin_id]
167
+ utxos = kwargs[:utxos]
168
+ memo = kwargs[:memo]
169
+ extra = kwargs[:extra]
170
+ access_token = kwargs[:access_token]
171
+ outputs = kwargs[:outputs] || []
172
+ hint = kwargs[:hint]
191
173
 
192
174
  raise 'access_token required!' if access_token.nil? && !senders.include?(client_id)
193
175
 
194
- # default to use all(first 100) unspent utxo
195
- utxos ||= multisigs(
196
- members: senders,
197
- threshold: threshold,
198
- state: 'unspent',
199
- access_token: access_token
200
- )['data'].filter(
201
- &lambda { |utxo|
202
- utxo['asset_id'] == kwargs[:asset_id]
203
- }
204
- )
205
-
206
176
  amount = amount.to_f.round(8)
207
177
  input_amount = utxos.map(
208
178
  &lambda { |utxo|
@@ -227,29 +197,34 @@ module MixinBot
227
197
  }
228
198
  )
229
199
 
230
- outputs = []
231
- output0 = create_output(receivers: receivers, index: 0)['data']
232
- outputs << {
233
- 'amount': format('%<amount>.8f', amount: amount),
234
- 'script': build_threshold_script(receivers.length),
235
- 'mask': output0['mask'],
236
- 'keys': output0['keys']
237
- }
238
-
239
- if input_amount > amount
240
- output1 = create_output(receivers: senders, index: 1)['data']
241
- outputs << {
242
- 'amount': format('%<amount>.8f', amount: input_amount - amount),
243
- 'script': build_threshold_script(threshold.to_i),
244
- 'mask': output1['mask'],
245
- 'keys': output1['keys']
246
- }
200
+ if outputs.empty?
201
+ receivers_threshold = 1 if receivers.size == 1
202
+ output0 = build_output(
203
+ receivers: receivers,
204
+ index: 0,
205
+ amount: amount,
206
+ threshold: receivers_threshold,
207
+ hint: hint
208
+ )
209
+ outputs.push output0
210
+
211
+ if input_amount > amount
212
+ output1 = build_output(
213
+ receivers: senders,
214
+ index: 1,
215
+ amount: (input_amount - amount),
216
+ threshold: threshold,
217
+ hint: hint
218
+ )
219
+ outputs.push output1
220
+ end
247
221
  end
248
222
 
249
- extra = Digest.hexencode memo.to_s.slice(0, 140)
223
+ extra = extra || Digest.hexencode(memo.to_s.slice(0, 140))
224
+ asset = asset_mixin_id || SHA3::Digest::SHA256.hexdigest(asset_id)
250
225
  tx = {
251
- version: 1,
252
- asset: SHA3::Digest::SHA256.hexdigest(asset_id),
226
+ version: 2,
227
+ asset: asset,
253
228
  inputs: inputs,
254
229
  outputs: outputs,
255
230
  extra: extra
@@ -258,63 +233,24 @@ module MixinBot
258
233
  tx.to_json
259
234
  end
260
235
 
261
- def str_to_bin(str)
262
- return if str.nil?
263
-
264
- str.scan(/../).map(&:hex).pack('c*')
265
- end
266
-
267
- def build_inputs(inputs)
268
- res = []
269
- prototype = {
270
- 'Hash' => nil,
271
- 'Index' => nil,
272
- 'Genesis' => nil,
273
- 'Deposit' => nil,
274
- 'Mint' => nil
236
+ def build_output(receivers:, index:, amount:, threshold:, hint: nil)
237
+ _output = create_output receivers: receivers, index: index, hint: hint
238
+ {
239
+ amount: format('%.8f', amount.to_f),
240
+ script: build_threshold_script(threshold),
241
+ mask: _output['mask'],
242
+ keys: _output['keys']
275
243
  }
276
- inputs.each do |input|
277
- struc = prototype.dup
278
- struc['Hash'] = str_to_bin input['hash']
279
- struc['Index'] = input['index']
280
- res << struc
281
- end
282
-
283
- res
284
244
  end
285
245
 
286
- def build_outputs(outputs)
287
- res = []
288
- prototype = {
289
- 'Type' => 0,
290
- 'Amount' => nil,
291
- 'Keys' => nil,
292
- 'Script' => nil,
293
- 'Mask' => nil
294
- }
295
- outputs.each do |output|
296
- struc = prototype.dup
297
- struc['Type'] = str_to_bin output['type']
298
- struc['Amount'] = str_to_bin output['amount']
299
- struc['Keys'] = output['keys'].map(&->(key) { str_to_bin(key) })
300
- struc['Script'] = str_to_bin output['script']
301
- struc['Mask'] = str_to_bin output['mask']
302
- res << struc
303
- end
246
+ def str_to_bin(str)
247
+ return if str.nil?
304
248
 
305
- res
249
+ str.scan(/../).map(&:hex).pack('c*')
306
250
  end
307
251
 
308
252
  def generate_trace_from_hash(hash, output_index = 0)
309
- md5 = Digest::MD5.new
310
- md5 << hash
311
- md5 << [output_index].pack('c*') if output_index.positive? && output_index < 256
312
- digest = md5.digest
313
- digest[6] = ((digest[6].ord & 0x0f) | 0x30).chr
314
- digest[8] = ((digest[8].ord & 0x3f) | 0x80).chr
315
- hex = digest.unpack1('H*')
316
-
317
- [hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..]].join('-')
253
+ MixinBot::Utils.generate_trace_from_hash hash, output_index
318
254
  end
319
255
  end
320
256
  end
data/lib/mixin_bot/api.rb CHANGED
@@ -6,6 +6,7 @@ require_relative './api/asset'
6
6
  require_relative './api/attachment'
7
7
  require_relative './api/auth'
8
8
  require_relative './api/blaze'
9
+ require_relative './api/collectible'
9
10
  require_relative './api/conversation'
10
11
  require_relative './api/me'
11
12
  require_relative './api/message'
@@ -43,8 +44,16 @@ module MixinBot
43
44
  end
44
45
  end
45
46
 
47
+ def sign_raw_transaction(tx)
48
+ MixinBot::Utils.sign_raw_transaction tx
49
+ end
50
+
51
+ def decode_raw_transaction(raw)
52
+ MixinBot::Utils.decode_raw_transaction raw
53
+ end
54
+
46
55
  # Use a mixin software to implement transaction build
47
- def sign_raw_transaction(json)
56
+ def sign_raw_transaction_native(json)
48
57
  ensure_mixin_command_exist
49
58
  command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
50
59
 
@@ -54,11 +63,23 @@ module MixinBot
54
63
  output.chomp
55
64
  end
56
65
 
66
+ # Use a mixin software to implement transaction build
67
+ def decode_raw_transaction_native(raw)
68
+ ensure_mixin_command_exist
69
+ command = format("mixin decoderawtransaction --raw '%<arg>s'", arg: raw)
70
+
71
+ output, error = Open3.capture3(command)
72
+ raise error unless error.empty?
73
+
74
+ JSON.parse output.chomp
75
+ end
76
+
57
77
  include MixinBot::API::App
58
78
  include MixinBot::API::Asset
59
79
  include MixinBot::API::Attachment
60
80
  include MixinBot::API::Auth
61
81
  include MixinBot::API::Blaze
82
+ include MixinBot::API::Collectible
62
83
  include MixinBot::API::Conversation
63
84
  include MixinBot::API::Me
64
85
  include MixinBot::API::Message
@@ -0,0 +1,608 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ module Utils
5
+ class << self
6
+ MAGIC = [0x77, 0x77]
7
+ TX_VERSION = 2
8
+ MAX_ENCODE_INT = 0xFFFF
9
+ NULL_BYTES = [0x00, 0x00]
10
+ AGGREGATED_SIGNATURE_PREFIX = 0xFF01
11
+ AGGREGATED_SIGNATURE_ORDINAY_MASK = [0x00]
12
+ 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
+
20
+ def unique_uuid(user_id, opponent_id = nil)
21
+ opponent_id ||= client_id
22
+ md5 = Digest::MD5.new
23
+ md5 << [user_id, opponent_id].min
24
+ md5 << [user_id, opponent_id].max
25
+ digest = md5.digest
26
+ digest6 = (digest[6].ord & 0x0f | 0x30).chr
27
+ digest8 = (digest[8].ord & 0x3f | 0x80).chr
28
+ cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
29
+ hex = cipher.unpack1('H*')
30
+
31
+ format(
32
+ '%<first>s-%<second>s-%<third>s-%<forth>s-%<fifth>s',
33
+ first: hex[0..7],
34
+ second: hex[8..11],
35
+ third: hex[12..15],
36
+ forth: hex[16..19],
37
+ fifth: hex[20..]
38
+ )
39
+ end
40
+
41
+ def generate_trace_from_hash(hash, output_index = 0)
42
+ md5 = Digest::MD5.new
43
+ md5 << hash
44
+ md5 << [output_index].pack('c*') if output_index.positive? && output_index < 256
45
+ digest = md5.digest
46
+ digest[6] = ((digest[6].ord & 0x0f) | 0x30).chr
47
+ digest[8] = ((digest[8].ord & 0x3f) | 0x80).chr
48
+ hex = digest.unpack1('H*')
49
+
50
+ hex_to_uuid hex
51
+ end
52
+
53
+ def hex_to_uuid(hex)
54
+ [hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..]].join('-')
55
+ end
56
+
57
+ def sign_raw_transaction(tx)
58
+ if tx.is_a? String
59
+ tx = JSON.parse tx
60
+ end
61
+ raise "#{tx} is not a valid json" unless tx.is_a? Hash
62
+
63
+ tx = tx.with_indifferent_access
64
+ bytes = []
65
+
66
+ # magic number
67
+ bytes += MAGIC
68
+
69
+ # version
70
+ bytes += [0, tx['version']]
71
+
72
+ # asset
73
+ bytes += [tx['asset']].pack('H*').bytes
74
+
75
+ # inputs
76
+ bytes += encode_inputs tx['inputs']
77
+
78
+ # output
79
+ bytes += encode_outputs tx['outputs']
80
+
81
+ # extra
82
+ extra_bytes = [tx['extra']].pack('H*').bytes
83
+ bytes += encode_int extra_bytes.size
84
+ bytes += extra_bytes
85
+
86
+ # aggregated
87
+ aggregated = tx['aggregated']
88
+ if aggregated.nil?
89
+ # signatures
90
+ bytes += encode_signatures tx['signatures']
91
+ else
92
+ bytes += encode_aggregated_signature aggregated
93
+ end
94
+
95
+ bytes.pack('C*').unpack1('H*')
96
+ end
97
+
98
+ def decode_raw_transaction(raw)
99
+ bytes = [raw].pack('H*').bytes
100
+ tx = {}
101
+
102
+ magic = bytes.shift(2)
103
+ raise 'Not valid raw' unless magic == MAGIC
104
+
105
+ version = bytes.shift(2)
106
+ tx['version'] = bytes_to_int version
107
+
108
+ asset = bytes.shift(32)
109
+ tx['asset'] = asset.pack('C*').unpack1('H*')
110
+
111
+ # read inputs
112
+ bytes, tx = decode_inputs bytes, tx
113
+
114
+ # read outputs
115
+ bytes, tx = decode_outputs bytes, tx
116
+
117
+ extra_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
118
+ tx['extra'] = bytes.shift(extra_size).pack('C*').unpack1('H*')
119
+
120
+ num = bytes.shift(2).reverse.pack('C*').unpack1('S*')
121
+ if num == MAX_ENCODE_INT
122
+ # aggregated
123
+ aggregated = {}
124
+
125
+ raise 'invalid aggregated' unless bytes.shift(2).reverse.pack('C*').unpack1('S*') == AGGREGATED_SIGNATURE_PREFIX
126
+
127
+ aggregated['signature'] = bytes.shift(64).pack('C*').unpack1('H*')
128
+
129
+ byte = bytes.shift
130
+ case byte
131
+ when AGGREGATED_SIGNATURE_ORDINAY_MASK.first
132
+ aggregated['signers'] = []
133
+ masks_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
134
+ masks = bytes.shift(masks_size)
135
+ masks = [masks] unless masks.is_a? Array
136
+
137
+ masks.each_with_index do |mask, i|
138
+ 8.times do |j|
139
+ k = 1 << j
140
+ aggregated['signers'].push(i * 8 + j) if mask & k == k
141
+ end
142
+ end
143
+ when AGGREGATED_SIGNATURE_SPARSE_MASK.first
144
+ signers_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
145
+ return if signers_size == 0
146
+
147
+ aggregated['signers'] = []
148
+ signers_size.times do
149
+ aggregated['signers'].push bytes.shift(2).reverse.pack('C*').unpack1('S*')
150
+ end
151
+ end
152
+
153
+ tx['aggregated'] = aggregated
154
+ else
155
+ if !bytes.empty? && bytes[...2] != NULL_BYTES
156
+ signatures_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
157
+ tx['signatures'] = bytes.shift(signatures_size).pack('C*').unpack1('H*')
158
+ end
159
+ end
160
+
161
+ tx
162
+ end
163
+
164
+ def nft_memo_hash(collection, token_id, meta)
165
+ collection = NULL_UUID if collection.empty?
166
+ meta = meta.to_json if meta.is_a?(Hash)
167
+
168
+ memo = {
169
+ prefix: NFT_MEMO_PREFIX,
170
+ version: NFT_MEMO_VERSION,
171
+ mask: 0,
172
+ chain: NFT_MEMO_DEFAULT_CHAIN,
173
+ class: NFT_MEMO_DEFAULT_CLASS,
174
+ collection: collection,
175
+ token: token_id,
176
+ extra: SHA3::Digest::SHA256.hexdigest(meta)
177
+ }
178
+
179
+ mark = [0]
180
+ mark.map do |index|
181
+ if index >= 64
182
+ raise "invalid NFO memo index #{index}"
183
+ end
184
+ memo[:mask] = memo[:mask] ^ (1 << index)
185
+ end
186
+
187
+ memo
188
+ end
189
+
190
+ def nft_memo(collection, token_id, meta)
191
+ encode_nft_memo nft_memo_hash(collection, token_id, meta)
192
+ end
193
+
194
+ def encode_nft_memo(memo)
195
+ bytes = []
196
+
197
+ bytes += NFT_MEMO_PREFIX.bytes
198
+ bytes += [NFT_MEMO_VERSION]
199
+
200
+ if memo[:mask] != 0
201
+ bytes += [1]
202
+ bytes += encode_unit_64 memo[:mask]
203
+ bytes += memo[:chain].split('-').pack('H* H* H* H* H*').bytes
204
+
205
+ class_bytes = [memo[:class]].pack('H*').bytes
206
+ bytes += bytes_of class_bytes.size
207
+ bytes += class_bytes
208
+
209
+ collection_bytes = memo[:collection].split('-').pack('H* H* H* H* H*').bytes
210
+ bytes += bytes_of collection_bytes.size
211
+ bytes += collection_bytes
212
+
213
+ # token_bytes = memo[:token].split('-').pack('H* H* H* H* H*').bytes
214
+ token_bytes = bytes_of memo[:token]
215
+ bytes += bytes_of token_bytes.size
216
+ bytes += token_bytes
217
+ end
218
+
219
+ extra_bytes = [memo[:extra]].pack('H*').bytes
220
+ bytes += bytes_of extra_bytes.size
221
+ bytes += extra_bytes
222
+
223
+ Base64.urlsafe_encode64 bytes.pack('C*'), padding: false
224
+ end
225
+
226
+ def decode_nft_memo(encoded)
227
+ bytes = Base64.urlsafe_decode64(encoded).bytes
228
+ memo = {}
229
+ memo[:prefix] = bytes.shift(3).pack('C*')
230
+ memo[:version] = bytes.shift
231
+
232
+ hint = bytes.shift
233
+ if hint == 1
234
+ memo[:mask] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
235
+ memo[:chain] = hex_to_uuid bytes.shift(16).pack('C*').unpack1('H*')
236
+
237
+ class_length = bytes.shift
238
+ memo[:class] = bytes.shift(class_length).pack('C*').unpack1('H*')
239
+
240
+ collection_length = bytes.shift
241
+ memo[:collection] = hex_to_uuid bytes.shift(collection_length).pack('C*').unpack1('H*')
242
+
243
+ token_length = bytes.shift
244
+ memo[:token] = bytes_to_int bytes.shift(token_length)
245
+ end
246
+
247
+ extra_length = bytes.shift
248
+ memo[:extra] = bytes.shift(extra_length).pack('C*').unpack1('H*')
249
+
250
+ memo
251
+ end
252
+
253
+ private
254
+
255
+ def encode_int(int)
256
+ raise "only support int #{int}" unless int.is_a?(Integer)
257
+ raise "int #{int} is larger than MAX_ENCODE_INT #{MAX_ENCODE_INT}" if int > MAX_ENCODE_INT
258
+
259
+ [int].pack('S*').bytes.reverse
260
+ end
261
+
262
+ def encode_unit_64(int)
263
+ [int].pack('Q*').bytes.reverse
264
+ end
265
+
266
+ def bytes_of(int)
267
+ raise 'not integer' unless int.is_a?(Integer)
268
+
269
+ bytes = []
270
+ loop do
271
+ break if int === 0
272
+ bytes.push int & 255
273
+ int = int / (2 ** 8) | 0
274
+ end
275
+
276
+ bytes.reverse
277
+ end
278
+
279
+ def bytes_to_int(bytes)
280
+ int = 0
281
+ bytes.each do |byte|
282
+ int = int * (2 ** 8) + byte
283
+ end
284
+
285
+ int
286
+ end
287
+
288
+ def encode_inputs(inputs, bytes = [])
289
+ bytes += encode_int(inputs.size)
290
+ inputs.each do |input|
291
+ bytes += [input['hash']].pack('H*').bytes
292
+ bytes += encode_int(input['index'])
293
+
294
+ # genesis
295
+ genesis = input['genesis'] || ''
296
+ if genesis.empty?
297
+ bytes += NULL_BYTES
298
+ else
299
+ genesis_bytes = [genesis].pack('H*').bytes
300
+ bytes += encode_int genesis_bytes.size
301
+ bytes += genesis_bytes
302
+ end
303
+
304
+ # deposit
305
+ deposit = input['deposit']
306
+ if deposit.nil?
307
+ bytes += NULL_BYTES
308
+ else
309
+ bytes += MAGIC
310
+ bytes += [deposit['chain']].pack('H*').bytes
311
+
312
+ asset_bytes = [deposit['asset']].pack('H*')
313
+ bytes += encode_int asset_bytes.size
314
+ bytes += asset_bytes
315
+
316
+ transaction_bytes = [deposit['transaction']].pack('H*')
317
+ bytes += encode_int transaction_bytes.size
318
+ bytes += transaction_bytes
319
+
320
+ bytes += encode_unit_64 deposit['index']
321
+
322
+ amount_bytes = bytes_of deposit['amount']
323
+ bytes += encode_int amount_bytes.size
324
+ bytes += amount_bytes
325
+ end
326
+
327
+ # mint
328
+ mint = input['mint']
329
+ if mint.nil?
330
+ bytes += NULL_BYTES
331
+ else
332
+ bytes += MAGIC
333
+
334
+ # group
335
+ group = mint['group'] || ''
336
+ if group.empty?
337
+ bytes += encode_int NULL_BYTES
338
+ else
339
+ group_bytes = [group].pack('H*')
340
+ bytes += encode_int group_bytes.size
341
+ bytes += group_bytes
342
+ end
343
+
344
+ bytes += encode_unit_64 mint['batch']
345
+
346
+ amount_bytes = bytes_of mint['amount']
347
+ bytes += encode_int amount_bytes.size
348
+ bytes += amount_bytes
349
+ end
350
+ end
351
+
352
+ bytes
353
+ end
354
+
355
+ def encode_outputs(outputs, bytes = [])
356
+ bytes += encode_int(outputs.size)
357
+ outputs.each do |output|
358
+ type = output['type'] || 0
359
+ bytes += [0x00, type]
360
+
361
+ # amount
362
+ amount_bytes = bytes_of (output['amount'].to_f * 1e8).to_i
363
+ bytes += encode_int amount_bytes.size
364
+ bytes += amount_bytes
365
+
366
+ # keys
367
+ bytes += encode_int output['keys'].size
368
+ output['keys'].each do |key|
369
+ bytes += [key].pack('H*').bytes
370
+ end
371
+
372
+ # mask
373
+ bytes += [output['mask']].pack('H*').bytes
374
+
375
+ # script
376
+ script_bytes = [output['script']].pack('H*').bytes
377
+ bytes += encode_int script_bytes.size
378
+ bytes += script_bytes
379
+
380
+ # withdrawal
381
+ withdrawal = output['withdrawal']
382
+ if withdrawal.nil?
383
+ bytes += NULL_BYTES
384
+ else
385
+ bytes += MAGIC
386
+
387
+ # chain
388
+ bytes += [withdrawal['chain']].pack('H*').bytes
389
+
390
+ # asset
391
+ asset_bytes = [withdrawal['asset']].pack('H*')
392
+ bytes += encode_int asset_bytes.size
393
+ bytes += asset_bytes
394
+
395
+ # address
396
+ address = withdrawal['address'] || ''
397
+ if address.empty?
398
+ bytes += NULL_BYTES
399
+ else
400
+ address_bytes = [address].pack('H*').bytes
401
+ bytes += encode_int address.size
402
+ bytes += address_bytes
403
+ end
404
+
405
+ # tag
406
+ tag = withdrawal['tag'] || ''
407
+ if tag.empty?
408
+ bytes += NULL_BYTES
409
+ else
410
+ address_bytes = [tag].pack('H*').bytes
411
+ bytes += encode_int tag.size
412
+ bytes += address_bytes
413
+ end
414
+ end
415
+ end
416
+
417
+ bytes
418
+ end
419
+
420
+ def encode_aggregated_signature(aggregated, bytes = [])
421
+ bytes += encode_int MAX_ENCODE_INT
422
+ bytes += encode_int AGGREGATED_SIGNATURE_PREFIX
423
+ bytes += [aggregated['signature']].pack('H*').bytes
424
+
425
+ signers = aggregated['signers']
426
+ if signers.size == 0
427
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
428
+ bytes += NULL_BYTES
429
+ else
430
+ signers.each do |sig, i|
431
+ raise 'signers not sorted' if i > 0 && sig <= signers[i - 1]
432
+ raise 'signers not sorted' if sig > MAX_ENCODE_INT
433
+ end
434
+
435
+ max = signers.last
436
+ if (((max / 8 | 0) + 1 | 0) > aggregated['signature'].size * 2)
437
+ bytes += AGGREGATED_SIGNATURE_SPARSE_MASK
438
+ bytes += encode_int aggregated['signers'].size
439
+ signers.map(&->(signer) { bytes += encode_int(signer) })
440
+ end
441
+
442
+ masks_bytes = Array.new(max / 8 + 1, 0)
443
+ signers.each do |signer|
444
+ masks[signer/8] = masks[signer/8] ^ (1 << (signer % 8))
445
+ end
446
+ bytes += AGGREGATED_SIGNATURE_ORDINAY_MASK
447
+ bytes += encode_int masks_bytes.size
448
+ bytes += masks_bytes
449
+ end
450
+
451
+ bytes
452
+ end
453
+
454
+ def encode_signatures(signatures, bytes = [])
455
+ sl =
456
+ if signatures.is_a? Hash
457
+ signatures.keys.size
458
+ else
459
+ 0
460
+ end
461
+
462
+ raise 'signatures overflow' if sl == MAX_ENCODE_INT
463
+ bytes += encode_int sl
464
+
465
+ if sl > 0
466
+ bytes += encode_int signatures.keys.size
467
+ signatures.keys.sort.each do |key|
468
+ bytes += encode_int key
469
+ bytes += [signatures[key]].pack('H*').bytes
470
+ end
471
+ end
472
+
473
+ bytes
474
+ end
475
+
476
+ def decode_inputs(bytes, tx)
477
+ inputs_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
478
+ tx['inputs'] = []
479
+ inputs_size.times do
480
+ input = {}
481
+ hash = bytes.shift(32)
482
+ input['hash'] = hash.pack('C*').unpack1('H*')
483
+
484
+ index = bytes.shift(2)
485
+ input['index'] = index.reverse.pack('C*').unpack1('S*')
486
+
487
+ if bytes[...2] != NULL_BYTES
488
+ genesis_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
489
+ genesis = bytes.shift(genesis_size)
490
+ input['genesis'] = genesis.pack('C*').unpack1('H*')
491
+ else
492
+ bytes.shift 2
493
+ end
494
+
495
+ if bytes[...2] != NULL_BYTES
496
+ magic = bytes.shift(2)
497
+ raise 'Not valid input' unless magic == MAGIC
498
+
499
+ deposit = {}
500
+ deposit['chain'] = bytes.shift(32).pack('C*').unpack1('H*')
501
+
502
+ asset_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
503
+ deposit['asset'] = bytes.shift(asset_size).unpack1('H*')
504
+
505
+ transaction_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
506
+ deposit['transaction'] = bytes.shift(transaction_size).unpack1('H*')
507
+
508
+ deposit['index'] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
509
+
510
+ amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
511
+ deposit['amount'] = bytes_to_int bytes.shift(amount_size)
512
+
513
+ input['deposit'] = deposit
514
+ else
515
+ bytes.shift 2
516
+ end
517
+
518
+ if bytes[...2] != NULL_BYTES
519
+ magic = bytes.shift(2)
520
+ raise 'Not valid input' unless magic == MAGIC
521
+
522
+ mint = {}
523
+ if bytes[...2] != NULL_BYTES
524
+ group_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
525
+ mint['group'] = bytes.shift(group_size).unpack1('H*')
526
+ else
527
+ bytes.shift 2
528
+ end
529
+
530
+ mint['batch'] = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
531
+ _amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
532
+ mint['amount'] = bytes_to_int bytes.shift(_amount_size)
533
+
534
+ input['mint'] = mint
535
+ else
536
+ bytes.shift 2
537
+ end
538
+
539
+ tx['inputs'].push input
540
+ end
541
+
542
+ [bytes, tx]
543
+ end
544
+
545
+ def decode_outputs(bytes, tx)
546
+ outputs_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
547
+ tx['outputs'] = []
548
+ outputs_size.times do
549
+ output = {}
550
+
551
+ bytes.shift
552
+ type = bytes.shift
553
+ output['type'] = type
554
+
555
+ amount_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
556
+ output['amount'] = format('%.8f', bytes_to_int(bytes.shift(amount_size)).to_f / 1e8)
557
+
558
+ output['keys'] = []
559
+ keys_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
560
+ keys_size.times do
561
+ output['keys'].push bytes.shift(32).pack('C*').unpack1('H*')
562
+ end
563
+
564
+ output['mask'] = bytes.shift(32).pack('C*').unpack1('H*')
565
+
566
+ script_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
567
+ output['script'] = bytes.shift(script_size).pack('C*').unpack1('H*')
568
+
569
+ if bytes[...2] != NULL_BYTES
570
+ magic = bytes.shift(2)
571
+ raise 'Not valid output' unless magic == MAGIC
572
+
573
+ withdraw = {}
574
+
575
+ output['chain'] = bytes.shift(32).pack('C*').unpack1('H*')
576
+
577
+ asset_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
578
+ output['asset'] = bytes.shift(asset_size).unpack1('H*')
579
+
580
+ if bytes[...2] != NULL_BYTES
581
+ address = {}
582
+
583
+ adderss_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
584
+ output['adderss'] = bytes.shift(adderss_size).pack('C*').unpack1('H*')
585
+ else
586
+ bytes.shift 2
587
+ end
588
+
589
+ if bytes[...2] != NULL_BYTES
590
+ tag = {}
591
+
592
+ tag_size = bytes.shift(2).reverse.pack('C*').unpack1('S*')
593
+ output['tag'] = bytes.shift(tag_size).pack('C*').unpack1('H*')
594
+ else
595
+ bytes.shift 2
596
+ end
597
+ else
598
+ bytes.shift 2
599
+ end
600
+
601
+ tx['outputs'].push output
602
+ end
603
+
604
+ [bytes, tx]
605
+ end
606
+ end
607
+ end
608
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MixinBot
4
- VERSION = '0.5.6'
4
+ VERSION = '0.6.3'
5
5
  end
data/lib/mixin_bot.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  # third-party dependencies
4
4
  require 'English'
5
+ require 'active_support/core_ext/hash'
5
6
  require 'base64'
6
7
  require 'digest'
7
8
  require 'faye/websocket'
@@ -15,6 +16,7 @@ require 'sha3'
15
16
 
16
17
  require_relative './mixin_bot/api'
17
18
  require_relative './mixin_bot/cli'
19
+ require_relative './mixin_bot/utils'
18
20
  require_relative './mixin_bot/version'
19
21
 
20
22
  module MixinBot
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixin_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - an-lee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-30 00:00:00.000000000 Z
11
+ date: 2021-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: awesome_print
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -265,6 +279,7 @@ files:
265
279
  - lib/mixin_bot/api/attachment.rb
266
280
  - lib/mixin_bot/api/auth.rb
267
281
  - lib/mixin_bot/api/blaze.rb
282
+ - lib/mixin_bot/api/collectible.rb
268
283
  - lib/mixin_bot/api/conversation.rb
269
284
  - lib/mixin_bot/api/me.rb
270
285
  - lib/mixin_bot/api/message.rb
@@ -281,6 +296,7 @@ files:
281
296
  - lib/mixin_bot/cli/multisig.rb
282
297
  - lib/mixin_bot/cli/node.rb
283
298
  - lib/mixin_bot/client.rb
299
+ - lib/mixin_bot/utils.rb
284
300
  - lib/mixin_bot/version.rb
285
301
  homepage: https://github.com/an-lee/mixin_bot
286
302
  licenses: