mixin_bot 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) 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 +31 -53
  7. data/lib/mixin_bot/api/blaze.rb +4 -3
  8. data/lib/mixin_bot/api/collectible.rb +60 -58
  9. data/lib/mixin_bot/api/conversation.rb +29 -49
  10. data/lib/mixin_bot/api/encrypted_message.rb +17 -17
  11. data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
  12. data/lib/mixin_bot/api/legacy_output.rb +50 -0
  13. data/lib/mixin_bot/api/legacy_payment.rb +31 -0
  14. data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
  15. data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
  16. data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
  17. data/lib/mixin_bot/api/me.rb +13 -17
  18. data/lib/mixin_bot/api/message.rb +13 -10
  19. data/lib/mixin_bot/api/multisig.rb +16 -221
  20. data/lib/mixin_bot/api/output.rb +46 -0
  21. data/lib/mixin_bot/api/payment.rb +9 -20
  22. data/lib/mixin_bot/api/pin.rb +57 -65
  23. data/lib/mixin_bot/api/rpc.rb +9 -11
  24. data/lib/mixin_bot/api/snapshot.rb +15 -29
  25. data/lib/mixin_bot/api/tip.rb +43 -0
  26. data/lib/mixin_bot/api/transaction.rb +184 -60
  27. data/lib/mixin_bot/api/transfer.rb +64 -32
  28. data/lib/mixin_bot/api/user.rb +83 -53
  29. data/lib/mixin_bot/api/withdraw.rb +52 -53
  30. data/lib/mixin_bot/api.rb +78 -45
  31. data/lib/mixin_bot/cli/api.rb +151 -8
  32. data/lib/mixin_bot/cli/utils.rb +14 -4
  33. data/lib/mixin_bot/cli.rb +13 -10
  34. data/lib/mixin_bot/client.rb +76 -127
  35. data/lib/mixin_bot/configuration.rb +98 -0
  36. data/lib/mixin_bot/nfo.rb +174 -0
  37. data/lib/mixin_bot/transaction.rb +505 -0
  38. data/lib/mixin_bot/utils/address.rb +108 -0
  39. data/lib/mixin_bot/utils/crypto.rb +182 -0
  40. data/lib/mixin_bot/utils/decoder.rb +58 -0
  41. data/lib/mixin_bot/utils/encoder.rb +63 -0
  42. data/lib/mixin_bot/utils.rb +8 -109
  43. data/lib/mixin_bot/uuid.rb +41 -0
  44. data/lib/mixin_bot/version.rb +1 -1
  45. data/lib/mixin_bot.rb +39 -14
  46. data/lib/mvm/bridge.rb +2 -19
  47. data/lib/mvm/client.rb +11 -33
  48. data/lib/mvm/nft.rb +4 -4
  49. data/lib/mvm/registry.rb +9 -9
  50. data/lib/mvm/scan.rb +3 -5
  51. data/lib/mvm.rb +5 -6
  52. metadata +101 -44
  53. data/lib/mixin_bot/utils/nfo.rb +0 -176
  54. data/lib/mixin_bot/utils/transaction.rb +0 -478
  55. data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -3,77 +3,67 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module Conversation
6
- def conversation(conversation_id)
7
- path = format('/conversations/%<conversation_id>s', conversation_id: conversation_id)
8
- access_token ||= access_token('GET', path, '')
9
- authorization = format('Bearer %<access_token>s', access_token: access_token)
10
- client.get(path, headers: { 'Authorization': authorization })
6
+ def conversation(conversation_id, access_token: nil)
7
+ path = format('/conversations/%<conversation_id>s', conversation_id:)
8
+ client.get path, access_token:
11
9
  end
12
- alias read_conversation conversation
13
10
 
14
11
  def conversation_by_user_id(user_id)
15
- conversation_id = unique_conversation_id(user_id)
16
- read_conversation(conversation_id)
12
+ conversation_id = unique_uuid user_id
13
+ conversation conversation_id
17
14
  end
18
- alias read_conversation_by_user_id conversation_by_user_id
19
15
 
