Skip to main content
Mobile Wallet Adapter (MWA) is Solana’s standard protocol for mobile dApps to communicate with wallet apps. Synto Mobile implements MWA to provide secure, seamless transaction signing without requiring private keys to leave your wallet app.

How it works

MWA establishes a secure session between Synto Mobile and your wallet app:
1

Session request

Synto Mobile initiates a transact session when wallet interaction is needed.
2

Wallet selection

Your device prompts you to choose a compatible wallet app (Phantom, Solflare, etc.).
3

Authorization

The wallet app requests permission to share your public key and sign transactions.
4

Secure communication

All subsequent requests go through the encrypted MWA session.
MWA sessions are ephemeral and require reauthorization after expiration. Auth tokens are cached locally to reduce repeated prompts.

Core implementation

The useMobileWallet hook provides the primary MWA interface:
import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js";
import { Transaction, VersionedTransaction } from "@solana/web3.js";

export function useMobileWallet() {
  const { authorizeSessionWithSignIn, authorizeSession, deauthorizeSessions } = 
    useAuthorization();
  
  const connect = useCallback(async (): Promise<Account> => {
    try {
      return await transact(async (wallet) => {
        return await authorizeSession(wallet);
      });
    } catch (error) {
      await handleWalletError(error);
      throw error;
    }
  }, [authorizeSession, handleWalletError]);
  
  const signAndSendTransaction = useCallback(
    async (
      transaction: Transaction | VersionedTransaction,
      minContextSlot?: number
    ): Promise<TransactionSignature> => {
      try {
        return await transact(async (wallet) => {
          await authorizeSession(wallet);
          const signatures = await wallet.signAndSendTransactions({
            transactions: [transaction],
            minContextSlot,
          });
          return signatures[0];
        });
      } catch (error) {
        await handleWalletError(error);
        throw error;
      }
    },
    [authorizeSession, handleWalletError]
  );
  
  return {
    connect,
    disconnect,
    signAndSendTransaction,
    signMessage,
    signTransactions,
  };
}

MWA operations

MWA supports several key operations:

Connect

Establish initial authorization and retrieve public key

Sign and send

Sign transactions and submit to blockchain in one step

Sign only

Sign transactions without sending (for multi-sig)

Sign message

Sign arbitrary messages for authentication

Connecting wallets

The connect operation establishes the initial session:
const connect = useCallback(async (): Promise<Account> => {
  return await transact(async (wallet) => {
    return await authorizeSession(wallet);
  });
}, [authorizeSession]);
The authorizeSession function sends the authorization request:
const authorizeSession = useCallback(
  async (wallet: AuthorizeAPI) => {
    const authorizationResult = await wallet.authorize({
      identity: {
        name: 'Synto Mobile',
        uri: 'https://synto.io'
      },
      chain: selectedCluster.id, // solana:mainnet, solana:devnet, etc.
      auth_token: cachedAuthToken, // Reuse if available
    });
    return handleAuthorizationResult(authorizationResult).selectedAccount;
  },
  [selectedCluster, cachedAuthToken]
);
Authorization result contains:
interface AuthorizationResult {
  accounts: AuthorizedAccount[];  // All authorized accounts
  auth_token: string;             // Session token for reauthorization
  wallet_uri_base: string;        // Wallet app identifier
}

interface AuthorizedAccount {
  address: Base64EncodedAddress;  // Base64-encoded public key
  label?: string;                 // Account name/label
  icon?: WalletIcon;              // Wallet icon URI
}
Auth tokens are persisted to AsyncStorage and reused for subsequent authorizations within the same session.

Signing and sending transactions

The most common operation is signing and immediately sending:
const signAndSendTransaction = useCallback(
  async (
    transaction: Transaction | VersionedTransaction,
    minContextSlot?: number
  ): Promise<TransactionSignature> => {
    return await transact(async (wallet) => {
      await authorizeSession(wallet); // Reauthorize with cached token
      
      const signatures = await wallet.signAndSendTransactions({
        transactions: [transaction],
        minContextSlot,
      });
      
      return signatures[0];
    });
  },
  [authorizeSession]
);
Parameters:
  • transactions: Array of unsigned transactions (usually just one)
  • minContextSlot: Minimum slot the transaction must be evaluated at
