Module: MixinBot::API::User

Included in:
MixinBot::API
Defined in:
lib/mixin_bot/api/user.rb

Overview

User-related API endpoints: bot/network user lookup, plain network-user creation, and the full Safe Network registration flow.

The Safe-network registration mirrors the official Go SDK reference implementation in RegisterSafeWithSetupPin / RegisterSafeBareUser: github.com/MixinNetwork/bot-api-go-client/blob/master/safe_user.go

Constant Summary collapse

SAFE_REGISTER_MAX_RETRIES =

Maximum number of times to retry safe_register when the freshly-set TIP PIN has not yet propagated through the Mixin server.

3
SAFE_REGISTER_RETRY_BASE_DELAY =

Base seconds to wait between safe_register retries. The wait grows linearly with the attempt number.

1
TIP_PIN_PROPAGATION_DELAY =

Seconds to wait after update_pin before calling safe_register, so the new TIP PIN has time to propagate on the server side.

1

Instance Method Summary collapse

Instance Method Details

#create_safe_user(name, private_key: nil, spend_key: nil) ⇒ Hash

Creates a Safe-network user end-to-end.

Mirrors RegisterSafeWithSetupPin in the Go SDK:

  1. generate (or accept) a session keypair and a spend keypair

  2. create the network user via #create_user

  3. set the user’s PIN to the TIP public key derived from the spend key

  4. wait briefly for propagation, then register on the Safe network (retrying transient failures)

The returned keystore is suitable for instantiating a new MixinBot::API that authenticates as the freshly-registered user.

Parameters:

  • name (String)

    display name for the new user

  • private_key (String, nil) (defaults to: nil)

    optional 32-byte session Ed25519 seed

  • spend_key (String, nil) (defaults to: nil)

    optional 32-byte spend Ed25519 seed

Returns:

  • (Hash)

    keystore with :app_id, :session_id, :session_private_key, :server_public_key and :spend_key

Raises:



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/mixin_bot/api/user.rb', line 94

def create_safe_user(name, private_key: nil, spend_key: nil)
  session_keypair = JOSE::JWA::Ed25519.keypair private_key
  spend_keypair = JOSE::JWA::Ed25519.keypair spend_key

  spend_key_hex = spend_keypair[1].unpack1('H*')

  user = create_user name, key: session_keypair[1][...32]
  data = user.fetch('data')

  keystore = {
    app_id: data['user_id'],
    session_id: data['session_id'],
    session_private_key: session_keypair[1].unpack1('H*'),
    server_public_key: data['pin_token_base64'],
    spend_key: spend_key_hex
  }

  user_api = MixinBot::API.new(**keystore)

  tip_pin = MixinBot.utils.tip_public_key spend_keypair[0], counter: data['tip_counter']
  user_api.update_pin pin: tip_pin

  # Allow the freshly-set TIP PIN to propagate before registering.
  sleep TIP_PIN_PROPAGATION_DELAY

  with_safe_register_retries do
    user_api.safe_register spend_key_hex
  end

  keystore
end

#create_user(full_name, key: nil) ⇒ Hash

Creates a Mixin network user.

When key is omitted a fresh Ed25519 keypair is generated. The response is merged with the hex-encoded session private key under :private_key.

Parameters:

  • full_name (String)

    display name for the new user

  • key (String, nil) (defaults to: nil)

    optional 32-byte Ed25519 seed

Returns:

  • (Hash)

    Mixin response merged with the hex-encoded private key



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/mixin_bot/api/user.rb', line 42

def create_user(full_name, key: nil)
  keypair = JOSE::JWA::Ed25519.keypair key
  session_secret = Base64.urlsafe_encode64 keypair[0], padding: false
  private_key = keypair[1].unpack1('H*')

  path = '/users'
  payload = {
    full_name:,
    session_secret:
  }

  res = client.post path, **payload
  res.merge(private_key:).with_indifferent_access
end

#fetch_users(user_ids) ⇒ Object



63
64
65
66
67
68
69
# File 'lib/mixin_bot/api/user.rb', line 63

