mixin_bot 0.12.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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