20
- def create_conversation(category:, conversation_id:, participants:, name: nil, access_token: nil)
16
+ def create_conversation(**kwargs)
21
17
  path = '/conversations'
22
18
  payload = {
23
- category: category,
24
- conversation_id: conversation_id || SecureRandom.uuid,
25
- name: name,
26
- participants: participants
27
- }
19
+ category: kwargs[:category],
20
+ conversation_id: kwargs[:conversation_id] || SecureRandom.uuid,
21
+ name: kwargs[:name],
22
+ participants: kwargs[:participants]
23
+ }.compact_blank
28
24
 
29
- access_token ||= access_token('POST', path, payload.to_json)
30
- authorization = format('Bearer %<access_token>s', access_token: access_token)
31
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
25
+ client.post path, **payload, access_token: kwargs[:access_token]
32
26
  end
33
27
 
34
28
  def create_group_conversation(user_ids:, name:, conversation_id: nil, access_token: nil)
35
29
  create_conversation(
36
30
  category: 'GROUP',
37
- conversation_id: conversation_id,
38
- name: name,
31
+ conversation_id:,
32
+ name:,
39
33
  participants: user_ids.map(&->(participant) { { user_id: participant } }),
40
- access_token: access_token
34
+ access_token:
41
35
  )
42
36
  end
43
37
 
44
38
  def create_contact_conversation(user_id, access_token: nil)
45
39
  create_conversation(
46
40
  category: 'CONTACT',
47
- conversation_id: unique_conversation_id(user_id),
41
+ conversation_id: unique_uuid(user_id),
48
42
  participants: [
49
43
  {
50
- user_id: user_id
44
+ user_id:
51
45
  }
52
46
  ],
53
- access_token: access_token
47
+ access_token:
54
48
  )
55
49
  end
56
50
 
57
51
  def update_group_conversation_name(name:, conversation_id:, access_token: nil)
58
52
  path = format('/conversations/%<id>s', id: conversation_id)
59
53
  payload = {
60
- name: name
54
+ name:
61
55
  }
62
56
 
63
- access_token ||= access_token('POST', path, payload.to_json)
64
- authorization = format('Bearer %<access_token>s', access_token: access_token)
65
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
57
+ client.post path, **payload, access_token:
66
58
  end
67
59
 
68
60
  def update_group_conversation_announcement(announcement:, conversation_id:, access_token: nil)
69
61
  path = format('/conversations/%<id>s', id: conversation_id)
70
62
  payload = {
71
- announcement: announcement
63
+ announcement:
72
64
  }
73
65
 
74
- access_token ||= access_token('POST', path, payload.to_json)
75
- authorization = format('Bearer %<access_token>s', access_token: access_token)
76
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
66
+ client.post path, **payload, access_token:
77
67
  end
78
68
 
79
69
  # participants = [{ user_id: "" }]
@@ -81,9 +71,7 @@ module MixinBot
81
71
  path = format('/conversations/%<id>s/participants/ADD', id: conversation_id)
82
72
  payload = user_ids.map(&->(participant) { { user_id: participant } })
83
73
 
84
- access_token ||= access_token('POST', path, payload.to_json)
85
- authorization = format('Bearer %<access_token>s', access_token: access_token)
86
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
74
+ client.post path, *payload, access_token:
87
75
  end
88
76
 
89
77
  # participants = [{ user_id: "" }]
@@ -91,25 +79,19 @@ module MixinBot
91
79
  path = format('/conversations/%<id>s/participants/REMOVE', id: conversation_id)
92
80
  payload = user_ids.map(&->(participant) { { user_id: participant } })
93
81
 
94
- access_token ||= access_token('POST', path, payload.to_json)
95
- authorization = format('Bearer %<access_token>s', access_token: access_token)
96
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
82
+ client.post path, *payload, access_token:
97
83
  end
98
84
 
99
85
  def exit_conversation(conversation_id, access_token: nil)
100
86
  path = format('/conversations/%<id>s/exit', id: conversation_id)
101
87
 
