mixin_bot 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d5375d1baf6a03ec32d4e32ee415917e439780e6361473497a57f12ad2df248
4
- data.tar.gz: 5d03c3a70cccdc5a12b589f4b8697e6b408107af79884949a2b645ef942ca71e
3
+ metadata.gz: 490beec97c1c3ba36f339f4a947f7717ee7eafa0aad01ba7d36c0ad8962af0e2
4
+ data.tar.gz: e00974b0b39f0002d60ced9f3ad416ce4b0addc63ba2d6e14584fc5b3f64a4c3
5
5
  SHA512:
6
- metadata.gz: 5a4ff89a2d885624fe21c077137688593cd605791e8930c9cfe3926ed2f2b50607e4cd4cf992f5d1490e6ce430ab0d82606973a2504d3f12f9f4cf7d51e942c1
7
- data.tar.gz: a90fa0d4e3d1f50e0a40a01b9da057061789226cb988c1a3fbb73c9e74c979fe70ce34ff03ec489de984883e798d29e87d8d58ccee3c398bb05bc24c7a9a4eaa
6
+ metadata.gz: ccf975f89a77c9421151ec6235939a9b1e78359253597daa81e70e7e4badd1362e9c617425e547681ac25be72bdad25c0d56334dc9cc9a56b97abe0b08913dd7
7
+ data.tar.gz: c6d2dabfd14fa960f32549e2ba082d275a618f058b25ab54217e900197d42d10659c48a36e484aff55ed58c92e3355c35a121e599e6b110286bbd9ac3fc06b22
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/mixin_bot'
5
+
6
+ MixinBot::CLI.start(ARGV)
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'http'
3
+ require 'English'
4
4
  require 'base64'
5
+ require 'digest'
5
6
  require 'faye/websocket'
6
- require 'openssl'
7
- require 'jwt'
7
+ require 'http'
8
8
  require 'jose'
9
+ require 'jwt'
10
+ require 'msgpack'
11
+ require 'open3'
12
+ require 'openssl'
9
13
  require_relative './mixin_bot/api'
14
+ require_relative './mixin_bot/cli'
15
+ require_relative './mixin_bot/version'
10
16
 
11
17
  module MixinBot
12
18
  class<< self
@@ -2,12 +2,14 @@
2
2
 
3
3
  require_relative './client'
4
4
  require_relative './errors'
5
+ require_relative './api/app'
5
6
  require_relative './api/attachment'
6
7
  require_relative './api/auth'
7
8
  require_relative './api/blaze'
8
9
  require_relative './api/conversation'
9
10
  require_relative './api/me'
10
11
  require_relative './api/message'
12
+ require_relative './api/multisig'
11
13
  require_relative './api/payment'
12
14
  require_relative './api/pin'
13
15
  require_relative './api/snapshot'
@@ -17,30 +19,61 @@ require_relative './api/withdraw'
17
19
 
18
20
  module MixinBot
19
21
  class API
20
- attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key
21
- attr_reader :client, :blaze_host
22
+ attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key, :client, :blaze_host, :schmoozer
22
23
 
23
24
  def initialize(options = {})
24
25
  @client_id = options[:client_id] || MixinBot.client_id
25
26
  @client_secret = options[:client_secret] || MixinBot.client_secret
26
27
  @session_id = options[:session_id] || MixinBot.session_id
27
- @pin_token = Base64.decode64 options[:pin_token] || MixinBot.pin_token
28
- @private_key = OpenSSL::PKey::RSA.new options[:private_key] || MixinBot.private_key
29
- @client = Client.new(MixinBot.api_host)
28
+ @pin_token = Base64.urlsafe_decode64 options[:pin_token] || MixinBot.pin_token
29
+ @client = Client.new(MixinBot.api_host || 'api.mixin.one')
30
30
  @blaze_host = MixinBot.blaze_host || 'blaze.mixin.one'
31
+ _private_key = options[:private_key] || MixinBot.private_key
32
+ @private_key =
33
+ if /^-----BEGIN RSA PRIVATE KEY-----/.match? _private_key
34
+ _private_key.gsub('\\r\\n', "\n").gsub("\r\n", "\n")
35
+ else
36
+ Base64.urlsafe_decode64 _private_key
37
+ end
31
38
  end
32
39
 
