Solana Wallet API Developer Guide — Connecting, Signing, and Sending Transactions

Working with window.solana and wallet adapters: detecting Phantom and Solflare, handling the connect() differences, and signing without surprises.

BoltTx Team··8 min read
solanawalletwallet-adapterphantomsolflaredeveloper

If you're building a Solana frontend that lets users connect wallets and submit transactions, you're going to spend more time on wallet integration than the docs suggest. The Solana wallet ecosystem is reasonable but not perfect; the SDKs handle most of the work but the rough edges become production issues if you don't address them.

This piece covers what you actually need to know to integrate Solana wallets correctly: the SDK choices, the signing patterns, transaction submission, and the production concerns that distinguish working integrations from production-grade ones.

What "Solana Wallet API" Actually Means

The term covers a few different things:

We'll cover the first two here. RPC details for wallets are mostly covered in Solana RPC for Developers.

The SDK Landscape

For most frontend integrations, the choice is @solana/wallet-adapter. It handles:

For React frontends specifically, the related packages provide hooks:

import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import {
  PhantomWalletAdapter,
  SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";

const wallets = [
  new PhantomWalletAdapter(),
  new SolflareWalletAdapter(),
];

function App() {
  return (
    <ConnectionProvider endpoint={RPC_URL}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          <YourApp />
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
}

For non-React apps, use the lower-level @solana/wallet-adapter-base directly.

Basic Connection Flow

Standard pattern in a React component:

import { useWallet, useConnection } from "@solana/wallet-adapter-react";
import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";

function MyComponent() {
  const { connection } = useConnection();
  const { publicKey, signTransaction, sendTransaction } = useWallet();

  if (!publicKey) {
    return <WalletMultiButton />;
  }

  return <YourConnectedUI publicKey={publicKey} />;
}

Key things from the wallet adapter:

Sending Transactions Through Wallets

Two patterns:

Pattern 1: wallet adapter handles submission.

const tx = new Transaction().add(yourInstruction);
const signature = await sendTransaction(tx, connection, {
  skipPreflight: true,
  maxRetries: 0,
});

The wallet adapter signs and submits via the connection you provide. Simple; works fine for most cases.

Pattern 2: sign separately, submit yourself.

const tx = new Transaction().add(yourInstruction);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = publicKey;

const signed = await signTransaction(tx);

const signature = await connection.sendRawTransaction(signed.serialize(), {
  skipPreflight: true,
  maxRetries: 0,
});

This pattern gives you more control over submission. Useful when you want to use a different RPC for sends than the one passed to the wallet provider, or when you need to inspect/log the signed transaction before submission.

Why You Often Want Two Different RPCs

A pattern many production apps use: one RPC for reads (passed to ConnectionProvider), another for writes (used directly for sendRawTransaction).

Reasoning:

Using one RPC for both is a compromise. Using the right one for each gives better user experience (snappier UI from the read RPC) and better economics (lower sandwich tax from the write RPC).

const READ_RPC = "https://your-read-rpc.example/?api-key=...";
const WRITE_RPC = "https://bolttx.io/?api-key=...";

const readConnection = new Connection(READ_RPC, "confirmed");
const writeConnection = new Connection(WRITE_RPC, "processed");

// Pass readConnection to your provider
<ConnectionProvider endpoint={READ_RPC}>...</ConnectionProvider>

// Use writeConnection directly for sends
const signature = await writeConnection.sendRawTransaction(signed.serialize(), {
  skipPreflight: true,
  maxRetries: 0,
});

Common Wallet Integration Mistakes

Hardcoding wallet adapters. Lock the user into one wallet. Always include multiple options.

Ignoring connection failures. wallet-adapter will throw on rejection or wallet not installed. Wrap in try/catch and show meaningful errors.

Not handling network mismatch. User connected on devnet but your app expects mainnet. Detect and prompt to switch.

Polling the wallet for state. The hooks already give you reactive state. Don't poll wallet.publicKey in intervals.

Submitting transactions without confirmation. The wallet returns a signature but the transaction may not have landed. Always confirm.

Forgetting blockhash freshness. Same issue as anywhere: stale blockhashes silently fail. Refresh before signing.

Not handling the user-cancellation case. Some wallets return a specific error code; handle it gracefully (don't show "transaction failed" if the user cancelled).

Mobile Wallet Integration

Mobile is its own thing. Mobile wallets use deep links for transaction signing — your dApp opens the wallet via a deep link, the wallet signs, then redirects back. The flow is more complex than desktop:

Most teams use Mobile Wallet Adapter (MWA) which abstracts this. If you're building a mobile-first dApp, plan time for this — it's not the desktop flow.

Common Patterns by App Type

DEX UIs / swap interfaces. Quote on read RPC, sign via wallet adapter, submit through Anti-MEV write RPC. Show fill prices vs expected fills as user feedback.

NFT marketplaces. Heavy read traffic for listings; writes for purchases/listings. Both need to be reliable; reads dominate by volume.

Lending / borrowing dApps. Mixed read/write. Reads must be accurate (don't show stale collateral health). Writes must confirm reliably.

Token launchpads / IDOs. Burst write traffic during launches. The write RPC's behaviour during congestion is critical — exactly when you need it most is when most providers degrade.

Wallet apps themselves. Heavy read for balance/history, occasional write for transactions. Read-RPC quality dominates user perception.

What to Do This Week

If you're building a wallet-integrated dApp:

  1. Set up wallet-adapter with multiple wallet options. Include at least Phantom, Solflare, Backpack.
  2. Use separate read and write RPCs. Read for the provider, write for sendRawTransaction.
  3. Sign separately, submit yourself. Gives you control over the submission path.
  4. Confirm transactions after submission. Don't trust that signature returned means landed.
  5. Handle the cancellation case. Don't show errors for user-rejected transactions.
  6. Test on multiple wallets. Phantom and Solflare have subtle behavior differences.
  7. Add per-signature telemetry on the write side. Track what users actually experience.

Try BoltTx for Wallet App Submission

BoltTx is purpose-built for the write side of wallet-integrated dApps:

Drop-in usage:

import { Connection } from "@solana/web3.js";

const writeConnection = new Connection(
  "https://bolttx.io/?api-key=YOUR_API_KEY",
  "processed"
);

const signed = await wallet.signTransaction(tx);
const signature = await writeConnection.sendRawTransaction(signed.serialize(), {
  skipPreflight: true,
  maxRetries: 0,
});

Free tier signup. Pair with your read RPC of choice. Test against your real user transaction patterns for a week.

FAQ

Should I use wallet-adapter or roll my own? Use wallet-adapter. Rolling your own wallet detection is a tar pit you don't want.

Which wallets should I support? At minimum: Phantom, Solflare. Adding Backpack and Glow covers most users. Mobile wallet adapter for mobile.

How do I handle the user being on devnet when my app needs mainnet? Detect via the connection's genesis hash and prompt them to switch in their wallet. Don't try to switch programmatically.

What's the right way to sign messages for auth? signMessage from the wallet adapter. Generate a server-side nonce, have the user sign it, verify the signature on your backend.

Do all wallets support signAllTransactions? Most major ones do; some smaller wallets don't. Test on your target wallets.

Why do my transactions sometimes show as "successful" but the user didn't get the result? Signature returned doesn't mean transaction landed. Always confirm with the connection. Use confirmTransaction after sending.

Phantom vs Solflare — which should I prioritise supporting? Both. Phantom vs Solflare is the question users ask when choosing a wallet, but for an integrator, you support both via wallet-adapter and the user picks. Phantom has the larger user base; Solflare has stronger desktop / hardware features. For a web3 wallet development solution — i.e., you're the dApp side integrating wallets — wallet-adapter abstracts the difference so you don't really code against either one specifically.

Further Reading

Back to all posts