102
- access_token ||= access_token('POST', path)
103
- authorization = format('Bearer %<access_token>s', access_token: access_token)
104
- client.post(path, headers: { 'Authorization': authorization })
88
+ client.post path, access_token:
105
89
  end
106
90
 
107
91
  def rotate_conversation(conversation_id, access_token: nil)
108
92
  path = format('/conversations/%<id>s/rotate', id: conversation_id)
109
93
 
110
- access_token ||= access_token('POST', path)
111
- authorization = format('Bearer %<access_token>s', access_token: access_token)
112
- client.post(path, headers: { 'Authorization': authorization })
94
+ client.post path, access_token:
113
95
  end
114
96
 
115
97
  # participants = [{ user_id: "", role: "ADMIN" }]
@@ -117,14 +99,12 @@ module MixinBot
117
99
  path = format('/conversations/%<id>s/participants/ROLE', id: conversation_id)
118
100
  payload = participants
119
101
 
120
- access_token ||= access_token('POST', path, payload.to_json)
121
- authorization = format('Bearer %<access_token>s', access_token: access_token)
122
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
102
+ client.post path, *payload, access_token:
123
103
  end
124
104
 
125
105
  def unique_uuid(user_id, opponent_id = nil)
126
- opponent_id ||= client_id
127
- MixinBot::Utils.unique_uuid user_id, opponent_id
106
+ opponent_id ||= config.app_id
107
+ MixinBot.utils.unique_uuid user_id, opponent_id
128
108
  end
129
109
  alias unique_conversation_id unique_uuid
130
110
  end
@@ -90,11 +90,11 @@ module MixinBot
90
90
  category: options[:category],
91
91
  quote_message_id: options[:quote_message_id],
92
92
  message_id: options[:message_id] || SecureRandom.uuid,
93
- data_base64: data_base64,
94
- checksum: checksum,
93
+ data_base64:,
94
+ checksum:,
95
95
  recipient_sessions: session_ids.map(&->(s) { { session_id: s } }),
96
96
  silent: false
97
- }
97
+ }.compact
98
98
  end
99
99
 
100
100
  def send_encrypted_messages(messages)
@@ -104,19 +104,19 @@ module MixinBot
104
104
  # http post request
105
105
  def send_encrypted_message(payload)
106
106
  path = '/encrypted_messages'
107
- payload = [payload] unless payload.is_a?(Array)
108
- access_token ||= access_token('POST', path, payload.to_json)
109
- authorization = format('Bearer %<access_token>s', access_token: access_token)
110
- client.post(path, headers: { 'Authorization': authorization }, json: payload)
107
+ payload = [payload] if payload.is_a? Hash
108
+ raise ArgumentError, 'Wrong payload format!' unless payload.is_a? Array
109
+
110
+ client.post path, *payload
111
111
  end
112
112
 
113
113
  def encrypt_message(data, sessions = [], sk: nil, pk: nil)
114
114
  raise ArgumentError, 'Wrong sessions format!' unless sessions.all?(&->(s) { s.key?('session_id') && s.key?('public_key') })
115
115
 
116
- sk = private_key[0...32]
117
- pk ||= private_key[32...]
116
+ sk ||= config.session_private_key[0...32]
117
+ pk ||= config.session_private_key[32...]
118
118
 
119
- checksum = Digest::MD5.hexdigest sessions.map(&->(s) { s['session_id'] }).sort.join
119
+ Digest::MD5.hexdigest sessions.map(&->(s) { s['session_id'] }).sort.join
120
120
  encrypter = OpenSSL::Cipher.new('AES-128-GCM').encrypt
121
121
  key = encrypter.random_key
122
122
  nounce = encrypter.random_iv
@@ -128,14 +128,14 @@ module MixinBot
128
128
  bytes = [1]
129
129
  bytes += [sessions.size].pack('v*').bytes
130
130
  bytes += JOSE::JWA::Ed25519.pk_to_curve25519(pk).bytes
131
-
131
+
132
132
  sessions.each do |session|