Returns: Array of base58-encoded transaction signatures in the same order as input transactions.
import { Transaction, SystemProgram } from '@solana/web3.js';

const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: sender,
    toPubkey: recipient,
    lamports: amount,
  })
);

// Note: Synto throws error for legacy transactions
// Use VersionedTransaction instead
Synto Mobile currently only supports VersionedTransaction. Legacy Transaction objects will throw an error.

Signing without sending

For multi-signature or deferred execution:
const signTransactions = useCallback(
  async (transactions: VersionedTransaction[]): Promise<VersionedTransaction[]> => {
    return await transact(async (wallet) => {
      await authorizeSession(wallet);
      return await wallet.signTransactions({ transactions });
    });
  },
  [authorizeSession]
);
Use cases:
  • Multi-signature transactions requiring multiple signers
  • Transactions sent later or through custom RPC
  • Batch signing multiple transactions
Returns: Array of signed transactions with signatures populated, but not submitted to blockchain.

Signing messages

For authentication or off-chain verification:
const signMessage = useCallback(
  async (message: Uint8Array): Promise<Uint8Array> => {
    return await transact(async (wallet) => {
      const authResult = await authorizeSession(wallet);
      const signedMessages = await wallet.signMessages({
        addresses: [authResult.address],
        payloads: [message],
      });
      return signedMessages[0];
    });
  },
  [authorizeSession]
);
Common use cases:
  • Sign-in with Solana (SIWS) authentication
  • Proving wallet ownership
  • Off-chain message verification
  • Challenge-response protocols
Signed messages can be verified using nacl.sign.detached.verify() with the public key.

AI agent integration

The useSolanaAgent hook wraps MWA functions for AI tool execution:
export function useSolanaAgent() {
  const { signAndSendTransaction, signTransactions, signMessage } = useMobileWallet();
  const { account } = useWalletUi();
  
  const agent = useMemo(() => {
    if (!account?.publicKey) return null;
    
    const fns = {
      signTransaction: async <T extends Transaction | VersionedTransaction>(tx: T) => {
        if (tx instanceof Transaction) {
          throw new Error(
            "Legacy Transaction signing not supported. Use VersionedTransaction."
          );
        }
        const signed = await signTransactions([tx as VersionedTransaction]);
        return signed[0] as T;
      },
      signMessage: async (msg: Uint8Array) => {
        return await signMessage(msg);
      },
      sendTransaction: async <T extends Transaction | VersionedTransaction>(tx: T) => {
        return await signAndSendTransaction(tx);
      },
      signAllTransactions: async <T extends Transaction | VersionedTransaction>(txs: T[]) => {
        const signed = await signTransactions(txs as VersionedTransaction[]);
        return signed as T[];
      },
      signAndSendTransaction: async <T extends Transaction | VersionedTransaction>(tx: T) => {
        const signature = await signAndSendTransaction(tx);
        return { signature };
      },
      publicKey: account.publicKey,
    };
    
    return agentBuilder(fns, { signOnly: false });
  }, [account, signAndSendTransaction, signTransactions, signMessage]);
  
  const tools = useMemo(() => {
    if (agent) {
      return createVercelAITools(agent, agent.actions);
    }
    return null;
  }, [agent]);
  
  return { agent, tools, isReady: !!agent };
}
This integration allows AI tools to:
  1. Build transactions for blockchain operations
  2. Request user approval via MWA
  3. Submit signed transactions
  4. Return signatures and results
1

AI determines intent

User asks “Swap 1 SOL to USDC”. AI parses parameters and selects the TRADE tool.
2

Tool builds transaction

The trade tool constructs a Jupiter swap transaction with proper instructions.
3

Request MWA signature

Agent calls signAndSendTransaction which triggers MWA flow.
4

User approves in wallet

Wallet app shows transaction details. User reviews and approves.
5

Transaction submitted

Signed transaction is sent to blockchain. Signature returned to AI.
6

Result displayed

AI responds with success message and transaction signature.

Authorization management

MWA sessions are managed through the useAuthorization hook:

Persistent storage

