Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/connector-kit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solana/client": minor
---

Add ConnectorKit wallet connectors and shared wallet-standard session handling.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ coverage/
.cache/
.temp/
tmp/

#references
references/
.cursor/

31 changes: 31 additions & 0 deletions apps/docs/content/docs/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ if (wallet.status === "connected") {
await client.actions.disconnectWallet();
```

### Connector IDs

Connectors use **canonical IDs**:

- Wallet Standard: `wallet-standard:<wallet-name>` (example: `wallet-standard:phantom`)
- Mobile Wallet Adapter: `mwa:<wallet-name>`
- WalletConnect: `walletconnect`

For convenience, calls like `connectWallet("phantom")` also work (fallback-only: prefers `wallet-standard:phantom`, then `mwa:phantom`). The client persists the **canonical** ID in state for more reliable restore/auto-connect.

### Wallet Connectors

Framework Kit uses the Wallet Standard for wallet discovery:
Expand Down Expand Up @@ -95,6 +105,27 @@ const client = createClient({
});
```

## ConnectorKit (optional)

ConnectorKit integration is exposed as a **stable, opt-in entrypoint**:

```ts
import { connectorKit } from "@solana/client/connectorkit";
import { createClient } from "@solana/client";

const walletConnectors = connectorKit({
// Pass a ConnectorKit client/config/defaultConfig (see ConnectorKit docs).
defaultConfig: { /* ... */ },
});

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

`@solana/connector` is an **optional peer dependency** of `@solana/client`. Install it to use `@solana/client/connectorkit`.

## Fetching Data

### Account Data
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
'use client';

import type { SolanaClientConfig } from '@solana/client';
import { connectorKit } from '@solana/client/connectorkit';
import {
SolanaProvider,
useConnectWallet,
useDisconnectWallet,
useWallet,
useWalletConnection,
} from '@solana/react-hooks';
import { useCallback, useEffect, useMemo, useState } from 'react';

function formatError(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
return JSON.stringify(error);
}

function truncate(address: string): string {
return `${address.slice(0, 4)}…${address.slice(-4)}`;
}

function ConnectButtonContent(
props: Readonly<{
bootstrapError: string | null;
isBootstrapping: boolean;
refresh(): Promise<void>;
}>,
) {
const wallet = useWallet();
const connectWallet = useConnectWallet();
const disconnectWallet = useDisconnectWallet();
const { connectors, isReady } = useWalletConnection();
const [error, setError] = useState<string | null>(null);
const [open, setOpen] = useState(false);

const isConnected = wallet.status === 'connected';
const address = isConnected ? wallet.session.account.address.toString() : null;

async function handleConnect(connectorId: string) {
setError(null);
try {
await connectWallet(connectorId);
setOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to connect');
}
}

async function handleDisconnect() {
setError(null);
try {
await disconnectWallet();
setOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to disconnect');
}
}

return (
<div className="relative">
<button
type="button"
onClick={() => setOpen((prev) => !prev)}
className="btn btn-secondary w-full justify-between"
>
{address ? (
<span className="mono max-w-[12ch] truncate">{truncate(address)}</span>
) : (
<span>Connect wallet</span>
)}
<span className="text-xs text-slate-500">{open ? '▲' : '▼'}</span>
</button>

{open ? (
<div className="card absolute z-10 mt-2 w-full min-w-[240px] p-3">
{isConnected ? (
<div className="space-y-3">
<div className="rounded-lg border border-slate-100 bg-slate-50 px-3 py-2">
<p className="small-label">Connected</p>
<p className="mono text-sm text-slate-900 max-w-[18ch] truncate">{address}</p>
</div>
<button
type="button"
onClick={() => void handleDisconnect()}
className="btn btn-secondary w-full"
>
Disconnect
</button>
</div>
) : (
<div className="space-y-2">
<p className="small-label">ConnectorKit</p>
<div className="space-y-1.5">
{!isReady || props.isBootstrapping ? (
<div className="rounded-lg border border-slate-100 bg-slate-50 px-3 py-2 text-sm text-slate-700">
Loading connectors…
</div>
) : connectors.length === 0 ? (
<div className="space-y-2">
<p className="text-sm text-slate-700">
No connectors found. Make sure a Wallet Standard wallet extension is
installed and enabled for <code>localhost</code>.
</p>
<button
type="button"
onClick={() => void props.refresh()}
className="btn btn-secondary w-full"
disabled={props.isBootstrapping}
>
Refresh connectors
</button>
</div>
) : (
connectors.map((connector) => {
const supported = connector.isSupported();
return (
<button
key={connector.id}
type="button"
onClick={() => void handleConnect(connector.id)}
className="btn btn-secondary w-full justify-between"
disabled={!supported}
title={connector.id}
>
<span className="truncate">{connector.name}</span>
<span className="text-xs text-slate-500">
{supported ? 'Connect' : 'Not ready'}
</span>
</button>
);
})
)}
</div>
</div>
)}

{props.bootstrapError ? (
<p className="mt-2 text-sm font-semibold text-red-600">{props.bootstrapError}</p>
) : null}
{error ? <p className="mt-2 text-sm font-semibold text-red-600">{error}</p> : null}
</div>
) : null}
</div>
);
}

export function ConnectorkitWalletConnectButton() {
const [walletConnectors, setWalletConnectors] = useState<ReturnType<typeof connectorKit> | null>(null);
const [bootstrapError, setBootstrapError] = useState<string | null>(null);
const [isBootstrapping, setIsBootstrapping] = useState(false);

const refresh = useCallback(async () => {
setIsBootstrapping(true);
setBootstrapError(null);

try {
// Avoid the Wallet Standard registry "not ready yet" race by awaiting ConnectorKit's `ready`.
const { ready } = await import('@solana/connector/headless');
await ready;
} catch (error) {
setBootstrapError(formatError(error));
}

try {
setWalletConnectors(
connectorKit({
defaultConfig: {
appName: 'Framework Kit • Next.js example',
network: 'devnet',
},
}),
);
} catch (error) {
setBootstrapError(formatError(error));
setWalletConnectors(null);
} finally {
setIsBootstrapping(false);
}
}, []);

useEffect(() => {
void refresh();
}, [refresh]);

const config = useMemo<SolanaClientConfig>(
() => ({
cluster: 'devnet',
walletConnectors: walletConnectors ?? [],
}),
[walletConnectors],
);

return (
<SolanaProvider config={config} walletPersistence={false}>
<ConnectButtonContent bootstrapError={bootstrapError} isBootstrapping={isBootstrapping} refresh={refresh} />
</SolanaProvider>
);
}
42 changes: 42 additions & 0 deletions examples/nextjs/app/connectorkit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { connectorKit } from '@solana/client/connectorkit';

import { ConnectorkitWalletConnectButton } from './connectorkit-wallet-connect-button';

export default function ConnectorKitPage() {
// SSR smoke test: should not construct a ConnectorClient and should return an empty list.
const ssrConnectors = connectorKit({
defaultConfig: {
appName: 'Framework Kit • Next.js example',
network: 'devnet',
},
});

return (
<main className="shell">
<header className="space-y-3">
<p className="small-label">@solana/client/connectorkit</p>
<h1 className="text-3xl font-bold text-slate-900">ConnectorKit wallet connect</h1>
<p className="max-w-3xl text-base text-slate-700">
This route wires ConnectorKit connectors into <code>@solana/client</code> and uses the same dropdown
pattern as the example <code>WalletConnectButton</code>, but backed by ConnectorKit discovery.
</p>
</header>

<section className="card space-y-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<p className="small-label">Wallet</p>
<p className="text-sm text-slate-700">Pick a ConnectorKit connector.</p>
</div>
<div className="sm:min-w-[260px]">
<ConnectorkitWalletConnectButton />
</div>
</div>
<p className="text-xs text-slate-500">
SSR safety: <span className="mono">{ssrConnectors.length}</span> connectors on the server (expected{' '}
<span className="mono">0</span>).
</p>
</section>
</main>
);
}
Loading
Loading