NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4918 most downloaded on npm
Core types and helpers for encoding and decoding byte arrays on Solana
Last release 6 days ago
28 Sep 2026
Ships fairly regularly
a new release about every 8 days
Nearly every release is documented
notes for 39 of 39 stable releases
1 version withdrawn
withdrawn after publishing
3 years old
2267 releases · first in 2023
One column per quarter.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
[ @solana/instruction-plans ] #1924 769da66 Thanks @mcintyre94 ! - Deprecate transaction plan result APIs being removed in v8
[@solana/codecs-strings] #1926 1d074ed Thanks @latent-9! - Fix getBaseXDecoder returning an offset of 0 instead of the buffer length when there are no bytes left to decode, which corrupted the offset of any decoder composed after it.
[@solana/instruction-plans] #1924 769da66 Thanks @mcintyre94! - Deprecate transaction plan result APIs being removed in v8
Both of the following are deprecated and will be removed in the next major version.
successfulSingleTransactionPlanResultFromTransaction derives the transaction signature on your behalf by calling getSignatureFromTransaction, which throws when the transaction's fee payer has not signed it. Construct results with successfulSingleTransactionPlanResult instead, passing the context explicitly:
- successfulSingleTransactionPlanResultFromTransaction(message, transaction, context);
+ successfulSingleTransactionPlanResult(message, {
+ ...context,
+ signature: getSignatureFromTransaction(transaction),
+ transaction,
+ });BaseTransactionPlanResultContext goes away together with the intersections that graft it onto every SingleTransactionPlanResult. The context of a result is becoming entirely caller-defined — it will be exactly the TContext you supply — so there will be no separate base shape to merge in. If you refer to this type, declare whichever of its fields you need on your own context type instead:
type MyContext = {
message?: TransactionMessage & TransactionMessageWithFeePayer;
signature?: Signature;
transaction?: Transaction;
};[@solana/rpc-spec, @solana/rpc-spec-types, @solana/rpc-subscriptions-spec] #1748 9f8e4d0 Thanks @ChargingFoxSec! - Avoid treating JavaScript protocol hooks and Object prototype properties as RPC method names in proxy-backed RPC objects.
[@solana/rpc-subscriptions-api, @solana/rpc-subscriptions-spec, @solana/rpc-transformers] #1925 90e371b Thanks @o-mid! - Consult the subscriptions numeric allow-list for each notification
The allow-list is keyed by API names like blockNotifications, but the plan executor invoked the transformer with the rewritten subscribe request (blockSubscribe). The lookup missed and every notification numeric was upcast to bigint while still typechecking as number.
The transformer now maps *Subscribe / *Notification names back to *Notifications, and the plan executor derives the method name from each notification payload so subscriptions that share a channel are not transformed under whichever request created the publisher.
blockNotifications is also derived from the same innerInstructionsConfigs / messageConfig / tokenBalancesConfigs as getBlock, so transaction version, token balance uiAmount, and stackHeight stay numbers.
[@solana/signers] #1851 0a989a4 Thanks @rajanpanth! - Fix TransactionMessageWithSigners so its signer type parameter rejects fee payer and instruction signers of other transaction signer kinds.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Returning a Signature or a Transaction is deprecated. Both still behave exactly as before — a returned signature is stored as context.signature , and…
[@solana/errors, @solana/kit, @solana/react, @solana/subscribable] #1811 7022c26 Thanks @mcintyre94! - Add bridgeStoreToAsyncIterable to @solana/subscribable
bridgeStoreToAsyncIterable adapts a ReactiveStreamStore into the pull-based AsyncIterable contract that consumers like TanStack Query's experimental_streamedQuery expect. It is now a public export of @solana/subscribable (and re-exported from @solana/kit). It was previously an internal helper of @solana/react, but it is not React- or TanStack-specific and is useful to any consumer that needs to drive a stream store by for await-ing it.
The bridge only observes the store — consistent with the rest of the ecosystem, the caller owns the store's lifecycle (connect() it yourself, bound to the same signal, and reset() it when done). The bridge subscribes, seeds from the store's current snapshot, yields values, and unsubscribes when iteration ends.
It throws the new SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR when a store closes in an error state with a nullish payload. This is the error useSubscriptionQuery and useTrackedDataQuery now surface in that case; the SWR bridge is unaffected.
[@solana/errors, @solana/offchain-messages] #1888 14a3e5b Thanks @mcintyre94! - Add an assertOffchainMessageV1Equal helper that asserts that a version 1 offchain message you received from an untrusted signer (eg. a wallet) is the message you expected it to sign. Verifying a signature proves only that the signer produced it over the bytes it handed back, not that those bytes represent the message you asked for, so assert this before verifying signatures with verifyOffchainMessageEnvelope. The helper compares the content and the required signatories, and reports each kind of mismatch with its own error code: the new SOLANA_ERROR__OFFCHAIN_MESSAGE__CONTENT_DOES_NOT_MATCH_EXPECTED and SOLANA_ERROR__OFFCHAIN_MESSAGE__REQUIRED_SIGNATORIES_DO_NOT_MATCH_EXPECTED. Required signatories are compared without regard to order, since a decoded message lists them in the order the specification mandates while yours may be in any order. It accepts an OffchainMessageV1 rather than the OffchainMessage union that decoding produces, so narrow the decoded message to a version 1 message before calling it.
[@solana/instruction-plans] #1915 9e7daea Thanks @mcintyre94! - Let the createTransactionPlanExecutor callback return the context of a successful result
The executeTransactionMessage callback may now return the context that a successful result should carry, instead of a Signature or a Transaction. When it does, that context is used as-is: nothing is derived from it, and in particular getSignatureFromTransaction is never called on your behalf.
const transactionPlanExecutor = createTransactionPlanExecutor({
executeTransactionMessage: async (context, message) => {
const transaction = await signTransactionMessageWithSigners(message);
context.transaction = transaction;
+ const signature = getSignatureFromTransaction(transaction);
await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
- return transaction;
+ return { signature, transaction };
},
});Since a successful result always carries a signature, a returned context must include one — a callback that declares a custom context and forgets a property of it now fails to compile, rather than producing a result whose context is typed but undefined at runtime. That signature is also how the executor tells a returned context apart from a returned Transaction, which keeps its signatures in a signatures map and therefore never has one.
The mutable context argument is unchanged and still serves the failure path: whatever the callback stores on it before it throws is preserved in the resulting FailedSingleTransactionPlanResult. On success the two are merged, with the returned context taking precedence, so a property stored but not returned is still reported.
Returning a Signature or a Transaction is deprecated. Both still behave exactly as before — a returned signature is stored as context.signature, and a returned transaction is stored as context.transaction with its signature derived from it — and IDEs now flag those call sites, because createTransactionPlanExecutor gained a deprecated overload that only matches callbacks returning those types. Note that a config declared as TransactionPlanExecutorConfig up front is not flagged, since that type permits either return style.
Prefer returning a context, since deriving a signature from a transaction throws SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING when the fee payer slot is empty. An executor that deliberately produces partially signed transactions — signed by an authority, to be paid for and submitted by a relayer later — can now succeed by returning its own signature alongside the transaction. Dropping the signature from a successful result's context altogether remains impossible, since SuccessfulSingleTransactionPlanResult guarantees one.
Failure handling is unchanged, including the signature still derived from a transaction left on the context when the callback throws. Since the callback never returned anything in that case, there is nothing to bypass that derivation, so a callback working with fee-payer-unsigned transactions should avoid storing them on the context — otherwise deriving a signature from one replaces the error it meant to report.
[@solana/kit] #1898 4a5f717 Thanks @lorisleiva! - Add helpers to create client interfaces from a raw Rpc
Add createClientWithGetMinimumBalanceFromRpc, createClientWithFetchAccountsFromRpc and createClientWithInterfacesFromRpc to @solana/kit. These convenience helpers let consumers that only have a raw Rpc object construct the corresponding client interfaces (ClientWithGetMinimumBalance and ClientWithFetchAccounts) without assembling a full Kit client. createClientWithInterfacesFromRpc fills in whichever interfaces the RPC supports and narrows its return type accordingly.
[@solana/kit] #1824 b47feb6 Thanks @mcintyre94! - Re-export @solana/promises from @solana/kit
@solana/kit now re-exports the @solana/promises package, so its helpers — isAbortError, getAbortablePromise, and safeRace — are available directly from @solana/kit without a separate dependency. This is particularly useful alongside @solana/react's useAction, whose superseded or aborted dispatches reject with an AbortError that callers filter using isAbortError.
[@solana/plugin-interfaces] #1897 aa0b625 Thanks @lorisleiva! - Add a ClientWithFetchAccounts interface
This new plugin interface represents a client that can fetch the encoded content of accounts from their addresses via a fetchAccounts(addresses, config?) method. Like the other @solana/plugin-interfaces capabilities, it lets plugins provide or require account-fetching without coupling to a concrete RPC. The returned array matches the provided addresses in length and order, using MaybeEncodedAccount to represent accounts that may not exist.
[@solana/react] #1876 d6a1adb Thanks @mcintyre94! - Add usePayer and useIdentity React hooks. Each reads the corresponding value off the client and, when the client advertises subscribeToPayer/subscribeToIdentity, subscribes so the returned signer always reflects the latest payer/identity. Clients whose value is fixed fall back to a one-time read.
If the plugin value throws (for example as the wallet plugin does when it owns payer/identity and a wallet is not connected), this is surfaced as undefined in the hooks.
[@solana/react] #1841 94f49bb Thanks @mcintyre94! - Make the TClient type parameter of useClient required by removing its object default, matching useClientCapability. Callers should always pass their client's shape (typically an exported AppClient type) so installed capabilities are typed at the call site.
- const client = useClient();
+ const client = useClient<AppClient>();[@solana/react] #1869 2193459 Thanks @mcintyre94! - Add usePlanTransaction, usePlanTransactions, useSendTransaction, and useSendTransactions hooks for driving a client's transaction-planning and -sending capabilities as reactive actions.
[@solana/react] #1879 c27ce2f Thanks @mcintyre94! - Add a useAirdrop hook that wraps a client's airdrop capability (ClientWithAirdrop) as a tracked useAction. dispatch(address, amount) requests an airdrop with an injected AbortSignal, resolving with the transaction Signature (or undefined when the airdrop is applied without a transaction).
[@solana/rpc-api] #1776 c8235ca Thanks @mcintyre94! - Add the getTransactionsForAddress RPC method type. This method combines address-history discovery and per-transaction fetching into a single query, with server-side filtering, bidirectional sorting, and cursor-based pagination. It will be part of the upcoming solana-rpc spec and is part of the solana-rpc/superbank project, and is already available from major RPC providers.
It supports both signatures and full (json/jsonParsed/base58/base64) response modes. The shared transaction metadata types also gain an optional meta.costUnits field, which surfaces on getTransaction as well.
[@solana/rpc-transformers] #1919 80b3756 Thanks @amilz! - Stop upcasting token balance uiAmount and related numerics to bigint
The response transformer upcasts every JSON integer to a bigint unless its keypath appears in an allow-list. Because the upcast only applies to integers, uiTokenAmount.uiAmount — an f64 on the server — arrived as a bigint when the balance happened to be a whole number and as a number when it was fractional, so its declared type was correct for some values and wrong for others.
uiTokenAmount.uiAmount is now allow-listed on getTransaction, getBlock, and getTransactionsForAddress token balances.simulateTransaction had no token balance keypaths allow-listed at all, so accountIndex and uiTokenAmount.decimals were upcast there as well. All three are now allow-listed.@solana/rpc-transformers additionally exports a new tokenBalancesConfigs array of token-balance-relative keypaths, alongside the existing innerInstructionsConfigs and messageConfig.
[@solana/transaction-introspection] #1814 c45d5e0 Thanks @mcintyre94! - decodeTransactionFromRpcResponse now accepts confirmed transactions from any RPC method that returns them, not just getTransaction. It reads only the shared transaction / meta / version envelope, so getTransactionsForAddress results (map over its data array) and getBlock results (map over its transactions array, with transactionDetails: 'full') decode identically, including legacy transactions fetched without maxSupportedTransactionVersion. The 'json' overload now types its omitted transaction as never rather than an optional Transaction, reflecting that the JSON path never yields re-encodable wire bytes.
[@solana/codecs-data-structures] #1911 8c9eece Thanks @latent-9! - Fix getBitArrayEncoder returning the wrong next offset. Its write returned size instead of offset + size, so a bit array placed before another field in a struct or tuple was overwritten by the following field. It now returns offset + size, matching the decoder and the other codecs.
[@solana/codecs-data-structures] #1884 da10c5a Thanks @Swift42! - Avoid copying the remaining buffer in getArrayDecoder's emptiness check
getArrayDecoder's read() tested for an empty byte array with bytes.slice(offset).length === 0, which allocates and copies every byte from offset to the end just to read .length off the result. On large accounts containing many prefixed arrays, maps, or sets this made decoding quadratic in account size. The check is now the equivalent O(1) comparison offset >= bytes.length. getMapDecoder and getSetDecoder delegate to getArrayDecoder and benefit as well.
[@solana/codecs-data-structures] #1809 204ed6e Thanks @mcintyre94! - 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.
[@solana/kit, @solana/plugin-core] #1883 a900eeb Thanks @mcintyre94! - Fix withCleanup throwing DisposableStack is not defined on Safari
withCleanup constructed a DisposableStack unconditionally, but Safari has not shipped explicit resource management — as of Safari 27 it provides neither DisposableStack nor Symbol.dispose — so any plugin that registers a cleanup function threw ReferenceError: Can't find variable: DisposableStack while the client was being built.
The runtime's own DisposableStack is still used whenever it exists. Only where it is missing does withCleanup fall back to an internal stack that reproduces the behaviour it depends on. The withCleanup test suite now runs twice, once against each stack, so the two cannot drift apart.
Note that this fixes disposal on Safari but not using declarations in your own code, which additionally need a Symbol.dispose polyfill; disposing a client explicitly works either way.
[@solana/react] #1907 9d6be07 Thanks @mcintyre94! - Widen the @solana/kit peer dependency of @solana/react from an exact version to a caret range. @solana/react previously declared "@solana/kit": "workspace:*", which publishes as an exact pin ("@solana/kit": "7.0.0"), so a consumer who advanced @solana/kit without advancing @solana/react in the same step hit an unsatisfiable peer range even though the two are compatible. It now declares workspace:^ and publishes as ^7.1.0. The two packages continue to be released in lockstep at identical versions, so this does not loosen which combinations are actually shipped — it only stops describing a compatible pair as incompatible.
[@solana/react] #1825 d54b899 Thanks @mcintyre94! - Bump the @wallet-standard/ui and @wallet-standard/ui-registry dependencies to ^1.0.3 and ^1.1.1 respectively. The 1.1.x registry line is a backward-compatible superset that continues to export the names @solana/react relies on, and aligning with it lets consumers that also pull in @solana/kit-plugin-wallet resolve a single, shared copy of the wallet-standard UI registry (which is a runtime singleton) instead of splitting across two incompatible copies.
[@solana/rpc-api] #1919 80b3756 Thanks @amilz! - Stop upcasting token balance uiAmount and related numerics to bigint
The response transformer upcasts every JSON integer to a bigint unless its keypath appears in an allow-list. Because the upcast only applies to integers, uiTokenAmount.uiAmount — an f64 on the server — arrived as a bigint when the balance happened to be a whole number and as a number when it was fractional, so its declared type was correct for some values and wrong for others.
uiTokenAmount.uiAmount is now allow-listed on getTransaction, getBlock, and getTransactionsForAddress token balances.simulateTransaction had no token balance keypaths allow-listed at all, so accountIndex and uiTokenAmount.decimals were upcast there as well. All three are now allow-listed.@solana/rpc-transformers additionally exports a new tokenBalancesConfigs array of token-balance-relative keypaths, alongside the existing innerInstructionsConfigs and messageConfig.
[@solana/rpc-api] #1917 82c4ceb Thanks @amilz! - Stop upcasting transaction version to bigint
The response transformer upcasts every JSON integer to a bigint unless its keypath appears in an allow-list. version was missing from that allow-list on getTransaction, getBlock transactions, and getTransactionsForAddress, so it arrived at runtime as 0n while still typechecking as TransactionVersion ('legacy' | 0 | 1).
A check like if (transaction.version === 0) therefore compiled cleanly and was always false, with no compiler error and no runtime error. The keypath is now allow-listed and version arrives as a number, matching its declared type.
[@solana/transaction-messages] #1874 327760c Thanks @mcintyre94! - Update type of compressTransactionMessageUsingAddressLookupTables to reject v1 transactions
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
retry() is now deprecated; it remains as an error-only alias for connect() . Migrate to calling connect() directly. Code that previously relied on ret…
[@solana/codecs-data-structures] #1683 99667a1 Thanks @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 from FixedSize* to VariableSize* (when at least one branch is variable-size) or to a plain Encoder/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:
.fixedSize, or passing the result where a FixedSize* is required — will need to adjust.getPatternMatchEncoder<MyType>([...]) (and the decoder/codec equivalents) now fails to compile, and getPredicateEncoder<MyType>(...) silently degrades its return to a plain Encoder<MyType>. Instead, type the branch predicates' parameters (e.g. (value: MyType) => ...) and let the return type be inferred.[@solana/instruction-plans] #1723 069d56d Thanks @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 by createTransactionMessage or added by onTransactionMessageUpdated, 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_EXCEEDED error rather than the SOLANA_ERROR__TRANSACTION__TOO_MANY_INSTRUCTIONS error reserved for the hard 64-instruction limit, so the configurable soft limit is distinguishable from the format-enforced one. Throws SOLANA_ERROR__INSTRUCTION_PLANS__INVALID_MAX_INSTRUCTIONS_PER_TRANSACTION is 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: 64 on transaction planners or maxInstructions: 64 on 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] #1663 d09718d Thanks @mcintyre94! - Add withSignal() to ReactiveStreamStore for per-connection cancellation, replacing the construction-time abortSignal option. Mirrors the action store's per-dispatch withSignal() 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 exposing connect() that composes the caller-provided signal with the per-connection inner controller via AbortSignal.any. Aborting the caller's signal surfaces the abort reason on state as { status: 'error' }; supersession via the internal controller (a newer connect() or reset()) 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();. After killCtrl.abort(), every killable.connect() short-circuits to error.
createDataPublisher is 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 abortSignal option on createReactiveStoreFromDataPublisherFactory, createReactiveStoreWithInitialValueAndSlotTracking, and PendingRpcSubscriptionsRequest.reactiveStore() is removed. Callers wanting a long-lived kill switch use the bind-once withSignal pattern. ReactiveStreamSource<T>.reactiveStore() is now parameter-less (mirrors ReactiveActionSource<T>.reactiveStore()).
[@solana/kit, @solana/rpc-subscriptions-spec, @solana/subscribable] #1662 fa04323 Thanks @mcintyre94! - Drop auto-connect from ReactiveStreamStore; callers explicitly invoke connect() to open the underlying stream. Mirrors the action store's caller-driven dispatch() pattern — the store is a state machine that callers orchestrate, not a self-starting subscription.
The factory variant returned by createReactiveStoreFromDataPublisherFactory now starts in status: 'idle'. Call store.connect() to open the stream; from idle, the store transitions through loading → loaded (or error). A subsequent connect() from any non-idle status transitions through retrying while preserving the last known value. A new reset() method aborts the current connection and returns the store to idle without 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 constructionretry() is now deprecated; it remains as an error-only alias for connect(). Migrate to calling connect() directly. Code that previously relied on retry() being a no-op when the store was not in error state should add an explicit if (status === 'error') store.connect(); guard at the call site.
createReactiveStoreFromDataPublisher (the deprecated non-factory variant accepting a ready-made DataPublisher) is removed. Its only documented use was as a backwards-compatibility alias behind PendingRpcSubscriptionsRequest.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 use reactiveStore() for RPC subscriptions.
createReactiveStoreWithInitialValueAndSlotTracking in @solana/kit no longer fires the RPC request on construction — call store.connect() to start it, or wrap in a useEffect that calls connect() on mount and reset() on cleanup. The store starts in status: 'idle' and follows the same lifecycle as the underlying stream store.
[@solana/kit] #1708 03000e5 Thanks @mcintyre94! - createReactiveStoreWithInitialValueAndSlotTracking now consumes its two inputs as reactive sources rather than as request objects it calls send() / subscribe() on directly. The rpcRequest / rpcSubscriptionRequest config fields (and their rpcValueMapper / rpcSubscriptionValueMapper) are replaced by initialValueSource: ReactiveActionSource<...> / streamSource: ReactiveStreamSource<...> (with initialValueMapper / streamValueMapper).
Each source is consumed via its reactiveStore() method, so the helper reuses ReactiveActionStore / ReactiveStreamStore primitives. PendingRpcRequest satisfies ReactiveActionSource and PendingRpcSubscriptionsRequest satisfies ReactiveStreamSource, 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] #1677 a198b5c Thanks @mcintyre94! - Collapse loading and retrying into a single loading status on ReactiveStreamStore, mirroring the action store's running (which is itself the merged "first call vs subsequent call" state). data and error are preserved through loading for stale-while-revalidate — UI can render the prior outcome alongside an in-flight reconnect.
ReactiveState<T> drops the retrying variant. loading widens from { data: undefined, error: undefined } to { data: T | undefined, error: unknown }. Both createReactiveStoreFromDataPublisherFactory and createReactiveStoreWithInitialValueAndSlotTracking now transition every connect() through loading (preserving currentState.data and currentState.error); a subsequent loaded clears error, a subsequent error replaces 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 statusMigration: replace status === 'retrying' checks with status === 'loading' && data !== undefined (or just status === 'loading' if you don't need to distinguish first-load vs reconnect — the SWR pattern lets you render whatever is in data regardless).
[@solana/kit, @solana/plugin-core] #1786 6947740 Thanks @mcintyre94! - Remove deprecated getMinimumBalanceForRentExemption and createEmptyClient.
BREAKING CHANGES
Removed getMinimumBalanceForRentExemption from @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 the getMinimumBalanceForRentExemption RPC method or a ClientWithGetMinimumBalance plugin instead.
- import { getMinimumBalanceForRentExemption } from '@solana/kit';
- const rentExemptLamports = getMinimumBalanceForRentExemption(82n);
+ const { value: rentExemptLamports } = await rpc.getMinimumBalanceForRentExemption(82n).send();Removed createEmptyClient from @solana/plugin-core. Use createClient, 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] #1780 acec0be Thanks @mcintyre94! - Streamline the ReactiveStreamStore contract by removing deprecated members and unifying its state accessor with ReactiveActionStore. The getUnifiedState() method has been renamed to getState(), and the deprecated value-only getState(), getError(), and retry() members along with the ReactiveStore type alias have been removed.
BREAKING CHANGES
getUnifiedState() renamed to getState(). The unified { data, error, status } snapshot accessor is now simply getState(), matching ReactiveActionStore.getState().
- const state = useSyncExternalStore(store.subscribe, store.getUnifiedState);
+ const state = useSyncExternalStore(store.subscribe, store.getState);Removed the deprecated value-only getState() and getError(). 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(). Use connect(), 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 ReactiveStore type alias. Use ReactiveStreamStore directly.
- import type { ReactiveStore } from '@solana/subscribable';
+ import type { ReactiveStreamStore } from '@solana/subscribable';[@solana/rpc-api, @solana/rpc-parsed-types, @solana/rpc-transformers] #1795 8d3bbf1 Thanks @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 number instead of bigint. Agave returns these as small bounded integers (u16/u8), so kit no longer upcasts them. This affects blockRevenueCommissionBps, inflationRewardsCommissionBps, and each vote's latency on JsonParsedVoteAccount.
- 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 of lamportsPerByte (current) versus lamportsPerByteYear (deprecated).
- const perByteYear = rent.info.lamportsPerByteYear;
+ const perByte = 'lamportsPerByte' in rent.info
+ ? rent.info.lamportsPerByte
+ : rent.info.lamportsPerByteYear;warmupCooldownRate on 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, getVoteAccounts now includes an optional inflationRewardsCommissionBps field (added by Agave 4.1.0; absent on older validators), and the parsed stake-config account fields (slashPenalty, warmupCooldownRate) are marked @deprecated because 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 SysvarRentAccount type gains a nullable lamportsPerByte field to match the reshaped rent sysvar in Agave 4.1.0. The legacy burnPercent, exemptionThreshold, and lamportsPerByteYear fields are retained (and remain nullable) for validators running earlier versions.
[@solana/rpc-api] #1803 cab6d7e Thanks @mcintyre94! - Always surface replacementBlockhash on the simulateTransaction response. Agave v3.x validators unconditionally include this field, setting it to null when replaceRecentBlockhash was not true. The field now lives on the base response type as TransactionBlockhashLifetime | null, and is narrowed to a non-null TransactionBlockhashLifetime only on the overloads where replaceRecentBlockhash: true.
BREAKING CHANGES
replacementBlockhash is now always present on the simulateTransaction response. Previously the field was only present on the type when replaceRecentBlockhash was true. It is now always present, typed as TransactionBlockhashLifetime | null, with null indicating that no blockhash was replaced. Code that relied on the field being absent (e.g. to discriminate the response shape) must instead check for null.
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] #1628 a6783e0 Thanks @mcintyre94! - PendingRpcRequest.reactiveStore() no longer auto-fires the request on creation. It now returns a ReactiveActionStore in the idle state; the caller is responsible for the initial dispatch().
This brings reactiveStore() in line with createReactiveActionStore(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 an AbortSignal to 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[@solana/codecs-data-structures] #1731 ec4d3ef Thanks @kh0ra! - Add createDependentStructDecoder, a fluent builder for a struct decoder whose later fields may depend on the decoded values of earlier ones. Each call to field adds a name and either a static Decoder or a factory that receives a frozen snapshot of the fields decoded so far. Calling build produces a FixedSizeDecoder when every field added to the builder is itself a FixedSizeDecoder, and a VariableSizeDecoder otherwise.
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] #1719 3014977 Thanks @mcintyre94! - Add the SOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERROR error code. useSubscriptionSWR now surfaces this SolanaError when the underlying store reaches an error state without an error value (e.g. a DataPublisher emitting undefined on its error channel, or controller.abort(null)), instead of passing the nullish value to SWR's next — which would be treated as a success and silently wipe the cached data.
[@solana/errors, @solana/kit, @solana/rpc-api, @solana/transaction-introspection] #1611 772b82c Thanks @amilz! - Add @solana/transaction-introspection, a new package that bridges a getTransaction response and the auto-generated @solana-program/* parseXInstruction clients. Decodes the transaction (encoding: 'base64', 'base58', or 'json'), resolves account indices against static + ALT-loaded addresses, normalizes inner instructions from meta.innerInstructions, and exposes walkInstructions to enumerate every instruction in display order — each outer instruction followed by its inner instructions — with a trace recording its location. Each returned instruction is a ResolvedInstruction & { trace } directly usable with isInstructionForProgram from @solana/instructions and with the auto-generated identifyXInstruction / parseXInstruction helpers. Supports legacy, v0, and v1 compiled 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-api now exports the non-null getTransaction response shapes as named types (GetTransactionApiResponseBase58, GetTransactionApiResponseBase64, GetTransactionApiResponseJson, GetTransactionApiResponseJsonParsed), which decodeTransactionFromRpcResponse accepts as inputs. @solana/errors gains SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE plus a new TRANSACTION_INTROSPECTION domain (SOLANA_ERROR__TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION, SOLANA_ERROR__TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE).
[@solana/errors, @solana/react] #1607 e193711 Thanks @mcintyre94! - Add ClientProvider, useClient, and useClientCapability — the Kit client context layer for React.
ClientProvider publishes a caller-owned Kit client to its subtree. Required by useClient, useClientCapability, and any plugin-specific hook that depends on a client capability — generic primitives like useAction work 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 to React.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 base Client shape; callers who know a specific plugin is installed may widen the type via the generic. Throws a new SolanaError with code SOLANA_ERROR__REACT__MISSING_PROVIDER when called outside a provider.
useClientCapability<TClient>({ capability, hookName, providerHint }) runtime-checks that the requested capability (or capabilities) is installed on the client and throws SOLANA_ERROR__REACT__MISSING_CAPABILITY — surfacing the calling hookName and a providerHint — when it isn't. Plugin-hook authors use this to fail loudly at mount instead of letting a missing plugin surface later as undefined.
Two new error codes (SOLANA_ERROR__REACT__MISSING_PROVIDER, SOLANA_ERROR__REACT__MISSING_CAPABILITY) are reserved in the [9000000-9000999] range.
[@solana/kit] #1706 9063658 Thanks @mcintyre94! - Migrate @solana/react to depend on @solana/kit as a peer dependency (replacing its individual workspace sub-package deps) and re-export @solana/subscribable from @solana/kit so React consumers have a single import root. @solana/promises remains as a direct dep — it's a small utility that isn't part of Kit's public surface.
For @solana/react users:
@solana/kit must now be installed alongside @solana/react.@solana/kit instance — important for anything relying on shared types or instanceof SolanaError checks.For @solana/kit users:
ReactiveStreamSource, ReactiveStreamStore, ReactiveActionSource, ReactiveActionStore, ReactiveState, createReactiveActionStore, createReactiveStoreFromDataPublisherFactory, DataPublisher and the rest of @solana/subscribable's surface are now reachable directly through @solana/kit.[@solana/react] #1612 08777cf Thanks @mcintyre94! - Add useAction — a React hook that bridges any async function into a tracked action with dispatch / dispatchAsync / status / data / error / reset and supersede-on-second-call semantics. Built on createReactiveActionStore from @solana/subscribable.
The wrapped function receives a fresh AbortSignal per dispatch. dispatch(...) is fire-and-forget — it returns void, never throws, and is the variant to wire into UI event handlers, with outcomes read off status / 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 superseded dispatchAsync call see a rejection with an AbortError filterable via isAbortError from @solana/promises. data from a prior success persists through subsequent running states for stale-while-revalidate UX; only reset() clears it.
fn is 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 a deps array. In-flight calls are unaffected — they continue with the closure they captured at dispatch time. Matches the convention used by useMutation in TanStack Query and useWriteContract in wagmi.
The shared ActionResult<TArgs, TResult> type is also exported so plugin hooks can declare their return shape against it.
[@solana/react] #1619 fd6bdef Thanks @mcintyre94! - Add useRequest — a React hook for one-shot async reads. Pass either an async function (signal) => Promise<T> or a memoized ReactiveActionSource<T> (satisfied by PendingRpcRequest). 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 status as one of fetching | success | error | disabled. A request in flight is always fetching; inspect data and error to 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. Pass null for the source to gate the request off — useful while inputs aren't yet known. The result then reports status: 'disabled'.
Optional getAbortSignal: () => AbortSignal is a factory invoked on every attempt (initial fire + every refresh()). Each attempt gets a fresh signal that's composed with the store's internal per-dispatch controller via AbortSignal.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 — no useCallback needed. refresh() also accepts an optional { abortSignal } override to replace the factory for one specific attempt.
The new RequestResult<T> and UseRequestOptions types are exported alongside the hook so plugin hooks built on top can declare their return shape against them.
[@solana/react] #1719 3014977 Thanks @mcintyre94! - Add useSubscriptionSWR(key, source, options?) to the @solana/react/swr subpath — the SWR-backed counterpart to useSubscription. Routes a ReactiveStreamSource<T> through SWR's subscription cache (useSWRSubscription).
import { useSubscriptionSWR } from '@solana/react/swr';
const { data } = useSubscriptionSWR(['account', address], client.rpcSubscriptions.accountNotifications(address));data is the notification exactly as the source emits it. Pass null for either key or source to disable. Options accept SWR's config plus getAbortSignal for an abort signal.
[@solana/react] #1702 3a92f37 Thanks @mcintyre94! - Add useSubscription — a React hook for subscription-based live data. Pass a ReactiveStreamSource<T> (satisfied by PendingRpcSubscriptionsRequest) 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 status as one of loading | loaded | error | disabled. data is the notification exactly as the source emits it — no unwrapping or reshaping. For RPC subscriptions that emit SolanaRpcResponse<U> (account/program/signature), read the inner value at data.value and the slot at data.context.slot; for raw notifications (slot/logs/root) data is the raw shape. Pass null for the source to gate the subscription off — useful while inputs aren't yet known. The result then reports status: 'disabled'. After a notification arrives, an error transitions to status: 'error' while preserving the stale data; reconnect() returns to loading (preserving stale data and error for stale-while-revalidate) before settling on loaded or a fresh error.
Optional getAbortSignal: () => AbortSignal is a factory invoked on every connection (initial subscribe + every reconnect()). Each connection gets a fresh signal that the underlying store composes with its per-connection controller via AbortSignal.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 — no useCallback needed. 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 via useMemo, fire store.connect() in a useEffect, tear down via store.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 stays idle and the hook reports status: 'loading'; first client render hydrates from the same paint and commits the connect.
SubscriptionResult<T> and UseSubscriptionOptions are exported alongside the hook so plugin hooks built on top can declare their return shape against them.
[@solana/react] #1713 587ec07 Thanks @mcintyre94! - Add @solana/react/swr subpath with useRequestSWR(key, source, options?) — the SWR-backed counterpart to useRequest. Same source shape (ReactiveActionSource<T> or (signal) => Promise<T>); returns SWR's native SWRResponse<T>. Pass null for either key or source to disable. Requires swr@^2 as an optional peer dependency.
import { useRequestSWR } from '@solana/react/swr';
const { data } = useRequestSWR(['epochInfo'], client.rpc.getEpochInfo());Options accept any SWRConfiguration field plus the Kit-only getAbortSignal: () => AbortSignal (same option as useRequest), which threads a per-attempt signal into the source — typically a timeout via AbortSignal.timeout(). Use SWR's result.mutate() to re-fire on demand.
[@solana/react] #1707 da42ff8 Thanks @mcintyre94! - Add useTrackedData — 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 the loading paint is shorter than subscription-only would give you. Surfaces a unified { data, error, refresh, status } view where data is the SolanaRpcResponse<TItem> envelope that the underlying kit primitive emits — the primitive's type guarantees the envelope shape, so callers can read data.value and data.context.slot directly 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(
() Note truncated.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →