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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +75 -0
  3. data/API_COVERAGE.md +143 -0
  4. data/CHANGELOG.md +112 -0
  5. data/README.md +375 -0
  6. data/docs/agent/cli.md +149 -0
  7. data/docs/agent/cookbook.md +152 -0
  8. data/examples/blaze.rb +43 -0
  9. data/examples/config.yml.example +21 -0
  10. data/lib/mixin_bot/address.rb +43 -3
  11. data/lib/mixin_bot/api/app.rb +7 -0
  12. data/lib/mixin_bot/api/asset.rb +114 -3
  13. data/lib/mixin_bot/api/auth.rb +19 -10
  14. data/lib/mixin_bot/api/blaze.rb +81 -0
  15. data/lib/mixin_bot/api/chain.rb +94 -0
  16. data/lib/mixin_bot/api/code.rb +16 -0
  17. data/lib/mixin_bot/api/computer_api.rb +60 -0
  18. data/lib/mixin_bot/api/conversation.rb +7 -1
  19. data/lib/mixin_bot/api/deposit.rb +12 -0
  20. data/lib/mixin_bot/api/encrypted_message.rb +1 -1
  21. data/lib/mixin_bot/api/fiat.rb +12 -0
  22. data/lib/mixin_bot/api/inscription.rb +2 -2
  23. data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
  24. data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
  25. data/lib/mixin_bot/api/legacy_output.rb +10 -3
  26. data/lib/mixin_bot/api/legacy_payment.rb +2 -0
  27. data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
  28. data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
  29. data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
  30. data/lib/mixin_bot/api/legacy_user.rb +51 -0
  31. data/lib/mixin_bot/api/me.rb +99 -3
  32. data/lib/mixin_bot/api/message.rb +18 -27
  33. data/lib/mixin_bot/api/multisig.rb +19 -0
  34. data/lib/mixin_bot/api/network.rb +17 -0
  35. data/lib/mixin_bot/api/network_asset.rb +27 -0
  36. data/lib/mixin_bot/api/output.rb +1 -1
  37. data/lib/mixin_bot/api/pin.rb +16 -3
  38. data/lib/mixin_bot/api/pin_payload.rb +26 -0
  39. data/lib/mixin_bot/api/session.rb +14 -0
  40. data/lib/mixin_bot/api/snapshot.rb +6 -0
  41. data/lib/mixin_bot/api/tip.rb +74 -1
  42. data/lib/mixin_bot/api/transaction.rb +106 -17
  43. data/lib/mixin_bot/api/transfer.rb +141 -14
  44. data/lib/mixin_bot/api/turn.rb +12 -0
  45. data/lib/mixin_bot/api/user.rb +148 -45
  46. data/lib/mixin_bot/api/withdraw.rb +24 -23
  47. data/lib/mixin_bot/api.rb +248 -3
  48. data/lib/mixin_bot/bot_auth.rb +71 -0
  49. data/lib/mixin_bot/cli/api.rb +224 -143
  50. data/lib/mixin_bot/cli/base.rb +77 -0
  51. data/lib/mixin_bot/cli/call.rb +71 -0
  52. data/lib/mixin_bot/cli/errors.rb +56 -0
  53. data/lib/mixin_bot/cli/node.rb +9 -2
  54. data/lib/mixin_bot/cli/output.rb +196 -0
  55. data/lib/mixin_bot/cli/schema.rb +274 -0
  56. data/lib/mixin_bot/cli/schema_command.rb +21 -0
  57. data/lib/mixin_bot/cli/utils.rb +114 -18
  58. data/lib/mixin_bot/cli.rb +124 -48
  59. data/lib/mixin_bot/client/error_mapper.rb +40 -0
  60. data/lib/mixin_bot/client.rb +94 -64
  61. data/lib/mixin_bot/computer.rb +132 -0
  62. data/lib/mixin_bot/configuration.rb +108 -1
  63. data/lib/mixin_bot/errors.rb +102 -0
  64. data/lib/mixin_bot/models/address.rb +11 -0
  65. data/lib/mixin_bot/models/api_envelope.rb +67 -0
  66. data/lib/mixin_bot/models/asset.rb +11 -0
  67. data/lib/mixin_bot/models/ghost_keys.rb +14 -0
  68. data/lib/mixin_bot/models/output.rb +11 -0
  69. data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
  70. data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
  71. data/lib/mixin_bot/models/user.rb +11 -0
  72. data/lib/mixin_bot/models.rb +10 -0
  73. data/lib/mixin_bot/monitor.rb +77 -0
  74. data/lib/mixin_bot/transaction/buffer.rb +34 -0
  75. data/lib/mixin_bot/transaction/decoder.rb +227 -0
  76. data/lib/mixin_bot/transaction/encoder.rb +255 -0
  77. data/lib/mixin_bot/transaction.rb +6 -475
  78. data/lib/mixin_bot/url_scheme.rb +63 -0
  79. data/lib/mixin_bot/utils/address.rb +17 -80
  80. data/lib/mixin_bot/utils/crypto.rb +173 -1
  81. data/lib/mixin_bot/utils/decoder.rb +1 -1
  82. data/lib/mixin_bot/utils/encoder.rb +13 -0
  83. data/lib/mixin_bot/utils.rb +45 -0
  84. data/lib/mixin_bot/uuid.rb +78 -1
  85. data/lib/mixin_bot/version.rb +11 -1
  86. data/lib/mixin_bot.rb +172 -18
  87. data/lib/mvm/bridge.rb +46 -0
  88. data/lib/mvm/client.rb +60 -0
  89. data/lib/mvm/nft.rb +4 -2
  90. data/lib/mvm/registry.rb +2 -1
  91. data/lib/mvm.rb +93 -0
  92. data/lib/tasks/api_coverage.rake +20 -0
  93. data/llms.txt +29 -0
  94. 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
- # 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
- # }
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.sum { |o| o['amount'].to_d } >= amount
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Turn
6
+ def turn_servers(access_token: nil)
7
+ client.get '/turn', access_token:
8
+ end
9
+ alias get_turn_server turn_servers
10
+ end
11
+ end
12
+ end
@@ -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
- private_keypair = JOSE::JWA::Ed25519.keypair private_key
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
- user = create_user name, key: private_keypair[1][...32]
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: user['data']['user_id'],
51
- session_id: user['data']['session_id'],
52
- session_private_key: private_key,
53
- server_public_key: user['data']['pin_token_base64'],
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.update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: user['data']['tip_counter'])
111
+ user_api = MixinBot::API.new(**keystore)
59
112
 
60
- # wait for tip pin update in server
61
- sleep 1
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
- @__retry__ = 0
64
- begin
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
- sleep 1 + @__retry__
71
- retry
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
- def safe_register(pin, spend_key: nil)
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
- spend_key ||= MixinBot.utils.decode_key pin
81
- key = JOSE::JWA::Ed25519.keypair spend_key[...32]
82
- public_key = key[0].unpack1('H*')
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
- hex = SHA3::Digest::SHA256.hexdigest config.app_id
85
- signature = Base64.urlsafe_encode64 JOSE::JWA::Ed25519.sign([hex].pack('H*'), key[1]), padding: false
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 pin, 'SEQUENCER:REGISTER:', config.app_id, public_key
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
- spend_key = spend_keypair[1].unpack1('H*')
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(spend_keypair[0], counter: profile['tip_counter'])
107
- update_pin(pin: new_pin, old_pin: 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
- pin = new_pin
202
+ with_safe_register_retries do
203
+ safe_register spend_key_hex
110
204
  end
111
205
 
112
- # wait for tip pin update in server
113
- sleep 1
206
+ { spend_key: spend_key_hex }.with_indifferent_access
207
+ end
208
+
209
+ private
114
210
 
115
- @__retry__ = 0
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
- safe_register pin, spend_key
118
- rescue MixinBot::Error => e
119
- @__retry__ += 1
120
- raise e if @__retry__ > 3
224
+ yield
225
+ rescue *RETRIABLE_SAFE_REGISTER_ERRORS
226
+ attempt += 1
227
+ raise if attempt > SAFE_REGISTER_MAX_RETRIES
121
228
 
122
- sleep 1 + @__retry__
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
- 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
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
- 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
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