mixin_bot 0.12.0 → 1.0.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.
- checksums.yaml +4 -4
- data/lib/mixin_bot/api/address.rb +21 -0
- data/lib/mixin_bot/api/app.rb +5 -11
- data/lib/mixin_bot/api/asset.rb +9 -16
- data/lib/mixin_bot/api/attachment.rb +27 -22
- data/lib/mixin_bot/api/auth.rb +31 -53
- data/lib/mixin_bot/api/blaze.rb +4 -3
- data/lib/mixin_bot/api/collectible.rb +60 -58
- data/lib/mixin_bot/api/conversation.rb +29 -49
- data/lib/mixin_bot/api/encrypted_message.rb +17 -17
- data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
- data/lib/mixin_bot/api/legacy_output.rb +50 -0
- data/lib/mixin_bot/api/legacy_payment.rb +31 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
- data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
- data/lib/mixin_bot/api/me.rb +13 -17
- data/lib/mixin_bot/api/message.rb +13 -10
- data/lib/mixin_bot/api/multisig.rb +16 -221
- data/lib/mixin_bot/api/output.rb +46 -0
- data/lib/mixin_bot/api/payment.rb +9 -20
- data/lib/mixin_bot/api/pin.rb +57 -65
- data/lib/mixin_bot/api/rpc.rb +9 -11
- data/lib/mixin_bot/api/snapshot.rb +15 -29
- data/lib/mixin_bot/api/tip.rb +43 -0
- data/lib/mixin_bot/api/transaction.rb +184 -60
- data/lib/mixin_bot/api/transfer.rb +64 -32
- data/lib/mixin_bot/api/user.rb +83 -53
- data/lib/mixin_bot/api/withdraw.rb +52 -53
- data/lib/mixin_bot/api.rb +78 -45
- data/lib/mixin_bot/cli/api.rb +151 -8
- data/lib/mixin_bot/cli/utils.rb +14 -4
- data/lib/mixin_bot/cli.rb +13 -10
- data/lib/mixin_bot/client.rb +76 -127
- data/lib/mixin_bot/configuration.rb +98 -0
- data/lib/mixin_bot/nfo.rb +174 -0
- data/lib/mixin_bot/transaction.rb +505 -0
- data/lib/mixin_bot/utils/address.rb +108 -0
- data/lib/mixin_bot/utils/crypto.rb +182 -0
- data/lib/mixin_bot/utils/decoder.rb +58 -0
- data/lib/mixin_bot/utils/encoder.rb +63 -0
- data/lib/mixin_bot/utils.rb +8 -109
- data/lib/mixin_bot/uuid.rb +41 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +39 -14
- data/lib/mvm/bridge.rb +2 -19
- data/lib/mvm/client.rb +11 -33
- data/lib/mvm/nft.rb +4 -4
- data/lib/mvm/registry.rb +9 -9
- data/lib/mvm/scan.rb +3 -5
- data/lib/mvm.rb +5 -6
- metadata +101 -44
- data/lib/mixin_bot/utils/nfo.rb +0 -176
- data/lib/mixin_bot/utils/transaction.rb +0 -478
- data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -3,45 +3,31 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Snapshot
|
6
|
-
def
|
7
|
-
path =
|
8
|
-
|
9
|
-
limit: options[:limit],
|
10
|
-
offset: options[:offset],
|
11
|
-
asset: options[:asset],
|
12
|
-
order: options[:order]
|
13
|
-
)
|
14
|
-
|
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 })
|
18
|
-
end
|
19
|
-
alias read_network_snapshots network_snapshots
|
20
|
-
|
21
|
-
def snapshots(**options)
|
22
|
-
path = format(
|
23
|
-
'/snapshots?limit=%<limit>s&offset=%<offset>s&asset=%<asset>s&opponent=%<opponent>s&order=%<order>s',
|
6
|
+
def safe_snapshots(**options)
|
7
|
+
path = '/safe/snapshots'
|
8
|
+
params = {
|
24
9
|
limit: options[:limit],
|
25
10
|
offset: options[:offset],
|
26
11
|
asset: options[:asset],
|
27
12
|
opponent: options[:opponent],
|
13
|
+
app: options[:app_id],
|
28
14
|
order: options[:order]
|
29
|
-
|
15
|
+
}
|
30
16
|
|
31
|
-
|
32
|
-
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
33
|
-
client.get(path, headers: { 'Authorization': authorization })
|
17
|
+
client.get path, **params
|
34
18
|
end
|
35
|
-
alias read_snapshots snapshots
|
36
19
|
|
37
|
-
def
|
38
|
-
path =
|
20
|
+
def create_safe_snapshot_notification(**kwargs)
|
21
|
+
path = '/safe/snapshots/notifications'
|
22
|
+
|
23
|
+
payload = {
|
24
|
+
transaction_hash: kwargs[:transaction_hash],
|
25
|
+
output_index: kwargs[:output_index],
|
26
|
+
receiver_id: kwargs[:receiver_id]
|
27
|
+
}
|
39
28
|
|
40
|
-
access_token
|
41
|
-
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
42
|
-
client.get(path, headers: { 'Authorization': authorization })
|
29
|
+
client.post path, **payload, access_token: kwargs[:access_token]
|
43
30
|
end
|
44
|
-
alias read_network_snapshot network_snapshot
|
45
31
|
end
|
46
32
|
end
|
47
33
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class API
|
5
|
+
module Tip
|
6
|
+
TIP_ACTIONS = %w[
|
7
|
+
TIP:VERIFY:
|
8
|
+
TIP:ADDRESS:ADD:
|
9
|
+
TIP:ADDRESS:REMOVE:
|
10
|
+
TIP:USER:DEACTIVATE:
|
11
|
+
TIP:EMERGENCY:CONTACT:CREATE:
|
12
|
+
TIP:EMERGENCY:CONTACT:READ:
|
13
|
+
TIP:EMERGENCY:CONTACT:REMOVE:
|
14
|
+
TIP:PHONE:NUMBER:UPDATE:
|
15
|
+
TIP:MULTISIG:REQUEST:SIGN:
|
16
|
+
TIP:MULTISIG:REQUEST:UNLOCK:
|
17
|
+
TIP:COLLECTIBLE:REQUEST:SIGN:
|
18
|
+
TIP:COLLECTIBLE:REQUEST:UNLOCK:
|
19
|
+
TIP:TRANSFER:CREATE:
|
20
|
+
TIP:WITHDRAWAL:CREATE:
|
21
|
+
TIP:TRANSACTION:CREATE:
|
22
|
+
TIP:OAUTH:APPROVE:
|
23
|
+
TIP:PROVISIONING:UPDATE:
|
24
|
+
TIP:APP:OWNERSHIP:TRANSFER:
|
25
|
+
SEQUENCER:REGISTER:
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
def encrypt_tip_pin(pin, action, *params)
|
29
|
+
raise ArgumentError, 'invalid action' unless TIP_ACTIONS.include? action
|
30
|
+
|
31
|
+
pin_key = MixinBot.utils.decode_key pin
|
32
|
+
|
33
|
+
msg = action + params.flatten.map(&:to_s).join
|
34
|
+
|
35
|
+
msg = Digest::SHA256.digest(msg) unless action == 'TIP:VERIFY:'
|
36
|
+
|
37
|
+
signature = JOSE::JWA::Ed25519.sign msg, pin_key
|
38
|
+
|
39
|
+
encrypt_pin signature
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -3,75 +3,199 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Transaction
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
threshold: threshold
|
24
|
-
},
|
25
|
-
pin: encrypted_pin,
|
26
|
-
amount: format('%.8f', amount.to_r),
|
27
|
-
trace_id: trace_id,
|
28
|
-
memo: memo
|
29
|
-
}
|
6
|
+
SAFE_TX_VERSION = 0x05
|
7
|
+
OUTPUT_TYPE_SCRIPT = 0x00
|
8
|
+
OUTPUT_TYPE_WITHDRAW_SUBMIT = 0xa1
|
9
|
+
|
10
|
+
# ghost keys
|
11
|
+
def create_safe_keys(*payload, access_token: nil)
|
12
|
+
raise ArgumentError, 'payload should be an array' unless payload.is_a? Array
|
13
|
+
raise ArgumentError, 'payload should not be empty' unless payload.size.positive?
|
14
|
+
raise ArgumentError, 'invalid payload' unless payload.all?(&lambda { |param|
|
15
|
+
param.key?(:receivers) && param.key?(:index)
|
16
|
+
})
|
17
|
+
|
18
|
+
payload.each do |param|
|
19
|
+
param[:hint] ||= SecureRandom.uuid
|
20
|
+
end
|
21
|
+
|
22
|
+
path = '/safe/keys'
|
30
23
|
|
31
|
-
|
32
|
-
access_token ||= access_token('POST', path, payload.to_json)
|
33
|
-
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
34
|
-
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
24
|
+
client.post path, *payload
|
35
25
|
end
|
26
|
+
alias create_ghost_keys create_safe_keys
|
27
|
+
|
28
|
+
# kwargs:
|
29
|
+
# {
|
30
|
+
# utxos: [ utxo ],
|
31
|
+
# receivers: [ {
|
32
|
+
# members: [ uuid ],
|
33
|
+
# threshold: integer,
|
34
|
+
# amount: string,
|
35
|
+
# } ],
|
36
|
+
# ghosts: [ ghost ],
|
37
|
+
# extra: string,
|
38
|
+
# }
|
39
|
+
SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
|
40
|
+
def build_safe_transaction(**kwargs)
|
41
|
+
raise ArgumentError, "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction" unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
|
42
|
+
|
43
|
+
utxos = kwargs[:utxos].map(&:with_indifferent_access)
|
44
|
+
receivers = kwargs[:receivers].map(&:with_indifferent_access)
|
45
|
+
|
46
|
+
senders = utxos.map { |utxo| utxo['receivers'] }.uniq
|
47
|
+
raise ArgumentError, 'utxos should have same senders' if senders.size > 1
|
48
|
+
|
49
|
+
senders_threshold = utxos.map { |utxo| utxo['receivers_threshold'] }.uniq
|
50
|
+
raise ArgumentError, 'utxos should have same senders_threshold' if senders_threshold.size > 1
|
51
|
+
|
52
|
+
raise ArgumentError, 'utxos should not be empty' if utxos.empty?
|
53
|
+
raise ArgumentError, 'utxos too many' if utxos.size > 256
|
54
|
+
|
55
|
+
recipients = receivers.map do |receiver|
|
56
|
+
MixinBot.utils.build_safe_recipient(
|
57
|
+
members: receiver[:members],
|
58
|
+
threshold: receiver[:threshold],
|
59
|
+
amount: receiver[:amount]
|
60
|
+
).with_indifferent_access
|
61
|
+
end
|
62
|
+
|
63
|
+
inputs_sum = utxos.sum(&->(utxo) { utxo['amount'].to_d })
|
64
|
+
outputs_sum = recipients.sum(&->(recipient) { recipient['amount'].to_d })
|
65
|
+
change = inputs_sum - outputs_sum
|
66
|
+
raise InsufficientBalanceError, "inputs sum: #{inputs_sum}" if change.negative?
|
67
|
+
|
68
|
+
if change.positive?
|
69
|
+
recipients << MixinBot.utils.build_safe_recipient(
|
70
|
+
members: utxos[0]['receivers'],
|
71
|
+
threshold: utxos[0]['receivers_threshold'],
|
72
|
+
amount: change
|
73
|
+
).with_indifferent_access
|
74
|
+
end
|
75
|
+
raise ArgumentError, 'recipients too many' if recipients.size > 256
|
76
|
+
|
77
|
+
asset = utxos[0]['asset']
|
78
|
+
inputs = []
|
79
|
+
utxos.each do |utxo|
|
80
|
+
raise ArgumentError, 'utxo asset not match' unless utxo['asset'] == asset
|
81
|
+
|
82
|
+
inputs << {
|
83
|
+
hash: utxo['transaction_hash'],
|
84
|
+
index: utxo['output_index']
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
ghost_payload = recipients.map.with_index do |r, index|
|
89
|
+
{
|
90
|
+
receivers: r[:members],
|
91
|
+
index:,
|
92
|
+
hint: SecureRandom.uuid
|
93
|
+
}
|
94
|
+
end
|
95
|
+
ghosts = create_safe_keys(*ghost_payload)['data']
|
96
|
+
|
97
|
+
outputs = []
|
98
|
+
recipients.each_with_index do |recipient, index|
|
99
|
+
outputs << if recipient['destination']
|
100
|
+
{
|
101
|
+
type: OUTPUT_TYPE_WITHDRAW_SUBMIT,
|
102
|
+
amount: recipient['amount'],
|
103
|
+
withdrawal: {
|
104
|
+
address: recipient['destination'],
|
105
|
+
tag: recipient['tag'] || ''
|
106
|
+
}
|
107
|
+
}
|
108
|
+
else
|
109
|
+
{
|
110
|
+
type: OUTPUT_TYPE_SCRIPT,
|
111
|
+
amount: recipient['amount'],
|
112
|
+
keys: ghosts[index]['keys'],
|
113
|
+
mask: ghosts[index]['mask'],
|
114
|
+
script: build_threshold_script(recipient['threshold'])
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
36
118
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
amount = options[:amount].to_d
|
44
|
-
memo = options[:memo]
|
45
|
-
trace_id = options[:trace_id] || SecureRandom.uuid
|
46
|
-
encrypted_pin = options[:encrypted_pin] || encrypt_pin(pin)
|
47
|
-
|
48
|
-
path = '/transactions'
|
49
|
-
payload = {
|
50
|
-
asset_id: asset_id,
|
51
|
-
opponent_key: opponent_key,
|
52
|
-
pin: encrypted_pin,
|
53
|
-
amount: format('%.8f', amount.to_r),
|
54
|
-
trace_id: trace_id,
|
55
|
-
memo: memo
|
119
|
+
{
|
120
|
+
version: SAFE_TX_VERSION,
|
121
|
+
asset:,
|
122
|
+
inputs:,
|
123
|
+
outputs:,
|
124
|
+
extra: kwargs[:extra] || ''
|
56
125
|
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def create_safe_transaction_request(request_id, raw)
|
129
|
+
path = '/safe/transaction/requests'
|
130
|
+
payload = [{
|
131
|
+
request_id:,
|
132
|
+
raw:
|
133
|
+
}]
|
134
|
+
|
135
|
+
client.post path, *payload
|
136
|
+
end
|
137
|
+
|
138
|
+
def send_safe_transaction(request_id, raw)
|
139
|
+
path = '/safe/transactions'
|
140
|
+
payload = [{
|
141
|
+
request_id:,
|
142
|
+
raw:
|
143
|
+
}]
|
144
|
+
|
145
|
+
client.post path, *payload
|
146
|
+
end
|
147
|
+
|
148
|
+
def safe_transaction(request_id, access_token: nil)
|
149
|
+
path = format('/safe/transactions/%<request_id>s', request_id:)
|
57
150
|
|
58
|
-
|
59
|
-
access_token ||= access_token('POST', path, payload.to_json)
|
60
|
-
authorization = format('Bearer %<access_token>s', access_token: access_token)
|
61
|
-
client.post(path, headers: { 'Authorization': authorization }, json: payload)
|
151
|
+
client.get path, access_token:
|
62
152
|
end
|
63
153
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
154
|
+
SIGN_SAFE_TRANSACTION_ARGUMENTS = %i[raw utxos request spend_key].freeze
|
155
|
+
def sign_safe_transaction(**kwargs)
|
156
|
+
raise ArgumentError, "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction" unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
|
157
|
+
|
158
|
+
raw = kwargs[:raw]
|
159
|
+
tx = MixinBot.utils.decode_raw_transaction raw
|
160
|
+
utxos = kwargs[:utxos]
|
161
|
+
request = kwargs[:request]
|
162
|
+
spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
|
163
|
+
spend_key = Digest::SHA512.digest spend_key[...32]
|
164
|
+
|
165
|
+
msg = [raw].pack('H*')
|
166
|
+
|
167
|
+
y_point = JOSE::JWA::FieldElement.new(
|
168
|
+
JOSE::JWA::X25519.clamp_scalar(spend_key[...32]).x,
|
169
|
+
JOSE::JWA::Edwards25519Point::L
|
72
170
|
)
|
73
171
|
|
74
|
-
|
172
|
+
tx[:signatures] = []
|
173
|
+
tx[:inputs].each_with_index do |input, index|
|
174
|
+
utxo = utxos[index]
|
175
|
+
raise ArgumentError, 'utxo not match' unless input['hash'] == utxo['transaction_hash'] && input['index'] == utxo['output_index']
|
176
|
+
|
177
|
+
view = [request['views'][index]].pack('H*')
|
178
|
+
x_point = JOSE::JWA::FieldElement.new(
|
179
|
+
# https://github.com/potatosalad/ruby-jose/blob/e1be589b889f1e59ac233a5d19a3fa13f1e4b8a0/lib/jose/jwa/x25519.rb#L122C14-L122C48
|
180
|
+
OpenSSL::BN.new(view.reverse, 2),
|
181
|
+
JOSE::JWA::Edwards25519Point::L
|
182
|
+
)
|
183
|
+
|
184
|
+
t_point = x_point + y_point
|
185
|
+
key = t_point.to_bytes(JOSE::JWA::Edwards25519Point::B)
|
186
|
+
|
187
|
+
pub = MixinBot.utils.generate_public_key key
|
188
|
+
key_index = utxo['keys'].index pub.unpack1('H*')
|
189
|
+
raise ArgumentError, 'cannot find valid key' unless key_index.is_a? Integer
|
190
|
+
|
191
|
+
signature = MixinBot.utils.sign(msg, key:)
|
192
|
+
signature = signature.unpack1('H*')
|
193
|
+
sig = {}
|
194
|
+
sig[key_index] = signature
|
195
|
+
tx[:signatures] << sig
|
196
|
+
end
|
197
|
+
|
198
|
+
MixinBot.utils.encode_raw_transaction tx
|
75
199
|
end
|
76
200
|
end
|
77
201
|
end
|
@@ -3,40 +3,72 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Transfer
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
6
|
+
# kwargs:
|
7
|
+
# {
|
8
|
+
# members: uuid | [ uuid ],
|
9
|
+
# threshold: integer / nil,
|
10
|
+
# asset_id: uuid,
|
11
|
+
# amount: string / float,
|
12
|
+
# trace_id: uuid / nil,
|
13
|
+
# request_id: uuid / nil,
|
14
|
+
# memo: string,
|
15
|
+
# spend_key: string / nil,
|
16
|
+
# }
|
17
|
+
def create_safe_transfer(**kwargs)
|
18
|
+
asset_id = kwargs[:asset_id]
|
19
|
+
raise ArgumentError, 'asset_id required' if asset_id.blank?
|
20
|
+
|
21
|
+
amount = kwargs[:amount]&.to_d
|
22
|
+
raise ArgumentError, 'amount required' if amount.blank?
|
23
|
+
|
24
|
+
members = [kwargs[:members]].flatten.compact
|
25
|
+
raise ArgumentError, 'members required' if members.blank?
|
26
|
+
|
27
|
+
threshold = kwargs[:threshold] || members.length
|
28
|
+
request_id = kwargs[:request_id] || kwargs[:trace_id] || SecureRandom.uuid
|
29
|
+
memo = kwargs[:memo] || ''
|
30
|
+
|
31
|
+
# step 1: select inputs
|
32
|
+
outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
|
33
|
+
|
34
|
+
utxos = []
|
35
|
+
outputs.each do |output|
|
36
|
+
break if utxos.sum { |o| o['amount'].to_d } >= amount
|
37
|
+
|
38
|
+
utxos.shift if utxos.size >= 256
|
39
|
+
utxos << output
|
40
|
+
end
|
41
|
+
|
42
|
+
# step 2: build transaction
|
43
|
+
tx = build_safe_transaction(
|
44
|
+
utxos:,
|
45
|
+
receivers: [{
|
46
|
+
members:,
|
47
|
+
threshold:,
|
48
|
+
amount:
|
49
|
+
}],
|
50
|
+
extra: memo
|
51
|
+
)
|
52
|
+
raw = MixinBot.utils.encode_raw_transaction tx
|
53
|
+
|
54
|
+
# step 3: verify transaction
|
55
|
+
request = create_safe_transaction_request(request_id, raw)['data']
|
56
|
+
|
57
|
+
# step 4: sign transaction
|
58
|
+
spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
|
59
|
+
signed_raw = sign_safe_transaction(
|
60
|
+
raw:,
|
61
|
+
utxos:,
|
62
|
+
request: request[0],
|
63
|
+
spend_key:
|
64
|
+
)
|
32
65
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
66
|
+
# step 5: submit transaction
|
67
|
+
send_safe_transaction(
|
68
|
+
request_id,
|
69
|
+
signed_raw
|
70
|
+
)
|
38
71
|
end
|
39
|
-
alias read_transfer transfer
|
40
72
|
end
|
41
73
|
end
|
42
74
|
end
|
data/lib/mixin_bot/api/user.rb
CHANGED
@@ -3,76 +3,106 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module User
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
31
|
-
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.
|
22
|
+
res = client.post path, **payload
|
23
|
+
res.merge(private_key:).with_indifferent_access
|
38
24
|
end
|
39
25
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
77
|
+
|
78
|
+
pin_base64 = encrypt_tip_pin pin, 'SEQUENCER:REGISTER:', config.app_id, public_key
|
60
79
|
|
61
|
-
|
62
|
-
|
63
|
-
|
80
|
+
payload = {
|
81
|
+
public_key:,
|
82
|
+
signature:,
|
83
|
+
pin_base64:
|
84
|
+
}
|
85
|
+
|
86
|
+
client.post path, **payload
|
64
87
|
end
|
65
88
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
89
|
+
def migrate_to_safe(spend_key:, old_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
|
-
|
74
|
-
|
75
|
-
|
96
|
+
update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: profile['tip_counter']) if profile['tip_key_base64'].blank?
|
97
|
+
|
98
|
+
# wait for tip pin update in server
|
99
|
+
sleep 1
|
100
|
+
|
101
|
+
safe_register spend_key
|
102
|
+
|
103
|
+
{
|
104
|
+
spend_key:
|
105
|
+
}.with_indifferent_access
|
76
106
|
end
|
77
107
|
end
|
78
108
|
end
|