133
133
  aes_key = JOSE::JWA::X25519.shared_secret(
134
134
  Base64.urlsafe_decode64(session['public_key']),
135
135
  JOSE::JWA::Ed25519.secret_to_curve25519(sk)
136
136
  )
137
137
 
138
- padding = 16 - key.size % 16
138
+ padding = 16 - (key.size % 16)
139
139
  padtext = ([padding] * padding).pack('C*')
140
140
 
141
141
  encrypter = OpenSSL::Cipher.new('AES-256-CBC').encrypt
@@ -143,7 +143,7 @@ module MixinBot
143
143
  iv = encrypter.random_iv
144
144
  encrypter.iv = iv
145
145
 
146
- bytes += (MixinBot::Utils::UUID.new(hex: session['session_id']).packed + iv).bytes
146
+ bytes += (MixinBot::UUID.new(hex: session['session_id']).packed + iv).bytes
147
147
  bytes += encrypter.update(key + padtext).bytes
148
148
  end
149
149
 
@@ -156,19 +156,19 @@ module MixinBot
156
156
  def decrypt_message(data, sk: nil, si: nil)
157
157
  bytes = Base64.urlsafe_decode64(data).bytes
158
158
 
159
- si ||= session_id
160
- sk ||= private_key[0...32]
159
+ si ||= config.session_id
160
+ sk ||= config.session_private_key[0...32]
161
161
 
162
162
  size = 16 + 48
163
163
  return '' if bytes.size < 1 + 2 + 32 + size + 12
164
164
 
165
165
  session_length = bytes[1...3].pack('v*').unpack1('C*')
166
- prefix_size = 35 + session_length * size
166
+ prefix_size = 35 + (session_length * size)
167
167
 
168
168
  i = 35
169
169
  key = ''
170
170
  while i < prefix_size
171
- uuid = MixinBot::Utils::UUID.new(raw: bytes[i...(i + 16)].pack('C*')).unpacked
171
+ uuid = MixinBot::UUID.new(raw: bytes[i...(i + 16)].pack('C*')).unpacked
172
172
  if uuid == si
173
173
  pub = bytes[3...35]
