Mobile
移动钱包适配器

移动钱包适配器

此内容正在翻译中,完成后将会在此处提供。

Error Handling

Error Handling

Mobile wallet connections fail in ways desktop apps never see. The wallet app might not be installed. The user might switch away before signing. The session might timeout mid-transaction. Bluetooth on some Saga devices can interrupt the connection.

Robust error handling separates production dApps from demos. This lesson gives you patterns for every failure mode in MWA.

Don't catch errors and show generic messages. MWA errors tell you exactly what happened: use that information.

MWA Error Types

The MWA SDK throws specific error types. Each requires different handling:

typescript
import {
  SolanaMobileWalletAdapterError,
  SolanaMobileWalletAdapterProtocolError,
} from '@solana-mobile/mobile-wallet-adapter-protocol';

SolanaMobileWalletAdapterError: Protocol-level errors (session, transport, association failures)

SolanaMobileWalletAdapterProtocolError: Wallet-level errors (authorization denied, signing rejected)

Protocol Error Codes

MWA defines specific error codes. The SDK wraps protocol errors in JSON-RPC format. Here's the complete reference:

Note: The raw MWA protocol spec defines constants like ERROR_AUTHORIZATION_FAILED = -1, but the SDK presents these as JSON-RPC errors in the -32xxx range. The table below shows the codes as they appear in JavaScript/TypeScript error objects.

CodeConstantMeaningUser Action
-32700ERROR_JSON_RPC_PARSEInvalid JSON in requestBug in your code
-32600ERROR_JSON_RPC_INVALID_REQUESTMalformed RPC requestBug in your code
-32601ERROR_JSON_RPC_METHOD_NOT_FOUNDUnknown method calledCheck method name
-32602ERROR_JSON_RPC_INVALID_PARAMSInvalid parametersCheck param types
-32603ERROR_JSON_RPC_INTERNALWallet internal errorTry again
-32000ERROR_AUTHORIZATION_FAILEDAuth denied by userUser cancelled. Respect it
-32001ERROR_INVALID_PAYLOADSBad transaction formatCheck transaction building
-32002ERROR_NOT_SIGNEDUser declined to signUser cancelled. Respect it
-32003ERROR_NOT_SUBMITTEDWallet couldn't submitCheck network, retry
-32004ERROR_NOT_CLONEDClone failed (deprecated)Rare, retry
-32005ERROR_TOO_MANY_PAYLOADSToo many transactionsBatch in smaller groups
-32010ERROR_ATTEST_ORIGIN_ANDROIDOrigin attestation failedAndroid config issue

Structured Error Handler

Build a centralized error handler:

typescript
// 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: 'Wallet connection cancelled',
  ERROR_NOT_SIGNED: 'Transaction signing declined',
  ERROR_NOT_SUBMITTED: 'Could not submit transaction. Check your connection.',
  ERROR_TOO_MANY_PAYLOADS: 'Too many transactions. Try sending fewer.',
  ERROR_INVALID_PAYLOADS: 'Invalid transaction format',
  ERROR_ATTEST_ORIGIN_ANDROID: 'App verification failed. Please reinstall.',
};

export function handleMWAError(error: unknown): MWAErrorResult {
  // Not an MWA error
  if (!(error instanceof Error)) {
    return {
      userMessage: 'An unexpected error occurred',
      shouldRetry: true,
      isUserCancellation: false,
      originalError: new Error(String(error)),
    };
  }

  // Protocol-level error (wallet response)
  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,
    };
  }

  // SDK-level error (association, session)
  if (error instanceof SolanaMobileWalletAdapterError) {
    // Common: no wallet installed
    if (error.message.includes('Found no installed wallet')) {
      return {
        userMessage: 'No Solana wallet found. Please install a wallet app.',
        shouldRetry: false,
        isUserCancellation: false,
        originalError: error,
      };
    }

    // Session timeout
    if (error.message.includes('timeout') || error.message.includes('Timeout')) {
      return {
        userMessage: 'Wallet connection timed out. Please try again.',
        shouldRetry: true,
        isUserCancellation: false,
        originalError: error,
      };
    }

    // Generic SDK error
    return {
      userMessage: 'Could not connect to wallet. Please try again.',
      shouldRetry: true,
      isUserCancellation: false,
      originalError: error,
    };
  }

  // Standard Error (network issues, etc.)
  if (error.message.includes('Network') || error.message.includes('fetch')) {
    return {
      userMessage: 'Network error. Check your connection.',
      shouldRetry: true,
      isUserCancellation: false,
      originalError: error,
    };
  }

  return {
    userMessage: error.message || 'An error occurred',
    shouldRetry: true,
    isUserCancellation: false,
    originalError: error,
  };
}

