mixin_bot 0.1.0 → 0.3.1

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.
@@ -3,18 +3,19 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Transfer
6
- def create_transfer(pin, options)
6
+ def create_transfer(pin, options, access_token: nil)
7
7
  asset_id = options[:asset_id]
8
8
  opponent_id = options[:opponent_id]
9
9
  amount = options[:amount]
10
10
  memo = options[:memo]
11
11
  trace_id = options[:trace_id] || SecureRandom.uuid
12
+ encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
12
13
 
13
14
  path = '/transfers'
14
15
  payload = {
15
16
  asset_id: asset_id,
16
17
  opponent_id: opponent_id,
17
- pin: pin,
18
+ pin: encrypted_pin,
18
19
  amount: amount.to_s,
19
20
  trace_id: trace_id,
20
21
  memo: memo
@@ -25,7 +26,7 @@ module MixinBot
25
26
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
26
27
  end
27
28
 
28
- def read_transfer(trace_id)
29
+ def read_transfer(trace_id, access_token: nil)
29
30
  path = format('/transfers/trace/%<trace_id>s', trace_id: trace_id)
30
31
  access_token ||= access_token('GET', path, '')
31
32
  authorization = format('Bearer %<access_token>s', access_token: access_token)
@@ -3,31 +3,38 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module User
6
- def read_user(user_id, access_token = nil)
7
- # user_id: Mixin User Id
6
+ # https://developers.mixin.one/api/beta-mixin-message/read-user/
7
+ def read_user(user_id)
8
+ # user_id: Mixin User UUID
8
9
  path = format('/users/%<user_id>s', user_id: user_id)
9
- access_token ||= access_token('GET', path, '')
10
+ access_token = access_token('GET', path, '')
10
11
  authorization = format('Bearer %<access_token>s', access_token: access_token)
11
12
  client.get(path, headers: { 'Authorization': authorization })
12
13
  end
13
14
 
14
15
  # https://developers.mixin.one/api/alpha-mixin-network/app-user/
15
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.
16
- def create_user(full_name, rsa_key = nil)
17
- rsa_key ||= generate_rsa_key
18
- session_secret = rsa_key[:public_key]
19
- session_secret.gsub!(/^-----.*PUBLIC KEY-----$/, '').strip!
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
20
28
 
21
29
  payload = {
22
30
  full_name: full_name,
23
31
  session_secret: session_secret
24
32
  }
25
-
26
33
  access_token = access_token('POST', '/users', payload.to_json)
27
34
  authorization = format('Bearer %<access_token>s', access_token: access_token)
28
35
  res = client.post('/users', headers: { 'Authorization': authorization }, json: payload)
29
36
 
30
- res.merge(rsa_key: rsa_key)
37
+ res.merge(rsa_key: rsa_key, ed25519_key: ed25519_key)
31
38
  end
32
39
 
33
40
  def generate_rsa_key
@@ -37,25 +44,33 @@ module MixinBot
37
44
  public_key: rsa_key.public_key.to_pem
38
45
  }
39
46
  end
40
-
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
+
41
56
  # https://developers.mixin.one/api/beta-mixin-message/search-user/
42
57
  # search by Mixin Id or Phone Number
43
- def search_user(query, access_token = nil)
58
+ def search_user(query)
44
59
  path = format('/search/%<query>s', query: query)
45
60
 
46
- access_token ||= access_token('GET', path, '')
61
+ access_token = access_token('GET', path, '')
47
62
  authorization = format('Bearer %<access_token>s', access_token: access_token)
48
63
  client.get(path, headers: { 'Authorization': authorization })
49
64
  end
50
65
 
51
66
  # https://developers.mixin.one/api/beta-mixin-message/read-users/
52
- def fetch_users(user_ids, access_token = nil)
67
+ def fetch_users(user_ids)
53
68
  # user_ids: a array of user_ids
54
69
  path = '/users/fetch'
55
70
  user_ids = [user_ids] if user_ids.is_a? String
56
71
  payload = user_ids
57
72
 
