@solana/kit
v7.0.0 (2026-06-30)
Major Changes
-
[
@solana/codecs-data-structures] #168399667a1Thanks @oritwoen! - Align the return types of the union, predicate, and pattern-match codecs so that a fixed-size result is only exposed when every branch is fixed-size and of the same statically-known size. Branches whose sizes are unequal or not statically known now widen fromFixedSize*toVariableSize*(when at least one branch is variable-size) or to a plainEncoder/Decoder/Codec(when the branches are fixed-size but their sizes are not statically comparable), matching what these codecs actually produce at runtime.This is a type-only change with no runtime impact, but it is breaking in two ways:
- Return types change. Consumers that relied on the previous (unsound) fixed-size typing — e.g. reading
.fixedSize, or passing the result where aFixedSize*is required — will need to adjust. - The predicate and pattern-match signatures changed. Their value/output type is now inferred from the branch encoders, decoders, or codecs rather than from an explicit type argument. Passing an explicit type argument no longer sets the branch domain:
getPatternMatchEncoder<MyType>([...])(and the decoder/codec equivalents) now fails to compile, andgetPredicateEncoder<MyType>(...)silently degrades its return to a plainEncoder<MyType>. Instead, type the branch predicates' parameters (e.g.(value: MyType) => ...) and let the return type be inferred.
- Return types change. Consumers that relied on the previous (unsound) fixed-size typing — e.g. reading
-
[
@solana/instruction-plans] #1723069d56dThanks @mcintyre94! - Add configurable instruction-count limits to transaction planners and message packers, and default planned and packed transaction messages to 16 instructions. The planner limit applies to the final transaction message, including instructions returned bycreateTransactionMessageor added byonTransactionMessageUpdated, and can be overridden when creating a planner or for an individual planning call.This is useful because Solana limits transactions to 64 instructions, including inner instructions. Kit does not know how many inner instructions each instruction will require when executed. The default of 16 assumes an average of 3 additional inner instructions per top-level instruction.
When a transaction message reaches this configured ceiling, the planner and message packer throw the new
SOLANA_ERROR__INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDEDerror rather than theSOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONSerror reserved for the hard 64-instruction limit, so the configurable soft limit is distinguishable from the format-enforced one. ThrowsSOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTIONis the configured max is invalid (not a positive integer, or greater than 64).Configure a maximum for every plan created by a transaction planner:
const transactionPlanner = createTransactionPlanner({ createTransactionMessage, maxInstructionsPerTransaction: 32, });
Override the maximum for an individual planning request:
const transactionPlan = await transactionPlanner(instructionPlan, { maxInstructionsPerTransaction: 8, });
Override the maximum when packing a message directly:
const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage, { maxInstructions: 32, });
BREAKING CHANGES
Transaction planners and message packers now default to 16 instructions per transaction. Plans and direct message packer calls that previously fit 17 to 64 top-level instructions in one transaction message may now be split into multiple transaction messages. Apps that depend on larger single-transaction plans can preserve the previous top-level instruction limit by configuring
maxInstructionsPerTransaction: 64on transaction planners ormaxInstructions: 64on direct message packer calls; the hard transaction-message limit of 64 top-level instructions still applies.const transactionPlanner = createTransactionPlanner({ createTransactionMessage, + maxInstructionsPerTransaction: 64, });-const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage); +const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage, { maxInstructions: 64 });
-
[
@solana/kit,@solana/rpc-subscriptions-spec,@solana/subscribable] #1663d09718dThanks @mcintyre94! - AddwithSignal()toReactiveStreamStorefor per-connection cancellation, replacing the construction-timeabortSignaloption. Mirrors the action store's per-dispatchwithSignal()pattern — callers attach a per-connection signal at the call site instead of baking one into the store.const store = createReactiveStoreFromDataPublisherFactory({ createDataPublisher: signal => transport({ signal, ...plan }), dataChannelName: 'notification', errorChannelName: 'error', }); // Per-connection timeout — fresh clock per attempt: store.withSignal(AbortSignal.timeout(30_000)).connect();
store.withSignal(signal)returns a thin wrapper exposingconnect()that composes the caller-provided signal with the per-connection inner controller viaAbortSignal.any. Aborting the caller's signal surfaces the abort reason on state as{ status: 'error' }; supersession via the internal controller (a newerconnect()orreset()) stays silent so the newer call owns state. The "permanent kill switch" pattern is expressible by binding once:const killable = store.withSignal(killCtrl.signal); killable.connect();. AfterkillCtrl.abort(), everykillable.connect()short-circuits to error.createDataPublisheris widened from() => Promise<DataPublisher>to(signal: AbortSignal) => Promise<DataPublisher>. The store passes the composed per-connection signal to the factory so the underlying transport can stop on per-connection abort, not just the stream-store's listeners. Existing no-arg factories still satisfy the new shape — TypeScript allows fewer parameters than the declared type.The construction-time
abortSignaloption oncreateReactiveStoreFromDataPublisherFactory,createReactiveStoreWithInitialValueAndSlotTracking, andPendingRpcSubscriptionsRequest.reactiveStore()is removed. Callers wanting a long-lived kill switch use the bind-oncewithSignalpattern.ReactiveStreamSource<T>.reactiveStore()is now parameter-less (mirrorsReactiveActionSource<T>.reactiveStore()). -
[
@solana/kit,@solana/rpc-subscriptions-spec,@solana/subscribable] #1662fa04323Thanks @mcintyre94! - Drop auto-connect fromReactiveStreamStore; callers explicitly invokeconnect()to open the underlying stream. Mirrors the action store's caller-drivendispatch()pattern — the store is a state machine that callers orchestrate, not a self-starting subscription.The factory variant returned by
createReactiveStoreFromDataPublisherFactorynow starts instatus: 'idle'. Callstore.connect()to open the stream; fromidle, the store transitions throughloading→loaded(orerror). A subsequentconnect()from any non-idle status transitions throughretryingwhile preserving the last known value. A newreset()method aborts the current connection and returns the store toidlewithout permanently killing it — natural for React effect cleanup.const store = createReactiveStoreFromDataPublisherFactory({ abortSignal, createDataPublisher, dataChannelName: 'notification', errorChannelName: 'error', }); store.connect(); // opens the stream — previously this happened on construction
retry()is now deprecated; it remains as an error-only alias forconnect(). Migrate to callingconnect()directly. Code that previously relied onretry()being a no-op when the store was not inerrorstate should add an explicitif (status === 'error') store.connect();guard at the call site.createReactiveStoreFromDataPublisher(the deprecated non-factory variant accepting a ready-madeDataPublisher) is removed. Its only documented use was as a backwards-compatibility alias behindPendingRpcSubscriptionsRequest.reactive(), which is also removed in this release. Migrate to the factory variant — wrap a ready-made publisher in() => Promise.resolve(publisher)if needed — and usereactiveStore()for RPC subscriptions.createReactiveStoreWithInitialValueAndSlotTrackingin@solana/kitno longer fires the RPC request on construction — callstore.connect()to start it, or wrap in auseEffectthat callsconnect()on mount andreset()on cleanup. The store starts instatus: 'idle'and follows the same lifecycle as the underlying stream store. -
[
@solana/kit] #170803000e5Thanks @mcintyre94! -createReactiveStoreWithInitialValueAndSlotTrackingnow consumes its two inputs as reactive sources rather than as request objects it callssend()/subscribe()on directly. TherpcRequest/rpcSubscriptionRequestconfig fields (and theirrpcValueMapper/rpcSubscriptionValueMapper) are replaced byinitialValueSource: ReactiveActionSource<...>/streamSource: ReactiveStreamSource<...>(withinitialValueMapper/streamValueMapper).Each source is consumed via its
reactiveStore()method, so the helper reusesReactiveActionStore/ReactiveStreamStoreprimitives.PendingRpcRequestsatisfiesReactiveActionSourceandPendingRpcSubscriptionsRequestsatisfiesReactiveStreamSource, so callers can still pass eg.rpc.getBalance(addr)/rpcSubscriptions.accountNotifications(addr)results directly.const balanceStore = createReactiveStoreWithInitialValueAndSlotTracking({ initialValueSource: rpc.getBalance(myAddress, { commitment: 'confirmed' }), initialValueMapper: lamports => lamports, streamSource: rpcSubscriptions.accountNotifications(myAddress), streamValueMapper: ({ lamports }) => lamports, }); balanceStore.withSignal(AbortSignal.timeout(60_000)).connect();
-
[
@solana/kit,@solana/subscribable] #1677a198b5cThanks @mcintyre94! - Collapseloadingandretryinginto a singleloadingstatus onReactiveStreamStore, mirroring the action store'srunning(which is itself the merged "first call vs subsequent call" state).dataanderrorare preserved throughloadingfor stale-while-revalidate — UI can render the prior outcome alongside an in-flight reconnect.ReactiveState<T>drops theretryingvariant.loadingwidens from{ data: undefined, error: undefined }to{ data: T | undefined, error: unknown }. BothcreateReactiveStoreFromDataPublisherFactoryandcreateReactiveStoreWithInitialValueAndSlotTrackingnow transition everyconnect()throughloading(preservingcurrentState.dataandcurrentState.error); a subsequentloadedclearserror, a subsequenterrorreplaces it.// Previously: { status: 'error', data: lastValue, error: caughtError } // connect() → { status: 'retrying', data: lastValue, error: undefined } // error cleared, separate status // Now: { status: 'error', data: lastValue, error: caughtError } // connect() → { status: 'loading', data: lastValue, error: caughtError } // error preserved, unified status
Migration: replace
status === 'retrying'checks withstatus === 'loading' && data !== undefined(or juststatus === 'loading'if you don't need to distinguish first-load vs reconnect — the SWR pattern lets you render whatever is indataregardless). -
[
@solana/kit,@solana/plugin-core] #17866947740Thanks @mcintyre94! - Remove deprecatedgetMinimumBalanceForRentExemptionandcreateEmptyClient.BREAKING CHANGES
Removed
getMinimumBalanceForRentExemptionfrom@solana/kit. The minimum balance for an account is being actively reduced (see SIMD-0437) and is expected to become dynamic in future Solana upgrades (see SIMD-0194 and SIMD-0389), so a hardcoded local computation can no longer return accurate results. Use thegetMinimumBalanceForRentExemptionRPC method or aClientWithGetMinimumBalanceplugin instead.- import { getMinimumBalanceForRentExemption } from '@solana/kit'; - const rentExemptLamports = getMinimumBalanceForRentExemption(82n); + const { value: rentExemptLamports } = await rpc.getMinimumBalanceForRentExemption(82n).send();
Removed
createEmptyClientfrom@solana/plugin-core. UsecreateClient, which behaves identically and additionally accepts an optional initial value.- import { createEmptyClient } from '@solana/plugin-core'; - const client = createEmptyClient(); + import { createClient } from '@solana/plugin-core'; + const client = createClient();
-
[
@solana/kit,@solana/react,@solana/subscribable] #1780acec0beThanks @mcintyre94! - Streamline theReactiveStreamStorecontract by removing deprecated members and unifying its state accessor withReactiveActionStore. ThegetUnifiedState()method has been renamed togetState(), and the deprecated value-onlygetState(),getError(), andretry()members along with theReactiveStoretype alias have been removed.BREAKING CHANGES
getUnifiedState()renamed togetState(). The unified{ data, error, status }snapshot accessor is now simplygetState(), matchingReactiveActionStore.getState().- const state = useSyncExternalStore(store.subscribe, store.getUnifiedState); + const state = useSyncExternalStore(store.subscribe, store.getState);
Removed the deprecated value-only
getState()andgetError(). Read the value and error off the unified snapshot instead.- const data = store.getState(); - const error = store.getError(); + const { data, error } = store.getState();
Removed
retry(). Useconnect(), which always (re)connects regardless of status. Wrap it with a status guard if you need the error-only behavior.- store.retry(); + if (store.getState().status === 'error') store.connect();
Removed the
ReactiveStoretype alias. UseReactiveStreamStoredirectly.- import type { ReactiveStore } from '@solana/subscribable'; + import type { ReactiveStreamStore } from '@solana/subscribable';
-
[
@solana/rpc-api,@solana/rpc-parsed-types,@solana/rpc-transformers] #17958d3bbf1Thanks @mcintyre94! - Update RPC and parsed-account types to match Agave 4.1.0, and surface basis-points commission and vote-latency fields as numbers instead of bigints.BREAKING CHANGES
Parsed vote-account commissions and vote latency are now
numberinstead ofbigint. Agave returns these as small bounded integers (u16/u8), so kit no longer upcasts them. This affectsblockRevenueCommissionBps,inflationRewardsCommissionBps, and each vote'slatencyonJsonParsedVoteAccount.- const bps: bigint = voteAccount.inflationRewardsCommissionBps; + const bps: number = voteAccount.inflationRewardsCommissionBps;
The parsed rent sysvar is now a union of the current and pre-4.1.0 shapes. Agave 4.1.0 reshaped the rent sysvar from
{ burnPercent, exemptionThreshold, lamportsPerByteYear }to{ lamportsPerByte }. The type is now a union of both, so consumers must narrow before accessing the legacy fields. Narrow on the presence oflamportsPerByte(current) versuslamportsPerByteYear(deprecated).- const perByteYear = rent.info.lamportsPerByteYear; + const perByte = 'lamportsPerByte' in rent.info + ? rent.info.lamportsPerByte + : rent.info.lamportsPerByteYear;
warmupCooldownRateon the parsed stake delegation is now optional. Agave 4.1.0 removed it from the parsed output, so it is only present on accounts fetched from validators running earlier versions. It is marked@deprecated.Additionally,
getVoteAccountsnow includes an optionalinflationRewardsCommissionBpsfield (added by Agave 4.1.0; absent on older validators), and the parsed stake-config account fields (slashPenalty,warmupCooldownRate) are marked@deprecatedbecause the stake config program is no longer recognized by the RPC's JSON parser as of Agave 4.1.0 (such accounts now fall back to annotated base64).The GraphQL
SysvarRentAccounttype gains a nullablelamportsPerBytefield to match the reshaped rent sysvar in Agave 4.1.0. The legacyburnPercent,exemptionThreshold, andlamportsPerByteYearfields are retained (and remain nullable) for validators running earlier versions. -
[
@solana/rpc-api] #1803cab6d7eThanks @mcintyre94! - Always surfacereplacementBlockhashon thesimulateTransactionresponse. Agave v3.x validators unconditionally include this field, setting it tonullwhenreplaceRecentBlockhashwas nottrue. The field now lives on the base response type asTransactionBlockhashLifetime | null, and is narrowed to a non-nullTransactionBlockhashLifetimeonly on the overloads wherereplaceRecentBlockhash: true.BREAKING CHANGES
replacementBlockhashis now always present on thesimulateTransactionresponse. Previously the field was only present on the type whenreplaceRecentBlockhashwastrue. It is now always present, typed asTransactionBlockhashLifetime | null, withnullindicating that no blockhash was replaced. Code that relied on the field being absent (e.g. to discriminate the response shape) must instead check fornull.const { value } = await rpc.simulateTransaction(tx, { encoding: 'base64' }).send(); - // `value.replacementBlockhash` was not present on the type + // `value.replacementBlockhash` is now `TransactionBlockhashLifetime | null` (null here) -
[
@solana/rpc-spec] #1628a6783e0Thanks @mcintyre94! -PendingRpcRequest.reactiveStore()no longer auto-fires the request on creation. It now returns aReactiveActionStorein theidlestate; the caller is responsible for the initialdispatch().This brings
reactiveStore()in line withcreateReactiveActionStore(fn)(which also does not auto-fire) and removes the special-case at the start of the store's lifecycle. The previous auto-fire created an asymmetry around per-attempt cancellation: the initial request had no caller-visible dispatch site, so attaching anAbortSignalto that one specific attempt required a separate option distinct from the mechanism for all later attempts. Without auto-fire, every dispatch is the caller's, and signal attachment is uniform.Migration:
// Before: const store = rpc.getAccountInfo(address).reactiveStore(); // request was already in flight // After: const store = rpc.getAccountInfo(address).reactiveStore(); store.dispatch(); // request is now in flight
Minor Changes
-
[
@solana/codecs-data-structures] #1731ec4d3efThanks @kh0ra! - AddcreateDependentStructDecoder, a fluent builder for a struct decoder whose later fields may depend on the decoded values of earlier ones. Each call tofieldadds a name and either a staticDecoderor a factory that receives a frozen snapshot of the fields decoded so far. Callingbuildproduces aFixedSizeDecoderwhen every field added to the builder is itself aFixedSizeDecoder, and aVariableSizeDecoderotherwise.This is useful for binary formats where a count, version, or discriminator that appears near the start of the struct controls how a later field must be parsed, such as the per-instruction headers in a v1 transaction message.
const decoder = createDependentStructDecoder() .field('count', getU8Decoder()) .field('values', fields => getArrayDecoder(getU32Decoder(), { size: fields.count })) .build();
-
[
@solana/errors] #17193014977Thanks @mcintyre94! - Add theSOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERRORerror code.useSubscriptionSWRnow surfaces thisSolanaErrorwhen the underlying store reaches an error state without an error value (e.g. aDataPublisheremittingundefinedon its error channel, orcontroller.abort(null)), instead of passing the nullish value to SWR'snext— which would be treated as a success and silently wipe the cached data. -
[
@solana/errors,@solana/kit,@solana/rpc-api,@solana/transaction-introspection] #1611772b82cThanks @amilz! - Add@solana/transaction-introspection, a new package that bridges agetTransactionresponse and the auto-generated@solana-program/*parseXInstructionclients. Decodes the transaction (encoding: 'base64','base58', or'json'), resolves account indices against static + ALT-loaded addresses, normalizes inner instructions frommeta.innerInstructions, and exposeswalkInstructionsto enumerate every instruction in display order — each outer instruction followed by its inner instructions — with atracerecording its location. Each returned instruction is aResolvedInstruction & { trace }directly usable withisInstructionForProgramfrom@solana/instructionsand with the auto-generatedidentifyXInstruction/parseXInstructionhelpers. Supportslegacy,v0, andv1compiled transaction messages. Re-exported from@solana/kit.import { createSolanaRpc, signature } from '@solana/kit'; import { isInstructionForProgram } from '@solana/instructions'; import { decodeTransactionFromRpcResponse, walkInstructions } from '@solana/transaction-introspection'; import { identifyTokenInstruction, TOKEN_PROGRAM_ADDRESS, TokenInstruction } from '@solana-program/token'; const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com'); const rpcTx = await rpc .getTransaction(signature(txid), { commitment: 'confirmed', encoding: 'base64', maxSupportedTransactionVersion: 0, }) .send(); if (!rpcTx) throw new Error(`Transaction ${txid} not found`); const { compiledMessage, loadedAddresses } = decodeTransactionFromRpcResponse(rpcTx); for (const ix of walkInstructions({ compiledMessage, loadedAddresses, meta: rpcTx.meta })) { if (!isInstructionForProgram(ix, TOKEN_PROGRAM_ADDRESS)) continue; if (identifyTokenInstruction(ix) === TokenInstruction.SyncNative) { console.log('SyncNative found at', ix.trace); } }
@solana/rpc-apinow exports the non-nullgetTransactionresponse shapes as named types (GetTransactionApiResponseBase58,GetTransactionApiResponseBase64,GetTransactionApiResponseJson,GetTransactionApiResponseJsonParsed), whichdecodeTransactionFromRpcResponseaccepts as inputs.@solana/errorsgainsSOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGEplus a newTRANSACTION_INTROSPECTIONdomain (SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION,SOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE). -
[
@solana/errors,@solana/react] #1607e193711Thanks @mcintyre94! - AddClientProvider,useClient, anduseClientCapability— the Kit client context layer for React.ClientProviderpublishes a caller-owned Kit client to its subtree. Required byuseClient,useClientCapability, and any plugin-specific hook that depends on a client capability — generic primitives likeuseActionwork against arbitrary async functions and don't need a provider. The provider accepts both synchronous clients and promise-returning ones — when given a promise (e.g.createClient().use(asyncPlugin())), it suspends via the nearest<Suspense>boundary until the client resolves. On React 19 it delegates toReact.use(promise); on React 18 an internal thrown-promise shim, keyed by promise identity, honours the same contract.useClient<TClient>()is the basic context accessor. Defaults to the baseClientshape; callers who know a specific plugin is installed may widen the type via the generic. Throws a newSolanaErrorwith codeSOLANA_ERROR__REACT__MISSING_PROVIDERwhen called outside a provider.useClientCapability<TClient>({ capability, hookName, providerHint })runtime-checks that the requested capability (or capabilities) is installed on the client and throwsSOLANA_ERROR__REACT__MISSING_CAPABILITY— surfacing the callinghookNameand aproviderHint— when it isn't. Plugin-hook authors use this to fail loudly at mount instead of letting a missing plugin surface later asundefined.Two new error codes (
SOLANA_ERROR__REACT__MISSING_PROVIDER,SOLANA_ERROR__REACT__MISSING_CAPABILITY) are reserved in the[9000000-9000999]range. -
[
@solana/kit] #17069063658Thanks @mcintyre94! - Migrate@solana/reactto depend on@solana/kitas a peer dependency (replacing its individual workspace sub-package deps) and re-export@solana/subscribablefrom@solana/kitso React consumers have a single import root.@solana/promisesremains as a direct dep — it's a small utility that isn't part of Kit's public surface.For
@solana/reactusers:@solana/kitmust now be installed alongside@solana/react.- Apps that already use both get a single deduplicated
@solana/kitinstance — important for anything relying on shared types orinstanceof SolanaErrorchecks. - Kit can be bumped independently of React within the peer range.
For
@solana/kitusers:ReactiveStreamSource,ReactiveStreamStore,ReactiveActionSource,ReactiveActionStore,ReactiveState,createReactiveActionStore,createReactiveStoreFromDataPublisherFactory,DataPublisherand the rest of@solana/subscribable's surface are now reachable directly through@solana/kit.
-
[
@solana/react] #161208777cfThanks @mcintyre94! - AdduseAction— a React hook that bridges any async function into a tracked action withdispatch/dispatchAsync/status/data/error/resetand supersede-on-second-call semantics. Built oncreateReactiveActionStorefrom@solana/subscribable.The wrapped function receives a fresh
AbortSignalper dispatch.dispatch(...)is fire-and-forget — it returnsvoid, never throws, and is the variant to wire into UI event handlers, with outcomes read offstatus/data/error.dispatchAsync(...)returns a promise for imperative callers that need the resolved value. Calling either again while a prior call is in flight aborts the first; awaiters of a supersededdispatchAsynccall see a rejection with anAbortErrorfilterable viaisAbortErrorfrom@solana/promises.datafrom a priorsuccesspersists through subsequentrunningstates for stale-while-revalidate UX; onlyreset()clears it.fnis held in a ref synced to the latest render's closure, so values it captures (form state, route params, etc.) are always fresh on each new dispatch without the caller needing to maintain adepsarray. In-flight calls are unaffected — they continue with the closure they captured at dispatch time. Matches the convention used byuseMutationin TanStack Query anduseWriteContractin wagmi.The shared
ActionResult<TArgs, TResult>type is also exported so plugin hooks can declare their return shape against it. -
[
@solana/react] #1619fd6bdefThanks @mcintyre94! - AdduseRequest— a React hook for one-shot async reads. Pass either an async function(signal) => Promise<T>or a memoizedReactiveActionSource<T>(satisfied byPendingRpcRequest). The hook fires the call on mount, re-fires whenever the source identity changes, and aborts the in-flight call on cleanup.// `ReactiveActionSource` (e.g. `PendingRpcRequest`): const source = useMemo(() => client.rpc.getLatestBlockhash(), [client]); const { data, error, refresh } = useRequest(source); // Bare async function: const fetcher = useCallback( (signal: AbortSignal) => fetch(`/api/users/${userId}`, { signal }).then(r => r.json()), [userId], ); const { data, error, refresh } = useRequest(fetcher);
The result reports
statusas one offetching | success | error | disabled. A request in flight is alwaysfetching; inspectdataanderrorto know what stale content (if any) is available to render alongside a spinner — first attempt has neither, a refresh after a prior outcome carries one or both forward. Passnullfor the source to gate the request off — useful while inputs aren't yet known. The result then reportsstatus: 'disabled'.Optional
getAbortSignal: () => AbortSignalis a factory invoked on every attempt (initial fire + everyrefresh()). Each attempt gets a fresh signal that's composed with the store's internal per-dispatch controller viaAbortSignal.any. The natural use is per-attempt timeouts:getAbortSignal: () => AbortSignal.timeout(5_000)gives every attempt its own 5-second clock that resets on refresh. The factory is held in a ref synced to the latest render, so inline closures are fine — nouseCallbackneeded.refresh()also accepts an optional{ abortSignal }override to replace the factory for one specific attempt.The new
RequestResult<T>andUseRequestOptionstypes are exported alongside the hook so plugin hooks built on top can declare their return shape against them. -
[
@solana/react] #17193014977Thanks @mcintyre94! - AdduseSubscriptionSWR(key, source, options?)to the@solana/react/swrsubpath — the SWR-backed counterpart touseSubscription. Routes aReactiveStreamSource<T>through SWR's subscription cache (useSWRSubscription).import { useSubscriptionSWR } from '@solana/react/swr'; const { data } = useSubscriptionSWR(['account', address], client.rpcSubscriptions.accountNotifications(address));
datais the notification exactly as the source emits it. Passnullfor eitherkeyorsourceto disable. Options accept SWR's config plusgetAbortSignalfor an abort signal. -
[
@solana/react] #17023a92f37Thanks @mcintyre94! - AdduseSubscription— a React hook for subscription-based live data. Pass aReactiveStreamSource<T>(satisfied byPendingRpcSubscriptionsRequest) and the hook opens the subscription on mount, re-opens whenever the source identity changes, and tears it down on unmount.function AccountBalance({ address }: { address: Address }) { const client = useClient<ClientWithRpcSubscriptions<AccountNotificationsApi>>(); const source = useMemo(() => client.rpcSubscriptions.accountNotifications(address), [client, address]); const { data, error, reconnect } = useSubscription(source); if (error) return <button onClick={reconnect}>Reconnect</button>; return <p>{data ? `${data.value.lamports} lamports at slot ${data.context.slot}` : 'Connecting…'}</p>; }
The result reports
statusas one ofloading | loaded | error | disabled.datais the notification exactly as the source emits it — no unwrapping or reshaping. For RPC subscriptions that emitSolanaRpcResponse<U>(account/program/signature), read the inner value atdata.valueand the slot atdata.context.slot; for raw notifications (slot/logs/root)datais the raw shape. Passnullfor the source to gate the subscription off — useful while inputs aren't yet known. The result then reportsstatus: 'disabled'. After a notification arrives, an error transitions tostatus: 'error'while preserving the staledata;reconnect()returns toloading(preserving staledataanderrorfor stale-while-revalidate) before settling onloadedor a fresherror.Optional
getAbortSignal: () => AbortSignalis a factory invoked on every connection (initial subscribe + everyreconnect()). Each connection gets a fresh signal that the underlying store composes with its per-connection controller viaAbortSignal.any. The natural use is per-connection timeouts:getAbortSignal: () => AbortSignal.timeout(30_000)gives every connection its own 30-second clock that resets on reconnect. The factory is held in a ref synced to the latest render, so inline closures are fine — nouseCallbackneeded.reconnect()also accepts an optional{ abortSignal }override to replace the factory for one specific attempt (presence-based: omit to use the factory,{ abortSignal: signal }to override,{ abortSignal: undefined }to opt out).The hook mirrors
useRequest's structure exactly: construct the lazy store viauseMemo, firestore.connect()in auseEffect, tear down viastore.reset()in cleanup. Same StrictMode-safe lifecycle, same vocabulary, same per-call signal API. SSR-safe — on the server the connect effect doesn't run, so the store staysidleand the hook reportsstatus: 'loading'; first client render hydrates from the same paint and commits the connect.SubscriptionResult<T>andUseSubscriptionOptionsare exported alongside the hook so plugin hooks built on top can declare their return shape against them. -
[
@solana/react] #1713587ec07Thanks @mcintyre94! - Add@solana/react/swrsubpath withuseRequestSWR(key, source, options?)— the SWR-backed counterpart touseRequest. Same source shape (ReactiveActionSource<T>or(signal) => Promise<T>); returns SWR's nativeSWRResponse<T>. Passnullfor eitherkeyorsourceto disable. Requiresswr@^2as an optional peer dependency.import { useRequestSWR } from '@solana/react/swr'; const { data } = useRequestSWR(['epochInfo'], client.rpc.getEpochInfo());
Options accept any
SWRConfigurationfield plus the Kit-onlygetAbortSignal: () => AbortSignal(same option asuseRequest), which threads a per-attempt signal into the source — typically a timeout viaAbortSignal.timeout(). Use SWR'sresult.mutate()to re-fire on demand. -
[
@solana/react] #1707da42ff8Thanks @mcintyre94! - AdduseTrackedData— a React hook for an RPC subscription seeded by a one-shot RPC fetch, slot-deduped. The subscription (e.g.accountNotifications) is the primary source of live updates; the initial fetch (e.g.getBalance,getAccountInfo) provides a value to surface as soon as it resolves — typically before the first subscription notification arrives — so theloadingpaint is shorter than subscription-only would give you. Surfaces a unified{ data, error, refresh, status }view wheredatais theSolanaRpcResponse<TItem>envelope that the underlying kit primitive emits — the primitive's type guarantees the envelope shape, so callers can readdata.valueanddata.context.slotdirectly without a runtime check. The underlying store slot-dedupes between the two sources — out-of-order arrivals never regress the surfaced value (older slots are dropped silently, so a stale RPC response can't overwrite a fresher subscription notification).function AccountBalance({ address }: { address: Address }) { const client = useClient<ClientWithRpc<GetBalanceApi> & ClientWithRpcSubscriptions<AccountNotificationsApi>>(); const spec = useMemo( () => ({ rpcRequest: client.rpc.getBalance(address), rpcSubscriptionRequest: client.rpcSubscriptions.accountNotifications(address), rpcValueMapper: (lamports: bigint) => lamports, rpcSubscriptionValueMapper: ({ lamports }: { lamports: bigint }) => lamports, }), [client, address], ); const { data, error, refresh } = useTrackedData(spec); if (error) return <button onClick={refresh}>Retry</button>; return <p>{data ? `${data.value} lamports at slot ${data.context.slot}` : 'Loading…'}</p>; }
The result reports
statusas one ofloading | loaded | error | disabled. Passnullfor the spec to gate the work off — useful while inputs aren't yet known (e.g. anaddressthat hasn't been selected). After a notification arrives, an error transitions tostatus: 'error'while preserving the staledata(envelope intact);refresh()re-runs both the initial RPC and the subscription, returnsstatustoloading(preserving staledataanderrorfor stale-while-revalidate), and settles onloadedor a fresherror.Optional
getAbortSignal: () => AbortSignalis a factory invoked on every attempt (initial run + everyrefresh()). Each attempt gets a fresh signal that the underlying store composes with its per-attempt controller viaAbortSignal.any. The natural use is per-attempt timeouts:getAbortSignal: () => AbortSignal.timeout(30_000)gives every attempt its own 30-second clock that resets on refresh. The factory is held in a ref synced to the latest render, so inline closures are fine — nouseCallbackneeded.refresh()also accepts an optional{ abortSignal }override to replace the factory for one specific attempt (presence-based: omit to use the factory,{ abortSignal: signal }to override,{ abortSignal: undefined }to opt out).The hook is built on
createReactiveStoreWithInitialValueAndSlotTrackingfrom@solana/kit— the slot tracking, abort plumbing, and stale-while-revalidate behaviour live one layer down. The React surface reduces touseSyncExternalStoreglue plus the per-attempt signal API. The Kit primitive's config type is re-shaped asTrackedDataSpec<TRpcValue, TSubscriptionValue, TItem>for friendlier use-site naming; the two are mutually assignable. SSR-safe — on the server the connect effect doesn't run, so the store staysidleand the hook reportsstatus: 'loading'; first client render hydrates from the same paint and commits the connect.TrackedDataResult<T>,TrackedDataSpec<TRpc, TSub, T>, andUseTrackedDataOptionsare exported alongside the hook for plugin hooks built on top. -
[
@solana/react] #1727c32a0f7Thanks @mcintyre94! - AdduseTrackedDataSWR(key, spec, options?)to the@solana/react/swrsubpath — the SWR-backed counterpart touseTrackedData. Takes the sameTrackedDataSpecand routes the unified, slot-deduped stream through SWR'suseSWRSubscription.import { useTrackedDataSWR } from '@solana/react/swr'; const { data } = useTrackedDataSWR(['balance', address], spec); // data is `SolanaRpcResponse<TItem> | undefined`
datais shapeSolanaRpcResponse<TItem>, because this hook requires the slot for de-duping. Mirrors coreuseTrackedData. Passnullfor eitherkeyorspecto disable. Options accept SWR's config plusgetAbortSignalfor a custom abort signal. -
[
@solana/react] #1769205af00Thanks @mcintyre94! - AdduseTrackedDataQueryto the@solana/react/querysubpath. This is the TanStack Query-backed counterpart touseTrackedData: it pairs a one-shot RPC fetch with an ongoing subscription (slot-deduped) and routes the unified stream through TanStack Query's cache viaexperimental_streamedQuery, surfacing theSolanaRpcResponse<TItem>envelope asdata. Slot dedupe spans the cache, so arefetch()'s fresh store cannot regress the cached envelope to an older slot from a lagging RPC node. -
[
@solana/react] #17591032a79Thanks @mcintyre94! - Add a@solana/react/querysubpath that bridges Kit's reactive primitives into TanStack Query. The newuseRequestQuery(key, source, options?)hook is the TanStack Query-backed counterpart touseRequest— it accepts the sameReactiveActionSource<T>or(signal: AbortSignal) => Promise<T>source shape, routes it through TanStack's cache, and threads the query's cancellation signal (combined with the optionalgetAbortSignalfactory) into the source. Pass anullsource to disable the query (mapped to TanStack'senabled: false).@tanstack/react-query@^5is an optional peer dependency. -
[
@solana/react] #1760251b361Thanks @mcintyre94! - AdduseSubscriptionQuery(key, source, options?)to the@solana/react/querysubpath — the TanStack Query-backed counterpart touseSubscription, for streams with no one-shot RPC fetch. It routes a long-lived stream through TanStack Query's cache viaexperimental_streamedQuery, so components reading the samekeyshare one connection and the stream shows up in TanStack Query's devtools.import { useSubscriptionQuery } from '@solana/react/query'; const { data, error } = useSubscriptionQuery(['slot'], client.rpcSubscriptions.slotNotifications());
The source matches
useSubscription: aReactiveStreamSource<T>. The hook also accepts a raw(signal: AbortSignal) => AsyncIterable<T>factory, asexperimental_streamedQueryis built onAsyncIterable.datais the raw notification — theSolanaRpcResponseenvelope is not unwrapped — matchinguseSubscription. Passnullforsourceto disable (TanStack'senabled: false); callresult.refetch()to reconnect. Defaultsretry: false,staleTime: Infinity, andrefetchOnWindowFocus: falseso a focus revalidation doesn't tear down and re-open the connection. -
[
@solana/react,@solana/subscribable] #16241c8d215Thanks @mcintyre94! - Preserve the lasterroron aReactiveActionStorethrough subsequentrunningstates, matching the existing stale-while-revalidate behavior fordata. A re-dispatch after a failure now keeps the previous error visible until the new attempt resolves, mirroring how SWR and TanStack Query handle revalidation.successclears the error;reset()clears both. This also affectsuseAction, whoseerrorfield now persists through a newdispatch()until the new call resolves. -
[
@solana/rpc-graphql] #17958d3bbf1Thanks @mcintyre94! - Update RPC and parsed-account types to match Agave 4.1.0, and surface basis-points commission and vote-latency fields as numbers instead of bigints.BREAKING CHANGES
Parsed vote-account commissions and vote latency are now
numberinstead ofbigint. Agave returns these as small bounded integers (u16/u8), so kit no longer upcasts them. This affectsblockRevenueCommissionBps,inflationRewardsCommissionBps, and each vote'slatencyonJsonParsedVoteAccount.- const bps: bigint = voteAccount.inflationRewardsCommissionBps; + const bps: number = voteAccount.inflationRewardsCommissionBps;
The parsed rent sysvar is now a union of the current and pre-4.1.0 shapes. Agave 4.1.0 reshaped the rent sysvar from
{ burnPercent, exemptionThreshold, lamportsPerByteYear }to{ lamportsPerByte }. The type is now a union of both, so consumers must narrow before accessing the legacy fields. Narrow on the presence oflamportsPerByte(current) versuslamportsPerByteYear(deprecated).- const perByteYear = rent.info.lamportsPerByteYear; + const perByte = 'lamportsPerByte' in rent.info + ? rent.info.lamportsPerByte + : rent.info.lamportsPerByteYear;
warmupCooldownRateon the parsed stake delegation is now optional. Agave 4.1.0 removed it from the parsed output, so it is only present on accounts fetched from validators running earlier versions. It is marked@deprecated.Additionally,
getVoteAccountsnow includes an optionalinflationRewardsCommissionBpsfield (added by Agave 4.1.0; absent on older validators), and the parsed stake-config account fields (slashPenalty,warmupCooldownRate) are marked@deprecatedbecause the stake config program is no longer recognized by the RPC's JSON parser as of Agave 4.1.0 (such accounts now fall back to annotated base64).The GraphQL
SysvarRentAccounttype gains a nullablelamportsPerBytefield to match the reshaped rent sysvar in Agave 4.1.0. The legacyburnPercent,exemptionThreshold, andlamportsPerByteYearfields are retained (and remain nullable) for validators running earlier versions. -
[
@solana/rpc-types] #16782c47363Thanks @mcintyre94! - AddUnwrapRpcResponse<T>type andisSolanaRpcResponse()runtime helper alongsideSolanaRpcResponse. Use them to detect and unwrap notifications that may or may not be wrapped in aSolanaRpcResponseenvelope.UnwrapRpcResponse<T>is a conditional type:type UnwrapRpcResponse<T> = T extends SolanaRpcResponse<infer U> ? U : T;
isSolanaRpcResponse()is a type guard that validates the envelope shape by checkingcontext.slot: bigintand the presence ofvalue, leaving room for additional envelope fields without changing the guard's contract. The narrowed type isSolanaRpcResponse<UnwrapRpcResponse<T>>, so callers don't need to spell out the inner type separately.import { isSolanaRpcResponse } from '@solana/rpc-types'; function lift<T>(notification: T) { if (isSolanaRpcResponse(notification)) { return { slot: notification.context.slot, value: notification.value }; } return { slot: undefined, value: notification }; }
-
[
@solana/subscribable] #16143de3ddaThanks @mcintyre94! - Addstore.withSignal(signal)onReactiveActionStorefor attaching a caller-providedAbortSignalto a dispatch. The method returns a thin wrapper exposing onlydispatch/dispatchAsync; the supplied signal is composed with the store's internal per-dispatch controller viaAbortSignal.any, so aborting either cancels the in-flight call and surfaces the abort reason on state. The baredispatch/dispatchAsyncsignatures are unchanged — this is additive.Two common patterns the wrapper enables:
- Per-attempt timeout.
store.withSignal(AbortSignal.timeout(5_000)).dispatch(args)— a fresh clock per call. Different call sites can pass different timeouts. - Shared kill switch. Hold one
AbortController, bind the wrapper once (const killable = store.withSignal(killCtrl.signal)), and usekillable.dispatch(...)everywhere. Aborting the controller cancels the current call and makes future calls on the wrapper start aborted.
- Per-attempt timeout.
Patch Changes
-
[
@solana/errors] #1723069d56dThanks @mcintyre94! - Add configurable instruction-count limits to transaction planners and message packers, and default planned and packed transaction messages to 16 instructions. The planner limit applies to the final transaction message, including instructions returned bycreateTransactionMessageor added byonTransactionMessageUpdated, and can be overridden when creating a planner or for an individual planning call.This is useful because Solana limits transactions to 64 instructions, including inner instructions. Kit does not know how many inner instructions each instruction will require when executed. The default of 16 assumes an average of 3 additional inner instructions per top-level instruction.
When a transaction message reaches this configured ceiling, the planner and message packer throw the new
SOLANA_ERROR__INSTRUCTION_PLANS__MAX_INSTRUCTIONS_PER_TRANSACTION_EXCEEDEDerror rather than theSOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONSerror reserved for the hard 64-instruction limit, so the configurable soft limit is distinguishable from the format-enforced one. ThrowsSOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTIONis the configured max is invalid (not a positive integer, or greater than 64).Configure a maximum for every plan created by a transaction planner:
const transactionPlanner = createTransactionPlanner({ createTransactionMessage, maxInstructionsPerTransaction: 32, });
Override the maximum for an individual planning request:
const transactionPlan = await transactionPlanner(instructionPlan, { maxInstructionsPerTransaction: 8, });
Override the maximum when packing a message directly:
const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage, { maxInstructions: 32, });
BREAKING CHANGES
Transaction planners and message packers now default to 16 instructions per transaction. Plans and direct message packer calls that previously fit 17 to 64 top-level instructions in one transaction message may now be split into multiple transaction messages. Apps that depend on larger single-transaction plans can preserve the previous top-level instruction limit by configuring
maxInstructionsPerTransaction: 64on transaction planners ormaxInstructions: 64on direct message packer calls; the hard transaction-message limit of 64 top-level instructions still applies.const transactionPlanner = createTransactionPlanner({ createTransactionMessage, + maxInstructionsPerTransaction: 64, });-const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage); +const packedTransactionMessage = messagePacker.packMessageToCapacity(transactionMessage, { maxInstructions: 64 });
-
[
@solana/kit] #1740a4ef3b5Thanks @mcintyre94! - FixcreateReactiveStoreWithInitialValueAndSlotTrackingstranding the store inloadingwhen a freshconnect()window is answered only by a stale-slot valuelastUpdateSlotpersists acrossconnect()windows so the surfaced value never regresses. Previously, a successful response at a slot older than the high-water mark was dropped entirely — including the status transition — so a reconnect (e.g. auseTrackedDatarefresh()) answered by a lagging RPC node while a quiet account's subscription emitted nothing would sit inloadingforever. A stale-slot response now settles the store back toloaded, retaining the newer data it already holds rather than regressing to the older value. -
[
@solana/react] #17193014977Thanks @mcintyre94! - Add theSOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERRORerror code.useSubscriptionSWRnow surfaces thisSolanaErrorwhen the underlying store reaches an error state without an error value (e.g. aDataPublisheremittingundefinedon its error channel, orcontroller.abort(null)), instead of passing the nullish value to SWR'snext— which would be treated as a success and silently wipe the cached data. -
[
@solana/react] #17069063658Thanks @mcintyre94! - Migrate@solana/reactto depend on@solana/kitas a peer dependency (replacing its individual workspace sub-package deps) and re-export@solana/subscribablefrom@solana/kitso React consumers have a single import root.@solana/promisesremains as a direct dep — it's a small utility that isn't part of Kit's public surface.For
@solana/reactusers:@solana/kitmust now be installed alongside@solana/react.- Apps that already use both get a single deduplicated
@solana/kitinstance — important for anything relying on shared types orinstanceof SolanaErrorchecks. - Kit can be bumped independently of React within the peer range.
For
@solana/kitusers:ReactiveStreamSource,ReactiveStreamStore,ReactiveActionSource,ReactiveActionStore,ReactiveState,createReactiveActionStore,createReactiveStoreFromDataPublisherFactory,DataPublisherand the rest of@solana/subscribable's surface are now reachable directly through@solana/kit.
-
[
@solana/rpc-spec-types] #1766660bd74Thanks @mcintyre94! - Validate$nBigInt value objects inparseJsonWithBigIntsbefore materializing them. The parser now rejects non-integer and excessively large$nvalues with aSolanaError(SOLANA_ERROR__MALFORMED_BIGINT_STRING). -
[
@solana/rpc-transformers] #17304a22021Thanks @plutohan! - Stop downcastingbigintrequest params tonumberin the default Solana RPC request transformergetDefaultRequestTransformerForSolanaRpcno longer runsgetBigIntDowncastRequestTransformer. The Solana RPC transport already serializesbigintvalues losslessly as large integer literals (viastringifyJsonWithBigInts), and Agave parses JSON integers across the fullu64range without precision loss, so the lossybigint->numberdowncast was redundant. Removing it also fixes silent truncation for RPC APIs configured without anonIntegerOverflowhandler.getBigIntDowncastRequestTransformeris now deprecated and slated for removal in a future major version.