Auth tokens and account info are cached:
const AUTHORIZATION_STORAGE_KEY = "authorization-cache";

interface WalletAuthorization {
  accounts: Account[];
  authToken: AuthToken;
  selectedAccount: Account;
}

const usePersistAuthorization = () => {
  return useMutation({
    mutationFn: async (auth: WalletAuthorization | null): Promise<void> => {
      await AsyncStorage.setItem(
        AUTHORIZATION_STORAGE_KEY, 
        JSON.stringify(auth)
      );
    },
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['wallet-authorization'] }),
  });
};

const useFetchAuthorization = () => {
  return useQuery({
    queryKey: ['wallet-authorization'],
    queryFn: async (): Promise<WalletAuthorization | null> => {
      const cached = await AsyncStorage.getItem(AUTHORIZATION_STORAGE_KEY);
      return cached ? JSON.parse(cached, cacheReviver) : null;
    },
  });
};
Cache reviver reconstructs PublicKey objects:
function cacheReviver(key: string, value: any) {
  if (key === "publicKey") {
    return new PublicKey(value);
  }
  return value;
}
React Query manages authorization state reactivity. When authorization changes, all components using useAuthorization automatically update.

Reauthorization

Subsequent operations reuse cached tokens:
const authorizeSession = useCallback(
  async (wallet: AuthorizeAPI) => {
    const authorizationResult = await wallet.authorize({
      identity,
      chain: selectedCluster.id,
      auth_token: cachedAuthToken, // Reuse if valid
    });
    return handleAuthorizationResult(authorizationResult).selectedAccount;
  },
  [cachedAuthToken, selectedCluster]
);
If the cached token is valid, the wallet app won’t prompt the user again.

Deauthorization

Disconnecting clears the authorization:
const deauthorizeSession = useCallback(
  async (wallet: DeauthorizeAPI) => {
    if (cachedAuthToken == null) return;
    
    await wallet.deauthorize({ auth_token: cachedAuthToken });
    await persistMutation.mutateAsync(null);
  },
  [cachedAuthToken, persistMutation]
);

const deauthorizeSessions = useCallback(async () => {
  await invalidateAuthorizations();
  await persistMutation.mutateAsync(null);
}, [invalidateAuthorizations, persistMutation]);
Two deauthorization methods:
  1. deauthorizeSession: Calls wallet app to formally close session
  2. deauthorizeSessions: Just clears local cache (used for errors)

Error handling

MWA operations can fail for various reasons:

Authorization errors

const handleWalletError = useCallback(async (error: any) => {
  console.error('Wallet error:', error);
  
  const isAuthError = 
    error?.message?.includes('authorization') ||
    error?.message?.includes('not authorized') ||
    error?.message?.includes('wallet not connected') ||
    error?.code === 'UNAUTHORIZED';
  
  if (isAuthError) {
    console.log('Auto-disconnect due to authorization error');
    await deauthorizeSessions();
  }
  
  throw error;
}, [deauthorizeSessions]);
Common auth errors:
  • User denies authorization in wallet app
  • Auth token expires
  • Wallet app closed/crashed
  • Network switch invalidates session
Authorization errors trigger automatic disconnection. Users must reconnect to continue using wallet features.

Transaction errors

Transaction failures are caught and logged:
try {
  const signature = await signAndSendTransaction(transaction, minContextSlot);
  await connection.confirmTransaction(
    { signature, ...latestBlockhash },
    'confirmed'
  );
  return signature;
} catch (error: unknown) {
  console.log('Transaction failed', error);
  throw error;
}
Common transaction errors:
  • Insufficient balance for fees
  • Invalid instruction parameters
  • Transaction expired (stale blockhash)
  • User rejected in wallet app
  • Network congestion/timeout

User rejection

When users decline in their wallet app:
// MWA throws specific error
if (error?.message?.includes('User declined')) {
  console.log('User rejected the transaction');
  // Show user-friendly message
}
Always handle user rejection gracefully. Don’t automatically retry - let users reinitiate if desired.

Security considerations

No private keys

Private keys never leave the wallet app. Synto only receives signed transactions.

User approval

Every transaction requires explicit user approval in the wallet app.

Session isolation