40
+ # Use a mixin software to implement transaction build
41
+ def build_transaction(json)
42
+ ensure_mixin_command_exist
43
+ command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
44
+
45
+ output, error = Open3.capture3(command)
46
+ raise error unless error.empty?
47
+
48
+ output.chomp
49
+ end
50
+
51
+ include MixinBot::API::App
33
52
  include MixinBot::API::Attachment
34
53
  include MixinBot::API::Auth
35
54
  include MixinBot::API::Blaze
36
55
  include MixinBot::API::Conversation
37
56
  include MixinBot::API::Me
38
57
  include MixinBot::API::Message
58
+ include MixinBot::API::Multisig
39
59
  include MixinBot::API::Payment
40
60
  include MixinBot::API::Pin
41
61
  include MixinBot::API::Snapshot
42
62
  include MixinBot::API::Transfer
43
63
  include MixinBot::API::User
44
64
  include MixinBot::API::Withdraw
65
+
66
+ private
67
+
68
+ def ensure_mixin_command_exist
69
+ return if command?('mixin')
70
+
71
+ raise '`mixin` command is not valid!'
72
+ end
73
+
74
+ def command?(name)
75
+ `which #{name}`
76
+ $CHILD_STATUS.success?
77
+ end
45
78
  end
46
79
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module App
6
+ def add_favorite_app(app_id, access_token: nil)
7
+ path = format('/apps/%<id>s/favorite', id: app_id)
8
+
9
+ access_token ||= access_token('POST', path, {})
10
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
11
+ client.post(path, headers: { 'Authorization': authorization })
12
+ end
13
+
14
+ def remove_favorite_app(app_id, access_token: nil)
15
+ path = format('/apps/%<id>s/unfavorite', id: app_id)
16
+
17
+ access_token ||= access_token('POST', path, '')
18
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
19
+ client.post(path, headers: { 'Authorization': authorization })
20
+ end
21
+
22
+ def favorite_apps(user_id, access_token: nil)
23
+ path = format('/users/%<id>s/apps/favorite', id: user_id)
24
+
25
+ access_token ||= access_token('GET', path, '')
26
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
27
+ client.get(path, headers: { 'Authorization': authorization })
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,7 +3,7 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Auth
6
- def access_token(method, uri, body = '', exp_in = 600)
6
+ def access_token(method, uri, body = '', exp_in: 600, scp: 'FULL')
7
7
  sig = Digest::SHA256.hexdigest(method + uri + body)
8
8
  iat = Time.now.utc.to_i
9
9
  exp = (Time.now.utc + exp_in).to_i
@@ -14,9 +14,19 @@ module MixinBot
14
14
  iat: iat,
15
15
  exp: exp,
16
16
  jti: jti,
17
- sig: sig
17
+ sig: sig,
18
+ scp: scp
18
19
  }
19
- JWT.encode payload, private_key, 'RS512'
20
+ if pin_token.size == 32
21
+ jwk = JOSE::JWK.from_okp [:Ed25519, private_key]
22
+ jws = JOSE::JWS.from({ 'alg' => 'EdDSA' })
23
+ else
24
+ jwk = JOSE::JWK.from_pem private_key
25
+ jws = JOSE::JWS.from({ 'alg' => 'RS512' })
26
+ end
27
+
28
+ jwt = JOSE::JWT.from payload
29
+ JOSE::JWT.sign(jwk, jws, jwt).compact
20
30
  end
21
31
 
22
32
  def oauth_token(code)
@@ -14,7 +14,7 @@ module MixinBot
14
14
  )
15
15
  end
16
16
 
17
- def start_blaze_connect(reconnect = true, &_block)
17
+ def start_blaze_connect(reconnect: true, &_block)
18
18
  ws ||= blaze
19
19
  yield if block_given?
20
20
 
@@ -54,7 +54,7 @@ module MixinBot
54
54
  end
55
55
 
56
56
  ws = nil
57
- start_blaze_connect { yield } if reconnect
57
+ start_blaze_connect(&block) if reconnect
58
58
  end
59
59
  end
60
60
  end
@@ -41,7 +41,7 @@ module MixinBot
41
41
  digest = md5.digest
42
42
  digest6 = (digest[6].ord & 0x0f | 0x30).chr
43
43
  digest8 = (digest[8].ord & 0x3f | 0x80).chr
44
- cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..-1]
44
+ cipher = digest[0...6] + digest6 + digest[7] + digest8 + digest[9..]
45
45
  hex = cipher.unpack1('H*')
