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.
@@ -1,29 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Payment
4
6
  def pay_url(options)
5
- options = options.with_indifferent_access
6
- recipient_id = options.fetch('recipient_id')
7
- asset_id = options.fetch('asset_id')
8
- amount = options.fetch('amount')
9
- memo = options.fetch('memo')
10
- trace = options.fetch('trace')
11
- url = format('https://mixin.one/pay?recipient=%s&asset=%s&amount=%s&trace=%s&memo=%s', recipient_id, asset_id, amount, trace, memo)
7
+ format(
8
+ 'https://mixin.one/pay?recipient=%<recipient_id>s&asset=%<asset>s&amount=%<amount>s&trace=%<trace>s&memo=%<memo>s',
9
+ recipient_id: options[:recipient_id],
10
+ asset: options[:asset_id],
11
+ amount: options[:amount].to_s,
12
+ trace: options[:trace],
13
+ memo: options[:memo]
14
+ )
12
15
  end
13
16
 
17
+ # https://developers.mixin.one/api/alpha-mixin-network/verify-payment/
14
18
  def verify_payment(options)
15
- options = options.with_indifferent_access
16
- recipient_id = options.fetch('recipient_id')
17
- asset_id = options.fetch('asset_id')
18
- amount = options.fetch('amount')
19
- trace = options.fetch('trace')
20
19
  path = 'payments'
21
20
  payload = {
22
- asset_id: asset_id,
23
- opponent_id: recipient_id,
24
- amount: amount,
25
- trace_id: trace,
21
+ asset_id: options[:asset_id],
22
+ opponent_id: options[:opponent_id],
23
+ amount: options[:amount].to_s,
24
+ trace_id: options[:trace]
26
25
  }
26
+
27
27
  client.post(path, json: payload)
28
28
  end
29
29
  end
@@ -1,56 +1,92 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Pin
4
- def verify_pin(pin_code, access_token=nil)
6
+ # https://developers.mixin.one/api/alpha-mixin-network/verify-pin/
7
+ def verify_pin(pin_code)
5
8
  path = '/pin/verify'
6
9
  payload = {
7
10
  pin: encrypt_pin(pin_code)
8
11
  }
9
12
 
10
- access_token ||= self.access_token('POST', path, payload.to_json)
11
- authorization = format('Bearer %s', access_token)
13
+ access_token = access_token('POST', path, payload.to_json)
14
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
15
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
16
+ end
17
+
18
+ # https://developers.mixin.one/api/alpha-mixin-network/create-pin/
19
+ def update_pin(old_pin:, pin:)
20
+ path = '/pin/update'
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
+ payload = {
24
+ old_pin: encrypted_old_pin,
25
+ pin: encrypted_pin
26
+ }
27
+
28
+ access_token = access_token('POST', path, payload.to_json)
29
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
12
30
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
13
31
  end
14
32
 
33
+ # decrypt the encrpted pin, just for test
15
34
  def decrypt_pin(msg)
16
35
  msg = Base64.strict_decode64 msg
17
36
  iv = msg[0..15]
18
37
  cipher = msg[16..47]
19
- aes_key = JOSE::JWA::PKCS1::rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
20
38
  alg = 'AES-256-CBC'
21
39
  decode_cipher = OpenSSL::Cipher.new(alg)
22
40
  decode_cipher.decrypt
23
41
  decode_cipher.iv = iv
24
- decode_cipher.key = aes_key
25
- plain = decode_cipher.update(cipher)
26
- return plain
42
+ decode_cipher.key = _generate_aes_key
43
+ decoded = decode_cipher.update(cipher)
44
+ decoded[0..5]
27
45
  end
28
46
 
29
- def encrypt_pin(pin_code)
30
- aes_key = JOSE::JWA::PKCS1::rsaes_oaep_decrypt('SHA256', pin_token, private_key, session_id)
31
- ts = Time.now.utc.to_i
32
- tszero = ts % 0x100
33
- tsone = (ts % 0x10000) >> 8
34
- tstwo = (ts % 0x1000000) >> 16
35
- tsthree = (ts % 0x100000000) >> 24
36
- tsstring = tszero.chr + tsone.chr + tstwo.chr + tsthree.chr + "\0\0\0\0"
47
+ # https://developers.mixin.one/api/alpha-mixin-network/encrypted-pin/
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.
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"
37
56
  encrypt_content = pin_code + tsstring + tsstring
38
57
  pad_count = 16 - encrypt_content.length % 16
