Solana Integration Guide for Farcaster Mini Apps

Guide for using Solana wallet features in your Farcaster Mini App template.

How It Works

Conditional Solana Support

Not all Farcaster clients support Solana wallets, so your app should gracefully handle both scenarios.

import { useHasSolanaProvider } from "~/components/providers/SafeFarcasterSolanaProvider";
import { useConnection as useSolanaConnection, useWallet as useSolanaWallet } from '@solana/wallet-adapter-react';

function MyComponent() {
  const hasSolanaProvider = useHasSolanaProvider();
  
  // Only declare Solana hooks when provider is available
  let solanaWallet, solanaPublicKey, solanaSignMessage, solanaAddress;
  if (hasSolanaProvider) {
    solanaWallet = useSolanaWallet();
    ({ publicKey: solanaPublicKey, signMessage: solanaSignMessage } = solanaWallet);
    solanaAddress = solanaPublicKey?.toBase58();
  }

  return (
    <div>
      {/* EVM features always available */}
      <EvmFeatures />
      
      {/* Solana features when supported, not all clients support Solana */}
      {solanaAddress && (
        <div>
          <h2>Solana</h2>
          <div>Address: {solanaAddress}</div>
          <SignSolanaMessage signMessage={solanaSignMessage} />
          <SendSolana />
        </div>
      )}
    </div>
  );
}

Sign Message

Solana message signing requires converting text to bytes and handling the response properly for browser compatibility.

function SignSolanaMessage({ signMessage }: { signMessage?: (message: Uint8Array) => Promise<Uint8Array> }) {
  const [signature, setSignature] = useState<string | undefined>();
  const [signError, setSignError] = useState<Error | undefined>();
  const [signPending, setSignPending] = useState(false);

  const handleSignMessage = useCallback(async () => {
    setSignPending(true);
    try {
      if (!signMessage) {
        throw new Error('no Solana signMessage');
      }
      const input = new TextEncoder().encode("Hello from Solana!");
      const signatureBytes = await signMessage(input);
      const signature = btoa(String.fromCharCode(...signatureBytes));
      setSignature(signature);
      setSignError(undefined);
    } catch (e) {
      if (e instanceof Error) {
        setSignError(e);
      }
    } finally {
      setSignPending(false);
    }
  }, [signMessage]);

  return (
    <>
      <Button
        onClick={handleSignMessage}
        disabled={signPending}
        isLoading={signPending}
        className="mb-4"
      >
        Sign Message
      </Button>
      {signError && renderError(signError)}
      {signature && (
        <div className="mt-2 text-xs">
          <div>Signature: {signature}</div>
        </div>
      )}
    </>
  );
}

Send Transaction

Solana transactions require proper setup including blockhash, simulation, and error handling.

import { Transaction, SystemProgram, PublicKey } from '@solana/web3.js';

function SendSolana() {
  const [state, setState] = useState<
    | { status: 'none' }
    | { status: 'pending' }
    | { status: 'error'; error: Error }
    | { status: 'success'; signature: string }
  >({ status: 'none' });

  const { connection: solanaConnection } = useSolanaConnection();
  const { sendTransaction, publicKey } = useSolanaWallet();

  const handleSend = useCallback(async () => {
    setState({ status: 'pending' });
    try {
      if (!publicKey) {
        throw new Error('no Solana publicKey');
      }

      const { blockhash } = await solanaConnection.getLatestBlockhash();
      if (!blockhash) {
        throw new Error('failed to fetch latest Solana blockhash');
      }

      const transaction = new Transaction();
      transaction.add(
        SystemProgram.transfer({
          fromPubkey: publicKey,
          toPubkey: new PublicKey('DESTINATION_ADDRESS_HERE'),
          lamports: 0n, // 0 SOL for demo
        }),
      );
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = publicKey;

      // Simulate first
      const simulation = await solanaConnection.simulateTransaction(transaction);
      if (simulation.value.err) {
        const logs = simulation.value.logs?.join('\n') ?? 'No logs';
        const errDetail = JSON.stringify(simulation.value.err);
        throw new Error(`Simulation failed: ${errDetail}\nLogs:\n${logs}`);
      }
      
      const signature = await sendTransaction(transaction, solanaConnection);
      setState({ status: 'success', signature });
    } catch (e) {
      if (e instanceof Error) {
        setState({ status: 'error', error: e });
      }
    }
  }, [sendTransaction, publicKey, solanaConnection]);

  return (
    <>
      <Button
        onClick={handleSend}
        disabled={state.status === 'pending'}
        isLoading={state.status === 'pending'}
        className="mb-4"
      >
        Send Transaction (sol)
      </Button>
      {state.status === 'error' && renderError(state.error)}
      {state.status === 'success' && (
        <div className="mt-2 text-xs">
          <div>Signature: {state.signature.slice(0, 20)}...</div>
        </div>
      )}
    </>
  );
}

Key Points

  • Always check useHasSolanaProvider() before rendering Solana UI
  • Use TextEncoder and btoa for browser-compatible message signing
  • Simulate transactions before sending to catch errors early
  • Import Solana hooks from @solana/wallet-adapter-react not @farcaster/mini-app-solana
  • Replace placeholder addresses with real addresses for your app

Custom Program Interactions

For calling your own Solana programs, you’ll need to serialize instruction data and handle program-derived addresses.

import { 
  TransactionInstruction, 
  SYSVAR_RENT_PUBKEY,
  SystemProgram 
} from '@solana/web3.js';
import * as borsh from 'borsh';

class InstructionData {
  instruction: number;
  amount: number;
  
  constructor(props: { instruction: number; amount: number }) {
    this.instruction = props.instruction;
    this.amount = props.amount;
  }
}

const instructionSchema = new Map([
  [InstructionData, { 
    kind: 'struct', 
    fields: [
      ['instruction', 'u8'],
      ['amount', 'u64']
    ] 
  }]
]);

async function callCustomProgram(programId: string, instruction: number, amount: number) {
  if (!publicKey) throw new Error('Wallet not connected');
  
  // Serialize instruction data
  const instructionData = new InstructionData({ instruction, amount });
  const serializedData = borsh.serialize(instructionSchema, instructionData);
  
  // Create program-derived address (if needed)
  const [programDataAccount] = await PublicKey.findProgramAddress(
    [Buffer.from('your-seed'), publicKey.toBuffer()],
    new PublicKey(programId)
  );
  
  const transaction = new Transaction();
  transaction.add(
    new TransactionInstruction({
      keys: [
        { pubkey: publicKey, isSigner: true, isWritable: false },
        { pubkey: programDataAccount, isSigner: false, isWritable: true },
        { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
        { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
      ],
      programId: new PublicKey(programId),
      data: Buffer.from(serializedData),
    })
  );
  
  // ... rest of transaction setup and sending
}

For advanced contract interactions, token transfers, and error handling patterns, see the template’s Demo.tsx component.