46
46
 
47
47
  format(
@@ -50,7 +50,7 @@ module MixinBot
50
50
  second: hex[8..11],
51
51
  third: hex[12..15],
52
52
  forth: hex[16..19],
53
- fifth: hex[20..-1]
53
+ fifth: hex[20..]
54
54
  )
55
55
  end
56
56
  end
@@ -4,7 +4,7 @@ module MixinBot
4
4
  class API
5
5
  module Me
6
6
  # https://developers.mixin.one/api/beta-mixin-message/read-profile/
7
- def read_me(access_token = nil)
7
+ def read_me(access_token: nil)
8
8
  path = '/me'
9
9
  access_token ||= access_token('GET', path, '')
10
10
  authorization = format('Bearer %<access_token>s', access_token: access_token)
@@ -26,7 +26,7 @@ module MixinBot
26
26
  end
27
27
 
28
28
  # https://developers.mixin.one/api/alpha-mixin-network/read-assets/
29
- def read_assets(access_token = nil)
29
+ def read_assets(access_token: nil)
30
30
  path = '/assets'
31
31
  access_token ||= access_token('GET', path, '')
32
32
  authorization = format('Bearer %<access_token>s', access_token: access_token)
@@ -34,7 +34,7 @@ module MixinBot
34
34
  end
35
35
 
36
36
  # https://developers.mixin.one/api/alpha-mixin-network/read-asset/
37
- def read_asset(asset_id, access_token = nil)
37
+ def read_asset(asset_id, access_token: nil)
38
38
  path = format('/assets/%<asset_id>s', asset_id: asset_id)
39
39
  access_token ||= access_token('GET', path, '')
40
40
  authorization = format('Bearer %<access_token>s', access_token: access_token)
@@ -42,7 +42,7 @@ module MixinBot
42
42
  end
43
43
 
44
44
  # https://developers.mixin.one/api/beta-mixin-message/friends/
45
- def read_friends(access_token = nil)
45
+ def read_friends(access_token: nil)
46
46
  path = '/friends'
47
47
  access_token ||= access_token('GET', path, '')
48
48
  authorization = format('Bearer %<access_token>s', access_token: access_token)
@@ -41,6 +41,22 @@ module MixinBot
41
41
  base_message_params(options)
42
42
  end
43
43
 
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
59
+
44
60
  # {
45
61
  # "id": "UUID",
46
62
  # "action": "CREATE_MESSAGE",
@@ -216,24 +232,16 @@ module MixinBot
216
232
  end
217
233
 
218
234
  # base format of message params
219
- def base_message_params(
220
- conversation_id:,
221
- category:,
222
- data:,
223
- quote_message_id: nil,
224
- message_id: nil,
225
- recipient_id: nil,
226
- representative_id: nil
227
- )
228
- data = data.is_a?(String) ? data : data.to_json
235
+ def base_message_params(options)
236
+ data = options[:data].is_a?(String) ? options[:data] : options[:data].to_json
229
237
  {
230
- conversation_id: conversation_id,
231
- recipient_id: recipient_id,
232
- representative_id: representative_id,
233
- category: category,
238
+ conversation_id: options[:conversation_id],
239
+ recipient_id: options[:recipient_id],
240
+ representative_id: options[:representative_id],
241
+ category: options[:category],
234
242
  status: 'SENT',
235
- quote_message_id: quote_message_id,
236
- message_id: message_id || SecureRandom.uuid,
243
+ quote_message_id: options[:quote_message_id],
244
+ message_id: options[:message_id] || SecureRandom.uuid,
237
245
  data: Base64.encode64(data)
238
246
  }
239
247
  end
@@ -249,7 +257,7 @@ module MixinBot
249
257
  end
250
258
 
251
259
  # gzip the message for websocket
252
- def write_ws_message(action: 'CREATE_MESSAGE', params:)
260
+ def write_ws_message(params:, action: 'CREATE_MESSAGE')
253
261
  msg = {
254
262
  id: SecureRandom.uuid,
255
263
  action: action,
@@ -268,6 +276,10 @@ module MixinBot
268
276
  send_message plain_text(options)
269
277
  end
270
278
 
279
+ def send_post_message(options)
280
+ send_message plain_post(options)
281
+ end
282
+
271
283
  def send_contact_message(options)
272
284
  send_message plain_contact(options)
273
285
  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