39
- if pad_count > 0
40
- padded_content = encrypt_content + pad_count.chr * pad_count
41
- else
42
- padded_content = encrypt_content
43
- end
58
+ padded_content =
59
+ if pad_count.positive?
60
+ encrypt_content + pad_count.chr * pad_count
61
+ else
62
+ encrypt_content
63
+ end
44
64
 
45
65
  alg = 'AES-256-CBC'
46
66
  aes = OpenSSL::Cipher.new(alg)
47
67
  iv = OpenSSL::Cipher.new(alg).random_iv
48
68
  aes.encrypt
49
- aes.key = aes_key
69
+ aes.key = _generate_aes_key
50
70
  aes.iv = iv
51
71
  cipher = aes.update(padded_content)
52
72
  msg = iv + cipher
53
- return Base64.strict_encode64 msg
73
+ Base64.strict_encode64 msg
74
+ end
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
+ )
54
90
  end
55
91
  end
56
92
  end
@@ -1,26 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Snapshot
4
- def read_snapshots(options)
5
- options = options.with_indifferent_access
6
- limit = options['limit']
7
- offset = options['offset']
8
- asset = options['asset']
9
- order = options['order']
6
+ def read_network_snapshots(options = {})
7
+ path = format(
8
+ '/network/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&order=%<order>s',
9
+ limit: options[:limit],
10
+ offset: options[:offset],
11
+ asset: options[:asset],
12
+ order: options[:order]
13
+ )
10
14
 
11
- path = 'network/snapshots'
12
- payload = {
13
- limit: limit,
14
- offset: offset,
15
- asset: asset,
16
- order: order
17
- }
18
- client.get(path, params: payload)
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 })
19
18
  end
20
19
 
21
- def read_snapshot(snapshot_id)
22
- path = format('network/snapshots/%s', snapshot_id)
23
- client.get(path)
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 })
31
+ end
32
+
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 })
24
39
  end
25
40
  end
26
41
  end
@@ -1,35 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module Transfer
4
- def create_transfer(pin, options)
5
- options = options.with_indifferent_access
6
-
7
- asset_id = options.fetch('asset_id')
8
- opponent_id = options.fetch('opponent_id')
9
- amount = options.fetch('amount')
10
- memo = options.fetch('memo')
11
- trace_id = options.fetch('trace_id')
12
- trace_id ||= SecureRandom.uuid
6
+ def create_transfer(pin, options, access_token: nil)
7
+ asset_id = options[:asset_id]
8
+ opponent_id = options[:opponent_id]
9
+ amount = options[:amount]
10
+ memo = options[:memo]
11
+ trace_id = options[:trace_id] || SecureRandom.uuid
12
+ encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
13
13
 
14
14
  path = '/transfers'
15
15
  payload = {
16
16
  asset_id: asset_id,
17
17
  opponent_id: opponent_id,
18
- pin: pin,
19
- amount: amount,
18
+ pin: encrypted_pin,
19
+ amount: amount.to_s,
20
20
  trace_id: trace_id,
21
21
  memo: memo
22
22
  }
23
23
 
24
- access_token ||= self.access_token('POST', path, payload.to_json)
25
- authorization = format('Bearer %s', access_token)
24
+ access_token ||= access_token('POST', path, payload.to_json)
25
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
26
26
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
27
27
  end
28
28
 
29
- def read_transfer(trace_id)
30
- path = format('/transfers/trace/%s', trace_id)
31
- access_token ||= self.access_token('GET', path, '')
32
- authorization = format('Bearer %s', access_token)
29
+ def read_transfer(trace_id, access_token: nil)
30
+ path = format('/transfers/trace/%<trace_id>s', trace_id: trace_id)
31
+ access_token ||= access_token('GET', path, '')
32
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
33
33
  client.get(path, headers: { 'Authorization': authorization })
34
34
  end
35
35
  end
@@ -1,29 +1,77 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MixinBot
2
4
  class API
3
5
  module User
4
- def read_user(user_id, access_token=nil)
5
- # user_id: Mixin User Id
6
- path = format('/users/%s', user_id)
7
- access_token ||= self.access_token('GET', path, '')
8
- authorization = format('Bearer %s', access_token)
6
+ # https://developers.mixin.one/api/beta-mixin-message/read-user/
7
+ def read_user(user_id)
8
+ # user_id: Mixin User UUID
9
+ path = format('/users/%<user_id>s', user_id: user_id)
10
+ access_token = access_token('GET', path, '')
11
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
9
12
  client.get(path, headers: { 'Authorization': authorization })
10
13
  end
11
14
 
