mixin_bot 0.1.1 → 0.3.2
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/bin/mixinbot +6 -0
- data/lib/mixin_bot.rb +11 -4
- data/lib/mixin_bot/api.rb +43 -5
- data/lib/mixin_bot/api/app.rb +31 -0
- data/lib/mixin_bot/api/attachment.rb +32 -0
- data/lib/mixin_bot/api/auth.rb +14 -4
- data/lib/mixin_bot/api/blaze.rb +62 -0
- data/lib/mixin_bot/api/conversation.rb +6 -5
- data/lib/mixin_bot/api/me.rb +4 -4
- data/lib/mixin_bot/api/message.rb +46 -9
- data/lib/mixin_bot/api/multisig.rb +335 -0
- data/lib/mixin_bot/api/pin.rb +30 -16
- data/lib/mixin_bot/api/snapshot.rb +23 -14
- data/lib/mixin_bot/api/transfer.rb +4 -3
- data/lib/mixin_bot/api/user.rb +21 -7
- data/lib/mixin_bot/api/withdraw.rb +10 -10
- data/lib/mixin_bot/cli.rb +128 -0
- data/lib/mixin_bot/cli/me.rb +40 -0
- data/lib/mixin_bot/cli/multisig.rb +11 -0
- data/lib/mixin_bot/cli/node.rb +107 -0
- data/lib/mixin_bot/client.rb +17 -12
- data/lib/mixin_bot/version.rb +1 -1
- metadata +130 -36
@@ -0,0 +1,335 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class API
|
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
|
+
def multisigs(limit: 100, offset: nil, access_token: nil)
|
32
|
+
path = format('/multisigs?limit=%<limit>s&offset=%<offset>s', limit: limit, offset: offset)
|
33
|
+
access_token ||= access_token('GET', path, '')
|
34
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
35
|
+
client.get(path, headers: { 'Authorization': authorization })
|
36
|
+
end
|
37
|
+
|
38
|
+
def all_multisigs(utxos: [], offset: nil, access_token: nil)
|
39
|
+
res = multisigs(limit: 100, offset: offset, access_token: access_token)
|
40
|
+
|
41
|
+
return [] if res['data'].nil?
|
42
|
+
|
43
|
+
utxos += res['data']
|
44
|
+
|
45
|
+
if res['data'].length < 100
|
46
|
+
utxos
|
47
|
+
else
|
48
|
+
all_multisigs(utxos: utxos, offset: utxos[-1]['created_at'], access_token: access_token)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_output(receivers:, index:, access_token: nil)
|
53
|
+
path = '/outputs'
|
54
|
+
payload = {
|
55
|
+
receivers: receivers,
|
56
|
+
index: index
|
57
|
+
}
|
58
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
59
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
60
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
61
|
+
end
|
62
|
+
|
63
|
+
# transfer from the multisig address
|
64
|
+
# create a request for multi sign
|
65
|
+
# for now, raw(RAW-TRANSACTION-HEX) can only be generated by Mixin SDK of Golang or Javascript
|
66
|
+
def create_sign_multisig_request(raw, access_token: nil)
|
67
|
+
path = '/multisigs'
|
68
|
+
payload = {
|
69
|
+
action: 'sign',
|
70
|
+
raw: raw
|
71
|
+
}
|
72
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
73
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
74
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
75
|
+
end
|
76
|
+
|
77
|
+
# transfer from the multisig address
|
78
|
+
# create a request for unlock a multi-sign
|
79
|
+
def create_unlock_multisig_request(raw, access_token: nil)
|
80
|
+
path = '/multisigs'
|
81
|
+
payload = {
|
82
|
+
action: 'unlock',
|
83
|
+
raw: raw
|
84
|
+
}
|
85
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
86
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
87
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
88
|
+
end
|
89
|
+
|
90
|
+
def sign_multisig_request(request_id, pin)
|
91
|
+
path = format('/multisigs/%<request_id>s/sign', request_id: request_id)
|
92
|
+
payload = {
|
93
|
+
pin: encrypt_pin(pin)
|
94
|
+
}
|
95
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
96
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
97
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
98
|
+
end
|
99
|
+
|
100
|
+
def unlock_multisig_request(request_id, pin)
|
101
|
+
path = format('/multisigs/%<request_id>s/unlock', request_id: request_id)
|
102
|
+
payload = {
|
103
|
+
pin: encrypt_pin(pin)
|
104
|
+
}
|
105
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
106
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
107
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
108
|
+
end
|
109
|
+
|
110
|
+
def cancel_multisig_request(request_id, pin)
|
111
|
+
path = format('/multisigs/%<request_id>s/cancel', request_id: request_id)
|
112
|
+
payload = {
|
113
|
+
pin: encrypt_pin(pin)
|
114
|
+
}
|
115
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
116
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
117
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
118
|
+
end
|
119
|
+
|
120
|
+
# pay to the multisig address
|
121
|
+
# used for create multisig payment code_id
|
122
|
+
def create_multisig_payment(params)
|
123
|
+
path = '/payments'
|
124
|
+
payload = {
|
125
|
+
asset_id: params[:asset_id],
|
126
|
+
amount: params[:amount].to_s,
|
127
|
+
trace_id: params[:trace_id] || SecureRandom.uuid,
|
128
|
+
memo: params[:memo],
|
129
|
+
opponent_multisig: {
|
130
|
+
receivers: params[:receivers],
|
131
|
+
threshold: params[:threshold]
|
132
|
+
}
|
133
|
+
}
|
134
|
+
access_token = params[:access_token]
|
135
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
136
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
137
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
138
|
+
end
|
139
|
+
|
140
|
+
def verify_multisig(code_id, access_token: nil)
|
141
|
+
path = format('/codes/%<code_id>s', code_id: code_id)
|
142
|
+
access_token ||= access_token('GET', path, '')
|
143
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
144
|
+
client.get(path, headers: { 'Authorization': authorization })
|
145
|
+
end
|
146
|
+
|
147
|
+
# send a signed transaction to main net
|
148
|
+
def send_raw_transaction(raw, access_token: nil)
|
149
|
+
path = '/external/proxy'
|
150
|
+
payload = {
|
151
|
+
method: 'sendrawtransaction',
|
152
|
+
params: [raw]
|
153
|
+
}
|
154
|
+
|
155
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
156
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
157
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_threshold_script(threshold)
|
161
|
+
s = threshold.to_s(16)
|
162
|
+
s = s.length == 1 ? "0#{s}" : s
|
163
|
+
raise 'NVALID THRESHOLD' if s.length > 2
|
164
|
+
|
165
|
+
"fffe#{s}"
|
166
|
+
end
|
167
|
+
|
168
|
+
# filter utxo by members, asset_id and threshold
|
169
|
+
def filter_utxos(params)
|
170
|
+
utxos = all_multisigs(access_token: params[:access_token])
|
171
|
+
|
172
|
+
unless params[:members].nil?
|
173
|
+
utxos = utxos.filter(
|
174
|
+
&lambda { |utxo|
|
175
|
+
utxo['members'].sort == params[:members].sort
|
176
|
+
}
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
unless params[:asset_id].nil?
|
181
|
+
utxos = utxos.filter(
|
182
|
+
&lambda { |utxo|
|
183
|
+
utxo['asset_id'] == params[:asset_id]
|
184
|
+
}
|
185
|
+
)
|
186
|
+
end
|
187
|
+
|
188
|
+
unless params[:threshold].nil?
|
189
|
+
utxos = utxos.filter(
|
190
|
+
&lambda { |utxo|
|
191
|
+
utxo['threshold'] == params[:threshold]
|
192
|
+
}
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
unless params[:state].nil?
|
197
|
+
utxos = utxos.filter(
|
198
|
+
&lambda { |utxo|
|
199
|
+
utxo['state'] == params[:state]
|
200
|
+
}
|
201
|
+
)
|
202
|
+
end
|
203
|
+
|
204
|
+
utxos
|
205
|
+
end
|
206
|
+
|
207
|
+
# params:
|
208
|
+
# {
|
209
|
+
# senders: [ uuid ],
|
210
|
+
# receivers: [ uuid ],
|
211
|
+
# threshold: integer,
|
212
|
+
# asset_id: uuid,
|
213
|
+
# asset_mixin_id: string,
|
214
|
+
# amount: string / float,
|
215
|
+
# memo: string,
|
216
|
+
# }
|
217
|
+
def build_raw_transaction(params)
|
218
|
+
senders = params[:senders]
|
219
|
+
receivers = params[:receivers]
|
220
|
+
asset_id = params[:asset_id]
|
221
|
+
asset_mixin_id = params[:asset_mixin_id]
|
222
|
+
amount = params[:amount]
|
223
|
+
memo = params[:memo]
|
224
|
+
threshold = params[:threshold]
|
225
|
+
access_token = params[:access_token]
|
226
|
+
utxos = params[:utxos]
|
227
|
+
|
228
|
+
raise 'access_token required!' if access_token.nil? && !senders.include?(client_id)
|
229
|
+
|
230
|
+
# default to use all unspent utxo
|
231
|
+
utxos ||= filter_utxos(
|
232
|
+
members: senders,
|
233
|
+
asset_id: asset_id,
|
234
|
+
threshold: threshold,
|
235
|
+
state: 'unspent',
|
236
|
+
access_token: access_token
|
237
|
+
)
|
238
|
+
amount = amount.to_f.round(8)
|
239
|
+
input_amount = utxos.map(
|
240
|
+
&lambda { |utxo|
|
241
|
+
utxo['amount'].to_f
|
242
|
+
}
|
243
|
+
).sum.round(8)
|
244
|
+
|
245
|
+
if input_amount < amount
|
246
|
+
raise format(
|
247
|
+
'not enough amount! %<input_amount>s < %<amount>s',
|
248
|
+
input_amount: input_amount,
|
249
|
+
amount: amount
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
inputs = utxos.map(
|
254
|
+
&lambda { |utx|
|
255
|
+
{
|
256
|
+
'hash' => utx['transaction_hash'],
|
257
|
+
'index' => utx['output_index']
|
258
|
+
}
|
259
|
+
}
|
260
|
+
)
|
261
|
+
|
262
|
+
outputs = []
|
263
|
+
output0 = create_output(receivers: receivers, index: 0)['data']
|
264
|
+
output0['amount'] = format('%<amount>.8f', amount: amount)
|
265
|
+
output0['script'] = build_threshold_script(receivers.length)
|
266
|
+
outputs << output0
|
267
|
+
|
268
|
+
if input_amount > amount
|
269
|
+
output1 = create_output(receivers: senders, index: 1)['data']
|
270
|
+
output1['amount'] = format('%<amount>.8f', amount: input_amount - amount)
|
271
|
+
output1['script'] = build_threshold_script(utxos[0]['threshold'].to_i)
|
272
|
+
outputs << output1
|
273
|
+
end
|
274
|
+
|
275
|
+
extra = Digest.hexencode memo.to_s.slice(0, 140)
|
276
|
+
tx = {
|
277
|
+
version: 1,
|
278
|
+
asset: asset_mixin_id,
|
279
|
+
inputs: inputs,
|
280
|
+
outputs: outputs,
|
281
|
+
extra: extra
|
282
|
+
}
|
283
|
+
|
284
|
+
build_transaction tx.to_json
|
285
|
+
end
|
286
|
+
|
287
|
+
def str_to_bin(str)
|
288
|
+
return if str.nil?
|
289
|
+
|
290
|
+
str.scan(/../).map(&:hex).pack('c*')
|
291
|
+
end
|
292
|
+
|
293
|
+
def build_inputs(inputs)
|
294
|
+
res = []
|
295
|
+
prototype = {
|
296
|
+
'Hash' => nil,
|
297
|
+
'Index' => nil,
|
298
|
+
'Genesis' => nil,
|
299
|
+
'Deposit' => nil,
|
300
|
+
'Mint' => nil
|
301
|
+
}
|
302
|
+
inputs.each do |input|
|
303
|
+
struc = prototype.dup
|
304
|
+
struc['Hash'] = str_to_bin input['hash']
|
305
|
+
struc['Index'] = input['index']
|
306
|
+
res << struc
|
307
|
+
end
|
308
|
+
|
309
|
+
res
|
310
|
+
end
|
311
|
+
|
312
|
+
def build_outputs(outputs)
|
313
|
+
res = []
|
314
|
+
prototype = {
|
315
|
+
'Type' => 0,
|
316
|
+
'Amount' => nil,
|
317
|
+
'Keys' => nil,
|
318
|
+
'Script' => nil,
|
319
|
+
'Mask' => nil
|
320
|
+
}
|
321
|
+
outputs.each do |output|
|
322
|
+
struc = prototype.dup
|
323
|
+
struc['Type'] = str_to_bin output['type']
|
324
|
+
struc['Amount'] = str_to_bin output['amount']
|
325
|
+
struc['Keys'] = output['keys'].map(&->(key) { str_to_bin(key) })
|
326
|
+
struc['Script'] = str_to_bin output['script']
|
327
|
+
struc['Mask'] = str_to_bin output['mask']
|
328
|
+
res << struc
|
329
|
+
end
|
330
|
+
|
331
|
+
res
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
data/lib/mixin_bot/api/pin.rb
CHANGED
@@ -15,14 +15,14 @@ module MixinBot
|
|
15
15
|
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
16
16
|
end
|
17
17
|
|
18
|
-
# not verified yet
|
19
18
|
# https://developers.mixin.one/api/alpha-mixin-network/create-pin/
|
20
|
-
def update_pin(old_pin:,
|
19
|
+
def update_pin(old_pin:, pin:)
|
21
20
|
path = '/pin/update'
|
22
|
-
|
21
|
+
encrypted_old_pin = old_pin.nil? ? '' : encrypt_pin(old_pin, iterator: Time.now.utc.to_i)
|
22
|
+
encrypted_pin = encrypt_pin(pin, iterator: Time.now.utc.to_i + 1)
|
23
23
|
payload = {
|
24
|
-
old_pin:
|
25
|
-
pin:
|
24
|
+
old_pin: encrypted_old_pin,
|
25
|
+
pin: encrypted_pin
|
26
26
|
}
|
27
27
|
|
28
28
|
access_token = access_token('POST', path, payload.to_json)
|
@@ -35,26 +35,24 @@ module MixinBot
|
|
35
35
|
msg = Base64.strict_decode64 msg
|
36
36
|
iv = msg[0..15]
|
37
37
|
cipher = msg[16..47]
|
38
|
-
aes_key = JOSE::JWA::PKCS1.rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
|
39
38
|
alg = 'AES-256-CBC'
|
40
39
|
decode_cipher = OpenSSL::Cipher.new(alg)
|
41
40
|
decode_cipher.decrypt
|
42
41
|
decode_cipher.iv = iv
|
43
|
-
decode_cipher.key =
|
42
|
+
decode_cipher.key = _generate_aes_key
|
44
43
|
decoded = decode_cipher.update(cipher)
|
45
44
|
decoded[0..5]
|
46
45
|
end
|
47
46
|
|
48
47
|
# https://developers.mixin.one/api/alpha-mixin-network/encrypted-pin/
|
49
48
|
# use timestamp(timestamp) for iterator as default: must be bigger than the previous, the first time must be greater than 0. After a new session created, it will be reset to 0.
|
50
|
-
def encrypt_pin(pin_code,
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
tsstring = tszero.chr + tsone.chr + tstwo.chr + tsthree.chr + "\0\0\0\0"
|
49
|
+
def encrypt_pin(pin_code, iterator: nil)
|
50
|
+
iterator ||= Time.now.utc.to_i
|
51
|
+
tszero = iterator % 0x100
|
52
|
+
tsone = (iterator % 0x10000) >> 8
|
53
|
+
tstwo = (iterator % 0x1000000) >> 16
|
54
|
+
tsthree = (iterator % 0x100000000) >> 24
|
55
|
+
tsstring = "#{tszero.chr}#{tsone.chr}#{tstwo.chr}#{tsthree.chr}\u0000\u0000\u0000\u0000"
|
58
56
|
encrypt_content = pin_code + tsstring + tsstring
|
59
57
|
pad_count = 16 - encrypt_content.length % 16
|
60
58
|
padded_content =
|
@@ -68,12 +66,28 @@ module MixinBot
|
|
68
66
|
aes = OpenSSL::Cipher.new(alg)
|
69
67
|
iv = OpenSSL::Cipher.new(alg).random_iv
|
70
68
|
aes.encrypt
|
71
|
-
aes.key =
|
69
|
+
aes.key = _generate_aes_key
|
72
70
|
aes.iv = iv
|
73
71
|
cipher = aes.update(padded_content)
|
74
72
|
msg = iv + cipher
|
75
73
|
Base64.strict_encode64 msg
|
76
74
|
end
|
77
75
|
end
|
76
|
+
|
77
|
+
def _generate_aes_key
|
78
|
+
if pin_token.size == 32
|
79
|
+
JOSE::JWA::X25519.x25519(
|
80
|
+
JOSE::JWA::Ed25519.secret_to_curve25519(private_key[0..31]),
|
81
|
+
pin_token
|
82
|
+
)
|
83
|
+
else
|
84
|
+
JOSE::JWA::PKCS1.rsaes_oaep_decrypt(
|
85
|
+
'SHA256',
|
86
|
+
pin_token,
|
87
|
+
OpenSSL::PKey::RSA.new(private_key),
|
88
|
+
session_id
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
78
92
|
end
|
79
93
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Snapshot
|
6
|
-
def
|
6
|
+
def read_network_snapshots(options = {})
|
7
7
|
path = format(
|
8
8
|
'/network/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&order=%<order>s',
|
9
9
|
limit: options[:limit],
|
@@ -12,21 +12,30 @@ module MixinBot
|
|
12
12
|
order: options[:order]
|
13
13
|
)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
access_token = options[:access_token] || access_token('GET', path)
|
16
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
17
|
+
client.get(path, headers: { 'Authorization': authorization })
|
18
|
+
end
|
19
|
+
|
20
|
+
def read_snapshots(options = {})
|
21
|
+
path = format(
|
22
|
+
'/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s',
|
23
|
+
limit: options[:limit],
|
24
|
+
offset: options[:offset],
|
25
|
+
asset: options[:asset]
|
26
|
+
)
|
27
|
+
|
28
|
+
access_token = options[:access_token] || access_token('GET', path)
|
29
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
30
|
+
client.get(path, headers: { 'Authorization': authorization })
|
25
31
|
end
|
26
32
|
|
27
|
-
def
|
28
|
-
path = format('network/snapshots/%<snapshot_id>s', snapshot_id: snapshot_id)
|
29
|
-
|
33
|
+
def read_network_snapshot(snapshot_id, options = {})
|
34
|
+
path = format('/network/snapshots/%<snapshot_id>s', snapshot_id: snapshot_id)
|
35
|
+
|
36
|
+
access_token = options[:access_token] || access_token('GET', path)
|
37
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
38
|
+
client.get(path, headers: { 'Authorization': authorization })
|
30
39
|
end
|
31
40
|
end
|
32
41
|
end
|