Documentation Index
Fetch the complete documentation index at: https://docs.neynar.com/llms.txt
Use this file to discover all available pages before exploring further.
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.