
Tratamento de Erros
Conexões com carteiras móveis falham de maneiras que aplicativos desktop nunca veem. O app da carteira pode não estar instalado. O usuário pode sair antes de assinar. A sessão pode expirar no meio de uma transação. O Bluetooth em alguns dispositivos Saga pode interromper a conexão.
Um tratamento de erros robusto é o que separa dApps de produção de demos. Esta lição fornece padrões para cada modo de falha no MWA.
Tipos de Erro do MWA
O SDK do MWA lança tipos de erro específicos. Cada um requer um tratamento diferente:
import {
SolanaMobileWalletAdapterError,
SolanaMobileWalletAdapterProtocolError,
} from '@solana-mobile/mobile-wallet-adapter-protocol';SolanaMobileWalletAdapterError: Erros de nível de protocolo (sessão, transporte, falhas de associação)
SolanaMobileWalletAdapterProtocolError: Erros de nível da carteira (autorização negada, assinatura rejeitada)
Códigos de Erro do Protocolo
O MWA define códigos de erro específicos. O SDK encapsula erros de protocolo no formato JSON-RPC. Aqui está a referência completa:
Tratador de Erros Estruturado
Construa um tratador de erros centralizado:
// src/utils/mwaErrorHandler.ts
import {
SolanaMobileWalletAdapterError,
SolanaMobileWalletAdapterProtocolError,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
export interface MWAErrorResult {
userMessage: string;
shouldRetry: boolean;
isUserCancellation: boolean;
originalError: Error;
}
const ERROR_MESSAGES: Record<string, string> = {
ERROR_AUTHORIZATION_FAILED: 'Conexão com a carteira cancelada',
ERROR_NOT_SIGNED: 'Assinatura da transação recusada',
ERROR_NOT_SUBMITTED: 'Não foi possível enviar a transação. Verifique sua conexão.',
ERROR_TOO_MANY_PAYLOADS: 'Muitas transações. Tente enviar menos.',
ERROR_INVALID_PAYLOADS: 'Formato de transação inválido',
ERROR_ATTEST_ORIGIN_ANDROID: 'Falha na verificação do app. Por favor, reinstale.',
};
export function handleMWAError(error: unknown): MWAErrorResult {
// Não é um erro do MWA
if (!(error instanceof Error)) {
return {
userMessage: 'Ocorreu um erro inesperado',
shouldRetry: true,
isUserCancellation: false,
originalError: new Error(String(error)),
};
}
// Erro de nível de protocolo (resposta da carteira)
if (error instanceof SolanaMobileWalletAdapterProtocolError) {
const message = ERROR_MESSAGES[error.code] ?? error.message;
const isUserCancellation =
error.code === 'ERROR_AUTHORIZATION_FAILED' ||
error.code === 'ERROR_NOT_SIGNED';
return {
userMessage: message,
shouldRetry: !isUserCancellation,
isUserCancellation,
originalError: error,
};
}
// Erro de nível do SDK (associação, sessão)
if (error instanceof SolanaMobileWalletAdapterError) {
// Comum: nenhuma carteira instalada
if (error.message.includes('Found no installed wallet')) {
return {
userMessage: 'Nenhuma carteira Solana encontrada. Por favor, instale um app de carteira.',
shouldRetry: false,
isUserCancellation: false,
originalError: error,
};
}
// Timeout de sessão
if (error.message.includes('timeout') || error.message.includes('Timeout')) {
return {
userMessage: 'A conexão com a carteira expirou. Por favor, tente novamente.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
// Erro genérico do SDK
return {
userMessage: 'Não foi possível conectar à carteira. Por favor, tente novamente.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
// Erro padrão (problemas de rede, etc.)
if (error.message.includes('Network') || error.message.includes('fetch')) {
return {
userMessage: 'Erro de rede. Verifique sua conexão.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
return {
userMessage: error.message || 'Ocorreu um erro',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}Usando o Tratador de Erros
Envolva suas chamadas transact:
import { Alert } from 'react-native';
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { handleMWAError } from '../utils/mwaErrorHandler';
async function connectWallet() {
try {
await transact(async (wallet) => {
const result = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Lidar com sucesso
});
} catch (error) {
const { userMessage, isUserCancellation, shouldRetry } = handleMWAError(error);
if (isUserCancellation) {
// Usuário cancelou intencionalmente — não mostrar erro
console.log('Usuário cancelou');
return;
}
if (shouldRetry) {
Alert.alert('Erro', userMessage, [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Tentar Novamente', onPress: () => connectWallet() },
]);
} else {
Alert.alert('Erro', userMessage);
}
}
}Padrões de Retry
Implemente backoff exponencial para falhas transitórias:
// src/utils/retry.ts
export interface RetryOptions {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_OPTIONS: RetryOptions = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
};
export async function withRetry<T>(
operation: () => Promise<T>,
shouldRetry: (error: unknown) => boolean,
options: Partial<RetryOptions> = {}
): Promise<T> {
const { maxAttempts, baseDelayMs, maxDelayMs } = {
...DEFAULT_OPTIONS,
...options,
};
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!shouldRetry(error) || attempt === maxAttempts) {
throw error;
}
// Backoff exponencial com jitter
const delay = Math.min(
baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 1000,
maxDelayMs
);
console.log(`Tentativa ${attempt} falhou, tentando novamente em ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}Use com o MWA:
import { handleMWAError } from '../utils/mwaErrorHandler';
import { withRetry } from '../utils/retry';
async function sendTransactionWithRetry(transaction: VersionedTransaction) {
return await withRetry(
async () => {
return await transact(async (wallet) => {
await authorizeSession(wallet);
const [signature] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signature;
});
},
(error) => {
const result = handleMWAError(error);
return result.shouldRetry && !result.isUserCancellation;
},
{ maxAttempts: 3 }
);
}Erros do Ciclo de Vida da Sessão
A sessão do MWA pode encerrar inesperadamente:
Usuário sai do app: A sessão permanece ativa brevemente, mas a carteira pode fechá-la.
Carteira em background por muito tempo: O Android pode encerrar o processo da carteira.
Token de autenticação expirado: Os tokens não duram para sempre.
Trate erros de sessão re-autorizando:
async function robustTransact<T>(
callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
return await transact(async (wallet) => {
try {
return await callback(wallet);
} catch (error) {
// Se for um erro de autenticação, o token pode estar desatualizado
if (
error instanceof SolanaMobileWalletAdapterProtocolError &&
error.code === 'ERROR_AUTHORIZATION_FAILED'
) {
// Limpar token em cache
await AsyncStorage.removeItem('mwa_auth_token');
// Re-autorizar sem o token em cache
await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Tentar a operação novamente
return await callback(wallet);
}
throw error;
}
});
}Nenhuma Carteira Instalada
Trate o caso em que nenhuma carteira está instalada:
import { Linking, Platform } from 'react-native';
const WALLET_STORE_URLS = {
phantom: {
ios: 'https://apps.apple.com/app/phantom-solana-wallet/id1598432977',
android: 'https://play.google.com/store/apps/details?id=app.phantom',
},
solflare: {
ios: 'https://apps.apple.com/app/solflare/id1580902717',
android: 'https://play.google.com/store/apps/details?id=com.solflare.mobile',
},
};
function NoWalletPrompt() {
const openStore = async (wallet: 'phantom' | 'solflare') => {
const url = WALLET_STORE_URLS[wallet][Platform.OS === 'ios' ? 'ios' : 'android'];
await Linking.openURL(url);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Nenhuma Carteira Encontrada</Text>
<Text style={styles.subtitle}>
Instale uma carteira Solana para continuar
</Text>
<Button
title="Obter Phantom"
onPress={() => openStore('phantom')}
/>
<Button
title="Obter Solflare"
onPress={() => openStore('solflare')}
/>
</View>
);
}Integre com seu fluxo de conexão:
function ConnectButton() {
const [showNoWallet, setShowNoWallet] = useState(false);
const handleConnect = async () => {
try {
await transact(async (wallet) => {
await wallet.authorize({ /* ... */ });
});
} catch (error) {
const result = handleMWAError(error);
if (result.userMessage.includes('Nenhuma carteira Solana')) {
setShowNoWallet(true);
return;
}
if (!result.isUserCancellation) {
Alert.alert('Erro', result.userMessage);
}
}
};
if (showNoWallet) {
return <NoWalletPrompt />;
}
return <Button title="Conectar Carteira" onPress={handleConnect} />;
}Erros de Simulação de Transação
Antes de enviar para uma carteira, simule transações para capturar erros cedo:
import {
Connection,
VersionedTransaction,
SendTransactionError,
} from '@solana/web3.js';
interface SimulationResult {
success: boolean;
error?: string;
logs?: string[];
}
async function simulateTransaction(
connection: Connection,
transaction: VersionedTransaction
): Promise<SimulationResult> {
try {
const result = await connection.simulateTransaction(transaction, {
commitment: 'confirmed',
});
if (result.value.err) {
return {
success: false,
error: formatSimulationError(result.value.err),
logs: result.value.logs ?? undefined,
};
}
return { success: true, logs: result.value.logs ?? undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'A simulação falhou',
};
}
}
function formatSimulationError(err: any): string {
if (typeof err === 'string') return err;
if (err.InstructionError) {
const [index, reason] = err.InstructionError;
if (typeof reason === 'object' && reason.Custom !== undefined) {
return `Instrução ${index} falhou com erro personalizado ${reason.Custom}`;
}
return `Instrução ${index} falhou: ${JSON.stringify(reason)}`;
}
return JSON.stringify(err);
}Use antes da assinatura na carteira:
async function sendWithSimulation(
transaction: VersionedTransaction,
connection: Connection
): Promise<string> {
// Simular primeiro
const simulation = await simulateTransaction(connection, transaction);
if (!simulation.success) {
throw new Error(`A transação falharia: ${simulation.error}`);
}
// Abrir a carteira apenas se a simulação passou
return await transact(async (wallet) => {
await authorizeSession(wallet);
const [signature] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signature;
});
}Componente de Feedback do Usuário
Crie um componente para o status da transação:
// components/TransactionStatus.tsx
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
type Status = 'idle' | 'simulating' | 'signing' | 'confirming' | 'success' | 'error';
interface Props {
status: Status;
signature?: string;
error?: string;
}
const STATUS_MESSAGES: Record<Status, string> = {
idle: '',
simulating: 'Simulando transação...',
signing: 'Aprove na sua carteira...',
confirming: 'Confirmando na blockchain...',
success: 'Transação confirmada!',
error: 'A transação falhou',
};
export function TransactionStatus({ status, signature, error }: Props) {
if (status === 'idle') return null;
return (
<View style={styles.container}>
{(status === 'simulating' || status === 'signing' || status === 'confirming') && (
<ActivityIndicator size="small" />
)}
<Text style={[styles.text, status === 'error' && styles.errorText]}>
{STATUS_MESSAGES[status]}
</Text>
{status === 'error' && error && (
<Text style={styles.errorDetail}>{error}</Text>
)}
{status === 'success' && signature && (
<Text style={styles.signature}>
{signature.slice(0, 20)}...
</Text>
)}
</View>
);
}Logging para Debug
Em desenvolvimento, registre todas as interações do MWA:
// src/utils/mwaLogger.ts
const DEBUG = __DEV__;
export function logMWARequest(method: string, params: unknown): void {
if (!DEBUG) return;
console.log(`[MWA] → ${method}`, JSON.stringify(params, null, 2));
}
export function logMWAResponse(method: string, result: unknown): void {
if (!DEBUG) return;
console.log(`[MWA] ← ${method}`, JSON.stringify(result, null, 2));
}
export function logMWAError(method: string, error: unknown): void {
console.error(`[MWA] ✕ ${method}`, error);
}Envolva suas chamadas transact:
async function trackedTransact<T>(
callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await transact(callback);
console.log(`[MWA] Sessão concluída em ${Date.now() - startTime}ms`);
return result;
} catch (error) {
console.error(`[MWA] Sessão falhou após ${Date.now() - startTime}ms`, error);
throw error;
}
}Um bom tratamento de erros faz seu dApp parecer profissional. Na próxima lição, abordaremos como testar sua implementação em dispositivos reais.