Learn how to integrate Solana wallet features in your Farcaster Mini App with conditional support, message signing, and transaction handling
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>
);
}
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>
)}
</>
);
}
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>
)}
</>
);
}
useHasSolanaProvider()
before rendering Solana UITextEncoder
and btoa
for browser-compatible message signing@solana/wallet-adapter-react
not @farcaster/mini-app-solana
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
}
Was this page helpful?