12
- def search_user(q, access_token=nil)
13
- # q: Mixin Id or Phone Number
14
- path = format('/search/%s', q)
15
- access_token ||= self.access_token('GET', path, '')
16
- authorization = format('Bearer %s', access_token)
15
+ # https://developers.mixin.one/api/alpha-mixin-network/app-user/
16
+ # Create a new Mixin Network user (like a normal Mixin Messenger user). You should keep PrivateKey which is used to sign an AuthenticationToken and encrypted PIN for the user.
17
+ def create_user(full_name, key_type: 'RSA', rsa_key: nil, ed25519_key: nil)
18
+ case key_type
19
+ when 'RSA'
20
+ rsa_key ||= generate_rsa_key
21
+ session_secret = rsa_key[:public_key].gsub(/^-----.*PUBLIC KEY-----$/, '').strip
22
+ when 'Ed25519'
23
+ ed25519_key ||= generate_ed25519_key
24
+ session_secret = ed25519_key[:public_key]
25
+ else
26
+ raise 'Only RSA and Ed25519 are supported'
27
+ end
28
+
29
+ payload = {
30
+ full_name: full_name,
31
+ session_secret: session_secret
32
+ }
33
+ access_token = access_token('POST', '/users', payload.to_json)
34
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
35
+ res = client.post('/users', headers: { 'Authorization': authorization }, json: payload)
36
+
37
+ res.merge(rsa_key: rsa_key, ed25519_key: ed25519_key)
38
+ end
39
+
40
+ def generate_rsa_key
41
+ rsa_key = OpenSSL::PKey::RSA.new 1024
42
+ {
43
+ private_key: rsa_key.to_pem,
44
+ public_key: rsa_key.public_key.to_pem
45
+ }
46
+ end
47
+
48
+ def generate_ed25519_key
49
+ ed25519_key = JOSE::JWA::Ed25519.keypair
50
+ {
51
+ private_key: Base64.strict_encode64(ed25519_key[1]),
52
+ public_key: Base64.strict_encode64(ed25519_key[0])
53
+ }
54
+ end
55
+
56
+ # https://developers.mixin.one/api/beta-mixin-message/search-user/
57
+ # search by Mixin Id or Phone Number
58
+ def search_user(query)
59
+ path = format('/search/%<query>s', query: query)
60
+
61
+ access_token = access_token('GET', path, '')
62
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
17
63
  client.get(path, headers: { 'Authorization': authorization })
18
64
  end
19
65
 
20
- def fetch_users(user_ids, access_token=nil)
66
+ # https://developers.mixin.one/api/beta-mixin-message/read-users/
67
+ def fetch_users(user_ids)
21
68
  # user_ids: a array of user_ids
22
69
  path = '/users/fetch'
23
70
  user_ids = [user_ids] if user_ids.is_a? String
24
71
  payload = user_ids
25
- access_token ||= self.access_token('POST', path, payload.to_json)
26
- authorization = format('Bearer %s', access_token)
72
+
73
+ access_token = access_token('POST', path, payload.to_json)
74
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
27
75
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
28
76
  end
