Mobile Wallet Adapter: how Solana dapps talk to wallets on a phone
Mobile Wallet Adapter is the protocol Solana mobile dapps use to talk to wallets. Intent URIs, session establishment, auth tokens, signing flow.
On desktop Solana, the wallet is a browser extension and the dapp talks to it via window.solana. On mobile, there's no window.solana — the wallet is a separate app, and the dapp has to spin up an out-of-process protocol to ask it for signatures. That protocol is Mobile Wallet Adapter (MWA).
MWA isn't "wallet-adapter, but mobile." It's a different protocol with a different trust model. Here's the on-the-wire view.
The session handshake
A dapp running on a phone (in a browser, in a React Native app, in a native iOS/Android app) invokes the wallet via an intent URI:
solana-wallet://v1/associate/local?association=<base64url-public-key>
&port=<dapp-listening-port>The OS resolves this URI to whichever installed app registered for the solana-wallet:// scheme (Phantom Mobile, Solflare Mobile, Backpack Mobile, etc). The wallet opens, sees the association public key + dapp port, and opens a WebSocket back to the dapp on that port. The handshake completes via ECDH key agreement using the association key.
Everything that flows after this point is encrypted end-to-end between the dapp and the wallet using keys derived from the ECDH-shared secret. The OS sees opaque bytes; only the two ends see the JSON-RPC traffic.
Authorize: capabilities, not signatures
After the WebSocket is up, the dapp's first call is authorize:
{
"jsonrpc": "2.0",
"id": 1,
"method": "authorize",
"params": {
"identity": {
"uri": "https://my-dapp.example.com",
"icon": "favicon.ico",
"name": "My Dapp"
},
"chain": "solana:mainnet",
"features": ["solana:signAndSendTransaction"],
"addresses": []
}
}The wallet shows a permission prompt: "My Dapp wants to connect — approve?". The user picks an account and approves. Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"auth_token": "<opaque-token>",
"accounts": [
{ "address": "<base64-pubkey>", "label": "My Wallet 1" }
],
"wallet_uri_base": "https://wallet.example/",
"sign_in_result": null
}
}The auth_token is the key thing. It's an opaque capability — proof that this dapp has been granted access to this account. The dapp stores it, includes it on every subsequent request, and can use it for the lifetime of the wallet's session policy (typically until the user deauthorises).
signAndSendTransactions: the actual signing
{
"jsonrpc": "2.0",
"id": 2,
"method": "sign_and_send_transactions",
"params": {
"auth_token": "<from-authorize>",
"payloads": [
"<base64-encoded-transaction>"
],
"options": {
"min_context_slot": 234567,
"commitment": "confirmed",
"skip_preflight": false,
"max_retries": 3
}
}
}The wallet:
- Validates the auth_token still grants access to the signing account in the tx
- Shows the user a transaction preview (decoded instructions, fee impact)
- On approval, signs with the user's key, submits via its own RPC, returns the signature
- On rejection, returns a structured error
Response includes the signature(s) — your dapp gets a signed, submitted tx without ever holding the user's private key.
Other methods
authorize / reauthorize — establish or refresh capability
deauthorize — revoke this dapp's auth_token
sign_transactions — sign without submitting
sign_messages — sign an arbitrary off-chain message
sign_in_with_solana (SIWS) — auth + sign-in attestation in one shot
get_capabilities — query what features the wallet supports
clone_authorization — transfer a session (rare; remote MWA)Remote MWA
The flow above is "local MWA" — dapp and wallet on the same phone. Remote MWA uses the same protocol between a desktop browser and a mobile wallet: the user scans a QR code containing the association URI; the mobile wallet opens a WebSocket back to a relay server; the desktop session pipes through the relay.
Same ECDH handshake, same auth_token model, same method set. The only difference is the transport: local WebSocket vs relayed WebSocket.
Using it via the JS SDK
Most dapps don't implement the protocol directly — @solana-mobile/mobile-wallet-adapter-protocol-web3js wraps it with a web3.js-shaped API:
import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js"
const signature = await transact(async (wallet) => {
// 1. authorize (or reauthorize if you cached the auth_token)
const auth = await wallet.authorize({
cluster: "mainnet-beta",
identity: { name: "My Dapp", uri: "https://my-dapp.example.com" },
})
// 2. build the transaction normally with web3.js
const tx = /* your Transaction */
// 3. sign and send via the wallet
const sigs = await wallet.signAndSendTransactions({ transactions: [tx] })
return sigs[0]
})The transact wrapper opens the association URI, waits for the wallet to connect back, runs your callback against the connected wallet, then tears down the session when the callback returns. Single round-trip from the dapp's perspective.
Why this matters for Solana mobile
- No injected globals. The dapp can't accidentally leak the user's wallet to other dapps via shared window state. Each session is its own end-to-end encrypted channel.
- The wallet stays the trust boundary. The user's private key never leaves the wallet process. Even a fully compromised dapp can't exfiltrate it.
- SIWS gives mobile a real session auth model. Sign In With Solana via MWA produces a verifiable attestation your backend can use for session tokens — no need to redirect to a web flow.
- Cross-wallet by design. Any wallet that implements the MWA spec works. Your dapp never branches on wallet identity.
References
- solana-mobile/mobile-wallet-adapter — reference implementation + spec
- Solana Mobile docs — MWA overview
- @solana-mobile/mobile-wallet-adapter-protocol-web3js
MWA is the part of Solana Mobile that's actually shipped to every wallet in the ecosystem. If you're building a Solana mobile dapp, this is the only protocol that matters.