Skip to content

Latest commit

 

History

History

README.md

@solana/client

Framework-agnostic building blocks for Solana RPC, subscriptions, wallets, and transactions. Works in any runtime (React, Svelte, API routes, workers, etc.).

Status: Experimental – expect rapid iteration.

Install

npm install @solana/client

ConnectorKit (optional)

ConnectorKit integration is available as a stable, opt-in entrypoint: @solana/client/connectorkit.

This requires installing @solana/connector (an optional peer dependency of @solana/client):

npm install @solana/connector
import { connectorKit } from "@solana/client/connectorkit";

const walletConnectors = connectorKit({ defaultConfig: { /* ... */ } });

Quickstart

  1. Choose Wallet Standard connectors (auto-discovery is the fastest way to start).
  2. Create a Solana client.
  3. Call actions, watchers, and helpers anywhere in your app (React, APIs, workers, etc.).
import { autoDiscover, createClient } from "@solana/client";

const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  websocketEndpoint: "wss://api.devnet.solana.com",
  walletConnectors: autoDiscover(),
});

// Connect Wallet Standard apps via their connector ids.
// Recommended: use canonical ids like "wallet-standard:phantom" (aliases like "phantom" also work).
await client.actions.connectWallet("wallet-standard:phantom");

// Fetch an account once.
const wallet = client.store.getState().wallet;
if (wallet.status === "connected") {
  const account = await client.actions.fetchAccount(wallet.session.account.address);
  console.log(account.lamports?.toString());
}

Common Solana flows (copy/paste)

Connect, disconnect, and inspect wallet state

const connectors = client.connectors.all; // Wallet Standard-aware connectors

await client.actions.connectWallet(connectors[0].id);

const wallet = client.store.getState().wallet;
if (wallet.status === "connected") {
  console.log(wallet.session.account.address.toString());
}

await client.actions.disconnectWallet();

Fetch and watch lamports

import { toAddress } from "@solana/client";

const address = toAddress("Fg6PaFpoGXkYsidMpWFKfwtz6DhFVyG4dL1x8kj7ZJup");

const lamports = await client.actions.fetchBalance(address);
console.log(`Lamports: ${lamports.toString()}`);

const watcher = client.watchers.watchBalance({ address }, (nextLamports) => {
  console.log("Updated balance:", nextLamports.toString());
});

// Later…
watcher.abort();

Request an airdrop (devnet/testnet)

const signature = await client.actions.requestAirdrop(address, 1_000_000_000n); // 1 SOL
console.log(signature.toString());

Send SOL

const wallet = client.store.getState().wallet;
if (wallet.status !== "connected") throw new Error("Connect wallet first");

const signature = await client.solTransfer.sendTransfer({
  amount: 100_000_000n, // 0.1 SOL
  authority: wallet.session, // Wallet Standard session
  destination: "Ff34MXWdgNsEJ1kJFj9cXmrEe7y2P93b95mGu5CJjBQJ",
});
console.log(signature.toString());

SPL token balance + transfer

const wallet = client.store.getState().wallet;
if (wallet.status !== "connected") throw new Error("Connect wallet first");

const usdc = client.splToken({ mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }); // USDC

const balance = await usdc.fetchBalance(wallet.session.account.address);
console.log(`Balance: ${balance.uiAmount}`);

const signature = await usdc.sendTransfer({
  amount: 1n,
  authority: wallet.session,
  destinationOwner: "Ff34MXWdgNsEJ1kJFj9cXmrEe7y2P93b95mGu5CJjBQJ",
});
console.log(signature.toString());

Token 2022 support

Token 2022 mints are supported via the tokenProgram option:

// Auto-detect Token or Token 2022 (recommended)
const token = client.splToken({
  mint: "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD
  tokenProgram: "auto",
});

// Or explicitly specify Token 2022 program
import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana/client";

const token2022 = client.splToken({
  mint: mintAddress,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
});

// Balance and transfers work the same way
const balance = await token.fetchBalance(wallet.session.account.address);
const signature = await token.sendTransfer({
  amount: 10,
  authority: wallet.session,
  destinationOwner: recipientAddress,
});

Fetch address lookup tables

import { toAddress } from "@solana/client";

// Single lookup table
const lut = await client.actions.fetchLookupTable(
  toAddress("AddressLookupTab1e1111111111111111111111111"),
);
console.log(`Addresses in LUT: ${lut.addresses.length}`);

// Multiple lookup tables
const luts = await client.actions.fetchLookupTables([lutAddress1, lutAddress2]);

Fetch nonce accounts

