Konten ini sedang diterjemahkan dan akan tersedia di sini ketika siap.
Connecting to Wallets

The transact() function looks deceptively simple. It takes a callback, opens a wallet, runs your code, and closes the session. One function, one callback, done.
But understanding what happens inside that callback, and what guarantees it provides, is the key to building professional mobile dApps that don't frustrate users with cryptic errors or stuck states.
In this lesson, you'll build the wallet connection logic. By the end, you'll have a working ConnectButton component that authorizes with any MWA-compatible wallet.
The transact Pattern
Import transact from the web3.js wrapper package (not the base protocol package):
import {
transact,
Web3MobileWallet
} from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';The basic pattern:
const result = await transact(async (wallet: Web3MobileWallet) => {
// wallet is your interface to the wallet app
// You can call authorize, signTransactions, etc.
// Whatever you return becomes the result of transact()
return someValue;
});
// result === someValueWhen transact() is called:
Your app generates association credentials
A URI intent launches the wallet app
The wallet comes to the foreground
Your callback executes with the
walletparameterWhen your callback returns (or throws), the session closes
Control returns to your app
The wallet app is visible during step 4. Users see your app's identity and can approve or reject requests.
Authorization Flow
Before you can sign anything, you must authorize. This tells the wallet "I'm this app, and I want access to the user's accounts."
import { transact, Web3MobileWallet } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { PublicKey } from '@solana/web3.js';
import { toByteArray } from 'react-native-quick-base64';
const APP_IDENTITY = {
name: 'My Solana dApp',
uri: 'https://mydapp.com',
icon: 'favicon.ico',
};
async function connectWallet(): Promise<PublicKey> {
return await transact(async (wallet: Web3MobileWallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// authResult.accounts contains the authorized accounts
// addresses are base64-encoded
const firstAccount = authResult.accounts[0];
const publicKey = new PublicKey(toByteArray(firstAccount.address));
return publicKey;
});
}The authorize response contains:
type AuthorizationResult = {
accounts: Account[];
auth_token: string;
wallet_uri_base?: string;
sign_in_result?: SolanaSignInOutput;
};
type Account = {
address: string; // base64-encoded public key
display_address?: string; // base58-encoded (human-readable)
label?: string; // User's name for this account
chains: string[]; // Supported chains
features: string[]; // Supported features
};Most wallets currently return a single account, but the protocol supports multiple. Always handle accounts as an array.
Base64 vs Base58
A quirk of MWA: account addresses come as base64 strings, not the base58 format you see in Solana explorers.
// What you receive
const base64Address = authResult.accounts[0].address;
// e.g., "JBgT8LS5+..."
// What you need for @solana/web3.js
const publicKey = new PublicKey(toByteArray(base64Address));
// Now you have a real PublicKey object
// Human-readable version (optional, for display)
const display = authResult.accounts[0].display_address;
// e.g., "7F9k..." (base58)Caching Auth Tokens
Nobody wants to approve wallet connections repeatedly. MWA provides auth tokens for silent re-authorization.
On first connect, you get an auth_token:
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Save this token
await AsyncStorage.setItem('mwa_auth_token', authResult.auth_token);On subsequent connects, include the token:
async function connectWithCachedToken(): Promise<PublicKey | null> {
const cachedToken = await AsyncStorage.getItem('mwa_auth_token');
return await transact(async (wallet) => {
try {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
auth_token: cachedToken ?? undefined,
});
// Save the potentially updated token
await AsyncStorage.setItem('mwa_auth_token', authResult.auth_token);
return new PublicKey(toByteArray(authResult.accounts[0].address));
} catch (error: any) {
if (error.code === -32000 && cachedToken) {
// Token expired or invalid, clear it and try fresh
await AsyncStorage.removeItem('mwa_auth_token');
// Request fresh authorization
const freshResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
await AsyncStorage.setItem('mwa_auth_token', freshResult.auth_token);
return new PublicKey(toByteArray(freshResult.accounts[0].address));
}
throw error;
}
});
}When a valid token is provided, the wallet may skip the user approval dialog entirely; the session authorizes silently.
Important: Auth tokens are opaque strings. Their format varies by wallet. Never parse or modify them; just store and pass them.
Querying Capabilities
Wallets differ in what they support. Before assuming a feature works, query the wallet:
await transact(async (wallet) => {
const capabilities = await wallet.getCapabilities();
console.log('Max transactions per request:', capabilities.max_transactions_per_request);
console.log('Max messages per request:', capabilities.max_messages_per_request);
console.log('Supported transaction versions:', capabilities.supported_transaction_versions);
console.log('Optional features:', capabilities.features);
});The response tells you:
max_transactions_per_request: How many transactions can be signed at once
max_messages_per_request: How many messages can be signed at once
supported_transaction_versions:
['legacy', 0]or similarfeatures: Optional features like
solana:signInWithSolana,solana:cloneAuthorization
This is a non-privileged method; you don't need to authorize first.
Deauthorizing
To "disconnect" a wallet, invalidating any cached auth tokens, use deauthorize:
async function disconnect(): Promise<void> {
const authToken = await AsyncStorage.getItem('mwa_auth_token');
if (!authToken) return;
await transact(async (wallet) => {
await wallet.deauthorize({ auth_token: authToken });
});
await AsyncStorage.removeItem('mwa_auth_token');
}After deauthorization, the token is invalidated wallet-side. Even if you try to use it later, it won't work.
Note: This opens a session just to deauthorize. Some apps skip this and just clear local storage. The token becomes useless anyway when it expires. But explicit deauthorization is cleaner and faster for users who switch wallets frequently.
Handling Multiple Accounts
Some wallets support multiple accounts. The authorize response may contain several:
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Could be multiple accounts
authResult.accounts.forEach((account, index) => {
console.log(`Account ${index}:`, account.display_address);
console.log(` Label: ${account.label ?? 'No label'}`);
console.log(` Chains: ${account.chains.join(', ')}`);
});If you need a specific account, let the user choose:
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// After authorization
if (authResult.accounts.length > 1) {
// Show picker UI
showAccountPicker(authResult.accounts, (account) => {
setSelectedAccount(account);
});
} else {
setSelectedAccount(authResult.accounts[0]);
}When signing, use the selected account's address.
Building a Connect Button
Here's a complete, production-ready connect button component:
import React, { useState, useCallback } from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { transact, Web3MobileWallet } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { PublicKey } from '@solana/web3.js';
import { toByteArray } from 'react-native-quick-base64';
import AsyncStorage from '@react-native-async-storage/async-storage';
const APP_IDENTITY = {
name: 'My Solana dApp',
uri: 'https://mydapp.com',
icon: 'favicon.ico',
};
interface ConnectButtonProps {
onConnect: (publicKey: PublicKey, authToken: string) => void;
onError: (error: Error) => void;
}
export function ConnectButton({ onConnect, onError }: ConnectButtonProps) {
const [connecting, setConnecting] = useState(false);
const handleConnect = useCallback(async () => {
if (connecting) return;
setConnecting(true);
try {
const cachedToken = await AsyncStorage.getItem('mwa_auth_token');
await transact(async (wallet: Web3MobileWallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
auth_token: cachedToken ?? undefined,
});
await AsyncStorage.setItem('mwa_auth_token', authResult.auth_token);
const publicKey = new PublicKey(
toByteArray(authResult.accounts[0].address)
);
onConnect(publicKey, authResult.auth_token);
});
} catch (error: any) {
if (error.code === 4001) {
// User cancelled - not an error to report
console.log('User cancelled connection');
} else {
onError(error);
}
} finally {
setConnecting(false);
}
}, [connecting, onConnect, onError]);
return (
<TouchableOpacity
style={[styles.button, connecting && styles.buttonDisabled]}
onPress={handleConnect}
disabled={connecting}
>
{connecting ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Connect Wallet</Text>
)}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#512da8',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});Key details:
Loading state: Prevents double-taps while connecting
Cached token: Attempts silent re-auth first
Error handling: Distinguishes user cancellation from real errors
Callback pattern: Lets parent components handle the connected state
Session Timing
The MWA protocol specifies timeouts:
Association timeout: 30 seconds for the wallet app to launch and start its WebSocket server
Request timeout: 10 seconds for individual RPC requests within a session
If the wallet doesn't respond in time, you get a timeout error. This usually means:
The wallet app isn't installed
The wallet crashed during startup
The device is under heavy load
Handle timeouts gracefully:
try {
await transact(async (wallet) => {
await wallet.authorize({ identity: APP_IDENTITY, chain: 'solana:devnet' });
});
} catch (error: any) {
if (error.message?.includes('timeout')) {
Alert.alert(
'Connection Timeout',
'The wallet took too long to respond. Make sure you have a Solana wallet installed.',
[{ text: 'OK' }]
);
}
}What's Next
You can now connect to wallets, cache auth tokens for frictionless re-connection, and handle the common edge cases. But connecting is just the beginning.
In the next lesson, we'll build and sign actual transactions: transferring SOL, working with versioned transactions, and handling the wallet's response.