mixin_bot 0.12.1 → 1.1.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mixin_bot/api/address.rb +21 -0
  3. data/lib/mixin_bot/api/app.rb +5 -11
  4. data/lib/mixin_bot/api/asset.rb +9 -16
  5. data/lib/mixin_bot/api/attachment.rb +27 -22
  6. data/lib/mixin_bot/api/auth.rb +34 -56
  7. data/lib/mixin_bot/api/blaze.rb +4 -3
  8. data/lib/mixin_bot/api/conversation.rb +29 -49
  9. data/lib/mixin_bot/api/encrypted_message.rb +19 -19
  10. data/lib/mixin_bot/api/inscription.rb +71 -0
  11. data/lib/mixin_bot/api/legacy_collectible.rb +140 -0
  12. data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
  13. data/lib/mixin_bot/api/legacy_output.rb +50 -0
  14. data/lib/mixin_bot/api/legacy_payment.rb +31 -0
  15. data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
  16. data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
  17. data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
  18. data/lib/mixin_bot/api/me.rb +13 -17
  19. data/lib/mixin_bot/api/message.rb +13 -10
  20. data/lib/mixin_bot/api/multisig.rb +17 -222
  21. data/lib/mixin_bot/api/output.rb +48 -0
  22. data/lib/mixin_bot/api/payment.rb +9 -20
  23. data/lib/mixin_bot/api/pin.rb +57 -65
  24. data/lib/mixin_bot/api/rpc.rb +12 -14
  25. data/lib/mixin_bot/api/snapshot.rb +15 -29
  26. data/lib/mixin_bot/api/tip.rb +43 -0
  27. data/lib/mixin_bot/api/transaction.rb +295 -60
  28. data/lib/mixin_bot/api/transfer.rb +69 -31
  29. data/lib/mixin_bot/api/user.rb +88 -53
  30. data/lib/mixin_bot/api/withdraw.rb +52 -53
  31. data/lib/mixin_bot/api.rb +81 -46
  32. data/lib/mixin_bot/cli/api.rb +149 -5
  33. data/lib/mixin_bot/cli/utils.rb +14 -4
  34. data/lib/mixin_bot/cli.rb +13 -10
  35. data/lib/mixin_bot/client.rb +74 -127
  36. data/lib/mixin_bot/configuration.rb +98 -0
  37. data/lib/mixin_bot/nfo.rb +174 -0
  38. data/lib/mixin_bot/transaction.rb +524 -0
  39. data/lib/mixin_bot/utils/address.rb +121 -0
  40. data/lib/mixin_bot/utils/crypto.rb +218 -0
  41. data/lib/mixin_bot/utils/decoder.rb +56 -0
  42. data/lib/mixin_bot/utils/encoder.rb +63 -0
  43. data/lib/mixin_bot/utils.rb +8 -109
  44. data/lib/mixin_bot/uuid.rb +41 -0
  45. data/lib/mixin_bot/version.rb +1 -1
  46. data/lib/mixin_bot.rb +39 -14
  47. data/lib/mvm/bridge.rb +2 -19
  48. data/lib/mvm/client.rb +11 -33
  49. data/lib/mvm/nft.rb +4 -4
  50. data/lib/mvm/registry.rb +9 -9
  51. data/lib/mvm/scan.rb +3 -5
  52. data/lib/mvm.rb +5 -6
  53. metadata +77 -103
  54. data/lib/mixin_bot/api/collectible.rb +0 -138
  55. data/lib/mixin_bot/utils/nfo.rb +0 -176
  56. data/lib/mixin_bot/utils/transaction.rb +0 -478
  57. data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -3,76 +3,111 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module User
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)
12
- client.get(path, headers: { 'Authorization': authorization })
6
+ def user(user_id, access_token: nil)
7
+ path = format('/users/%<user_id>s', user_id:)
8
+ client.get path, access_token:
13
9
  end
14
10
 
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
11
+ def create_user(full_name, key: nil)
12
+ keypair = JOSE::JWA::Ed25519.keypair key
13
+ session_secret = Base64.urlsafe_encode64 keypair[0], padding: false
14
+ private_key = keypair[1].unpack1('H*')
28
15
 
16
+ path = '/users'
29
17
  payload = {
30
- full_name: full_name,
31
- session_secret: session_secret
18
+ full_name:,
19
+ session_secret:
32
20
  }
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
21
 
37
- res.merge(rsa_key: rsa_key, ed25519_key: ed25519_key)
22
+ res = client.post path, **payload
23
+ res.merge(private_key:).with_indifferent_access
38
24
  end
39
25
 
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
- }
26
+ def search_user(query, access_token: nil)
27
+ path = format('/search/%<query>s', query:)
28
+
29
+ client.get path, access_token:
46
30
  end
47
31
 
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])
32
+ def fetch_users(user_ids)
33
+ path = '/users/fetch'
34
+ user_ids = [user_ids] if user_ids.is_a? String
35
+ payload = user_ids
36
+
37
+ client.post path, *payload
38
+ end
39
+
40
+ def create_safe_user(name, private_key: nil, spend_key: nil)
41
+ private_keypair = JOSE::JWA::Ed25519.keypair private_key
42
+ private_key = private_keypair[1].unpack1('H*')
43
+
44
+ spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
45
+ spend_key = spend_keypair[1].unpack1('H*')
46
+
47
+ user = create_user name, key: private_keypair[1][...32]
48
+
49
+ keystore = {
50
+ app_id: user['data']['user_id'],
51
+ session_id: user['data']['session_id'],
52
+ private_key:,
53
+ pin_token: user['data']['pin_token_base64'],
54
+ spend_key: spend_keypair[1].unpack1('H*')
53
55
  }
56
+ user_api = MixinBot::API.new(**keystore)
57
+
58
+ user_api.update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: user['data']['tip_counter'])
59
+
60
+ # wait for tip pin update in server
61
+ sleep 1
62
+
63
+ user_api.safe_register spend_key
64
+
65
+ keystore
54
66
  end
55
67
 
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)
68
+ def safe_register(pin, spend_key: nil)
69
+ path = '/safe/users'
70
+
71
+ spend_key ||= MixinBot.utils.decode_key pin
72
+ key = JOSE::JWA::Ed25519.keypair spend_key[...32]
73
+ public_key = key[0].unpack1('H*')
74
+
75
+ hex = SHA3::Digest::SHA256.hexdigest config.app_id
76
+ signature = Base64.urlsafe_encode64 JOSE::JWA::Ed25519.sign([hex].pack('H*'), key[1]), padding: false
60
77
 
61
- access_token = access_token('GET', path, '')
62
- authorization = format('Bearer %<access_token>s', access_token: access_token)
63
- client.get(path, headers: { 'Authorization': authorization })
78
+ pin_base64 = encrypt_tip_pin pin, 'SEQUENCER:REGISTER:', config.app_id, public_key
79
+
80
+ payload = {
81
+ public_key:,
82
+ signature:,
83
+ pin_base64:
84
+ }
85
+
86
+ client.post path, **payload
64
87
  end
65
88
 
66
- # https://developers.mixin.one/api/beta-mixin-message/read-users/
67
- def fetch_users(user_ids)
68
- # user_ids: a array of user_ids
69
- path = '/users/fetch'
70
- user_ids = [user_ids] if user_ids.is_a? String
71
- payload = user_ids
89
+ def migrate_to_safe(spend_key:, pin: nil)
90
+ profile = me['data']
91
+ return true if profile['has_safe']
92
+
93
+ spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
94
+ spend_key = spend_keypair[1].unpack1('H*')
72
95
 
73
- access_token = access_token('POST', path, payload.to_json)
74
- authorization = format('Bearer %<access_token>s', access_token: access_token)
75
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
96
+ if profile['tip_key_base64'].blank?
97
+ new_pin = MixinBot.utils.tip_public_key(spend_keypair[0], counter: profile['tip_counter'])
98
+ update_pin(pin: new_pin, old_pin: pin)
99
+
100
+ pin = new_pin
101
+ end
102
+
103
+ # wait for tip pin update in server
104
+ sleep 1
105
+
106
+ safe_register pin, spend_key
107
+
108
+ {
109
+ spend_key:
110
+ }.with_indifferent_access
76
111
  end
77
112
  end
78
113
  end
@@ -3,75 +3,74 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Withdraw
6
- # https://developers.mixin.one/api/alpha-mixin-network/create-address/
7
- def create_withdraw_address(options, access_token: nil)
6
+ def create_withdraw_address(**kwargs)
8
7
  path = '/addresses'
9
- encrypted_pin = encrypt_pin(options[:pin])
8
+ pin = kwargs[:pin]
10
9
  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
10
+ {
11
+ asset_id: kwargs[:asset_id],
12
+ destination: kwargs[:destination],
13
+ tag: kwargs[:tag],
14
+ label: kwargs[:label]
15
+ }
16
+
17
+ if pin.length > 6
18
+ payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:ADDRESS:ADD:', payload[:asset_id], payload[:destination], payload[:tag], payload[:label]
19
+ else
20
+ payload[:pin] = encrypt_pin pin
21
+ end
29
22
 
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)
23
+ client.post path, **payload
33
24
  end
34
25
 
35
- # https://developers.mixin.one/api/alpha-mixin-network/read-address/
36
26
  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 })
27
+ path = format('/addresses/%<address>s', address:)
28
+
29
+ client.get path, access_token:
41
30
  end
42
31
 
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
- }
32
+ def delete_withdraw_address(address, **kwargs)
33
+ pin = kwargs[:pin]
49
34
 
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)
35
+ path = format('/addresses/%<address>s/delete', address:)
36
+ payload =
37
+ if pin.length > 6
38
+ {
39
+ pin_base64: encrypt_tip_pin(pin, 'TIP:ADDRESS:REMOVE:', address)
40
+ }
41
+ else
42
+ {
43
+ pin: encrypt_pin(pin)
44
+ }
45
+ end
46
+
47
+ client.post path, **payload
53
48
  end
54
49
 
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]
50
+ def withdrawals(**kwargs)
51
+ address_id = kwargs[:address_id]
52
+ pin = kwargs[:pin]
53
+ amount = format('%.8f', kwargs[:amount].to_d.to_r)
54
+ trace_id = kwargs[:trace_id]
55
+ memo = kwargs[:memo]
56
+ kwargs[:access_token]
62
57
 
63
58
  path = '/withdrawals'
64
59
  payload = {
65
- address_id: address_id,
66
- amount: amount,
67
- trace_id: trace_id,
68
- memo: memo,
69
- pin: encrypt_pin(pin)
60
+ address_id:,
61
+ amount:,
62
+ trace_id:,
63
+ memo:
70
64
  }
71
65
 
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)
66
+ if pin.length > 6
67
+ fee = '0'
68
+ payload[:pin_base64] = encrypt_tip_pin pin, 'TIP:WITHDRAW:', address_id, amount, fee, trace_id, memo
69
+ else
70
+ payload[:pin] = encrypt_pin pin
71
+ end
72
+
73
+ client.post path, **payload
75
74
  end
76
75
  end
77
76
  end
data/lib/mixin_bot/api.rb CHANGED
@@ -1,62 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './client'
4
- require_relative './api/app'
5
- require_relative './api/asset'
6
- require_relative './api/attachment'
7
- require_relative './api/auth'
8
- require_relative './api/blaze'
9
- require_relative './api/collectible'
10
- require_relative './api/conversation'
11
- require_relative './api/encrypted_message'
12
- require_relative './api/me'
13
- require_relative './api/message'
14
- require_relative './api/multisig'
15
- require_relative './api/payment'
16
- require_relative './api/pin'
17
- require_relative './api/rpc'
18
- require_relative './api/snapshot'
19
- require_relative './api/transaction'
20
- require_relative './api/transfer'
21
- require_relative './api/user'
22
- require_relative './api/withdraw'
3
+ require_relative 'client'
4
+ require_relative 'configuration'
5
+ require_relative 'api/address'
6
+ require_relative 'api/app'
7
+ require_relative 'api/asset'
8
+ require_relative 'api/attachment'
9
+ require_relative 'api/auth'
10
+ require_relative 'api/blaze'
11
+ require_relative 'api/conversation'
12
+ require_relative 'api/encrypted_message'
13
+ require_relative 'api/inscription'
14
+ require_relative 'api/legacy_collectible'
15
+ require_relative 'api/legacy_multisig'
16
+ require_relative 'api/legacy_output'
17
+ require_relative 'api/legacy_payment'
18
+ require_relative 'api/legacy_snapshot'
19
+ require_relative 'api/legacy_transaction'
20
+ require_relative 'api/legacy_transfer'
21
+ require_relative 'api/me'
22
+ require_relative 'api/message'
23
+ require_relative 'api/multisig'
24
+ require_relative 'api/output'
25
+ require_relative 'api/payment'
26
+ require_relative 'api/pin'
27
+ require_relative 'api/rpc'
28
+ require_relative 'api/snapshot'
29
+ require_relative 'api/tip'
30
+ require_relative 'api/transaction'
31
+ require_relative 'api/transfer'
32
+ require_relative 'api/user'
33
+ require_relative 'api/withdraw'
23
34
 
24
35
  module MixinBot
25
36
  class API
26
- attr_reader :client_id, :client_secret, :session_id, :pin_token, :private_key, :client, :blaze_host, :key_type
27
-
28
- def initialize(options = {})
29
- @client_id = options[:client_id] || MixinBot.client_id
30
- @client_secret = options[:client_secret] || MixinBot.client_secret
31
- @session_id = options[:session_id] || MixinBot.session_id
32
- @client = Client.new(MixinBot.api_host || 'api.mixin.one')
33
- @blaze_host = MixinBot.blaze_host || 'blaze.mixin.one'
34
- @pin_token =
35
- begin
36
- Base64.urlsafe_decode64 options[:pin_token] || MixinBot.pin_token
37
- rescue StandardError
38
- ''
37
+ attr_reader :config, :client
38
+
39
+ def initialize(**kwargs)
40
+ @config =
41
+ if kwargs.present?
42
+ MixinBot::Configuration.new(**kwargs)
43
+ else
44
+ MixinBot.config
39
45
  end
40
- _private_key = options[:private_key] || MixinBot.private_key
41
- if /^-----BEGIN RSA PRIVATE KEY-----/.match? _private_key
42
- @private_key = _private_key.gsub('\\r\\n', "\n").gsub("\r\n", "\n")
43
- @key_type = :rsa
44
- else
45
- @private_key = Base64.urlsafe_decode64 _private_key
46
- @key_type = :ed25519
47
- end
46
+
47
+ @client = Client.new(@config)
48
+ end
49
+
50
+ def utils
51
+ MixinBot::Utils
52
+ end
53
+
54
+ def client_id
55
+ config.app_id
48
56
  end
49
57
 
50
- def sign_raw_transaction(tx)
51
- MixinBot::Utils.sign_raw_transaction tx
58
+ def access_token(method, uri, body, **kwargs)
59
+ utils.access_token(
60
+ method,
61
+ uri,
62
+ body,
63
+ exp_in: kwargs.delete(:exp_in) || 600,
64
+ scp: kwargs.delete(:scp) || 'FULL',
65
+ app_id: config.app_id,
66
+ session_id: config.session_id,
67
+ private_key: config.session_private_key
68
+ )
69
+ end
70
+
71
+ def encode_raw_transaction(txn)
72
+ utils.encode_raw_transaction txn
52
73
  end
53
74
 
54
75
  def decode_raw_transaction(raw)
55
- MixinBot::Utils.decode_raw_transaction raw
76
+ utils.decode_raw_transaction raw
77
+ end
78
+
79
+ def generate_trace_from_hash(hash, output_index = 0)
80
+ utils.generate_trace_from_hash hash, output_index
56
81
  end
57
82
 
58
83
  # Use a mixin software to implement transaction build
59
- def sign_raw_transaction_native(json)
84
+ def encode_raw_transaction_native(json)
60
85
  ensure_mixin_command_exist
61
86
  command = format("mixin signrawtransaction --raw '%<arg>s'", arg: json)
62
87
 
@@ -77,21 +102,31 @@ module MixinBot
77
102
  JSON.parse output.chomp
78
103
  end
79
104
 
105
+ include MixinBot::API::Address
80
106
  include MixinBot::API::App
81
107
  include MixinBot::API::Asset
82
108
  include MixinBot::API::Attachment
83
109
  include MixinBot::API::Auth
84
110
  include MixinBot::API::Blaze
85
- include MixinBot::API::Collectible
86
111
  include MixinBot::API::Conversation
87
112
  include MixinBot::API::EncryptedMessage
113
+ include MixinBot::API::Inscription
114
+ include MixinBot::API::LegacyCollectible
115
+ include MixinBot::API::LegacyMultisig
116
+ include MixinBot::API::LegacyOutput
117
+ include MixinBot::API::LegacyPayment
118
+ include MixinBot::API::LegacySnapshot
119
+ include MixinBot::API::LegacyTransaction
120
+ include MixinBot::API::LegacyTransfer
88
121
  include MixinBot::API::Me
89
122
  include MixinBot::API::Message
90
123
  include MixinBot::API::Multisig
124
+ include MixinBot::API::Output
91
125
  include MixinBot::API::Payment
92
126
  include MixinBot::API::Pin
93
127
  include MixinBot::API::Rpc
94
128
  include MixinBot::API::Snapshot
129
+ include MixinBot::API::Tip
95
130
  include MixinBot::API::Transaction
96
131
  include MixinBot::API::Transfer
97
132
  include MixinBot::API::User
@@ -34,16 +34,16 @@ module MixinBot
34
34
  end
35
35
 
36
36
  access_token = options[:accesstoken] || api_instance.access_token(options[:method].upcase, path, payload.blank? ? '' : payload.to_json)
37
- authorization = format('Bearer %<access_token>s', access_token: access_token)
37
+ authorization = format('Bearer %<access_token>s', access_token:)
38
38
  res = {}
39
39
 
40
40
  CLI::UI::Spinner.spin("#{options[:method]} #{path}, payload: #{payload}") do |_spinner|
41
41
  res =
42
42
  case options[:method].downcase.to_sym
43
43
  when :post
44
- api_instance.client.post(path, headers: { 'Authorization': authorization }, json: payload)
44
+ api_instance.client.post(path, headers: { Authorization: authorization }, json: payload)
45
45
  when :get
46
- api_instance.client.get(path, headers: { 'Authorization': authorization })
46
+ api_instance.client.get(path, headers: { Authorization: authorization })
47
47
  end
48
48
  end
49
49
 
@@ -52,19 +52,163 @@ module MixinBot
52
52
 
53
53
  desc 'authcode', 'code to authorize other mixin account'
54
54
  option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
55
- option :client_id, type: :string, required: true, aliases: '-c', desc: 'client_id of bot to authorize'
55
+ option :app_id, type: :string, required: true, aliases: '-c', desc: 'app_id of bot to authorize'
56
56
  option :scope, type: :array, default: ['PROFILE:READ'], aliases: '-s', desc: 'scope to authorize'
57
57
  def authcode
58
58
  res = {}
59
59
  CLI::UI::Spinner.spin('POST /oauth/authorize') do |_spinner|
60
60
  res =
61
61
  api_instance.authorize_code(
62
- user_id: options[:client_id],
62
+ user_id: options[:app_id],
63
63
  scope: options[:scope],
64
64
  pin: keystore['pin']
65
65
  )
66
66
  end
67
67
  log res['data']
68
68
  end
