mixin_bot 0.0.1.4 → 0.3.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/bin/mixinbot +6 -0
- data/lib/mixin_bot.rb +14 -6
- data/lib/mixin_bot/api.rb +49 -7
- 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 +24 -8
- data/lib/mixin_bot/api/blaze.rb +62 -0
- data/lib/mixin_bot/api/conversation.rb +25 -14
- data/lib/mixin_bot/api/me.rb +25 -16
- data/lib/mixin_bot/api/message.rb +286 -34
- data/lib/mixin_bot/api/multisig.rb +335 -0
- data/lib/mixin_bot/api/payment.rb +16 -16
- data/lib/mixin_bot/api/pin.rb +58 -22
- data/lib/mixin_bot/api/snapshot.rb +32 -17
- data/lib/mixin_bot/api/transfer.rb +17 -17
- data/lib/mixin_bot/api/user.rb +61 -13
- data/lib/mixin_bot/api/withdraw.rb +78 -0
- 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 +37 -38
- data/lib/mixin_bot/errors.rb +3 -1
- data/lib/mixin_bot/version.rb +3 -1
- metadata +150 -27
@@ -1,69 +1,266 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
module MixinBot
|
2
4
|
class API
|
5
|
+
# https://developers.mixin.one/api/beta-mixin-message/websocket-messages/
|
3
6
|
module Message
|
4
7
|
def list_pending_message
|
5
|
-
|
8
|
+
write_ws_message(action: 'LIST_PENDING_MESSAGES', params: {})
|
6
9
|
end
|
7
10
|
|
11
|
+
# ACKNOWLEDGE_MESSAGE_RECEIPT ack server received message
|
12
|
+
# {
|
13
|
+
# "id": "UUID",
|
14
|
+
# "action": "ACKNOWLEDGE_MESSAGE_RECEIPT",
|
15
|
+
# "params": {
|
16
|
+
# "message_id": "UUID // message_id is you received message's message_id",
|
17
|
+
# "status": "READ"
|
18
|
+
# }
|
19
|
+
# }
|
8
20
|
def acknowledge_message_receipt(message_id)
|
9
21
|
params = {
|
10
22
|
message_id: message_id,
|
11
23
|
status: 'READ'
|
12
24
|
}
|
13
|
-
|
25
|
+
write_ws_message(action: 'ACKNOWLEDGE_MESSAGE_RECEIPT', params: params)
|
14
26
|
end
|
15
27
|
|
16
|
-
|
17
|
-
|
28
|
+
# {
|
29
|
+
# "id": "UUID // generated by client",
|
30
|
+
# "action": "CREATE_MESSAGE",
|
31
|
+
# "params": {
|
32
|
+
# "conversation_id": "UUID",
|
33
|
+
# "category": "PLAIN_TEXT",
|
34
|
+
# "status": "SENT",
|
35
|
+
# "message_id": "UUID // generated by client",
|
36
|
+
# "data": "Base64 encoded data" ,
|
37
|
+
# }
|
38
|
+
# }
|
39
|
+
def plain_text(options)
|
40
|
+
options.merge!(category: 'PLAIN_TEXT')
|
41
|
+
base_message_params(options)
|
42
|
+
end
|
18
43
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
44
|
+
# {
|
45
|
+
# "id": "UUID // generated by client",
|
46
|
+
# "action": "CREATE_MESSAGE",
|
47
|
+
# "params": {
|
48
|
+
# "conversation_id": "UUID",
|
49
|
+
# "category": "PLAIN_POST",
|
50
|
+
# "status": "SENT",
|
51
|
+
# "message_id": "UUID // generated by client",
|
52
|
+
# "data": "Base64 encoded data content is markdown" ,
|
53
|
+
# }
|
54
|
+
# }
|
55
|
+
def plain_post(options)
|
56
|
+
options.merge!(category: 'PLAIN_POST')
|
57
|
+
base_message_params(options)
|
58
|
+
end
|
26
59
|
|
27
|
-
|
60
|
+
# {
|
61
|
+
# "id": "UUID",
|
62
|
+
# "action": "CREATE_MESSAGE",
|
63
|
+
# "params": {
|
64
|
+
# "conversation_id": "UUID"
|
65
|
+
# "category": "PLAIN_IMAGE"
|
66
|
+
# "status": "SENT",
|
67
|
+
# "message_id": "UUID",
|
68
|
+
# "data": "Base64 encoded data"
|
69
|
+
# }
|
70
|
+
# }
|
71
|
+
# data format:
|
72
|
+
# {
|
73
|
+
# "attachment_id":
|
74
|
+
# "Read From POST /attachments",
|
75
|
+
# "mime_type": "",
|
76
|
+
# "width": 1024,
|
77
|
+
# "height": 1024,
|
78
|
+
# "size": 1024,
|
79
|
+
# "thumbnail": "base64 encoded"
|
80
|
+
# }
|
81
|
+
def plain_image(options)
|
82
|
+
options.merge!(category: 'PLAIN_IMAGE')
|
83
|
+
base_message_params(options)
|
28
84
|
end
|
29
85
|
|
30
|
-
|
31
|
-
|
86
|
+
# {
|
87
|
+
# "id": "UUID",
|
88
|
+
# "action": "CREATE_MESSAGE",
|
89
|
+
# "params": {
|
90
|
+
# "conversation_id": "UUID",
|
91
|
+
# "category": "PLAIN_DATA",
|
92
|
+
# "status": "SENT",
|
93
|
+
# "message_id": "UUID",
|
94
|
+
# "data": "Base64 encoded data",
|
95
|
+
# }
|
96
|
+
# }
|
97
|
+
# data format:
|
98
|
+
# {
|
99
|
+
# "attachment_id": "Read From POST /attachments",
|
100
|
+
# "mime_type": "",
|
101
|
+
# "size": 1024,
|
102
|
+
# "name": "Share"
|
103
|
+
# }
|
104
|
+
def plain_data(options)
|
105
|
+
options.merge!(category: 'PLAIN_DATA')
|
106
|
+
base_message_params(options)
|
32
107
|
end
|
33
108
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
109
|
+
# {
|
110
|
+
# "id": "UUID",
|
111
|
+
# "action": "CREATE_MESSAGE",
|
112
|
+
# "params": {
|
113
|
+
# "conversation_id": "UUID",
|
114
|
+
# "category": "PLAIN_STICKER",
|
115
|
+
# "status": "SENT",
|
116
|
+
# "message_id": "UUID",
|
117
|
+
# "data": "Base64 encoded data"
|
118
|
+
# }
|
119
|
+
# }
|
120
|
+
# data format:
|
121
|
+
# {
|
122
|
+
# "name": "hello",
|
123
|
+
# "album_id": "UUID"
|
124
|
+
# }
|
125
|
+
def plain_sticker(options)
|
126
|
+
options.merge!(category: 'PLAIN_STICKER')
|
127
|
+
base_message_params(options)
|
128
|
+
end
|
39
129
|
|
40
|
-
|
41
|
-
|
130
|
+
# {
|
131
|
+
# "id": "UUID",
|
132
|
+
# "action": "CREATE_MESSAGE",
|
133
|
+
# "params": {
|
134
|
+
# "conversation_id": "UUID",
|
135
|
+
# "category": "PLAIN_CONTACT"
|
136
|
+
# "status": "SENT",
|
137
|
+
# "message_id": "UUID",
|
138
|
+
# "data": "Base64 encoded data"
|
139
|
+
# }
|
140
|
+
# }
|
141
|
+
# data format:
|
142
|
+
# { "user_id": "UUID"}
|
143
|
+
def plain_contact(options)
|
144
|
+
options.merge!(category: 'PLAIN_CONTACT')
|
145
|
+
base_message_params(options)
|
146
|
+
end
|
42
147
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
148
|
+
# {
|
149
|
+
# "id": "UUID",
|
150
|
+
# "action": "CREATE_MESSAGE",
|
151
|
+
# "params": {
|
152
|
+
# "conversation_id": "UUID",
|
153
|
+
# "category": "APP_CARD",
|
154
|
+
# "status": "SENT",
|
155
|
+
# "message_id": "UUID",
|
156
|
+
# "data": "Base64 encoded data"
|
157
|
+
# }
|
158
|
+
# }
|
159
|
+
# data format:
|
160
|
+
# {
|
161
|
+
# "icon_url": "https://mixin.one/assets/98b586edb270556d1972112bd7985e9e.png",
|
162
|
+
# "title": "Mixin",
|
163
|
+
# "description": "A free and lightning fast peer-to-peer transactional network for digital assets.",
|
164
|
+
# "action": "https://mixin.one"
|
165
|
+
# }
|
166
|
+
def app_card(options)
|
167
|
+
options.merge!(category: 'APP_CARD')
|
168
|
+
base_message_params(options)
|
169
|
+
end
|
170
|
+
|
171
|
+
# {
|
172
|
+
# "id": "UUID",
|
173
|
+
# "action": "CREATE_MESSAGE",
|
174
|
+
# "params": {
|
175
|
+
# "conversation_id": "UUID",
|
176
|
+
# "category": "APP_BUTTON_GROUP",
|
177
|
+
# "status": "SENT",
|
178
|
+
# "message_id": "UUID",
|
179
|
+
# "data": "Base64 encoded data"
|
180
|
+
# }
|
181
|
+
# }
|
182
|
+
# data format:
|
183
|
+
# [
|
184
|
+
# {
|
185
|
+
# "label": "Mixin Website",
|
186
|
+
# "color": "#ABABAB",
|
187
|
+
# "action": "https://mixin.one"
|
188
|
+
# },
|
189
|
+
# ...
|
190
|
+
# ]
|
191
|
+
def app_button_group(options)
|
192
|
+
options.merge!(category: 'APP_BUTTON_GROUP')
|
193
|
+
base_message_params(options)
|
194
|
+
end
|
195
|
+
|
196
|
+
# {
|
197
|
+
# "id": "UUID",
|
198
|
+
# "action": "CREATE_MESSAGE",
|
199
|
+
# "params": {
|
200
|
+
# "conversation_id": "UUID",
|
201
|
+
# "category": "PLAIN_VIDEO",
|
202
|
+
# "status": "SENT",
|
203
|
+
# "message_id": "UUID",
|
204
|
+
# "data": "Base64 encoded data"
|
205
|
+
# }
|
206
|
+
# }
|
207
|
+
# data format:
|
208
|
+
# {
|
209
|
+
# "attachment_id": "Read From POST /attachments",
|
210
|
+
# "mime_type": "",
|
211
|
+
# "width": 1024,
|
212
|
+
# "height": 1024,
|
213
|
+
# "size": 1024,
|
214
|
+
# "duration": 1024,
|
215
|
+
# "thumbnail": "base64 encoded"
|
216
|
+
# }
|
217
|
+
def plain_video(options)
|
218
|
+
options.merge!(category: 'PLAIN_VIDEO')
|
219
|
+
base_message_params(options)
|
220
|
+
end
|
221
|
+
|
222
|
+
def recall_message_params(message_id, options)
|
223
|
+
raise 'recipient_id is required!' if options[:recipient_id].nil?
|
224
|
+
|
225
|
+
options.merge!(
|
226
|
+
category: 'MESSAGE_RECALL',
|
227
|
+
data: {
|
228
|
+
message_id: message_id
|
229
|
+
}
|
230
|
+
)
|
231
|
+
base_message_params(options)
|
232
|
+
end
|
233
|
+
|
234
|
+
# base format of message params
|
235
|
+
def base_message_params(options)
|
236
|
+
data = options[:data].is_a?(String) ? options[:data] : options[:data].to_json
|
237
|
+
{
|
238
|
+
conversation_id: options[:conversation_id],
|
239
|
+
recipient_id: options[:recipient_id],
|
240
|
+
representative_id: options[:representative_id],
|
241
|
+
category: options[:category],
|
47
242
|
status: 'SENT',
|
48
|
-
|
49
|
-
|
243
|
+
quote_message_id: options[:quote_message_id],
|
244
|
+
message_id: options[:message_id] || SecureRandom.uuid,
|
245
|
+
data: Base64.encode64(data)
|
50
246
|
}
|
51
|
-
|
52
|
-
write_message('CREATE_MESSAGE', params)
|
53
247
|
end
|
54
248
|
|
55
|
-
|
249
|
+
# read the gzipped message form websocket
|
250
|
+
def read_ws_message(data)
|
56
251
|
io = StringIO.new(data.pack('c*'), 'rb')
|
57
252
|
gzip = Zlib::GzipReader.new io
|
58
253
|
msg = gzip.read
|
59
254
|
gzip.close
|
60
|
-
|
255
|
+
|
256
|
+
msg
|
61
257
|
end
|
62
258
|
|
63
|
-
|
259
|
+
# gzip the message for websocket
|
260
|
+
def write_ws_message(params:, action: 'CREATE_MESSAGE')
|
64
261
|
msg = {
|
65
262
|
id: SecureRandom.uuid,
|
66
|
-
action:
|
263
|
+
action: action,
|
67
264
|
params: params
|
68
265
|
}.to_json
|
69
266
|
|
@@ -71,7 +268,62 @@ module MixinBot
|
|
71
268
|
gzip = Zlib::GzipWriter.new io
|
72
269
|
gzip.write msg
|
73
270
|
gzip.close
|
74
|
-
|
271
|
+
io.string.unpack('c*')
|
272
|
+
end
|
273
|
+
|
274
|
+
# use HTTP to send message
|
275
|
+
def send_text_message(options)
|
276
|
+
send_message plain_text(options)
|
277
|
+
end
|
278
|
+
|
279
|
+
def send_post_message(options)
|
280
|
+
send_message plain_post(options)
|
281
|
+
end
|
282
|
+
|
283
|
+
def send_contact_message(options)
|
284
|
+
send_message plain_contact(options)
|
285
|
+
end
|
286
|
+
|
287
|
+
def send_app_card_message(options)
|
288
|
+
send_message app_card(options)
|
289
|
+
end
|
290
|
+
|
291
|
+
def send_app_button_group_message(options)
|
292
|
+
send_message app_button_group(options)
|
293
|
+
end
|
294
|
+
|
295
|
+
def recall_message(message_id, options)
|
296
|
+
send_message [recall_message_params(message_id, options)]
|
297
|
+
end
|
298
|
+
|
299
|
+
# {
|
300
|
+
# "id": "UUID",
|
301
|
+
# "action": "CREATE_PLAIN_MESSAGES",
|
302
|
+
# "params": {
|
303
|
+
# "messages": [
|
304
|
+
# {
|
305
|
+
# "conversation_id": "UUID",
|
306
|
+
# "recipient_id": "UUID",
|
307
|
+
# "message_id": "UUID",
|
308
|
+
# "representative_id": "UUID (optional, only supported in peer to peer conversation)",
|
309
|
+
# "quote_message_id": "UUID (optional, only supported text, e.g. PLAIN_TEXT)",
|
310
|
+
# "category": "Only support plain category e.g.: PLAIN_TEXT, PLAIN_STICKER etc",
|
311
|
+
# "data": "Correspond to category."
|
312
|
+
# },
|
313
|
+
# ...
|
314
|
+
# ]
|
315
|
+
# }
|
316
|
+
# }
|
317
|
+
def send_plain_messages(messages)
|
318
|
+
send_message messages
|
319
|
+
end
|
320
|
+
|
321
|
+
# http post request
|
322
|
+
def send_message(payload)
|
323
|
+
path = '/messages'
|
324
|
+
access_token ||= access_token('POST', path, payload.to_json)
|
325
|
+
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
326
|
+
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
75
327
|
end
|
76
328
|
end
|
77
329
|
end
|
@@ -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
|