@solutiofi/connectorkit-svelte
v0.7.1
Published
Solana wallet connection library for Svelte 5, powered by @solana/connector headless API
Readme
@solutiofi/connectorkit-svelte
The ultimate Solana wallet connection library for Svelte 5. Powered by @solana/connector headless API and built with Svelte Runes.
Features
- ⚡ Zero-Config Polyfills — Works with Vite/SvelteKit out of the box (no
vite-plugin-node-polyfillsneeded) - 🎨 Fully Themeable — Customize every color with CSS variables; optional shadcn-token shim for drop-in integration with shadcn-styled apps
- 📱 Mobile-First — Deep links to wallet apps, MWA on Android (Seeker / Seed Vault), WalletConnect QR
- 🔁 Cross-tab Deep-Link Returns — Phantom/Solflare/Backpack often redirect back into a new Chrome tab; we propagate the connection back to the original tab via
BroadcastChannelso the user doesn't notice - 🔧 Svelte 5 Runes — Built from the ground up for the new reactivity model; SSR-safe
- 🌐 Wallet Standard — Supports all modern Solana wallets (Phantom, Solflare, Backpack, Jupiter, Magic Eden, …) + your own via
additionalWallets - 🔐 Remote Signer — Drop-in
/remoteadapter for custodial backends (Fireblocks, Privy, Turnkey, your own KMS) — the signing key stays on your server - ✍️ Signing — Sign messages and transactions with full TypeScript support
- 💰 Balances — Auto-fetching for SOL and SPL tokens, with token logo proxy support
- 🎯 Filtering — Precise control over which wallets are shown, hidden, or featured; "Recent" badge for the last-used wallet
- 👥 Multi-Account — Seamless switching between accounts; standalone
<AccountSwitcher />component - 🛟 Cancel Anywhere — Tap the connect button again while it's connecting to abort cleanly (essential for Seeker MWA where the wallet emits no cancellation signal)
- 🪪 Hardened Sessions — Deep-link ephemeral keypairs expire after 24h (configurable) instead of living forever in
localStorage
Installation
npm install @solutiofi/connectorkit-svelte @solana/web3.js
# or
pnpm add @solutiofi/connectorkit-svelte @solana/web3.js
@solana/web3.jsis a peer dependency — install it alongside the library so your bundler resolves a single version.
Quick Start
- Wrap your application with
SvelteConnectorProvider. ThegetDefaultConfig()helper fills in sensible defaults (clusters, autoConnect, WalletConnect metadata) so you only specify what's unique to your app:
<!-- +layout.svelte -->
<script lang="ts">
import {
SvelteConnectorProvider,
WalletButton,
getDefaultConfig,
} from '@solutiofi/connectorkit-svelte';
import '@solutiofi/connectorkit-svelte/theme.css';
const config = getDefaultConfig({
appName: 'My Solana App',
network: 'mainnet',
walletConnect: true, // auto-reads PUBLIC_WALLETCONNECT_PROJECT_ID / VITE_WALLETCONNECT_PROJECT_ID / NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
});
</script>
<SvelteConnectorProvider {config}>
<nav>
<WalletButton />
</nav>
<slot />
</SvelteConnectorProvider>Prefer to hand-roll the config object instead? You can — getDefaultConfig() returns a plain SvelteConnectorConfig you can spread or replace. See Configuration below.
- Use the context in valid child components:
<!-- +page.svelte -->
<script lang="ts">
import { getConnectorContext, TokenList } from '@solutiofi/connectorkit-svelte';
const ctx = getConnectorContext();
</script>
{#if ctx.state.isConnected}
<h1>Welcome, {ctx.state.publicKey}</h1>
<TokenList />
{/if}Configuration
Detailed configuration reference for SvelteConnectorConfig:
interface SvelteConnectorConfig {
// --- Basic ---
appName: string; // Name shown in wallet connection prompts
appUrl?: string; // URL of your app (for wallet metadata)
appIcon?: string; // Icon URL — forwarded to MWA `appIdentity.icon`
// and WalletConnect metadata. Defaults to `/favicon.svg`.
network?: 'mainnet' | 'devnet' | 'testnet' | 'localnet'
| 'solana:mainnet' | 'solana:devnet' | 'solana:testnet' | 'solana:localnet';
// Default: 'mainnet'. Both the legacy short form
// and the CAIP-2 chain-id form are accepted —
// pick whichever matches the docs you're copying
// from. See "Cluster IDs" below.
// --- Connection ---
autoConnect?: boolean; // Attempt to auto-connect on load (default: false).
// When enabled, restores the LAST-connected wallet
// (not the first in the list).
rpcUrl?: string; // Custom RPC URL (defaults to public Alchemy endpoint)
debug?: boolean; // Enable verbose logging (gates internal console.log)
// --- UI/UX ---
walletOrder?: string[]; // Sort wallets by name: ['Phantom', 'Backpack']
walletLimit?: number; // Max wallets to show before "More" (default: 4)
imageProxy?: string; // Proxy URL for wallet icons + token logos
// (CORS / privacy). See "Image Proxy" below.
// --- Filtering ---
wallets?: {
allowList?: string[]; // Strict list: ONLY show these wallets
denyList?: string[]; // Blacklist: NEVER show these wallets
featured?: string[]; // Highlight these wallets at the top with a badge
};
// --- Custom / Embedded Wallets ---
additionalWallets?: Wallet[]; // Wallet Standard wallets registered alongside
// the auto-discovered ones. Useful for embedded
// signers, dev/test wallets, or remote-signer
// adapters. Each must implement
// `@wallet-standard/base`'s `Wallet` interface.
// --- Network Switching ---
clusters?: { // Custom networks for ClusterSelector
name: string; // Display name (shown in ClusterSelector)
id: string; // Cluster id — accepts both 'mainnet' / 'devnet' /
// 'testnet' / 'localnet' AND the CAIP-2 form
// 'solana:mainnet' / etc. Custom ids pass through
// unchanged.
rpcUrl: string;
wsUrl?: string;
}[];
// --- WalletConnect (Optional) ---
walletConnect?: {
projectId: string; // Get from https://cloud.walletconnect.com
metadata?: {
name: string;
description: string;
url: string;
icons: string[];
};
};
}getDefaultConfig({ ... })
A small factory that fills in the boilerplate so you can write:
const config = getDefaultConfig({
appName: 'My App',
walletConnect: true, // shorthand — auto-detects PROJECT_ID env var
});| Option | Type | Default |
|---|---|---|
| appName | string | required |
| appUrl | string | undefined (provider falls back to window.location.origin at runtime) |
| appIcon | string | undefined |
| network | 'mainnet' \| 'devnet' \| 'testnet' \| 'localnet' | 'mainnet' |
| clusters | ClusterConfig[] | DEFAULT_CLUSTERS (Mainnet/Devnet/Testnet) |
| autoConnect | boolean | true |
| walletConnect | true \| { projectId, metadata? } | not enabled |
| additionalWallets | Wallet[] | none |
| debug | boolean | false |
When walletConnect: true, the project ID is auto-detected (first non-empty wins):
PUBLIC_WALLETCONNECT_PROJECT_ID(SvelteKit$env/static/public)VITE_WALLETCONNECT_PROJECT_ID(plain Vite)NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID(cross-framework convention)globalThis.__CONNECTORKIT_ENV__.<KEY>(escape hatch for non-bundler environments)
If none is found, WC is silently skipped — the rest of the connector still works. Pass an explicit walletConnect: { projectId: '...' } to override.
DEFAULT_CLUSTERS is also exported in case you want to extend rather than replace.
Cluster IDs
@solutiofi/connectorkit-svelte accepts cluster ids in both the legacy short form and the CAIP-2 chain-id form — the library normalizes between them internally:
| Network | Legacy form | CAIP-2 form (canonical) |
|---|---|---|
| Mainnet | mainnet | solana:mainnet |
| Devnet | devnet | solana:devnet |
| Testnet | testnet | solana:testnet |
| Localnet | localnet | solana:localnet |
Both work as input to selectCluster(id), config.network, and clusters[].id. Custom cluster ids (anything not in the table above) pass through unchanged. Snippets copy-pasted from upstream Solana docs that use the CAIP-2 form work without translation.
A handful of helpers are exported for callers that need to interop with both forms:
import {
toCaip2ClusterId, // 'mainnet' → 'solana:mainnet' (unknown ids pass through)
toLegacyClusterId, // 'solana:devnet' → 'devnet'
isCaip2ClusterId, // true for 'solana:*'
clusterIdsMatch, // form-agnostic equality: ('mainnet', 'solana:mainnet') → true
} from '@solutiofi/connectorkit-svelte';ctx.currentCluster.id preserves whichever form was originally configured (no surprise rewrites). Use toCaip2ClusterId(ctx.currentCluster.id) if you need the canonical form regardless.
Component Reference
<WalletButton />
The primary connect button. Automatically handles "Connect", "Disconnect", "Copy Address", and "Change Wallet".
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | 'sm' \| 'md' \| 'lg' | 'md' | Scaling of the button |
| showIcon | boolean | true | Show wallet icon when connected |
| class | string | '' | Custom classes |
<WalletModal />
A standalone modal component. Use this if you want to create your own trigger button.
<script>
let isOpen = $state(false);
</script>
<button onclick={() => isOpen = true}>Connect Custom</button>
<WalletModal bind:open={isOpen} />| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | false | Control modal visibility |
| title | string | 'Connect Wallet' | Modal title text |
| onclose | () => void | - | Called when modal closes |
| footer | Snippet | - | Custom footer content (see below) |
The modal ships with:
- Real focus trap — Tab cycles inside, Shift+Tab cycles back, ESC closes, focus returns to the invoking element on close
- Body scroll lock while open
role="alert"error banner so screen readers announce connection failures immediately- "Recent" badge on the last-connected wallet — it's also floated to the top of the picker
- "Don't have a wallet yet?" install-link section when none of Phantom / Solflare / Backpack / Glow are detected (desktop only — mobile uses deep-link wallets for the same purpose)
- Reactive Android layout — MWA + WalletConnect promoted; deep-link wallets in a collapsible "All Wallets" section
Custom Footer Slot
You can pass a custom footer to add additional functionality like Ledger or hardware wallet connections:
<WalletModal bind:open={isOpen}>
{#snippet footer()}
<button onclick={handleLedgerConnect}>
🔒 Connect Ledger
</button>
{/snippet}
</WalletModal><ClusterSelector />
A dropdown to switch between networks (Mainnet/Devnet/Testnet) or custom clusters. ESC closes the dropdown via a <svelte:window> keydown handler.
Multi-Account Support
If a connected wallet provides multiple accounts, @solutiofi/connectorkit-svelte automatically handles them. The <WalletButton /> and <WalletDropdown /> include a built-in UI for switching accounts.
The selected account is persisted per-wallet, so users don't need to re-select when they reconnect.
<AccountSwitcher />
Standalone account picker — use it in your app header or any spot where the in-dropdown switcher isn't enough. Reads from ctx.state.allAccounts and calls ctx.selectAccount(address) on click.
<script>
import { AccountSwitcher } from '@solutiofi/connectorkit-svelte';
</script>
<AccountSwitcher />| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | '' | Extra class names on the root |
| hideWhenSingle | boolean | true | Hide entirely if the wallet only exposes one account |
<TokenList />
Displays a list of SPL tokens held by the connected wallet.
- Auto-fetches from RPC; refetches when the cluster's RPC URL changes
- Shows token icons, symbols, and formatted balances
- Filters out zero-balance tokens
- Token logos go through
imageProxyif configured
| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | '' | Extra class names on the root |
| maxHeight | string | '300px' | Scroll-area max-height (any CSS length) |
<TransactionHistory />
Recent on-chain activity for the connected wallet, classified per-row (sent / received / swap / nft / stake / program / tokenAccountClosed). Drop it into a sidebar / drawer.
- Auto-fetches via
getSignaturesForAddress+getParsedTransaction(in batches) when the wallet connects or the active cluster changes - Classifier inspects parsed instructions + pre/post token balances — see Transaction classifier below
- Each row links to Solana Explorer on the active cluster (
mainnet/devnet/testnet/localnet) - Token enrichment (symbol / logo / decimals) via optional
lookup— wire to your<TokenList>cache - Pagination via signature cursor (
Load morebutton)
<TransactionHistory pageSize={20} lookup={(mint) => tokenStore.tokens.find(t => t.mint === mint)} />| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | '' | Extra class names on the root |
| maxHeight | string | '400px' | Scroll-area max-height (any CSS length) |
| pageSize | number | 20 | Signatures pulled per page |
| lookup | (mint) => { symbol?, logo?, decimals? } \| undefined | none | Mint → token info resolver for enrichment |
| hideHeader | boolean | false | Hide the title bar (useful inside a drawer with its own header) |
The headless <TransactionHistoryElement> variant ships in the same module — see below.
<WalletConnectQR />
Rendered by the modal automatically when the user picks WalletConnect, but also exported for standalone use. The canvas redraws when the uri prop changes (WC v2 rotates URIs on session expiry), has aria-label for screen readers, and includes a "Copy link instead" button for users who can't scan (CLI wallets, mobile-only-paired wallets, etc.).
<WalletConnectQR uri={ctx.state.walletConnectUri} /><AddressDisplay />
Displays a truncated wallet address with click-to-copy.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| address | string | required | The address to display |
| chars | number | 4 | Chars to show (e.g. ABCD...WXYZ) |
| copyable | boolean | false | Enable copy-to-clipboard |
Headless Element composables
The pre-styled components above are built on top of headless Element counterparts. Pass a children snippet to render with your own markup; receive the slice + actions as the snippet's args. Use these when you want the library's state machine but completely custom styling — drop them anywhere <SvelteConnectorProvider> is mounted.
<script>
import { BalanceElement, AccountElement, ClusterElement, DisconnectElement } from '@solutiofi/connectorkit-svelte';
</script>
<BalanceElement precision={4}>
{#snippet children({ formattedBalance, isLoading, refetch })}
<button onclick={refetch}>
{isLoading ? '…' : `${formattedBalance} SOL`}
</button>
{/snippet}
</BalanceElement>
<AccountElement addressChars={6}>
{#snippet children({ formattedAddress, walletName, walletIcon, copy, copied, disconnect, isConnected })}
{#if isConnected}
<div class="my-pill">
<img src={walletIcon} alt="" />
<span>{walletName}</span>
<button onclick={copy}>{copied ? 'Copied' : formattedAddress}</button>
<button onclick={disconnect} aria-label="Sign out">×</button>
</div>
{/if}
{/snippet}
</AccountElement>
<ClusterElement>
{#snippet children({ current, clusters, selectCluster, isMainnet })}
<select value={current?.id} onchange={(e) => selectCluster(e.currentTarget.value)}>
{#each clusters as c}<option value={c.id}>{c.name}</option>{/each}
</select>
{#if !isMainnet}<span class="warn">⚠ Test cluster</span>{/if}
{/snippet}
</ClusterElement>
<DisconnectElement>
{#snippet children({ disconnect, isConnected, walletName })}
{#if isConnected}
<button onclick={disconnect}>Sign out of {walletName}</button>
{/if}
{/snippet}
</DisconnectElement>| Element | Snippet args |
|---|---|
| <BalanceElement> | { balance, formattedBalance, symbol, isLoading, error, lastFetched, refetch } + prop precision?: number |
| <AccountElement> | { address, formattedAddress, walletName, walletIcon, isConnected, isConnecting, allAccounts, selectAccount, copy, copied, disconnect } + prop addressChars?: number |
| <ClusterElement> | { current, clusters, selectCluster, isMainnet, currentRpcUrl } |
| <DisconnectElement> | { disconnect, isConnected, walletName } |
| <TransactionHistoryElement> | { transactions, isLoading, hasMore, error, lastFetched, refetch, loadMore, explorerUrlFor } + props pageSize?: number, lookup?: TokenInfoLookup, commitment?: 'processed' \| 'confirmed' \| 'finalized' |
Transaction classifier
The classifier feeding <TransactionHistory> (classifyTransaction from _utils/tx-classifier.ts) maps a ParsedTransactionWithMeta to a ClassifiedTransaction { type, amount?, symbol?, swapFrom?, swapTo?, counterparty?, programName?, status, fee, ... }. Priority-ordered rules (earlier wins): swap → tokenAccountClosed → token transfer → SOL transfer → NFT → stake → program fallback. Known DEX / NFT / stake program IDs live in _utils/programs.ts — add to that registry to get programName on additional protocols. Use the headless <TransactionHistoryElement> to plug in custom row markup while keeping classification + pagination logic.
Element components render null when no children snippet is provided — they hold no state of their own.
Hooks & API
Access the connector state reactively anywhere in your app:
import { getConnectorContext } from '@solutiofi/connectorkit-svelte';
const ctx = getConnectorContext();State Properties
ctx.state.isConnected:booleanctx.state.publicKey:string | nullctx.state.account:WalletAccount | null(currently selected account)ctx.state.allAccounts:WalletAccount[](All available accounts)ctx.state.isConnecting:booleanctx.state.connectingWalletId:string | null(which wallet ID is mid-connect)ctx.state.connectors:WalletConnector[](every detected + registered wallet)ctx.state.walletConnectUri:string | null(WC QR URI when active)ctx.state.error:WalletError | nullctx.currentCluster:ClusterConfigused for network switchingctx.currentRpcUrl:stringcurrent RPC endpointctx.clusters:ClusterConfig[]available clustersctx.config:SvelteConnectorConfigthe provider was constructed with (read-only)
Methods
ctx.connect(walletId): Connect to a specific walletctx.cancelConnect(): Abort an in-flight connect attempt. No-op when nothing is connecting. Required for MWA on Seeker, where the SDK emits no signal when the user dismisses the wallet's authorization popup — surface a "Cancel" button in your UI whileisConnectingis true (<WalletButton />does this automatically).ctx.disconnect(): Disconnect current walletctx.clearError(): Clearstate.errorctx.openWalletConnect(): Open the WC flow directlyctx.signMessage(message): Sign a byte arrayctx.signTransaction(tx): Sign a serialized transactionctx.signAndSendTransaction(tx, options): Sign and submit to networkctx.selectCluster(id): Switch networkctx.selectAccount(address): Switch active accountctx.balanceStore: shared SOL balance store ({ balance, isLoading, fetchBalance, refresh, formatBalance, ... })
getConnectorContext()is strict — it throws if called outside a<SvelteConnectorProvider>. Pair it withhasConnectorContext()(also exported) for components that need to render with or without the provider.
@solana/kit signer
For consumers on the modern @solana/kit SDK (formerly gill), getKitSigner(ctx?) returns a TransactionPartialSigner backed by the connected wallet. Forwards through the same ctx.signTransaction() pipeline (so MWA / DeepLink / Wallet-Standard wallets all work uniformly).
import { getConnectorContext, getKitSigner } from '@solutiofi/connectorkit-svelte';
import {
pipe,
setTransactionMessageFeePayerSigner,
signTransaction
} from '@solana/kit';
const signer = getKitSigner(); // throws if no wallet connected
const signedTx = await pipe(
message,
(m) => setTransactionMessageFeePayerSigner(signer, m),
(m) => signTransaction(m, [signer])
);@solana/kit is an optional peer dependency (peerDependenciesMeta.optional: true). Importing getKitSigner alone doesn't pull kit at runtime — its types are import type only. Consumers who don't use kit pay nothing.
Theming
All styles are defined as CSS custom properties. Light mode is the default; add .dark class — or set data-theme="dark" on <html> — to activate dark mode. Both selectors are supported for compatibility with Tailwind v4's darkMode: 'class' setup.
Two import paths
// Default — defines `--sck-*` tokens
import '@solutiofi/connectorkit-svelte/theme.css';
// OR: shadcn-compatible shim that maps shadcn's standard tokens
// (`--background`, `--foreground`, `--primary`, `--border`, `--radius`,
// etc.) onto the library's `--sck-*` tokens. Drop-in for any app already
// running a shadcn-styled palette + dark-mode toggle.
import '@solutiofi/connectorkit-svelte/theme.shadcn.css';The shadcn shim falls back to the library's own defaults when a shadcn token is undefined, so partial themes still render correctly.
Complete CSS Variable Reference
| Variable | Light Mode | Dark Mode | Description |
|----------|------------|-----------|-------------|
| Brand Colors ||||
| --sck-primary | #008AFF | #3DA6FF | Main action color |
| --sck-primary-hover | #0075D9 | #008AFF | Hover state |
| Backgrounds ||||
| --sck-bg | #FBFCFD | #191C21 | Modal/dropdown background |
| --sck-bg-secondary | #F4F5F7 | #272C33 | Card backgrounds |
| --sck-bg-hover | #E6E7EB | #30363F | Hover backgrounds |
| Text ||||
| --sck-text | #1F2329 | #FBFCFD | Primary text |
| --sck-text-muted | #5B6778 | #939EAE | Muted text |
| Borders ||||
| --sck-border | #CBD1D8 | #383F4A | Border color |
| Status Colors ||||
| --sck-success | #248C3E | #34D058 | Success states |
| --sck-error | #E42E2C | #F85149 | Error states |
| --sck-warning | #FFA500 | #FFCC00 | Warning states |
Enabling Dark Mode
Add the .dark class to the body element to activate dark mode:
<script>
import { onMount } from 'svelte';
let isDark = $state(true);
onMount(() => {
// Restore from localStorage or use system preference
const saved = localStorage.getItem('theme');
isDark = saved ? saved === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme();
});
function toggleTheme() {
isDark = !isDark;
localStorage.setItem('theme', isDark ? 'dark' : 'light');
applyTheme();
}
function applyTheme() {
document.body.classList.toggle('dark', isDark);
}
</script>
<button onclick={toggleTheme}>
{isDark ? '☀️' : '🌙'}
</button>Image Proxy
Token logos + wallet icons frequently live on third-party CDNs that throttle anonymous traffic or block CORS. Pass imageProxy on your config to route them through your own server (or an image-CDN like imgproxy / wsrv.nl / Cloudinary):
getDefaultConfig({
appName: 'My App',
// Append form (default):
imageProxy: 'https://images.weserv.nl/?url=',
// …or placeholder form:
// imageProxy: 'https://my.cdn.example/img?src={url}&w=128',
});- Source URL is URL-encoded and appended as
?url=...by default - If
{url}placeholder is present, it's substituted in-place data:andblob:URIs bypass the proxy- Applied to both
<WalletIcon />(wallet logos) and<TokenList />(token logos)
Adding Custom Wallets
Pass additionalWallets to register a Wallet-Standard wallet alongside auto-discovered ones. Useful for embedded signers, dev/test wallets, or remote-signer adapters:
import type { Wallet } from '@wallet-standard/base';
import { myEmbeddedWallet } from './my-embedded-wallet';
const config = getDefaultConfig({
appName: 'My App',
additionalWallets: [myEmbeddedWallet], // Any object implementing the Wallet interface
});The wallet is forwarded straight into the wallet-standard registry; discovery / dedupe / filtering downstream is identical to auto-discovered wallets.
Remote Signer (@solutiofi/connectorkit-svelte/remote)
For custodial / server-side signing setups — Fireblocks, Privy, Turnkey, or your own KMS — createRemoteSignerWallet returns a Wallet-Standard wallet that delegates signing to an HTTP endpoint. The signing key lives on your server; the browser never holds it.
import { getDefaultConfig } from '@solutiofi/connectorkit-svelte';
import { createRemoteSignerWallet } from '@solutiofi/connectorkit-svelte/remote';
const remoteWallet = createRemoteSignerWallet({
name: 'My Custodial Wallet',
address: 'HeLpfu1Vau1tHpZmvbsCRYrSk9oHHFGuesxBdxYHvLW', // base58 pubkey
endpoint: '/api/connector-signer',
// Async headers — recompute per request for short-lived bearer tokens
headers: async () => ({ authorization: `Bearer ${await getSessionToken()}` }),
});
const config = getDefaultConfig({
appName: 'My App',
additionalWallets: [remoteWallet],
});Server contract
Three JSON-over-POST handlers. Defaults are /sign-transaction, /sign-and-send-transaction, /sign-message (override via paths).
POST /sign-transaction
{ account, transaction (base64), chain ('solana:mainnet'|...) }
→ { signedTransaction (base64) }
POST /sign-and-send-transaction
{ account, transaction (base64), chain, options? }
→ { signature (base58) }
POST /sign-message
{ account, message (base64) }
→ { signature (base64) }Non-2xx responses surface as a RemoteSignerError with .status and .bodyText populated, so app code can react to e.g. 403 (token expired) or 503 (signer locked) distinctly from network failures.
Options
| Option | Type | Default |
|---|---|---|
| name | string | required — shown in the wallet picker |
| address | string | required — base58 pubkey the signer represents |
| endpoint | string | required — base URL, e.g. '/api/signer' or 'https://signer.example.com' |
| icon | WalletIcon | generic placeholder |
| headers | Record<string,string> | () => Promise<Record<string,string>> | none |
| paths | Partial<RemoteSignerPaths> | /sign-transaction, /sign-and-send-transaction, /sign-message |
| chains | readonly ('solana:mainnet'\|'solana:devnet'\|'solana:testnet'\|'solana:localnet')[] | all four |
| fetch | typeof fetch | global fetch |
The full wire types (SignTransactionRequest, SignAndSendTransactionResponse, etc.) are exported from the same subpath if you want to share them between client and server route handlers.
Troubleshooting & FAQ
"Global is not defined" or "Buffer is not defined"?
You generally do not need to configure this manually. @solutiofi/connectorkit-svelte automatically polyfills global, Buffer, and process in the browser environment when the provider mounts.
Does this work with SvelteKit SSR?
Yes — <WalletButton>, <WalletModal>, <WalletDropdown> and the rest of the surface are SSR-safe. All localStorage / sessionStorage access goes through internal safeLocalStorage / safeSessionStorage wrappers that return null server-side instead of throwing. SSR-relevant derived state (showAndroidLayout, recentConnectorId, etc.) is wrapped in $derived so it re-evaluates after hydration without mismatch.
My wallet isn't showing up?
- Ensure the wallet is installed and supports the Wallet Standard (most do).
- Check if you have
wallets.allowListconfigured and the wallet is missing from it. - Check
wallets.denyList. - On desktop, if no wallets are detected the modal shows an "Install …" section with links for Phantom / Solflare / Backpack / Glow — the user may need to install one first.
autoConnect is reconnecting to the wrong wallet on refresh
As of 0.3.1 the auto-connect logic prefers the last-connected wallet (read from localStorage) and only falls back to the first rehydrated wallet in the list. Earlier versions silently picked whichever wallet wallet-standard listed first — Solflare always beat Phantom if both had cached accounts. Update to ≥ 0.3.1.
Mobile wallets aren't detected?
On mobile Safari/Chrome, wallet extensions can't inject — wallets are standalone apps. @solutiofi/connectorkit-svelte handles this automatically:
Native Mobile App Support (Automatic):
- On mobile devices, wallets like Phantom, Solflare, Backpack, Jupiter, and Magic Eden are auto-detected and added to the wallet list
- They appear as standard wallets in the connect modal
- Tapping them initiates an encrypted (NaCl
box/ X25519 + XSalsa20-Poly1305) deep-link connection — redirects to the wallet app - After approval, the wallet redirects back to your dApp and the connection is auto-established
On Android (MWA):
- Solana Mobile Wallet Adapter is registered as an additional connector ("Solana Mobile")
- On Seeker / Seed Vault, this is the native path; the SDK is dynamic-imported so desktop bundles don't carry the MWA dep tree
cluster,appIcon,appName,appUrlare all threaded through fromSvelteConnectorConfig— no silent fallback to mainnet-beta or/favicon.svg
WalletConnect (recommended as cross-platform fallback):
Configure WalletConnect for universal mobile support. The provider package is dynamic-imported so consumers without a projectId pay no runtime cost.
const config = getDefaultConfig({
appName: 'My App',
network: 'mainnet',
walletConnect: { projectId: 'YOUR_PROJECT_ID' }, // or `true` to auto-detect
});Mobile connection flow:
- User opens your dApp in mobile Safari/Chrome
- Taps "Connect Wallet"
- Sees Phantom, Solflare, etc. in the standard wallet list
- Taps "Phantom" → redirects to Phantom app → user approves
- Wallet redirects back to your dApp → connection surfaces in your UI
Cross-tab deep-link returns: Phantom/Solflare/Backpack universal-link returns frequently open a new Chrome tab. We use BroadcastChannel('connectorkit-svelte:deeplink') to propagate the connection from the new tab back to your original tab — so ctx.state.isConnected flips in the tab the user was already on, no manual tab-switching needed.
Deep-link session storage: ephemeral encryption keypairs are stored in localStorage with a configurable 24h TTL (sessionTtlMs). Sessions older than the TTL are discarded on load so XSS-extractable keys don't live forever.
Cancelling MWA / a wallet app: tap the "Connecting…" button to abort. On Seeker the SDK emits no signal when the user dismisses the wallet popup, so we detect the cancellation via window.blur/window.focus transitions and accept an explicit ctx.cancelConnect() call as a hard fallback. <WalletButton> wires this up automatically.
Pro tip: If users open your dApp from within a wallet's in-app browser (e.g., Phantom's browser), the wallet injects itself via Wallet Standard — no deep links needed.
Dev / debug
Set debug: true on your config (or use getDefaultConfig({ ..., debug: true })) to enable verbose internal logging. The library uses a gated logger (logger.log); logger.warn / logger.error always emit. No console.log ships unguarded.
License
MIT © 2026 Connectorkit Authors