def fetch_users(user_ids)
  path = '/users/fetch'
  user_ids = [user_ids] if user_ids.is_a? String
  payload = user_ids

  client.post path, *payload
end

#migrate_to_safe(spend_key:, pin: nil) ⇒ TrueClass, Hash

Migrates an existing legacy user to the Safe network.

When the user has not yet upgraded to a TIP PIN, pin must be the user’s current 6-digit PIN so Pin#update_pin can rotate it to a TIP PIN derived from spend_key. When the user already has a TIP PIN, pin may be omitted.

Parameters:

  • spend_key (String)

    the user’s spend Ed25519 seed or full key

  • pin (String, nil) (defaults to: nil)

    the user’s current PIN (only required when the user has not yet upgraded to a TIP PIN)

Returns:

  • (TrueClass, Hash)

    true if the user already has Safe enabled, otherwise { spend_key: <hex> }

Raises:



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/mixin_bot/api/user.rb', line 187

def migrate_to_safe(spend_key:, pin: nil)
  profile = me['data']
  return true if profile['has_safe']

  spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
  spend_key_hex = spend_keypair[1].unpack1('H*')

  if profile['tip_key_base64'].blank?
    new_pin = MixinBot.utils.tip_public_key spend_keypair[0], counter: profile['tip_counter']
    update_pin pin: new_pin, old_pin: pin
  end

  # Allow the freshly-set TIP PIN to propagate before registering.
  sleep TIP_PIN_PROPAGATION_DELAY

  with_safe_register_retries do
    safe_register spend_key_hex
  end

  { spend_key: spend_key_hex }.with_indifferent_access
end

#safe_register(spend_key) ⇒ Hash

Registers an existing user on the Safe network.

spend_key may be supplied as raw bytes, a hex string, or a Base64-encoded string. It must encode the user’s full Ed25519 spend private key (or a 32-byte seed).

Parameters:

  • spend_key (String)

    the user’s spend Ed25519 private key

Returns:

  • (Hash)

    Mixin response

Raises:

  • (ArgumentError)

    when spend_key cannot be decoded into at least 32 bytes



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/mixin_bot/api/user.rb', line 138

def safe_register(spend_key)
  path = '/safe/users'

  spend_key_bytes = MixinBot.utils.decode_key spend_key
  raise ArgumentError, 'invalid spend_key' if spend_key_bytes.nil? || spend_key_bytes.size < 32

  keypair = JOSE::JWA::Ed25519.keypair spend_key_bytes[...32]
  public_key = keypair[0].unpack1('H*')
  # Normalize to a 64-byte signing key in hex so that callers may pass a
  # 32-byte seed without crashing the downstream signer.
  signing_key_hex = keypair[1].unpack1('H*')

  # NOTE: the Go SDK's +crypto.Sha256Hash+ is misleadingly named — it
  # actually computes SHA3-256, so +SHA3::Digest::SHA256+ is the correct
  # match. See bot-api-go-client safe_user.go +RegisterSafeBareUser+.
  app_id_hash = SHA3::Digest::SHA256.hexdigest config.app_id
  signature = Base64.urlsafe_encode64(
    JOSE::JWA::Ed25519.sign([app_id_hash].pack('H*'), keypair[1]),
    padding: false
  )

  pin_base64 = encrypt_tip_pin signing_key_hex, 'SEQUENCER:REGISTER:', config.app_id, public_key

  payload = {
    public_key:,
    signature:,
    pin_base64:
  }

  client.post path, **payload
end

#search_user(query, access_token: nil) ⇒ Object



57
58
59
60
61
# File 'lib/mixin_bot/api/user.rb', line 57

def search_user(query, access_token: nil)
  path = format('/search/%<query>s', query:)

  client.get path, access_token:
end

#user(user_id, access_token: nil) ⇒ Object



26
27
28
29
# File 'lib/mixin_bot/api/user.rb', line 26

def user(user_id, access_token: nil)
  path = format('/users/%<user_id>s', user_id:)
  client.get path, access_token:
end