58
- access_token ||= access_token('POST', path, payload.to_json)
73
+ access_token = access_token('POST', path, payload.to_json)
59
74
  authorization = format('Bearer %<access_token>s', access_token: access_token)
60
75
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
61
76
  end
@@ -4,10 +4,10 @@ module MixinBot
4
4
  class API
5
5
  module Withdraw
6
6
  # https://developers.mixin.one/api/alpha-mixin-network/create-address/
7
- def create_withdraw_address(options)
7
+ def create_withdraw_address(options, access_token: nil)
8
8
  path = '/addresses'
9
9
  encrypted_pin = encrypt_pin(options[:pin])
10
- payload =
10
+ payload =
11
11
  # for EOS withdraw, account_name & account_tag must be valid
12
12
  if options[:public_key].nil?
13
13
  {
@@ -27,33 +27,33 @@ module MixinBot
27
27
  }
28
28
  end
29
29
 
30
- access_token = access_token('POST', path, payload.to_json)
30
+ access_token ||= access_token('POST', path, payload.to_json)
31
31
  authorization = format('Bearer %<access_token>s', access_token: access_token)
32
32
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
33
33
  end
34
34
 
35
35
  # https://developers.mixin.one/api/alpha-mixin-network/read-address/
36
- def get_withdraw_address(address)
36
+ def get_withdraw_address(address, access_token: nil)
37
37
  path = format('/addresses/%<address>s', address: address)
38
- access_token = access_token('GET', path, '')
38
+ access_token ||= access_token('GET', path, '')
39
39
  authorization = format('Bearer %<access_token>s', access_token: access_token)
40
40
  client.get(path, headers: { 'Authorization': authorization })
41
41
  end
42
42
 
43
43
  # https://developers.mixin.one/api/alpha-mixin-network/delete-address/
44
- def delete_withdraw_address(address, pin)
44
+ def delete_withdraw_address(address, pin, access_token: nil)
45
45
  path = format('/addresses/%<address>s/delete', address: address)
46
46
  payload = {
47
47
  pin: encrypt_pin(pin)
48
48
  }
49
49
 
50
- access_token = access_token('POST', path, payload.to_json)
50
+ access_token ||= access_token('POST', path, payload.to_json)
51
51
  authorization = format('Bearer %<access_token>s', access_token: access_token)
52
52
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
53
53
  end
54
54
 
55
55
  # https://developers.mixin.one/api/alpha-mixin-network/withdrawal-addresses/
56
- def withdrawals(options)
56
+ def withdrawals(options, access_token: nil)
57
57
  address_id = options[:address_id]
58
58
  pin = options[:pin]
59
59
  amount = options[:amount]
@@ -69,10 +69,10 @@ module MixinBot
69
69
  pin: encrypt_pin(pin)
70
70
  }
71
71
 
72
- access_token = access_token('POST', path, payload.to_json)
72
+ access_token ||= access_token('POST', path, payload.to_json)
73
73
  authorization = format('Bearer %<access_token>s', access_token: access_token)
74
74
  client.post(path, headers: { 'Authorization': authorization }, json: payload)
75
75
  end
76
76
  end
77
77
  end