174
174
  aes_key = JOSE::JWA::X25519.shared_secret(
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyMultisig
6
+ MULTISIG_REQUEST_ACTIONS = %i[sign unlock].freeze
7
+ def create_multisig_request(action, raw, access_token: nil)
8
+ raise ArgumentError, "request action is limited in #{MULTISIG_REQUEST_ACTIONS.join(', ')}" unless MULTISIG_REQUEST_ACTIONS.include? action.to_sym
9
+
10
+ path = '/multisigs/requests'
11
+ payload = {
12
+ action:,
13
+ raw:
14
+ }
15
+ client.post path, **payload, access_token:
16
+ end
17
+
18
+ # transfer from the multisig address
19
+ def create_sign_multisig_request(raw, access_token: nil)
20
+ create_multisig_request 'sign', raw, access_token:
21
+ end
22
+
23
+ # transfer from the multisig address
24
+ # create a request for unlock a multi-sign
25
+ def create_unlock_multisig_request(raw, access_token: nil)
26
+ create_multisig_request 'unlock', raw, access_token:
27
+ end
28
+
29
+ def sign_multisig_request(request_id, pin = nil)
30
+ pin ||= config.pin
31
+ path = format('/multisigs/requests/%<request_id>s/sign', request_id:)
32
+ payload =
33
+ if pin.length > 6
34
+ {
35
+ pin_base64: encrypt_tip_pin(pin, 'TIP:MULTISIG:REQUEST:SIGN:', request_id)
36
+ }
37
+ else
38
+ {
39
+ pin: encrypt_pin(pin)
40
+ }
41
+ end
42
+
43
+ client.post path, **payload, access_token:
44
+ end
45
+
46
+ def unlock_multisig_request(request_id, pin = nil)
47
+ pin ||= config.pin
48
+
49
+ path = format('/multisigs/requests/%<request_id>s/unlock', request_id:)
50
+ payload =
51
+ if pin.length > 6
52
+ {
53
+ pin_base64: encrypt_tip_pin(pin, 'TIP:MULTISIG:REQUEST:UNLOCK:', request_id)
54
+ }
55
+ else
56
+ {
57
+ pin: encrypt_pin(pin)
58
+ }
59
+ end
60
+
61
+ client.post path, **payload
62
+ end
63
+
64
+ # pay to the multisig address
65
+ # used for create multisig payment code_id
66
+ def create_multisig_payment(**kwargs)
67
+ path = '/payments'
68
+ payload = {
69
+ asset_id: kwargs[:asset_id],
70
+ amount: format('%.8f', kwargs[:amount].to_d),
71
+ trace_id: kwargs[:trace_id] || SecureRandom.uuid,
72
+ memo: kwargs[:memo],
73
+ opponent_multisig: {
74
+ receivers: kwargs[:receivers],
75
+ threshold: kwargs[:threshold]
76
+ }
77
+ }
78
+ client.post path, **payload, access_token: kwargs[:access_token]
79
+ end
80
+
81
+ def verify_multisig(code_id, access_token: nil)
82
+ path = format('/codes/%<code_id>s', code_id:)
83
+ client.get path, access_token:
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyOutput
6
+ def outputs(**kwargs)
7
+ limit = kwargs[:limit] || 100
8
+ offset = kwargs[:offset] || ''
9
+ state = kwargs[:state] || ''
10
+ members = kwargs[:members] || []
11
+ threshold = kwargs[:threshold] || ''
12
+ access_token = kwargs[:access_token]
13
+ members = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
14
+
15
+ path = '/multisigs/outputs'
16
+ params = {
17
+ limit:,
18
+ offset:,
19
+ state:,
20
+ members:,
21
+ threshold:
22
+ }.compact_blank
23
+
24
+ client.get path, **params, access_token:
25
+ end
26
+ alias multisigs outputs
27
+ alias multisig_outputs outputs
28
+
29
+ def create_output(receivers:, index:, hint: nil, access_token: nil)
30
+ path = '/outputs'
31
+ payload = {
32
+ receivers:,
33
+ index:,
34
+ hint:
35
+ }
36
+ client.post path, **payload, access_token:
37
+ end
38
+
39
+ def build_output(receivers:, index:, amount:, threshold:, hint: nil)
40
+ _output = create_output(receivers:, index:, hint:)
41
+ {
42
+ amount: format('%.8f', amount.to_d.to_r),
43
+ script: build_threshold_script(threshold),
44
+ mask: _output['mask'],
45
+ keys: _output['keys']
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyPayment
6
+ def pay_url(**kwargs)
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: kwargs[:recipient_id],
10
+ asset: kwargs[:asset_id],
11
+ amount: kwargs[:amount].to_s,
12
+ trace: kwargs[:trace],
13
+ memo: kwargs[:memo]
14
+ )
15
+ end
16
+
17
+ # https://developers.mixin.one/api/alpha-mixin-network/verify-payment/
18
+ def verify_payment(**kwargs)
19
+ path = '/payments'
20
+ payload = {
21
+ asset_id: kwargs[:asset_id],
22
+ opponent_id: kwargs[:opponent_id],
23
+ amount: kwargs[:amount].to_s,
24
+ trace_id: kwargs[:trace]
25
+ }
26
+
27
+ client.post path, **payload
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacySnapshot
6
+ def network_snapshots(**kwargs)
7
+ path = '/network/snapshots'
8
+ params = {
9
+ limit: kwargs[:limit],
10
+ offset: kwargs[:offset],
11
+ asset: kwargs[:asset],
12
+ order: kwargs[:order]
13
+ }
14
+
15
+ client.get path, **params, access_token: kwargs[:access_token]
16
+ end
17
+
18
+ def snapshots(**kwargs)
19
+ path = '/snapshots'
20
+
21
+ params = {
22
+ limit: kwargs[:limit],
23
+ offset: kwargs[:offset],
24
+ asset: kwargs[:asset],
25
+ opponent: kwargs[:opponent],
26
+ order: kwargs[:order]
27
+ }
28
+
29
+ client.get path, **params, access_token: kwargs[:access_token]
30
+ end
31
+
32
+ def network_snapshot(snapshot_id, **kwargs)
33
+ path = format('/network/snapshots/%<snapshot_id>s', snapshot_id:)
34
+
35
+ client.get path, access_token: kwargs[:access_token]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyTransaction
6
+ LEGACY_TX_VERSION = 0x04
7
+
8
+ # use safe transaction protocol instead
9
+ # kwargs:
10
+ # {
11
+ # senders: [ uuid ],
12
+ # senders_threshold: integer,
13
+ # receivers: [ uuid ],
14
+ # receivers_threshold: integer,
15
+ # asset_id: uuid,
16
+ # amount: string / float,
17
+ # memo: string,
18
+ # }
19
+ RAW_TRANSACTION_ARGUMENTS = %i[utxos senders senders_threshold receivers receivers_threshold amount].freeze
20
+ def build_raw_transaction(**kwargs)
21
+ raise ArgumentError, "#{RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build raw transaction" unless RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
22
+
23
+ senders = kwargs[:senders]
24
+ senders_threshold = kwargs[:senders_threshold]
25
+ receivers = kwargs[:receivers]
26
+ receivers_threshold = kwargs[:receivers_threshold]
27
+ amount = kwargs[:amount]
28
+ asset_id = kwargs[:asset_id]
29
+ asset_mixin_id = kwargs[:asset_mixin_id]
30
+ utxos = kwargs[:utxos]
31
+ extra = kwargs[:extra]
32
+ access_token = kwargs[:access_token]
33
+ outputs = kwargs[:outputs] || []
34
+ hint = kwargs[:hint]
35
+ version = kwargs[:version] || LEGACY_TX_VERSION
36
+
37
+ raise 'access_token required!' if access_token.nil? && !senders.include?(config.app_id)
38
+
39
+ amount = amount.to_d.round(8)
40
+ input_amount = utxos.map(
41
+ &lambda { |utxo|
42
+ utxo['amount'].to_d
43
+ }
44
+ ).sum
45
+
46
+ if input_amount < amount
47
+ raise format(
48
+ 'not enough amount! %<input_amount>s < %<amount>s',
49
+ input_amount:,
50
+ amount:
51
+ )
52
+ end
53
+
54
+ inputs = utxos.map(
55
+ &lambda { |utx|
56
+ {
57
+ 'hash' => utx['transaction_hash'],
58
+ 'index' => utx['output_index']
59
+ }
60
+ }
61
+ )
62
+
63
+ if outputs.empty?
64
+ receivers_threshold = 1 if receivers.size == 1
65
+ output0 = build_output(
66
+ receivers:,
67
+ index: 0,
68
+ amount:,
69
+ threshold: receivers_threshold,
70
+ hint:
71
+ )
72
+ outputs.push output0
73
+
74
+ if input_amount > amount
75
+ output1 = build_output(
76
+ receivers: senders,
77
+ index: 1,
78
+ amount: input_amount - amount,
79
+ threshold: senders_threshold,
80
+ hint:
81
+ )
82
+ outputs.push output1
83
+ end
84
+ end
85
+
86
+ asset = asset_mixin_id || SHA3::Digest::SHA256.hexdigest(asset_id)
87
+ {
88
+ version:,
89
+ asset:,
90
+ inputs:,
91
+ outputs:,
92
+ extra:
93
+ }
94
+ end
95
+
96
+ # use safe transaction protocol instead
97
+ MULTISIG_TRANSACTION_ARGUMENTS = %i[asset_id receivers threshold amount].freeze
98
+ def create_multisig_transaction(pin, **options)
99
+ raise ArgumentError, "#{MULTISIG_TRANSACTION_ARGUMENTS.join(', ')} are needed for create multisig transaction" unless MULTISIG_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
100
+
101
+ asset_id = options[:asset_id]
102
+ receivers = options[:receivers].sort
103
+ threshold = options[:threshold]
104
+ amount = format('%.8f', options[:amount].to_d.to_r)
105
+ memo = options[:memo]
106
+ trace_id = options[:trace_id] || SecureRandom.uuid
107
+
108
+ path = '/transactions'
109
+ payload = {
110
+ asset_id:,
111
+ opponent_multisig: {
112
+ receivers:,
113
+ threshold:
114
+ },
115
+ amount:,
116
+ trace_id:,
117
+ memo:
118
+ }
119
+
120
+ if pin.length > 6
121
+ payload[:pin_base64] = encrypt_tip_pin(pin, 'TIP:TRANSACTION:CREATE:', asset_id, receivers.join, threshold, amount, trace_id, memo)
122
+ else
123
+ payload[:pin] = encrypt_pin(pin)
124
+ end
125
+
126
+ client.post path, **payload
127
+ end
128
+
129
+ # use safe transaction protocol instead
130
+ MAINNET_TRANSACTION_ARGUMENTS = %i[asset_id opponent_id amount].freeze
131
+ def create_mainnet_transaction(pin, **options)
132
+ raise ArgumentError, "#{MAINNET_TRANSACTION_ARGUMENTS.join(', ')} are needed for create main net transactions" unless MAINNET_TRANSACTION_ARGUMENTS.all? { |param| options.keys.include? param }
133
+
134
+ asset_id = options[:asset_id]
135
+ opponent_id = options[:opponent_id]
136
+ amount = format('%.8f', options[:amount].to_d)
137
+ memo = options[:memo]
138
+ trace_id = options[:trace_id] || SecureRandom.uuid
139
+
140
+ path = '/transactions'
141
+ payload = {
142
+ asset_id:,
143
+ opponent_id:,
144
+ amount:,
145
+ trace_id:,
146
+ memo:
147
+ }
148
+
149
+ if pin.length > 6
150
+ payload[:pin_base64] = encrypt_tip_pin(pin, 'TIP:TRANSACTION:CREATE:', asset_id, opponent_id, amount, trace_id, memo)
151
+ else
152
+ payload[:pin] = encrypt_pin(pin)
153
+ end
154
+
155
+ client.post path, **payload
156
+ end
157
+
158
+ # use safe transaction protocol instead
159
+ def transactions(**options)
160
+ path = '/external/transactions'
161
+ params = {
162
+ limit: options[:limit],
163
+ offset: options[:offset],
164
+ asset: options[:asset],
165
+ destination: options[:destination],
166
+ tag: options[:tag]
167
+ }
168
+
169
+ client.get path, **params
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module LegacyTransfer
6
+ TRANSFER_ARGUMENTS = %i[asset_id opponent_id amount].freeze
7
+ def create_transfer(pin, **kwargs)
8
+ raise ArgumentError, "#{TRANSFER_ARGUMENTS.join(', ')} are needed for create transfer" unless TRANSFER_ARGUMENTS.all? { |param| kwargs.keys.include? param }
9
+
10
+ asset_id = kwargs[:asset_id]
11
+ opponent_id = kwargs[:opponent_id]
12
+ amount = format('%.8f', kwargs[:amount].to_d.to_r).gsub(/\.?0+$/, '')
13
+ trace_id = kwargs[:trace_id] || SecureRandom.uuid
14
+ memo = kwargs[:memo] || ''
15
+
16
+ payload = {
17
+ asset_id:,
18
+ opponent_id:,
19
+ amount:,
20
+ trace_id:,
21
+ memo:
22
+ }
23
+
24
+ if pin.length > 6
25
+ pin_base64 = encrypt_tip_pin pin, 'TIP:TRANSFER:CREATE:', asset_id, opponent_id, amount, trace_id, memo
26
+ payload[:pin_base64] = pin_base64
27
+ else
28
+ encrypted_pin = encrypt_pin pin
29
+ payload[:pin] = encrypted_pin
30
+ end
31
+
32
+ path = '/transfers'
33
+ client.post path, **payload
34
+ end
35
+
36
+ def transfer(trace_id, access_token: nil)
37
+ path = format('/transfers/trace/%<trace_id>s', trace_id:)
38
+ client.get path, access_token:
39
+ end
40
+ end
41
+ end
42
+ end