const nonce = await client.actions.fetchNonceAccount(
  toAddress("NonceAccountAddress111111111111111111111111"),
);
console.log(`Nonce: ${nonce.blockhash}`);
console.log(`Authority: ${nonce.authority}`);

Build and send arbitrary transactions

import { getTransferSolInstruction } from "@solana-program/system";

const wallet = client.store.getState().wallet;
if (wallet.status !== "connected") throw new Error("Connect wallet first");

const prepared = await client.transaction.prepare({
  authority: wallet.session,
  instructions: [
    getTransferSolInstruction({
      destination: "Ff34MXWdgNsEJ1kJFj9cXmrEe7y2P93b95mGu5CJjBQJ",
      lamports: 10_000n,
      source: wallet.session.account.address,
    }),
  ],
  version: "auto", // defaults to 0 when lookups exist, otherwise 'legacy'
});

// Inspect or serialize first.
const wire = await client.transaction.toWire(prepared);

// Submit.
const signature = await client.transaction.send(prepared);
console.log(signature.toString());

Watch signature confirmations

const watcher = client.watchers.watchSignature(
  { signature, commitment: "confirmed" },
  (notification) => console.log("Signature update:", notification),
);

// Later…
watcher.abort();

Cluster monikers and endpoints

Pass a cluster moniker to auto-resolve RPC + WebSocket URLs. Monikers map to:

  • mainnet / mainnet-betahttps://api.mainnet-beta.solana.com
  • testnethttps://api.testnet.solana.com
  • devnet (default) → https://api.devnet.solana.com
  • localnet / localhosthttp://127.0.0.1:8899

WebSocket URLs are inferred (wss:// or ws://) when not supplied. Override with endpoint/rpc or websocket/websocketEndpoint when you need a custom host.

import { autoDiscover, createClient } from "@solana/client";

const client = createClient({
  cluster: "mainnet", // or 'devnet' | 'testnet' | 'localnet' | 'localhost'
  walletConnectors: autoDiscover(),
});

Custom endpoint with inferred WebSocket:

const client = createClient({
  endpoint: "http://127.0.0.1:8899", // websocket inferred as ws://127.0.0.1:8900
});

Use resolveCluster directly when you need the resolved URLs without creating a client:

import { resolveCluster } from "@solana/client";

const resolved = resolveCluster({ moniker: "testnet" });
console.log(resolved.endpoint, resolved.websocketEndpoint);

Notes:

  • Default moniker is devnet when nothing is provided; moniker becomes custom when you pass a raw endpoint.
  • createClient, createDefaultClient (resolveClientConfig), and SolanaProvider all use resolveCluster under the hood, so the moniker/endpoint behavior is consistent across entrypoints.

Wallet connector filtering

Use filterByNames with autoDiscover() to filter wallets by name without wallet-specific code:

import { autoDiscover, filterByNames } from "@solana/client";

// Only show Phantom and Solflare
const connectors = autoDiscover({
  filter: filterByNames("phantom", "solflare"),
});

const client = createClient({
  cluster: "devnet",
  walletConnectors: connectors,
});

This approach follows Wallet Standard's wallet-agnostic discovery pattern while still allowing you to curate which wallets appear in your app.

You can also write custom filter functions:

const connectors = autoDiscover({
  filter: (wallet) => wallet.name.toLowerCase().includes("phantom"),
});

Notes and defaults

  • Wallet connectors: autoDiscover() picks up Wallet Standard injectables; use filterByNames() to filter by name, or compose phantom(), solflare(), backpack(), etc. when you need explicit control.
  • Store: built on Zustand; pass createStore to createClient for custom persistence or server-side stores. serializeSolanaState / deserializeSolanaState help save and restore cluster + wallet metadata.
  • Actions: fetchAccount, fetchBalance, fetchLookupTable, fetchLookupTables, fetchNonceAccount, setCluster, requestAirdrop, sendTransaction, and wallet connect/disconnect keep the store in sync.
  • Watchers: watchAccount, watchBalance, and watchSignature stream updates into the store and return an abort() handle for cleanup.
  • Helpers: solTransfer, splToken, and transaction cover common transfers plus low-level prepare/sign/toWire flows. Transaction versions default to 0 when any instruction references address lookup tables, otherwise legacy; override with version when needed.

Scripts

  • pnpm build – compile JS and type definitions
  • pnpm test:typecheck – strict type-checking
  • pnpm lint / pnpm format – Biome-powered linting and formatting

More resources

  • Documentation — full guides and API reference
  • Playground: examples/vite-react (run with pnpm install && pnpm dev).
  • Next.js reference app: examples/nextjs.
  • Client APIs live in src/actions.ts, src/watchers, and src/features/* for helper internals.