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/quick-stars-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/codecs-data-structures': patch
---

Allow boolean predicates passed to `getPatternMatchCodec` and `getPatternMatchEncoder` to narrow to a subtype of the variant's value type. Previously, matching against codecs whose value type is a union — such as the number codecs, whose encode type is `number | bigint` — forced predicates to be typed against the full union (e.g. `(value: number | bigint) => …`). The predicate parameter is now checked bivariantly, so a narrower predicate like `(value: number) => …` is accepted, mirroring the ergonomics of `getPredicateCodec` and `getPredicateEncoder`.
6 changes: 3 additions & 3 deletions docs/content/docs/advanced-guides/codecs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2575,17 +2575,17 @@ import { getPatternMatchCodec, getU8Codec, getU16Codec, getU32Codec, Codec } fro

const codec: Codec<number> = getPatternMatchCodec([
[
(value: number) => value < 256,
(value: number | bigint) => value < 256,
bytes => bytes.length === 1,
getU8Codec(),
],
[
(value: number) => value < 2 ** 16,
(value: number | bigint) => value < 2 ** 16,
bytes => bytes.length === 2,
getU16Codec(),
],
[
(value: number) => value < 2 ** 32,
(value: number | bigint) => value < 2 ** 32,
bytes => bytes.length <= 4,
getU32Codec(),
],
Expand Down
16 changes: 8 additions & 8 deletions docs/content/docs/advanced-guides/reactive-stores.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A `ReactiveActionStore` wraps an async function as a reactive state machine. Eac

Any Kit RPC request is an action source: call `.reactiveStore()` on it to get a store that re-fires the same request on every `dispatch()`.

```ts
```ts twoslash
import { createSolanaRpc } from '@solana/kit';

const rpc = createSolanaRpc('https://api.devnet.solana.com');
Expand Down Expand Up @@ -51,7 +51,7 @@ Each `dispatch()` aborts the previous in-flight call, so only the most recent di

Use `withSignal` to attach your own cancellation source. A fresh timeout per attempt:

```ts
```ts twoslash
import { createSolanaRpc } from '@solana/kit';

const rpc = createSolanaRpc('https://api.devnet.solana.com');
Expand All @@ -68,7 +68,7 @@ The `subscribe` / `getState` pair works with reactive UI frameworks. Wire the bl
<Tabs items={["Vanilla JS", "Svelte", "Vue"]}>
<Tab value="Vanilla JS">

```ts
```ts twoslash
import { createSolanaRpc } from '@solana/kit';

const rpc = createSolanaRpc('https://api.devnet.solana.com');
Expand Down Expand Up @@ -147,7 +147,7 @@ Another common use case is to call `dispatch` on mount for data loading. React s

`.reactiveStore()` is sugar for RPC requests. For anything else - a `fetch`, your own SDK etc., you can build a store with `createReactiveActionStore`. The wrapped function receives the per-dispatch `AbortSignal` first, then whatever you pass to `dispatch`:

```ts
```ts twoslash
import { createReactiveActionStore } from '@solana/kit';

const store = createReactiveActionStore(async (signal: AbortSignal, accountId: string) => {
Expand All @@ -167,7 +167,7 @@ A `ReactiveStreamStore` holds the latest value from an ongoing stream. Where an

Any Kit RPC subscription is a stream source - call `.reactiveStore()` on it.

```ts
```ts twoslash
import { createSolanaRpcSubscriptions } from '@solana/kit';

const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');
Expand Down Expand Up @@ -203,7 +203,7 @@ The contract is the same `subscribe` / `getState` pair, but it is important to c
<Tabs items={["Vanilla JS", "Svelte", "Vue"]}>
<Tab value="Vanilla JS">

```ts
```ts twoslash
import { createSolanaRpcSubscriptions } from '@solana/kit';

const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');
Expand Down Expand Up @@ -287,7 +287,7 @@ onScopeDispose(() => {

For a stream that is not a Kit subscription, you can build a store with `createReactiveStoreFromDataPublisherFactory`. You give it a factory that produces a fresh `DataPublisher` on every `connect()`, plus the channel names to read data and errors from. The factory receives the per-connection `AbortSignal`; thread it into the transport so the connection itself tears down on reset, not just the store's listeners.

```ts
```ts twoslash
import { createReactiveStoreFromDataPublisherFactory } from '@solana/kit';

const store = createReactiveStoreFromDataPublisherFactory<string>({
Expand Down Expand Up @@ -331,7 +331,7 @@ A common pattern is "load a value, then keep it current" - fetch an account bala

`createReactiveStoreWithInitialValueAndSlotTracking` solves exactly this. It pairs an action source (the one-shot read) with a stream source (the live updates) and deduplicates the two by slot, so the store always holds the value observed at the highest slot. The result is an ordinary `ReactiveStreamStore` - `connect()` to start, and bind it with the same `subscribe` / `getState` pattern as any stream store. This is the primitive behind React's [`useTrackedData`](/docs/guides/react/core-hooks#usetrackeddata).

```ts
```ts twoslash
import {
address,
createReactiveStoreWithInitialValueAndSlotTracking,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ These utilities are **included within the `@solana/kit` library** but you may al

[`decodeTransactionFromRpcResponse`](/api/functions/decodeTransactionFromRpcResponse) turns a `getTransaction` response into a [`DecodedRpcTransaction`](/api/type-aliases/DecodedRpcTransaction): the `compiledMessage` (always carrying the recent blockhash in `lifetimeToken`), the `loadedAddresses` pulled from `meta`, and — for `base64` and `base58` encodings only — a re-encodable `transaction`.

```ts
```ts twoslash
import { createSolanaRpc, signature } from '@solana/kit';
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const txid = '5pX...';
Expand Down Expand Up @@ -63,7 +63,7 @@ Prefer `encoding: 'base64'` when bandwidth allows — it is the most compact, th

Here we tally every USDC transfer in a transaction — outer instructions and inner CPI alike — using the `@solana-program/token` client to parse each `TransferChecked` and filter by mint:

```ts
```ts twoslash
import { createSolanaRpc, signature } from '@solana/kit';
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const txid = '5pX...';
Expand Down Expand Up @@ -128,7 +128,7 @@ Each entry carries a `trace` property typed as [`InstructionTrace`](/api/type-al

The same pattern works with any codama-generated `@solana-program/*` client. Swap in `@solana-program/system` to tally lamports moved by every `TransferSol`, outer or inner:

```ts
```ts twoslash
import {
CompiledTransactionMessage,
CompiledTransactionMessageWithLifetime,
Expand Down Expand Up @@ -178,7 +178,7 @@ If you do not need the interleaved outer-and-inner ordering, the lower-level hel

[`getInstructionsFromCompiledTransactionMessage`](/api/functions/getInstructionsFromCompiledTransactionMessage) returns just the outer instructions as [`ResolvedInstruction`](/api/type-aliases/ResolvedInstruction)s. [`getInnerInstructionsFromMeta`](/api/functions/getInnerInstructionsFromMeta) returns just the inner instructions (decoding their base58 data and resolving indices against a supplied `AccountMeta` list). [`getAccountMetasFromCompiledTransactionMessage`](/api/functions/getAccountMetasFromCompiledTransactionMessage) builds the ordered `AccountMeta` list both rely on.

```ts
```ts twoslash
import {
CompiledTransactionMessage,
CompiledTransactionMessageWithLifetime,
Expand Down
12 changes: 6 additions & 6 deletions docs/content/docs/guides/react/core-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Every hook on this page must be rendered under a [`ClientProvider`](/docs/guides

Reads the Kit client published by the nearest `ClientProvider`. It defaults to the base `Client` shape; when you know a plugin is installed, narrow the type through the generic. This is a pure type-cast with no runtime check - reach for `useClientCapability` instead when a missing plugin should fail. Throws `SOLANA_ERROR__REACT__MISSING_PROVIDER` if no provider is mounted above it.

```tsx
```tsx twoslash
import { ClientWithRpc, GetEpochInfoApi } from '@solana/kit';
import { useClient } from '@solana/react';

Expand All @@ -25,7 +25,7 @@ function FetchEpochButton() {

Reads the client and asserts at mount that a capability is installed, narrowing the return type via the generic. If the capability is absent it throws `SOLANA_ERROR__REACT__MISSING_CAPABILITY`, including the `hookName` and `providerHint` you supply so the mistake is easy to locate. This is the building block for plugin-specific hooks.

```tsx
```tsx twoslash
import { ClientWithRpc, GetEpochInfoApi } from '@solana/kit';
import { useClientCapability } from '@solana/react';

Expand All @@ -48,7 +48,7 @@ Fires a one-shot request on mount and re-fires whenever the source changes ident

Pass either a Kit request source (most commonly a `PendingRpcRequest`) or an `async (signal) => Promise<T>` function to wrap any one-shot async call. Memoize the source (`useMemo`) or function (`useCallback`) on its inputs.

```tsx
```tsx twoslash
import { useMemo } from 'react';
import { ClientWithRpc, GetLatestBlockhashApi } from '@solana/kit';
import { useClient, useRequest } from '@solana/react';
Expand All @@ -72,7 +72,7 @@ The `getAbortSignal` factory runs on every attempt (initial fire and every `refr

Subscribes to a stream source and surfaces the latest notification - no initial fetch. The subscription opens on mount, re-opens when the source changes identity, and tears down on unmount. Use `reconnect()` to re-open manually. Status is `loading`, `loaded`, `error`, or `disabled`.

```tsx
```tsx twoslash
import { useMemo } from 'react';
import { Address, ClientWithRpcSubscriptions, AccountNotificationsApi } from '@solana/kit';
import { useClient, useSubscription } from '@solana/react';
Expand All @@ -99,7 +99,7 @@ Renders a value that loads quickly and then stays live: a one-shot fetch seeds t

Pass a memoized spec with an `initialValueSource` + `initialValueMapper` (the initial fetch) and a `streamSource` + `streamValueMapper` (the subscription).

```tsx
```tsx twoslash
import { useMemo } from 'react';
import {
Address,
Expand Down Expand Up @@ -137,7 +137,7 @@ Reach for `useSubscription` when there is no meaningful "initial value" to fetch

Wraps an arbitrary async function and tracks each invocation through React state. Each `dispatch(...)` runs the function with a fresh `AbortSignal`, dispatching again while a call is in flight aborts the first. Status is `idle`, `running`, `success`, or `error`. Use `dispatch` from event handlers (fire-and-forget, never throws) and `dispatchAsync` when you need the resolved value or to propagate errors.

```tsx
```tsx twoslash
import { useAction } from '@solana/react';

function PostMessageButton({ url, body }: { url: string; body: string }) {
Expand Down
43 changes: 39 additions & 4 deletions docs/content/docs/guides/react/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ const client = createClient().use(generatedSigner()).use(solanaDevnetRpc());

Wrap your app in the provider and pass the client in:

```tsx
```tsx twoslash
import { createClient } from '@solana/kit';
import { solanaDevnetRpc } from '@solana/kit-plugin-rpc';
import { generatedSigner } from '@solana/kit-plugin-signer';
const client = createClient().use(generatedSigner()).use(solanaDevnetRpc());
function Dashboard() {
return null;
}
// ---cut-before---
import { ClientProvider } from '@solana/react';

export function App() {
Expand All @@ -42,7 +50,17 @@ The client reference must be **stable** across renders. Build it at module scope

When a configuration value changes at runtime - a cluster toggle, an RPC URL switch - rebuild the client in `useMemo` keyed on that value and pass the new reference. The subtree re-subscribes against the new client identity.

```tsx
```tsx twoslash
function ClusterToggle(props: {
value: 'devnet' | 'mainnet';
onChange: (value: 'devnet' | 'mainnet') => void;
}) {
return null;
}
function Dashboard() {
return null;
}
// ---cut-before---
import { useMemo, useState } from 'react';
import { createClient } from '@solana/kit';
import { solanaDevnetRpc, solanaMainnetRpc } from '@solana/kit-plugin-rpc';
Expand All @@ -55,7 +73,11 @@ export function App() {
() =>
createClient()
.use(generatedSigner())
.use(cluster === 'mainnet' ? solanaMainnetRpc() : solanaDevnetRpc()),
.use(
cluster === 'mainnet'
? solanaMainnetRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })
: solanaDevnetRpc(),
),
[cluster],
);
return (
Expand All @@ -71,7 +93,20 @@ export function App() {

If any plugin's `.use()` is async, `createClient().use(...)` returns a `Promise<Client>`. Pass the promise straight to `ClientProvider` and it suspends the subtree via the nearest `<Suspense>` boundary until the client resolves. The promise identity must be stable - pass a `useMemo`'d or module-scope value, never an inline `new Promise(...)`.

```tsx
```tsx twoslash
import { createClient } from '@solana/kit';
import { solanaDevnetRpc } from '@solana/kit-plugin-rpc';
import { generatedSigner } from '@solana/kit-plugin-signer';
async function createClientWithAsyncPlugins() {
return createClient().use(generatedSigner()).use(solanaDevnetRpc());
}
function Dashboard() {
return null;
}
function Splash() {
return null;
}
// ---cut-before---
import { Suspense, useMemo } from 'react';
import { ClientProvider } from '@solana/react';

Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/guides/react/query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ There's a Query variant for each core read hook. All three take a cache `key` up

The TanStack Query-backed counterpart to `useRequest`. It takes a query `key`, the same source shape as `useRequest`, and any `useQuery` options. It returns TanStack's own `useQuery` result, so `data`, `error`, `isLoading`, and `refetch` behave as they do everywhere else.

```tsx
```tsx twoslash
import { ClientWithRpc, GetLatestBlockhashApi } from '@solana/kit';
import { useClient } from '@solana/react';
import { useRequestQuery } from '@solana/react/query';
Expand Down Expand Up @@ -51,7 +51,7 @@ In addition to TanStack's `enabled: false`, you can pass `null` for `source` to

The counterpart to `useSubscription`, for a long-lived stream with no one-shot fetch. It takes a `key` and the same source shape as `useSubscription`, routes the stream through TanStack Query's cache (via `experimental_streamedQuery`), and returns a `UseQueryResult`. `data` is the raw notification exactly as the source emits it.

```tsx
```tsx twoslash
import { ClientWithRpcSubscriptions, SlotNotificationsApi } from '@solana/kit';
import { useClient } from '@solana/react';
import { useSubscriptionQuery } from '@solana/react/query';
Expand All @@ -76,7 +76,7 @@ By default `retry`, `staleTime`, and `refetchOnWindowFocus` are tuned for a long

The counterpart to `useTrackedData`: a one-shot fetch seeds the value and a subscription keeps it live, with the unified stream routed through TanStack Query's cache. It takes a `key` and the same `TrackedDataSpec` as `useTrackedData`. `data` is the `SolanaRpcResponse<TItem>` envelope, so read `data.value` and `data.context.slot` directly.

```tsx
```tsx twoslash
import {
Address,
ClientWithRpc,
Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/guides/react/swr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ There's an SWR variant for each core read hook, each taking a cache `key` up fro

The SWR-backed counterpart to `useRequest`. It takes an SWR `key`, the same source shape as `useRequest`, and any SWR configuration. It returns SWR's `SWRResponse`, so `data`, `error`, `isLoading`, and `mutate` behave exactly as they do everywhere else.

```tsx
```tsx twoslash
import { ClientWithRpc, GetLatestBlockhashApi } from '@solana/kit';
import { useClient } from '@solana/react';
import { useRequestSWR } from '@solana/react/swr';
Expand All @@ -49,7 +49,7 @@ Pass `null` for the `key` or the `source` to disable the request. Call `mutate()

The counterpart to `useSubscription`, for a long-lived stream with no one-shot fetch. It takes a `key` and the same source shape as `useSubscription`, and routes the stream through SWR's subscription cache (`useSWRSubscription`). It returns SWR's `SWRSubscriptionResponse`. `data` is the raw notification exactly as the source emits it.

```tsx
```tsx twoslash
import { ClientWithRpcSubscriptions, SlotNotificationsApi } from '@solana/kit';
import { useClient } from '@solana/react';
import { useSubscriptionSWR } from '@solana/react/swr';
Expand All @@ -74,7 +74,7 @@ When `key` flips to `null` the `data` is cleared, unlike core `useSubscription`

The counterpart to `useTrackedData`: a one-shot fetch seeds the value and a subscription keeps it live, with the unified stream routed through SWR's subscription cache. It takes a `key` and the same `TrackedDataSpec` as `useTrackedData`. Like `useSubscriptionSWR` it returns an `SWRSubscriptionResponse`, but `data` is the `SolanaRpcResponse<TItem>` envelope, so read `data.value` and `data.context.slot` directly.

```tsx
```tsx twoslash
import {
Address,
ClientWithRpc,
Expand Down
16 changes: 9 additions & 7 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,26 @@
"@solana-program/memo": "^0.11.2",
"@solana-program/system": "^0.12.2",
"@solana-program/token": "^0.14.0",
"@solana/compat": "6.10.0",
"@solana/kit": "6.10.0",
"@solana/compat": "7.0.0",
"@solana/kit": "7.0.0",
"@solana/kit-plugin-litesvm": "^0.10.0",
"@solana/kit-plugin-rpc": "^0.11.0",
"@solana/kit-plugin-signer": "^0.10.0",
"@solana/react": "6.10.0",
"@solana/wallet-account-signer": "6.10.0",
"@solana/react": "7.0.0",
"@solana/wallet-account-signer": "7.0.0",
"@solana/web3.js": "^1.98.4",
"@solana/webcrypto-ed25519-polyfill": "6.10.0",
"@wallet-standard/ui": "^1.0.2",
"@solana/webcrypto-ed25519-polyfill": "7.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.101.0",
"@types/mdx": "^2.0.13",
"@types/node": "25.4.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@wallet-standard/ui": "^1.0.2",
"eslint": "^10",
"eslint-config-next": "^16.1.6",
"postcss": "^8.5.14",
"swr": "^2.4.1",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vercel": "^52.2.0"
Expand All @@ -62,4 +64,4 @@
}
},
"packageManager": "pnpm@10.4.1"
}
}
Loading
Loading