mixin_bot 0.1.4 → 0.3.4
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 +9 -3
- data/lib/mixin_bot/api.rb +43 -5
- data/lib/mixin_bot/api/app.rb +31 -0
- data/lib/mixin_bot/api/auth.rb +13 -3
- data/lib/mixin_bot/api/blaze.rb +2 -2
- data/lib/mixin_bot/api/conversation.rb +2 -2
- data/lib/mixin_bot/api/me.rb +4 -4
- data/lib/mixin_bot/api/message.rb +29 -17
- data/lib/mixin_bot/api/multisig.rb +335 -0
- data/lib/mixin_bot/api/pin.rb +24 -9
- data/lib/mixin_bot/api/snapshot.rb +10 -4
- data/lib/mixin_bot/api/transfer.rb +4 -3
- data/lib/mixin_bot/api/user.rb +20 -6
- data/lib/mixin_bot/api/withdraw.rb +8 -8
- 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 +10 -10
- data/lib/mixin_bot/version.rb +1 -1
- metadata +91 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d2d1a5483fbd7210ab5f4e9a08697c0ed2d6d1f90975192b9727d36bc4ad71f
|
4
|
+
data.tar.gz: 3d412e3b95814a3ac245d9ab46b0fa1dcfd4571b05903f363e5841b1108bb958
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7503cd89d0f30a0517b6ce1022a6aaabf6d66737f16d38b7b1011fa342fab22e5cd3bd658516bf4e3faf784a06235ead91dd654a16f3b21d2df3f864bc00514
|
7
|
+
data.tar.gz: b5a3ec55cd38d404c8e6a458668927e3047c932ed8e7723b92fe8a701790ac61f2f81a3eef30e6e2ad457222290435e32856637b1aee8e9a4e141107834364a9
|
data/bin/mixinbot
ADDED
data/lib/mixin_bot.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'English'
|
4
4
|
require 'base64'
|
5
|
+
require 'digest'
|
5
6
|
require 'faye/websocket'
|
6
|
-
require '
|
7
|
-
require 'jwt'
|
7
|
+
require 'http'
|
8
8
|
require 'jose'
|
9
|
+
require 'msgpack'
|
10
|
+
require 'open3'
|
11
|
+
require 'openssl'
|
12
|
+
require 'rbnacl'
|
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
|
data/lib/mixin_bot/api.rb
CHANGED
@@ -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,66 @@ 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
|
-
@
|
28
|
-
@private_key = OpenSSL::PKey::RSA.new options[:private_key] || MixinBot.private_key
|
29
|
-
@client = Client.new(MixinBot.api_host)
|
28
|
+
@client = Client.new(MixinBot.api_host || 'api.mixin.one')
|
30
29
|
@blaze_host = MixinBot.blaze_host || 'blaze.mixin.one'
|
30
|
+
@pin_token =
|
31
|
+
begin
|
32
|
+
Base64.urlsafe_decode64 options[:pin_token] || MixinBot.pin_token
|
33
|
+
rescue StandardError
|
34
|
+
''
|
35
|
+
end
|
36
|
+
_private_key = options[:private_key] || MixinBot.private_key
|
37
|
+
@private_key =
|
38
|
+
if /^-----BEGIN RSA PRIVATE KEY-----/.match? _private_key
|
39
|
+
_private_key.gsub('\\r\\n', "\n").gsub("\r\n", "\n")
|
40
|
+
else
|
41
|
+
Base64.urlsafe_decode64 _private_key
|
42
|
+
end
|
31
43
|
end
|
32
44
|
|
45
|
+
# Use a mixin software to implement transaction build
|
46
|
+
def build_transaction(json)
|
47
|
+
ensure_mixin_command_exist
|
48
|
+
command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
|
49
|
+
|
50
|
+
output, error = Open3.capture3(command)
|
51
|
+
raise error unless error.empty?
|
52
|
+
|
53
|
+
output.chomp
|
54
|
+
end
|
55
|
+
|
56
|
+
include MixinBot::API::App
|
33
57
|
include MixinBot::API::Attachment
|
34
58
|
include MixinBot::API::Auth
|
35
59
|
include MixinBot::API::Blaze
|
36
60
|
include MixinBot::API::Conversation
|
37
61
|
include MixinBot::API::Me
|
38
62
|
include MixinBot::API::Message
|
63
|
+
include MixinBot::API::Multisig
|
39
64
|
include MixinBot::API::Payment
|
40
65
|
include MixinBot::API::Pin
|
41
66
|
include MixinBot::API::Snapshot
|
42
67
|
include MixinBot::API::Transfer
|
43
68
|
include MixinBot::API::User
|
44
69
|
include MixinBot::API::Withdraw
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def ensure_mixin_command_exist
|
74
|
+
return if command?('mixin')
|
75
|
+
|
76
|
+
raise '`mixin` command is not valid!'
|
77
|
+
end
|
78
|
+
|
79
|
+
def command?(name)
|
80
|
+
`which #{name}`
|
81
|
+
$CHILD_STATUS.success?
|
82
|
+
end
|
45
83
|
end
|
46
84
|
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
|
data/lib/mixin_bot/api/auth.rb
CHANGED
@@ -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
|
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
|
-
|
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)
|
data/lib/mixin_bot/api/blaze.rb
CHANGED
@@ -14,7 +14,7 @@ module MixinBot
|
|
14
14
|
)
|
15
15
|
end
|
16
16
|
|
17
|
-
def start_blaze_connect(reconnect
|
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
|
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
|
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
|
53
|
+
fifth: hex[20..]
|
54
54
|
)
|
55
55
|
end
|
56
56
|
end
|
data/lib/mixin_bot/api/me.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
|
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'
|
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
|