mixin_bot 0.12.1 → 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 +29 -51
- 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 +149 -5
- 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,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
|
@@ -3,75 +3,74 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Withdraw
|
6
|
-
|
7
|
-
def create_withdraw_address(options, access_token: nil)
|
6
|
+
def create_withdraw_address(**kwargs)
|
8
7
|
path = '/addresses'
|
9
|
-
|
8
|
+
pin = kwargs[:pin]
|
10
9
|
payload =
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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:
|
38
|
-
|
39
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
66
|
-
amount
|
67
|
-
trace_id
|
68
|
-
memo:
|
69
|
-
pin: encrypt_pin(pin)
|
60
|
+
address_id:,
|
61
|
+
amount:,
|
62
|
+
trace_id:,
|
63
|
+
memo:
|
70
64
|
}
|
71
65
|
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|