Using the Error Handler

Wrap your transact calls:

typescript
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',
      });
      // Handle success
    });
  } catch (error) {
    const { userMessage, isUserCancellation, shouldRetry } = handleMWAError(error);
    
    if (isUserCancellation) {
      // User intentionally cancelled—don't show error
      console.log('User cancelled');
      return;
    }
    
    if (shouldRetry) {
      Alert.alert('Error', userMessage, [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Retry', onPress: () => connectWallet() },
      ]);
    } else {
      Alert.alert('Error', userMessage);
    }
  }
}

Retry Patterns

Implement exponential backoff for transient failures:

typescript
// 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;
      }

      // Exponential backoff with jitter
      const delay = Math.min(
        baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 1000,
        maxDelayMs
      );

      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

Use with MWA:

typescript
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 }
  );
}

Session Lifecycle Errors

The MWA session can end unexpectedly:

User switches away: The session stays alive briefly, but the wallet might close it.

Wallet backgrounded too long: Android may kill the wallet process.

Auth token expired: Tokens don't last forever.

Handle session errors by re-authorizing:

typescript
async function robustTransact<T>(
  callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
  return await transact(async (wallet) => {
    try {
      return await callback(wallet);
    } catch (error) {
      // If it's an auth error, the token might be stale
      if (
        error instanceof SolanaMobileWalletAdapterProtocolError &&
        error.code === 'ERROR_AUTHORIZATION_FAILED'
      ) {
        // Clear cached token
        await AsyncStorage.removeItem('mwa_auth_token');
        
        // Re-authorize without cached token
        await wallet.authorize({
          identity: APP_IDENTITY,
          chain: 'solana:devnet',
        });
        
        // Retry the operation
        return await callback(wallet);
      }
      
      throw error;
    }
  });
}

No Wallet Installed

Handle the case where no wallet is installed:

typescript
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}>No Wallet Found</Text>
      <Text style={styles.subtitle}>
        Install a Solana wallet to continue
      </Text>
      <Button 
        title="Get Phantom" 
        onPress={() => openStore('phantom')} 
      />
      <Button 
        title="Get Solflare" 
        onPress={() => openStore('solflare')} 
      />
    </View>
  );
}

Integrate with your connect flow:

typescript
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('No Solana wallet')) {
        setShowNoWallet(true);
        return;
      }
      
      if (!result.isUserCancellation) {
        Alert.alert('Error', result.userMessage);
      }
    }
  };

  if (showNoWallet) {
    return <NoWalletPrompt />;
  }

  return <Button title="Connect Wallet" onPress={handleConnect} />;
}

Transaction Simulation Errors

Before sending to a wallet, simulate transactions to catch errors early:

typescript
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 : 'Simulation failed',
    };
  }
}

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 `Instruction ${index} failed with custom error ${reason.Custom}`;
    }
    return `Instruction ${index} failed: ${JSON.stringify(reason)}`;
  }
  
  return JSON.stringify(err);
}

Use it before wallet signing:

typescript
async function sendWithSimulation(
  transaction: VersionedTransaction,
  connection: Connection
): Promise<string> {
  // Simulate first
  const simulation = await simulateTransaction(connection, transaction);
  
  if (!simulation.success) {
    throw new Error(`Transaction would fail: ${simulation.error}`);
  }
  
  // Only open wallet if simulation passed
  return await transact(async (wallet) => {
    await authorizeSession(wallet);
    const [signature] = await wallet.signAndSendTransactions({
      transactions: [transaction],
    });
    return signature;
  });
}

User Feedback Component

Create a component for transaction status:

typescript
// 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: 'Simulating transaction...',
  signing: 'Approve in your wallet...',
  confirming: 'Confirming on chain...',
  success: 'Transaction confirmed!',
  error: 'Transaction failed',
};

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 for Debug

In development, log all MWA interactions:

typescript
// 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);
}

Wrap your transact calls:

typescript
async function trackedTransact<T>(
  callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
  const startTime = Date.now();
  
  try {
    const result = await transact(callback);
    console.log(`[MWA] Session completed in ${Date.now() - startTime}ms`);
    return result;
  } catch (error) {
    console.error(`[MWA] Session failed after ${Date.now() - startTime}ms`, error);
    throw error;
  }
}

Good error handling makes your dApp feel professional. In the next lesson, we'll cover testing your implementation on actual devices.

Blueshift © 2026Commit: 1b8118f