Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/kit-plugin-wallet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
4 changes: 4 additions & 0 deletions packages/kit-plugin-wallet/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
test-ledger/
target/
CHANGELOG.md
22 changes: 22 additions & 0 deletions packages/kit-plugin-wallet/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2025 Anza

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
211 changes: 211 additions & 0 deletions packages/kit-plugin-wallet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Kit Plugins ➤ Wallet

[![npm][npm-image]][npm-url]
[![npm-downloads][npm-downloads-image]][npm-url]

[npm-downloads-image]: https://img.shields.io/npm/dm/@solana/kit-plugin-wallet.svg?style=flat
[npm-image]: https://img.shields.io/npm/v/@solana/kit-plugin-wallet.svg?style=flat&label=%40solana%2Fkit-plugin-wallet
[npm-url]: https://www.npmjs.com/package/@solana/kit-plugin-wallet

This package provides a plugin that adds browser wallet support to your Kit clients using [wallet-standard](https://github.com/wallet-standard/wallet-standard). It handles wallet discovery, connection lifecycle, account selection, and signer creation — and syncs the connected wallet's signer to `client.payer` automatically.

## Installation

```sh
pnpm install @solana/kit-plugin-wallet
```

## `wallet` plugin

The wallet plugin adds a `client.wallet` namespace with all wallet state and actions, and wires the connected wallet's signer to `client.payer`.

### Setup

```ts
import { createEmptyClient } from '@solana/kit';
import { rpc } from '@solana/kit-plugin-rpc';
import { wallet } from '@solana/kit-plugin-wallet';

const client = createEmptyClient()
.use(rpc('https://api.mainnet-beta.solana.com'))
.use(wallet({ chain: 'solana:mainnet' }));
```

Once a wallet is connected, `client.payer` resolves to the wallet's signer and you can pass it directly to transaction instructions:

```ts
import { getTransferSolInstruction } from '@solana-program/system';
import { lamports } from '@solana/kit';

// Read registered wallets
const selectedWallet = client.wallet.wallets[0];

// Connect a wallet
await client.wallet.connect(selectedWallet);

// client.payer is now the connected wallet's signer
await client.sendTransaction(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work w/o adding txn planner/executors?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - you get a TransactionSigner and can use it with any Kit APIs.

I did mean to flag the decision not to have client.wallet.signTransaction() though. I think we should just use the signer APIs and also offering signTransaction would be confusing.

I think most apps will want to just use their transaction plan/execute plugin

If you don't, you can just add client.wallet.connected.signer (will probably move a little but same idea) as a signer on the transaction and sign/send it however you like

At the lowest level you have client.wallet.connected.signer.signAndModifyTransaction([...]) which is basically the same API we'd be offering as client.signTransaction, but just the signer interface

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this approach. We even have helpers like signTransactionWithSigners(signers, tx) now so fully leaning into the Signer API makes total sense to me.

getTransferSolInstruction({
source: client.payer,
destination: recipientAddress,
amount: lamports(10_000_000n),
}),
);
```

### Features

- `client.wallet.wallets` — All discovered wallets that support the configured chain.

```ts
for (const w of client.wallet.wallets) {
console.log(w.name, w.icon);
}
```

- `client.wallet.connected` — The active connection (wallet, account, and signer), or `null` when disconnected.

```ts
const { wallet, account, signer } = client.wallet.connected ?? {};
console.log(account?.address);
```

- `client.wallet.status` — The current connection status: `'pending'`, `'disconnected'`, `'connecting'`, `'connected'`, `'disconnecting'`, or `'reconnecting'`.

- `client.wallet.connect(wallet)` — Connect to a wallet and select the first newly authorized account.

```ts
const accounts = await client.wallet.connect(selectedWallet);
```

- `client.wallet.disconnect()` — Disconnect the active wallet.

- `client.wallet.selectAccount(account)` — Switch to a different account within an already-authorized wallet without reconnecting.

```ts
client.wallet.selectAccount(selectedWallet);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be account w/i the accounts returned from connected??

Suggested change
client.wallet.selectAccount(selectedWallet);
client.wallet.selectAccount(accounts[0]);

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.wallet.selectAccount(selectedWallet);

This example passes selectedWallet (a UiWallet) but selectAccount takes a UiWalletAccount. Should be something like:

const accounts = await client.wallet.connect(selectedWallet);
client.wallet.selectAccount(accounts[1]); // switch to second account

- `client.wallet.signMessage(message)` — Sign a raw message with the connected account.

```ts
const signature = await client.wallet.signMessage(new TextEncoder().encode('Hello'));
```

- `client.wallet.signIn(input?)` / `client.wallet.signIn(wallet, input?)` — Sign In With Solana (SIWS). The two-argument form connects the wallet implicitly.

```ts
// Sign in with the already-connected wallet
const output = await client.wallet.signIn({ domain: window.location.host });

// Sign in and connect in one step
const output = await client.wallet.signIn(selectedWallet, { domain: window.location.host });
```

### Framework integration

The plugin exposes `subscribe` and `getSnapshot` for binding wallet state to any UI framework.

**React** — use `useSyncExternalStore` for concurrent-mode-safe rendering:

```tsx
import { useSyncExternalStore } from 'react';

function useWalletState() {
return useSyncExternalStore(client.wallet.subscribe, client.wallet.getSnapshot);
}

function App() {
const { wallets, connected, status } = useWalletState();

if (status === 'pending') return null; // avoid flashing a connect button before auto-reconnect

if (!connected) {
return wallets.map(w => (
<button key={w.name} onClick={() => client.wallet.connect(w)}>
{w.name}
</button>
));
}

return <p>Connected: {connected.account.address}</p>;
}
```

**Vue** — use a `shallowRef` composable:

```ts
import { onMounted, onUnmounted, shallowRef } from 'vue';

function useWalletState() {
const state = shallowRef(client.wallet.getSnapshot());
onMounted(() => {
const unsub = client.wallet.subscribe(() => {
state.value = client.wallet.getSnapshot();
});
onUnmounted(unsub);
});
return state;
}
```

**Svelte** — wrap in a `readable` store:

```ts
import { readable } from 'svelte/store';

export const walletState = readable(client.wallet.getSnapshot(), set => {
return client.wallet.subscribe(() => set(client.wallet.getSnapshot()));
});
```

### Persistence

By default the plugin uses `localStorage` to remember the last connected wallet and auto-reconnects on the next page load. Pass `storage: null` to disable, or provide a custom adapter (e.g. `sessionStorage` or an IndexedDB wrapper):

```ts
wallet({
chain: 'solana:mainnet',
storage: sessionStorage, // use session storage instead
storageKey: 'my-app:wallet', // custom key (default: 'kit-wallet')
autoConnect: false, // disable silent reconnect
});
```

### SSR / server-side rendering

The plugin is safe to include in a shared client that runs on both server and browser. On the server, `status` stays `'pending'` permanently, all actions throw `WalletNotConnectedError`, and no registry listeners or storage reads are made. In the browser the plugin initializes normally.

```ts
// This client chain works on both server and browser.
const client = createEmptyClient()
.use(rpc('https://api.mainnet-beta.solana.com'))
.use(payer(serverKeypair))
.use(wallet({ chain: 'solana:mainnet' }));

// Server: client.wallet.status === 'pending', client.payer === serverKeypair
// Browser: auto-connects, client.payer becomes the wallet signer
```

### Wallet discovery filtering

Use the `filter` option to restrict which wallets appear in `client.wallet.wallets`:

```ts
wallet({
chain: 'solana:mainnet',
// Only show wallets that support signAndSendTransaction
filter: w => w.features.includes('solana:signAndSendTransaction'),
});
```

### Cleanup

The plugin implements `[Symbol.dispose]`, so it integrates with the `using` declaration or explicit disposal:

```ts
{
using client = createEmptyClient().use(wallet({ chain: 'solana:mainnet' }));
// registry listeners and storage subscriptions are cleaned up on scope exit
}
```
75 changes: 75 additions & 0 deletions packages/kit-plugin-wallet/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@solana/kit-plugin-wallet",
"version": "0.1.0",
"description": "Wallet connection plugin for Kit clients",
"exports": {
"types": "./dist/types/index.d.ts",
"react-native": "./dist/index.react-native.mjs",
"browser": {
"import": "./dist/index.browser.mjs",
"require": "./dist/index.browser.cjs"
},
"node": {
"import": "./dist/index.node.mjs",
"require": "./dist/index.node.cjs"
}
},
"browser": {
"./dist/index.node.cjs": "./dist/index.browser.cjs",
"./dist/index.node.mjs": "./dist/index.browser.mjs"
},
"main": "./dist/index.node.cjs",
"module": "./dist/index.node.mjs",
"react-native": "./dist/index.react-native.mjs",
"types": "./dist/types/index.d.ts",
"type": "commonjs",
"files": [
"./dist/types",
"./dist/index.*",
"./src/"
],
"sideEffects": false,
"keywords": [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how much these matter but might make sese to add "wallet-adapter" since this is a big piece of migrating from wallet adapter

"solana",
"kit",
"plugin",
"wallet",
"wallet-standard",
"signer"
],
"scripts": {
"build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json",
"dev": "vitest --project node",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint --fix . && prettier --write .",
"test": "pnpm test:types && pnpm test:treeshakability",
"test:treeshakability": "for file in dist/index.*.mjs; do agadoo $file; done",
"test:types": "tsc --noEmit"
},
"peerDependencies": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@solana/kit appears as both a peer dependency here and as a resolved regular dependency in the lockfile. Was the intent for it to be peer-only? If so, it might be getting hoisted from the workspace root. If it's intentionally in both, the ^6.5.0 range in peer deps is fine, but having it resolved in the lockfile as a direct dep could cause issues for consumers who have a different minor version.

"@solana/kit": "^6.6.0"
},
"dependencies": {
"@solana/wallet-account-signer": "^6.6.0",
"@solana/wallet-standard-chains": "^1.1.1",
"@solana/wallet-standard-features": "^1.3.0",
"@wallet-standard/app": "^1.1.0",
"@wallet-standard/errors": "^0.1.1",
"@wallet-standard/features": "^1.1.0",
"@wallet-standard/ui": "^1.0.1",
"@wallet-standard/ui-features": "^1.0.1",
"@wallet-standard/ui-registry": "^1.0.1"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/anza-xyz/kit-plugins"
},
"bugs": {
"url": "http://github.com/anza-xyz/kit-plugins/issues"
},
"browserslist": [
"supports bigint and not dead",
"maintained node versions"
]
}
Loading
Loading