69
+
70
+ desc 'updatetip PIN', 'update TIP pin'
71
+ option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
72
+ def updatetip(pin)
73
+ profile = api_instance.me
74
+ log UI.fmt "{{v}} #{profile['full_name']}, TIP counter: #{profile['tip_counter']}"
75
+
76
+ counter = profile['tip_counter']
77
+ key = api_instance.prepare_tip_key counter
78
+ log UI.fmt "{{v}} Generated key: #{key[:private_key]}"
79
+
80
+ res = api_instance.update_pin old_pin: pin.to_s, pin: key[:public_key]
81
+
82
+ log({
83
+ pin: key[:private_key],
84
+ tip_key_base64: res['tip_key_base64']
85
+ })
86
+ rescue StandardError => e
87
+ log UI.fmt "{{x}} #{e.inspect}"
88
+ end
89
+
90
+ desc 'verifypin PIN', 'verify pin'
91
+ option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
92
+ def verifypin(pin)
93
+ res = api_instance.verify_pin pin.to_s
94
+
95
+ log res
96
+ rescue StandardError => e
97
+ log UI.fmt "{{x}} #{e.inspect}"
98
+ end
99
+
100
+ desc 'transfer USER_ID', 'transfer asset to USER_ID'
101
+ option :asset, type: :string, required: true, desc: 'Asset ID'
102
+ option :amount, type: :numeric, required: true, desc: 'Amount'
103
+ option :memo, type: :string, required: false, desc: 'memo'
104
+ option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
105
+ def transfer(user_id)
106
+ res = {}
107
+
108
+ CLI::UI::Spinner.spin "Try to transfer #{options[:amount]} #{options[:asset]} to #{user_id}" do |_spinner|
109
+ res = api_instance.create_transfer(
110
+ keystore['pin'],
111
+ {
112
+ asset_id: options[:asset],
113
+ opponent_id: user_id,
114
+ amount: options[:amount],
115
+ memo: options[:memo]
116
+ }
117
+ )
118
+ end
119
+
120
+ return unless res['snapshot_id'].present?
121
+
122
+ log UI.fmt "{{v}} Finished: https://mixin.one/snapshots/#{res['snapshot_id']}"
123
+ end
124
+
125
+ desc 'saferegister', 'register SAFE network'
126
+ option :spend_key, type: :string, required: true, desc: 'spend_key'
127
+ option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
128
+ def saferegister
129
+ res = api_instance.safe_register options[:spend_key]
130
+ log res
131
+ end
132
+
133
+ desc 'pay', 'generate payment url'
134
+ option :members, type: :array, required: true, desc: 'Reveivers, maybe multisig'
135
+ option :threshold, type: :numeric, required: false, default: 1, desc: 'Threshold of multisig'
136
+ option :asset, type: :string, required: true, desc: 'Asset ID'
137
+ option :amount, type: :numeric, required: true, desc: 'Amount'
138
+ option :trace, type: :string, required: false, desc: 'Trace ID'
139
+ option :memo, type: :string, required: false, desc: 'memo'
140
+ def pay
141
+ url = api_instance.safe_pay_url(
142
+ members: options[:members],
143
+ threshold: options[:threshold],
144
+ asset_id: options[:asset],
145
+ amount: options[:amount],
146
+ trace_id: options[:trace],
147
+ memo: options[:memo]
148
+ )
149
+
150
+ log UI.fmt "{{v}} #{url}"
151
+ end
152
+
153
+ desc 'safetransfer USER_ID', 'transfer asset to USER_ID with SAFE network'
154
+ option :asset, type: :string, required: true, desc: 'Asset ID'
155
+ option :amount, type: :numeric, required: true, desc: 'Amount'
156
+ option :trace, type: :string, required: false, desc: 'Trace ID'
157
+ option :memo, type: :string, required: false, desc: 'memo'
158
+ option :keystore, type: :string, aliases: '-k', required: true, desc: 'keystore or keystore.json file path'
159
+ def safetransfer(user_id)
160
+ amount = options[:amount].to_d
161
+ asset = options[:asset]
162
+ memo = options[:memo] || ''
163
+
164
+ # step 1: select inputs
165
+ outputs = api_instance.safe_outputs(state: 'unspent', asset_id: asset, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
166
+ balance = outputs.sum(&->(output) { output['amount'].to_d })
167
+
168
+ utxos = []
169
+ outputs.each do |output|
170
+ break if utxos.sum { |o| o['amount'].to_d } >= amount
171
+
172
+ utxos.shift if utxos.size >= 255
173
+ utxos << output
174
+ end
175
+
176
+ log UI.fmt "Step 1/7: {{v}} Found #{outputs.count} unspent outputs, balance: #{balance}, selected #{utxos.count} outputs"
177
+
178
+ # step 2: build transaction
179
+ tx = api_instance.build_safe_transaction(
180
+ utxos:,
181
+ receivers: [
182
+ members: [user_id],
183
+ threshold: 1,
184
+ amount:
185
+ ],
186
+ extra: memo
187
+ )
188
+ raw = MixinBot::Utils.encode_raw_transaction tx
189
+ log UI.fmt "Step 2/5: {{v}} Built raw: #{raw}"
190
+
191
+ # step 3: verify transaction
192
+ request_id = SecureRandom.uuid
193
+ request = api_instance.create_safe_transaction_request(request_id, raw)['data']
194
+ log UI.fmt "Step 3/5: {{v}} Verified transaction, request_id: #{request[0]['request_id']}"
195
+
196
+ # step 4: sign transaction
197
+ signed_raw = api_instance.sign_safe_transaction(
198
+ raw:,
199
+ utxos:,
200
+ request: request[0]
201
+ )
202
+ log UI.fmt "Step 4/5: {{v}} Signed transaction: #{signed_raw}"
203
+
204
+ # step 5: submit transaction
205
+ r = api_instance.send_safe_transaction(
206
+ request_id,
207
+ signed_raw
208
+ )
209
+ log UI.fmt "Step 5/5: {{v}} Submit transaction, hash: #{r['data'].first['transaction_hash']}"
210
+ rescue StandardError => e
211
+ log UI.fmt "{{x}} #{e.inspect}"
212
+ end
69
213
  end
70
214
  end