mixin_bot 1.4.0 → 2.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/AGENTS.md +75 -0
- data/API_COVERAGE.md +143 -0
- data/CHANGELOG.md +112 -0
- data/README.md +375 -0
- data/docs/agent/cli.md +149 -0
- data/docs/agent/cookbook.md +152 -0
- data/examples/blaze.rb +43 -0
- data/examples/config.yml.example +21 -0
- data/lib/mixin_bot/address.rb +43 -3
- data/lib/mixin_bot/api/app.rb +7 -0
- data/lib/mixin_bot/api/asset.rb +114 -3
- data/lib/mixin_bot/api/auth.rb +19 -10
- data/lib/mixin_bot/api/blaze.rb +81 -0
- data/lib/mixin_bot/api/chain.rb +94 -0
- data/lib/mixin_bot/api/code.rb +16 -0
- data/lib/mixin_bot/api/computer_api.rb +60 -0
- data/lib/mixin_bot/api/conversation.rb +7 -1
- data/lib/mixin_bot/api/deposit.rb +12 -0
- data/lib/mixin_bot/api/encrypted_message.rb +1 -1
- data/lib/mixin_bot/api/fiat.rb +12 -0
- data/lib/mixin_bot/api/inscription.rb +2 -2
- data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
- data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
- data/lib/mixin_bot/api/legacy_output.rb +10 -3
- data/lib/mixin_bot/api/legacy_payment.rb +2 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
- data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
- data/lib/mixin_bot/api/legacy_user.rb +51 -0
- data/lib/mixin_bot/api/me.rb +99 -3
- data/lib/mixin_bot/api/message.rb +18 -27
- data/lib/mixin_bot/api/multisig.rb +19 -0
- data/lib/mixin_bot/api/network.rb +17 -0
- data/lib/mixin_bot/api/network_asset.rb +27 -0
- data/lib/mixin_bot/api/output.rb +1 -1
- data/lib/mixin_bot/api/pin.rb +16 -3
- data/lib/mixin_bot/api/pin_payload.rb +26 -0
- data/lib/mixin_bot/api/session.rb +14 -0
- data/lib/mixin_bot/api/snapshot.rb +6 -0
- data/lib/mixin_bot/api/tip.rb +74 -1
- data/lib/mixin_bot/api/transaction.rb +106 -17
- data/lib/mixin_bot/api/transfer.rb +141 -14
- data/lib/mixin_bot/api/turn.rb +12 -0
- data/lib/mixin_bot/api/user.rb +148 -45
- data/lib/mixin_bot/api/withdraw.rb +24 -23
- data/lib/mixin_bot/api.rb +248 -3
- data/lib/mixin_bot/bot_auth.rb +71 -0
- data/lib/mixin_bot/cli/api.rb +224 -143
- data/lib/mixin_bot/cli/base.rb +77 -0
- data/lib/mixin_bot/cli/call.rb +71 -0
- data/lib/mixin_bot/cli/errors.rb +56 -0
- data/lib/mixin_bot/cli/node.rb +9 -2
- data/lib/mixin_bot/cli/output.rb +196 -0
- data/lib/mixin_bot/cli/schema.rb +274 -0
- data/lib/mixin_bot/cli/schema_command.rb +21 -0
- data/lib/mixin_bot/cli/utils.rb +114 -18
- data/lib/mixin_bot/cli.rb +124 -48
- data/lib/mixin_bot/client/error_mapper.rb +40 -0
- data/lib/mixin_bot/client.rb +94 -64
- data/lib/mixin_bot/computer.rb +132 -0
- data/lib/mixin_bot/configuration.rb +108 -1
- data/lib/mixin_bot/errors.rb +102 -0
- data/lib/mixin_bot/models/address.rb +11 -0
- data/lib/mixin_bot/models/api_envelope.rb +67 -0
- data/lib/mixin_bot/models/asset.rb +11 -0
- data/lib/mixin_bot/models/ghost_keys.rb +14 -0
- data/lib/mixin_bot/models/output.rb +11 -0
- data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
- data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
- data/lib/mixin_bot/models/user.rb +11 -0
- data/lib/mixin_bot/models.rb +10 -0
- data/lib/mixin_bot/monitor.rb +77 -0
- data/lib/mixin_bot/transaction/buffer.rb +34 -0
- data/lib/mixin_bot/transaction/decoder.rb +227 -0
- data/lib/mixin_bot/transaction/encoder.rb +255 -0
- data/lib/mixin_bot/transaction.rb +6 -475
- data/lib/mixin_bot/url_scheme.rb +63 -0
- data/lib/mixin_bot/utils/address.rb +17 -80
- data/lib/mixin_bot/utils/crypto.rb +173 -1
- data/lib/mixin_bot/utils/decoder.rb +1 -1
- data/lib/mixin_bot/utils/encoder.rb +13 -0
- data/lib/mixin_bot/utils.rb +45 -0
- data/lib/mixin_bot/uuid.rb +78 -1
- data/lib/mixin_bot/version.rb +11 -1
- data/lib/mixin_bot.rb +172 -18
- data/lib/mvm/bridge.rb +46 -0
- data/lib/mvm/client.rb +60 -0
- data/lib/mvm/nft.rb +4 -2
- data/lib/mvm/registry.rb +2 -1
- data/lib/mvm.rb +93 -0
- data/lib/tasks/api_coverage.rake +20 -0
- data/llms.txt +29 -0
- metadata +77 -9
|
@@ -2,18 +2,108 @@
|
|
|
2
2
|
|
|
3
3
|
module MixinBot
|
|
4
4
|
class API
|
|
5
|
+
##
|
|
6
|
+
# API methods for creating transfers using the Safe API.
|
|
7
|
+
#
|
|
8
|
+
# The Safe API is the recommended way to transfer assets on Mixin Network.
|
|
9
|
+
# It provides better security, lower fees, and more flexibility than legacy transfers.
|
|
10
|
+
#
|
|
11
|
+
# == Transfer Process
|
|
12
|
+
#
|
|
13
|
+
# A Safe transfer involves several steps:
|
|
14
|
+
# 1. Select UTXOs (unspent transaction outputs) as inputs
|
|
15
|
+
# 2. Build the transaction with outputs
|
|
16
|
+
# 3. Sign the transaction with spend key
|
|
17
|
+
# 4. Submit the signed transaction
|
|
18
|
+
#
|
|
19
|
+
# This module handles all these steps automatically.
|
|
20
|
+
#
|
|
21
|
+
# == Usage
|
|
22
|
+
#
|
|
23
|
+
# # Simple transfer to a single user
|
|
24
|
+
# api.create_transfer(
|
|
25
|
+
# members: 'user-uuid',
|
|
26
|
+
# asset_id: 'asset-uuid',
|
|
27
|
+
# amount: '0.01',
|
|
28
|
+
# memo: 'Payment'
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
# # Multisig transfer
|
|
32
|
+
# api.create_transfer(
|
|
33
|
+
# members: ['user1-uuid', 'user2-uuid', 'user3-uuid'],
|
|
34
|
+
# threshold: 2,
|
|
35
|
+
# asset_id: 'asset-uuid',
|
|
36
|
+
# amount: '0.01'
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
5
39
|
module Transfer
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
40
|
+
##
|
|
41
|
+
# Creates a Safe API transfer.
|
|
42
|
+
#
|
|
43
|
+
# This is the main method for sending assets on Mixin Network.
|
|
44
|
+
# It handles the complete transfer process including UTXO selection,
|
|
45
|
+
# transaction building, signing, and submission.
|
|
46
|
+
#
|
|
47
|
+
# @param kwargs [Hash] transfer options
|
|
48
|
+
# @option kwargs [String, Array<String>] :members recipient user ID(s)
|
|
49
|
+
# @option kwargs [Integer] :threshold multisig threshold (defaults to members.length)
|
|
50
|
+
# @option kwargs [String] :asset_id the asset UUID to transfer
|
|
51
|
+
# @option kwargs [String, Float] :amount the amount to transfer
|
|
52
|
+
# @option kwargs [String] :trace_id unique trace ID (defaults to random UUID)
|
|
53
|
+
# @option kwargs [String] :request_id alias for trace_id
|
|
54
|
+
# @option kwargs [String] :memo transaction memo (max 140 characters)
|
|
55
|
+
# @option kwargs [String] :spend_key spend private key (defaults to config.spend_key)
|
|
56
|
+
# @option kwargs [Array<Hash>] :utxos specific UTXOs to use (optional, will auto-select if not provided)
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] the transfer result including transaction hash and status
|
|
59
|
+
#
|
|
60
|
+
# @raise [ArgumentError] if required parameters are missing or invalid
|
|
61
|
+
# @raise [InsufficientBalanceError] if balance is insufficient
|
|
62
|
+
#
|
|
63
|
+
# @example Simple transfer
|
|
64
|
+
# result = api.create_transfer(
|
|
65
|
+
# members: '6ae1c7ae-1df1-498e-8f21-d48cb6d129b5',
|
|
66
|
+
# asset_id: '965e5c6e-434c-3fa9-b780-c50f43cd955c',
|
|
67
|
+
# amount: '0.01',
|
|
68
|
+
# memo: 'Test payment'
|
|
69
|
+
# )
|
|
70
|
+
# puts result['snapshot_id']
|
|
71
|
+
#
|
|
72
|
+
# @example Multisig transfer (2-of-3)
|
|
73
|
+
# result = api.create_transfer(
|
|
74
|
+
# members: [
|
|
75
|
+
# '6ae1c7ae-1df1-498e-8f21-d48cb6d129b5',
|
|
76
|
+
# '8017d200-7870-4b82-b53f-74bae1d2dad7',
|
|
77
|
+
# 'e8e8cd79-cd40-4796-8c54-3a13cfe50115'
|
|
78
|
+
# ],
|
|
79
|
+
# threshold: 2,
|
|
80
|
+
# asset_id: '965e5c6e-434c-3fa9-b780-c50f43cd955c',
|
|
81
|
+
# amount: '0.01'
|
|
82
|
+
# )
|
|
83
|
+
#
|
|
84
|
+
def create_transfer(pin = nil, **kwargs)
|
|
85
|
+
if pin.is_a?(String) && kwargs[:opponent_id].present?
|
|
86
|
+
create_legacy_transfer(pin, **kwargs)
|
|
87
|
+
else
|
|
88
|
+
create_safe_transfer(**(pin.is_a?(Hash) ? pin : kwargs))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def send_transaction(**)
|
|
93
|
+
create_safe_transfer(**)
|
|
94
|
+
end
|
|
95
|
+
alias send_transfer_transaction send_transaction
|
|
96
|
+
alias send_transaction_with_outputs send_transaction
|
|
97
|
+
|
|
98
|
+
def send_transaction_until_sufficient(**)
|
|
99
|
+
create_safe_transfer(**)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def send_transaction_with_change_outputs(**)
|
|
103
|
+
create_safe_transfer(**)
|
|
104
|
+
end
|
|
105
|
+
alias send_transaction_with_utxos_and_change_outputs send_transaction_with_change_outputs
|
|
106
|
+
|
|
17
107
|
def create_safe_transfer(**kwargs)
|
|
18
108
|
utxos = kwargs[:utxos]
|
|
19
109
|
raise ArgumentError, 'utxos must be array' if utxos.present? && !utxos.is_a?(Array)
|
|
@@ -70,17 +160,54 @@ module MixinBot
|
|
|
70
160
|
signed_raw
|
|
71
161
|
)
|
|
72
162
|
end
|
|
73
|
-
alias create_transfer create_safe_transfer
|
|
74
163
|
|
|
164
|
+
##
|
|
165
|
+
# Builds a UTXO set for a transfer.
|
|
166
|
+
#
|
|
167
|
+
# Selects unspent outputs (UTXOs) from the bot's wallet that sum up
|
|
168
|
+
# to at least the requested amount. This is used internally by
|
|
169
|
+
# create_safe_transfer but can be called directly if needed.
|
|
170
|
+
#
|
|
171
|
+
# The method:
|
|
172
|
+
# - Fetches unspent outputs for the asset
|
|
173
|
+
# - Sorts them by amount (smallest first)
|
|
174
|
+
# - Selects outputs until the amount is reached
|
|
175
|
+
# - Limits to 256 UTXOs maximum
|
|
176
|
+
#
|
|
177
|
+
# @param asset_id [String] the asset UUID
|
|
178
|
+
# @param amount [String, Float, BigDecimal] the amount needed
|
|
179
|
+
# @return [Array<Hash>] array of selected UTXOs
|
|
180
|
+
#
|
|
181
|
+
# @raise [InsufficientBalanceError] if balance is insufficient
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# utxos = api.build_utxos(
|
|
185
|
+
# asset_id: '965e5c6e-434c-3fa9-b780-c50f43cd955c',
|
|
186
|
+
# amount: '0.01'
|
|
187
|
+
# )
|
|
188
|
+
# puts "Selected #{utxos.length} UTXOs"
|
|
189
|
+
# puts "Total: #{utxos.sum { |u| u['amount'].to_d }}"
|
|
190
|
+
#
|
|
75
191
|
def build_utxos(asset_id:, amount:)
|
|
192
|
+
amount = amount.to_d
|
|
76
193
|
outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
|
|
77
194
|
|
|
78
195
|
utxos = []
|
|
79
196
|
outputs.each do |output|
|
|
80
|
-
break if utxos.
|
|
197
|
+
break if utxos.size >= 256
|
|
81
198
|
|
|
82
|
-
utxos.shift if utxos.size >= 256
|
|
83
199
|
utxos << output
|
|
200
|
+
break if utxos.sum { |o| o['amount'].to_d } >= amount
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
total_in = utxos.sum { |o| o['amount'].to_d }
|
|
204
|
+
if total_in < amount
|
|
205
|
+
raise MixinBot::UtxoInsufficientError.new(
|
|
206
|
+
'insufficient utxo',
|
|
207
|
+
total_input: total_in,
|
|
208
|
+
total_output: amount,
|
|
209
|
+
output_size: utxos.size
|
|
210
|
+
)
|
|
84
211
|
end
|
|
85
212
|
|
|
86
213
|
utxos
|
data/lib/mixin_bot/api/user.rb
CHANGED
|
@@ -2,12 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
module MixinBot
|
|
4
4
|
class API
|
|
5
|
+
##
|
|
6
|
+
# User-related API endpoints: bot/network user lookup, plain network-user
|
|
7
|
+
# creation, and the full Safe Network registration flow.
|
|
8
|
+
#
|
|
9
|
+
# The Safe-network registration mirrors the official Go SDK reference
|
|
10
|
+
# implementation in +RegisterSafeWithSetupPin+ / +RegisterSafeBareUser+:
|
|
11
|
+
# https://github.com/MixinNetwork/bot-api-go-client/blob/master/safe_user.go
|
|
12
|
+
#
|
|
5
13
|
module User
|
|
14
|
+
# Maximum number of times to retry +safe_register+ when the freshly-set
|
|
15
|
+
# TIP PIN has not yet propagated through the Mixin server.
|
|
16
|
+
SAFE_REGISTER_MAX_RETRIES = 3
|
|
17
|
+
|
|
18
|
+
# Base seconds to wait between +safe_register+ retries. The wait grows
|
|
19
|
+
# linearly with the attempt number.
|
|
20
|
+
SAFE_REGISTER_RETRY_BASE_DELAY = 1
|
|
21
|
+
|
|
22
|
+
# Seconds to wait after +update_pin+ before calling +safe_register+, so
|
|
23
|
+
# the new TIP PIN has time to propagate on the server side.
|
|
24
|
+
TIP_PIN_PROPAGATION_DELAY = 1
|
|
25
|
+
|
|
6
26
|
def user(user_id, access_token: nil)
|
|
7
27
|
path = format('/users/%<user_id>s', user_id:)
|
|
8
28
|
client.get path, access_token:
|
|
9
29
|
end
|
|
10
30
|
|
|
31
|
+
##
|
|
32
|
+
# Creates a Mixin network user.
|
|
33
|
+
#
|
|
34
|
+
# When +key+ is omitted a fresh Ed25519 keypair is generated. The
|
|
35
|
+
# response is merged with the hex-encoded session private key under
|
|
36
|
+
# +:private_key+.
|
|
37
|
+
#
|
|
38
|
+
# @param full_name [String] display name for the new user
|
|
39
|
+
# @param key [String, nil] optional 32-byte Ed25519 seed
|
|
40
|
+
# @return [Hash] Mixin response merged with the hex-encoded private key
|
|
41
|
+
#
|
|
11
42
|
def create_user(full_name, key: nil)
|
|
12
43
|
keypair = JOSE::JWA::Ed25519.keypair key
|
|
13
44
|
session_secret = Base64.urlsafe_encode64 keypair[0], padding: false
|
|
@@ -37,54 +68,95 @@ module MixinBot
|
|
|
37
68
|
client.post path, *payload
|
|
38
69
|
end
|
|
39
70
|
|
|
71
|
+
##
|
|
72
|
+
# Creates a Safe-network user end-to-end.
|
|
73
|
+
#
|
|
74
|
+
# Mirrors +RegisterSafeWithSetupPin+ in the Go SDK:
|
|
75
|
+
#
|
|
76
|
+
# 1. generate (or accept) a session keypair and a spend keypair
|
|
77
|
+
# 2. create the network user via {#create_user}
|
|
78
|
+
# 3. set the user's PIN to the TIP public key derived from the spend key
|
|
79
|
+
# 4. wait briefly for propagation, then register on the Safe network
|
|
80
|
+
# (retrying transient failures)
|
|
81
|
+
#
|
|
82
|
+
# The returned keystore is suitable for instantiating a new
|
|
83
|
+
# +MixinBot::API+ that authenticates as the freshly-registered user.
|
|
84
|
+
#
|
|
85
|
+
# @param name [String] display name for the new user
|
|
86
|
+
# @param private_key [String, nil] optional 32-byte session Ed25519 seed
|
|
87
|
+
# @param spend_key [String, nil] optional 32-byte spend Ed25519 seed
|
|
88
|
+
# @return [Hash] keystore with +:app_id+, +:session_id+,
|
|
89
|
+
# +:session_private_key+, +:server_public_key+ and +:spend_key+
|
|
90
|
+
# @raise [MixinBot::Error] when registration ultimately fails. Transient
|
|
91
|
+
# PIN/response errors are retried up to {SAFE_REGISTER_MAX_RETRIES}
|
|
92
|
+
# times; other errors bubble up immediately.
|
|
93
|
+
#
|
|
40
94
|
def create_safe_user(name, private_key: nil, spend_key: nil)
|
|
41
|
-
|
|
42
|
-
private_key = private_keypair[1].unpack1('H*')
|
|
43
|
-
|
|
95
|
+
session_keypair = JOSE::JWA::Ed25519.keypair private_key
|
|
44
96
|
spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
|
|
45
|
-
spend_key = spend_keypair[1].unpack1('H*')
|
|
46
97
|
|
|
47
|
-
|
|
98
|
+
spend_key_hex = spend_keypair[1].unpack1('H*')
|
|
99
|
+
|
|
100
|
+
user = create_user name, key: session_keypair[1][...32]
|
|
101
|
+
data = user.fetch('data')
|
|
48
102
|
|
|
49
103
|
keystore = {
|
|
50
|
-
app_id:
|
|
51
|
-
session_id:
|
|
52
|
-
session_private_key:
|
|
53
|
-
server_public_key:
|
|
54
|
-
spend_key:
|
|
104
|
+
app_id: data['user_id'],
|
|
105
|
+
session_id: data['session_id'],
|
|
106
|
+
session_private_key: session_keypair[1].unpack1('H*'),
|
|
107
|
+
server_public_key: data['pin_token_base64'],
|
|
108
|
+
spend_key: spend_key_hex
|
|
55
109
|
}
|
|
56
|
-
user_api = MixinBot::API.new(**keystore)
|
|
57
110
|
|
|
58
|
-
user_api
|
|
111
|
+
user_api = MixinBot::API.new(**keystore)
|
|
59
112
|
|
|
60
|
-
|
|
61
|
-
|
|
113
|
+
tip_pin = MixinBot.utils.tip_public_key spend_keypair[0], counter: data['tip_counter']
|
|
114
|
+
user_api.update_pin pin: tip_pin
|
|
62
115
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
user_api.safe_register keystore[:spend_key]
|
|
66
|
-
rescue MixinBot::Error => e
|
|
67
|
-
@__retry__ += 1
|
|
68
|
-
raise e if @__retry__ > 3
|
|
116
|
+
# Allow the freshly-set TIP PIN to propagate before registering.
|
|
117
|
+
sleep TIP_PIN_PROPAGATION_DELAY
|
|
69
118
|
|
|
70
|
-
|
|
71
|
-
|
|
119
|
+
with_safe_register_retries do
|
|
120
|
+
user_api.safe_register spend_key_hex
|
|
72
121
|
end
|
|
73
122
|
|
|
74
123
|
keystore
|
|
75
124
|
end
|
|
76
125
|
|
|
77
|
-
|
|
126
|
+
##
|
|
127
|
+
# Registers an existing user on the Safe network.
|
|
128
|
+
#
|
|
129
|
+
# +spend_key+ may be supplied as raw bytes, a hex string, or a
|
|
130
|
+
# Base64-encoded string. It must encode the user's full Ed25519 spend
|
|
131
|
+
# private key (or a 32-byte seed).
|
|
132
|
+
#
|
|
133
|
+
# @param spend_key [String] the user's spend Ed25519 private key
|
|
134
|
+
# @return [Hash] Mixin response
|
|
135
|
+
# @raise [ArgumentError] when +spend_key+ cannot be decoded into at
|
|
136
|
+
# least 32 bytes
|
|
137
|
+
#
|
|
138
|
+
def safe_register(spend_key)
|
|
78
139
|
path = '/safe/users'
|
|
79
140
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
spend_key_bytes = MixinBot.utils.decode_key spend_key
|
|
142
|
+
raise ArgumentError, 'invalid spend_key' if spend_key_bytes.nil? || spend_key_bytes.size < 32
|
|
143
|
+
|
|
144
|
+
keypair = JOSE::JWA::Ed25519.keypair spend_key_bytes[...32]
|
|
145
|
+
public_key = keypair[0].unpack1('H*')
|
|
146
|
+
# Normalize to a 64-byte signing key in hex so that callers may pass a
|
|
147
|
+
# 32-byte seed without crashing the downstream signer.
|
|
148
|
+
signing_key_hex = keypair[1].unpack1('H*')
|
|
83
149
|
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
# NOTE: the Go SDK's +crypto.Sha256Hash+ is misleadingly named — it
|
|
151
|
+
# actually computes SHA3-256, so +SHA3::Digest::SHA256+ is the correct
|
|
152
|
+
# match. See bot-api-go-client safe_user.go +RegisterSafeBareUser+.
|
|
153
|
+
app_id_hash = SHA3::Digest::SHA256.hexdigest config.app_id
|
|
154
|
+
signature = Base64.urlsafe_encode64(
|
|
155
|
+
JOSE::JWA::Ed25519.sign([app_id_hash].pack('H*'), keypair[1]),
|
|
156
|
+
padding: false
|
|
157
|
+
)
|
|
86
158
|
|
|
87
|
-
pin_base64 = encrypt_tip_pin
|
|
159
|
+
pin_base64 = encrypt_tip_pin signing_key_hex, 'SEQUENCER:REGISTER:', config.app_id, public_key
|
|
88
160
|
|
|
89
161
|
payload = {
|
|
90
162
|
public_key:,
|
|
@@ -95,37 +167,68 @@ module MixinBot
|
|
|
95
167
|
client.post path, **payload
|
|
96
168
|
end
|
|
97
169
|
|
|
170
|
+
##
|
|
171
|
+
# Migrates an existing legacy user to the Safe network.
|
|
172
|
+
#
|
|
173
|
+
# When the user has not yet upgraded to a TIP PIN, +pin+ must be the
|
|
174
|
+
# user's current 6-digit PIN so {#update_pin} can rotate it to a TIP
|
|
175
|
+
# PIN derived from +spend_key+. When the user already has a TIP PIN,
|
|
176
|
+
# +pin+ may be omitted.
|
|
177
|
+
#
|
|
178
|
+
# @param spend_key [String] the user's spend Ed25519 seed or full key
|
|
179
|
+
# @param pin [String, nil] the user's current PIN (only required when
|
|
180
|
+
# the user has not yet upgraded to a TIP PIN)
|
|
181
|
+
# @return [TrueClass, Hash] +true+ if the user already has Safe enabled,
|
|
182
|
+
# otherwise +{ spend_key: <hex> }+
|
|
183
|
+
# @raise [MixinBot::Error] when registration ultimately fails. Transient
|
|
184
|
+
# PIN/response errors are retried up to {SAFE_REGISTER_MAX_RETRIES}
|
|
185
|
+
# times; other errors bubble up immediately.
|
|
186
|
+
#
|
|
98
187
|
def migrate_to_safe(spend_key:, pin: nil)
|
|
99
188
|
profile = me['data']
|
|
100
189
|
return true if profile['has_safe']
|
|
101
190
|
|
|
102
191
|
spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
|
|
103
|
-
|
|
192
|
+
spend_key_hex = spend_keypair[1].unpack1('H*')
|
|
104
193
|
|
|
105
194
|
if profile['tip_key_base64'].blank?
|
|
106
|
-
new_pin = MixinBot.utils.tip_public_key
|
|
107
|
-
update_pin
|
|
195
|
+
new_pin = MixinBot.utils.tip_public_key spend_keypair[0], counter: profile['tip_counter']
|
|
196
|
+
update_pin pin: new_pin, old_pin: pin
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Allow the freshly-set TIP PIN to propagate before registering.
|
|
200
|
+
sleep TIP_PIN_PROPAGATION_DELAY
|
|
108
201
|
|
|
109
|
-
|
|
202
|
+
with_safe_register_retries do
|
|
203
|
+
safe_register spend_key_hex
|
|
110
204
|
end
|
|
111
205
|
|
|
112
|
-
|
|
113
|
-
|
|
206
|
+
{ spend_key: spend_key_hex }.with_indifferent_access
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
114
210
|
|
|
115
|
-
|
|
211
|
+
# Errors that are typically caused by the freshly-set TIP PIN not yet
|
|
212
|
+
# being visible to the Safe-network sequencer. Anything else (auth,
|
|
213
|
+
# missing user, validation, balance, ...) should bubble up immediately.
|
|
214
|
+
RETRIABLE_SAFE_REGISTER_ERRORS = [MixinBot::PinError, MixinBot::ResponseError].freeze
|
|
215
|
+
private_constant :RETRIABLE_SAFE_REGISTER_ERRORS
|
|
216
|
+
|
|
217
|
+
# Yields to the block, retrying transient errors up to
|
|
218
|
+
# {SAFE_REGISTER_MAX_RETRIES} times with linear backoff. The TIP PIN
|
|
219
|
+
# set by {#update_pin} can take a moment to propagate, so the first
|
|
220
|
+
# +safe_register+ attempts often fail.
|
|
221
|
+
def with_safe_register_retries
|
|
222
|
+
attempt = 0
|
|
116
223
|
begin
|
|
117
|
-
|
|
118
|
-
rescue
|
|
119
|
-
|
|
120
|
-
raise
|
|
224
|
+
yield
|
|
225
|
+
rescue *RETRIABLE_SAFE_REGISTER_ERRORS
|
|
226
|
+
attempt += 1
|
|
227
|
+
raise if attempt > SAFE_REGISTER_MAX_RETRIES
|
|
121
228
|
|
|
122
|
-
sleep
|
|
229
|
+
sleep(SAFE_REGISTER_RETRY_BASE_DELAY + attempt)
|
|
123
230
|
retry
|
|
124
231
|
end
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
spend_key:
|
|
128
|
-
}.with_indifferent_access
|
|
129
232
|
end
|
|
130
233
|
end
|
|
131
234
|
end
|
|
@@ -14,38 +14,30 @@ module MixinBot
|
|
|
14
14
|
label: kwargs[:label]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
payload[:pin] = encrypt_pin pin
|
|
21
|
-
end
|
|
17
|
+
payload.update(
|
|
18
|
+
tip_or_legacy_pin_payload(pin, 'TIP:ADDRESS:ADD:', payload[:asset_id], payload[:destination], payload[:tag], payload[:label])
|
|
19
|
+
)
|
|
22
20
|
|
|
23
21
|
client.post path, **payload
|
|
24
22
|
end
|
|
23
|
+
alias create_address create_withdraw_address
|
|
25
24
|
|
|
26
25
|
def get_withdraw_address(address, access_token: nil)
|
|
27
26
|
path = format('/addresses/%<address>s', address:)
|
|
28
27
|
|
|
29
28
|
client.get path, access_token:
|
|
30
29
|
end
|
|
30
|
+
alias read_address get_withdraw_address
|
|
31
31
|
|
|
32
32
|
def delete_withdraw_address(address, **kwargs)
|
|
33
33
|
pin = kwargs[:pin]
|
|
34
34
|
|
|
35
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
|
|
36
|
+
payload = tip_or_legacy_pin_payload(pin, 'TIP:ADDRESS:REMOVE:', address)
|
|
46
37
|
|
|
47
38
|
client.post path, **payload
|
|
48
39
|
end
|
|
40
|
+
alias delete_address delete_withdraw_address
|
|
49
41
|
|
|
50
42
|
def withdrawals(**kwargs)
|
|
51
43
|
address_id = kwargs[:address_id]
|
|
@@ -53,7 +45,7 @@ module MixinBot
|
|
|
53
45
|
amount = format('%.8f', kwargs[:amount].to_d.to_r)
|
|
54
46
|
trace_id = kwargs[:trace_id]
|
|
55
47
|
memo = kwargs[:memo]
|
|
56
|
-
kwargs[:access_token]
|
|
48
|
+
access_token = kwargs[:access_token]
|
|
57
49
|
|
|
58
50
|
path = '/withdrawals'
|
|
59
51
|
payload = {
|
|
@@ -63,14 +55,23 @@ module MixinBot
|
|
|
63
55
|
memo:
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
payload[:pin] = encrypt_pin pin
|
|
71
|
-
end
|
|
58
|
+
fee = '0'
|
|
59
|
+
payload.update(
|
|
60
|
+
tip_or_legacy_pin_payload(pin, 'TIP:WITHDRAWAL:CREATE:', address_id, amount, fee, trace_id, memo)
|
|
61
|
+
)
|
|
72
62
|
|
|
73
|
-
client.post path, **payload
|
|
63
|
+
client.post path, **payload, access_token:
|
|
64
|
+
end
|
|
65
|
+
alias send_withdrawal withdrawals
|
|
66
|
+
|
|
67
|
+
def withdraw_addresses(asset_id, access_token: nil)
|
|
68
|
+
path = format('/assets/%<asset_id>s/addresses', asset_id:)
|
|
69
|
+
client.get path, access_token:
|
|
70
|
+
end
|
|
71
|
+
alias get_addresses_by_asset_id withdraw_addresses
|
|
72
|
+
|
|
73
|
+
def check_address(asset:, destination:, tag: nil)
|
|
74
|
+
client.get '/external/addresses/check', asset:, destination:, tag:, access_token: ''
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
end
|