78
- 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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class CLI < Thor
5
+ desc 'read_me', 'fetch mixin bot profile'
6
+ option :config, required: true, aliases: '-c'
7
+ def read_me
8
+ api_method(:read_me)
9
+ end
10
+
11
+ desc 'read_assets', 'fetch mixin bot assets'
12
+ option :config, required: true, aliases: '-c'
13
+ def read_assets
14
+ api_method(:read_assets)
15
+ end
16
+
17
+ desc 'cal_assets_as_usd', 'fetch mixin bot assets'
18
+ option :config, required: true, aliases: '-c'
19
+ def cal_assets_as_usd
20
+ assets, success = read_assets
21
+ return unless success
22
+
23
+ sum = assets['data'].map(
24
+ &lambda { |asset|
25
+ asset['balance'].to_f * asset['price_usd'].to_f
26
+ }
27
+ ).sum
28
+ UI::Frame.open('USD') do
29
+ log sum
30
+ end
31
+ end
32
+
33
+ desc 'read_asset', 'fetch specific asset of mixin bot'
34
+ option :config, required: true, aliases: '-c'
35
+ option :assetid, required: true, aliases: '-s'
36
+ def read_asset
37
+ api_method(:read_asset, options[:assetid])
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class CLI < Thor
5
+ desc 'get_all_multisigs', 'fetch all utxos'
6
+ option :config, required: true, aliases: '-c'
7
+ def all_multisigs
8
+ api_method(:get_all_multisigs)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module MixinBot
6
+ class NodeCLI < Thor
7
+ # https://github.com/Shopify/cli-ui
8
+ UI = ::CLI::UI
9
+ UI::StdoutRouter.enable
10
+
11
+ desc 'listallnodes', 'List all mixin nodes'
12
+ def listallnodes
13
+ return unless ensure_mixin_command_exist
14
+
15
+ o, e, _s = Open3.capture3('mixin -n 35.188.235.212:8239 listallnodes')
16
+ log e unless e.empty?
17
+ log o
18
+ end
19
+
20
+ desc 'mint', 'Mint from mint distributions'
21
+ option :node, required: true, aliases: '-n', desc: 'node RPC address'
22
+ option :batch, type: :numeric, required: true, aliases: '-b', desc: 'mint batch'
23
+ option :view, type: :string, required: true, aliases: '-v', desc: 'view key'
24
+ option :address, type: :string, required: true, aliases: '-d', desc: 'address'
25
+ def mint
26
+ c = (Date.today - Date.new(2019, 2, 28)).to_i + 1
27
+ distributions = []
28
+ UI::Spinner.spin('Listing mint distributions') do |spinner|
29
+ o, _e, _s = Open3.capture3(
30
+ 'mixin',
31
+ '-n',
32
+ options[:node],
33
+ 'listmintdistributions',
34
+ '-c',
35
+ c.to_s
36
+ )
37
+ distributions = eval o
38
+ spinner.update_title "#{distributions.size} mint distributions listed"
39
+ end
40
+
41
+ tx = ''
42
+ UI::Spinner.spin('Finding transaction') do |spinner|
43
+ index = distributions.index(&->(d) { d[:batch] == options[:batch] })
44
+ tx = distributions[index][:transaction]
45
+ spinner.update_title "Transaction hash found: #{tx}"
46
+ end
47
+
48
+ UI::Spinner.spin('Fetching transaction') do |spinner|
49
+ o, _e, _s = Open3.capture3(
50
+ 'mixin',
51
+ '-n',
52
+ options[:node],
53
+ 'gettransaction',
54
+ '-x',
55
+ tx
56
+ )
57
+ tx = eval o
58
+ spinner.update_title "#{tx[:outputs].size} transaction outputs found"
59
+ end
60
+
61
+ tx[:outputs].each_with_index do |output, index|
62
+ address = ''
63
+ UI::Spinner.spin("Checking output index: #{index}") do |spinner|
64
+ o, _e, _s = Open3.capture3(
65
+ 'mixin',
66
+ 'decryptghostkey',
67
+ '--key',
68
+ output[:keys].first,
69
+ '--mask',
70
+ output[:mask],
71
+ '--view',
72
+ options[:view]
73
+ )
74
+ address = o.chomp
75
+ spinner.update_title "Index #{index} Address: #{address}"
76
+ end
77
+ log "Found Utxo: #{index}" if address == options[:address]
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def ensure_mixin_command_exist
84
+ return true if command?('mixin')
85
+
86
+ log UI.fmt '{{x}} `mixin` command is not valid!'
87
+ log UI.fmt 'Please install mixin software and provide a executable `mixin` command'
88
+ end
89
+
90
+ def command?(name)
91
+ `which #{name}`
92
+ $CHILD_STATUS.success?
93
+ end
94
+
95
+ def log(obj)
96
+ if options[:pretty]
97
+ if obj.is_a? String
98
+ puts obj
99
+ else
100
+ ap obj
101
+ end
102
+ else
103
+ puts obj.inspect
104
+ end
105
+ end
106
+ end
107
+ end