Each MWA session is isolated. Closing Synto ends the session.

Token expiration

Auth tokens expire and require reauthorization periodically.

App identity

Synto Mobile identifies itself in MWA requests:
const identity: AppIdentity = {
  name: 'Synto Mobile',
  uri: 'https://synto.io'
};
This identity is displayed in the wallet app during authorization, helping users verify they’re connecting to the legitimate Synto app.

Chain specification

The chain parameter ensures transactions go to the correct network:
chain: selectedCluster.id // "solana:mainnet" or "solana:devnet"
Wallet apps can reject transactions if the chain doesn’t match their current network.
Always verify the chain parameter matches your intended network. Mainnet transactions use real SOL and are irreversible.

Transaction flow diagram

Here’s the complete flow from user action to blockchain confirmation:
1

User initiates action

User taps “Send” or executes an AI tool requiring blockchain interaction.
2

Build transaction

App constructs unsigned transaction with:
  • Recent blockhash
  • Minimum context slot
  • Instructions (transfer, swap, etc.)
  • Fee payer (user’s public key)
3

Start MWA session

Call transact() to begin MWA session. System prompts for wallet app selection.
4

Reauthorize session

Send authorization request with cached token. Wallet validates token.
5

Request signature

Send unsigned transaction to wallet via signAndSendTransactions().
6

User reviews

Wallet displays transaction details:
  • Recipient address
  • Amount and token
  • Network fees
  • Program interactions
7

User approves/rejects

User taps approve or reject button in wallet app.
8

Sign transaction

If approved, wallet signs transaction with private key and submits to RPC.
9

Return signature

Wallet returns transaction signature to Synto Mobile.
10

Confirm transaction

Synto polls RPC endpoint until transaction reaches ‘confirmed’ commitment.
11

Update UI

Invalidate queries to refresh balances. Show success message with signature.

Wallet compatibility

MWA is supported by major Solana mobile wallets:
WalletMWA SupportSign-in SupportNotes
Phantom✅ Yes✅ YesFull support
Solflare✅ Yes✅ YesFull support
Glow✅ Yes✅ YesFull support
Ultimate✅ Yes⚠️ PartialLimited features
Backpack✅ Yes✅ YesFull support
For best experience, recommend users install Phantom or Solflare. These wallets have the most complete MWA implementations.

Best practices

1

Cache auth tokens

Always persist and reuse auth tokens to minimize user prompts. Expiration is handled automatically.
2

Use VersionedTransaction

Legacy transactions have limitations. Always use VersionedTransaction for compatibility.
3

Set minContextSlot

Include minContextSlot in transaction requests to prevent stale data issues.
4

Handle errors gracefully

Catch all MWA errors and show user-friendly messages. Auto-disconnect on auth failures.
5

Confirm before invalidating

Wait for transaction confirmation before invalidating cached balances. Prevents showing incorrect data.
6

Test on devnet

Always test MWA integration on devnet before deploying to mainnet.

Troubleshooting

Wallet not opening

Symptoms: Nothing happens when tapping connect/approve Solutions:
  • Ensure compatible wallet app is installed
  • Check wallet app is updated to latest version
  • Try restarting wallet app
  • Clear AsyncStorage and retry authorization

Session expired errors

Symptoms: “Authorization expired” or “Invalid auth token” Solutions:
  • Clear cached authorization: await AsyncStorage.removeItem(AUTHORIZATION_STORAGE_KEY)
  • Reconnect wallet
  • Check network connectivity

Transaction failures

Symptoms: Transactions rejected or timing out Solutions:
  • Verify sufficient balance for fees
  • Check blockhash isn’t stale (fetch fresh)
  • Ensure network parameter matches wallet
  • Try reducing minContextSlot requirement

Multiple accounts

Symptoms: Wrong account being used Solutions:
  • MWA returns all authorized accounts
  • Select correct account from accounts array
  • Update selectedAccount in authorization state
For detailed MWA debugging, enable console logs and inspect the wallet object returned by transact().

Next steps

AI chat interface

Learn how AI tools use MWA for blockchain operations

Wallet operations

Explore wallet features built on MWA

Build docs developers (and LLMs) love