29
77
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Withdraw
6
+ # https://developers.mixin.one/api/alpha-mixin-network/create-address/
7
+ def create_withdraw_address(options, access_token: nil)
8
+ path = '/addresses'
9
+ encrypted_pin = encrypt_pin(options[:pin])
10
+ payload =
11
+ # for EOS withdraw, account_name & account_tag must be valid
12
+ if options[:public_key].nil?
13
+ {
14
+ asset_id: options[:asset_id],
15
+ account_name: options[:account_name],
16
+ account_tag: options[:account_tag],
17
+ label: options[:label],
18
+ pin: encrypted_pin
19
+ }
20
+ # for other withdraw
21
+ else
22
+ {
23
+ asset_id: options[:asset_id],
24
+ public_key: options[:public_key],
25
+ label: options[:label],
26
+ pin: encrypted_pin
27
+ }
28
+ end
29
+
30
+ access_token ||= access_token('POST', path, payload.to_json)
31
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
32
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
33
+ end
34
+
35
+ # https://developers.mixin.one/api/alpha-mixin-network/read-address/
36
+ def get_withdraw_address(address, access_token: nil)
37
+ path = format('/addresses/%<address>s', address: address)
38
+ access_token ||= access_token('GET', path, '')
39
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
40
+ client.get(path, headers: { 'Authorization': authorization })
41
+ end
42
+
43
+ # https://developers.mixin.one/api/alpha-mixin-network/delete-address/
44
+ def delete_withdraw_address(address, pin, access_token: nil)
45
+ path = format('/addresses/%<address>s/delete', address: address)
46
+ payload = {
47
+ pin: encrypt_pin(pin)
48
+ }
49
+
50
+ access_token ||= access_token('POST', path, payload.to_json)
51
+ authorization = format('Bearer %<access_token>s', access_token: access_token)
52
+ client.post(path, headers: { 'Authorization': authorization }, json: payload)
53
+ end
54
+
55
+ # https://developers.mixin.one/api/alpha-mixin-network/withdrawal-addresses/
56
+ def withdrawals(options, access_token: nil)
57
+ address_id = options[:address_id]
58
+ pin = options[:pin]
59
+ amount = options[:amount]
60
+ trace_id = options[:trace_id]
61
+ memo = options[:memo]
62
+
63
+ path = '/withdrawals'
64
+ payload = {
65
+ address_id: address_id,
66
+ amount: amount,
67
+ trace_id: trace_id,
68
+ memo: memo,
69
+ pin: encrypt_pin(pin)
70
+ }
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
+ end
77
+ end
78
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'awesome_print'
4
+ require 'cli/ui'
5
+ require 'thor'
6
+ require 'yaml'
7
+ require 'json'
8
+ require_relative './cli/node'
9
+ require_relative './cli/me'
10
+ require_relative './cli/multisig'
11
+
12
+ module MixinBot
13
+ class CLI < Thor
14
+ # https://github.com/Shopify/cli-ui
15
+ UI = ::CLI::UI
16
+
17
+ class_option :apihost, type: :string, aliases: '-a', desc: 'Specify mixin api host, default as api.mixin.one'
18
+ class_option :pretty, type: :boolean, aliases: '-p', desc: 'Print output in pretty'
19
+
20
+ attr_reader :config, :api
21
+
22
+ def initialize(*args)
23
+ super
24
+ if File.exist? options[:config].to_s
25
+ @config =
26
+ begin
27
+ YAML.load_file options[:config]
28
+ rescue StandardError => e
29
+ log UI.fmt(
30
+ format(
31
+ '{{x}} %<file>s is not a valid .yml file',
32
+ file: options[:config]
33
+ )
34
+ )
35
+ UI::Frame.open('{{x}}', color: :red) do
36
+ log e
37
+ end
38
+ end
39
+ elsif options[:config]
40
+ @confg =
41
+ begin
42
+ JSON.parse options[:config]
43
+ rescue StandardError => e
44
+ log UI.fmt(
45
+ format(
46
+ '{{x}} Failed to parse %<config>s',
47
+ config: options[:config]
48
+ )
49
+ )
50
+ UI::Frame.open('{{x}}', color: :red) do
51
+ log e
52
+ end
53
+ end
54
+ end
55
+
56
+ return unless @config
57
+
58
+ MixinBot.api_host = options[:apihost]
59
+ @api ||=
60
+ begin
61
+ MixinBot::API.new(
62
+ client_id: @config['client_id'],
63
+ client_secret: @config['client_secret'],
64
+ session_id: @config['session_id'],
65
+ pin_token: @config['pin_token'],
66
+ private_key: @config['private_key'],
67
+ pin_code: @config['pin_code']
68
+ )
69
+ rescue StandardError => e
70
+ log UI.fmt '{{x}}: Failed to initialize api, maybe your config is incorrect.'
71
+ UI.Frame.open('{{x}}', color: :red) do
72
+ log e
73
+ end
74
+ end
75
+ end
76
+
77
+ desc 'node', 'mixin node commands helper'
78
+ subcommand 'node', MixinBot::NodeCLI
79
+
80
+ desc 'version', 'Distay MixinBot version'
81
+ def version
82
+ log MixinBot::VERSION
83
+ end
84
+
85
+ def self.exit_on_failure?
86
+ true
87
+ end
88
+
89
+ private
90
+
91
+ def api_method(method, *args, **params)
92
+ if api.nil?
93
+ log UI.fmt '{{x}} MixinBot api not initialized!'
94
+ return
95
+ end
96
+
97
+ res = if args.empty? && params.empty?
98
+ api&.public_send method
99
+ elsif args.empty? && !params.empty?
100
+ api&.public_send method params
101
+ elsif !args.empty? && params.empty?
102
+ api&.public_send method, args
103
+ else
104
+ args.push params
105
+ api&.public_send method, args
106
+ end
107
+ log res
108
+
109
+ [res, res && res['error'].nil?]
110
+ rescue MixinBot::Errors => e
111
+ UI::Frame.open('{{x}}', color: :red) do
112
+ log e
113
+ end
114
+ end
115
+
116
+ def log(obj)
117
+ if options[:pretty]
118
+ if obj.is_a? String
119
+ puts obj
120
+ else
121
+ ap obj
122
+ end
123
+ else
124
+ puts obj.inspect
125
+ end
126
+ end
127
+ end
128
+ end