Solana Wallet Authentication
Let users sign in with Phantom, Solflare, or Backpack using an Ed25519 challenge-sign flow.
How it works
Solana wallet auth uses the same challenge-sign pattern as EVM, but with Ed25519:
- Kevo generates a nonce for the Solana address.
- The SDK signs the challenge with the wallet's private key via
signMessage(). - Kevo verifies the 64-byte Ed25519 signature and issues a session.
Pre-built Modal
If the project has solana in enabledChains, the modal shows Solana wallet options automatically:
Phantom
window.phantom?.solanaSolflare
window.solflareBackpack
window.backpack?.solanaℹ
When both
evm and solana chains are enabled, the modal shows all wallet types with EVM / SOL chain badges.Programmatic Sign-in
typescript
import { useKevo } from '@kevo-ws/sdk/react'
function ConnectPhantom() {
const { client } = useKevo()
const connect = async () => {
const phantom = window.phantom?.solana
if (!phantom) {
alert('Please install Phantom')
return
}
await client.loginWithSolanaWallet({ provider: phantom })
}
return <button onClick={connect}>Connect Phantom</button>
}Provider Quirks
Solflare, connect() returns void
Unlike Phantom, connect() on Solflare returns void. The SDK reads the public key from provider.publicKey after connecting.
Phantom, requires 'utf8' hint
Phantom's signMessage() requires a second 'utf8' argument or it throws an internal -32603 "Unexpected error". The SDK branches onprovider.isPhantom and passes the hint only when needed.
API Flow
1. Get nonce
http
GET /v1/auth/sol-wallet/nonce
?address=8vCy...XYZ
&projectId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Response:
{
"message": "Sign in to Kevo\n\nWallet: 8vCy...XYZ\nNonce: abc123\nIssued At: ...",
"nonce": "abc123"
}2. Verify signature
http
POST /v1/auth/sol-wallet/verify
Content-Type: application/json
{
"address": "8vCy...XYZ",
"message": "Sign in to Kevo\n\n...",
"signature": "128-char hex string (64-byte Ed25519 sig)",
"projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# Response:
{
"accessToken": "eyJ...",
"expiresIn": 900,
"address": "8vCy...XYZ"
}
# Plus: Set-Cookie: kevo_refresh_token=...SolanaProvider Interface
typescript
interface SolanaProvider {
isPhantom?: boolean
isSolflare?: boolean
isBackpack?: boolean
publicKey?: { toBase58(): string }
connect(opts?: { onlyIfTrusted?: boolean }): Promise<void | { publicKey: { toBase58(): string } }>
disconnect(): Promise<void>
signMessage(message: Uint8Array): Promise<{ signature: Uint8Array } | Uint8Array>
}