@solana/react 7.0.0 → 7.1.0-canary-20260812160611

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +117 -39
  2. package/dist/index.browser.cjs +64 -0
  3. package/dist/index.browser.cjs.map +1 -1
  4. package/dist/index.browser.mjs +58 -1
  5. package/dist/index.browser.mjs.map +1 -1
  6. package/dist/index.native.mjs +58 -1
  7. package/dist/index.native.mjs.map +1 -1
  8. package/dist/index.node.cjs +64 -0
  9. package/dist/index.node.cjs.map +1 -1
  10. package/dist/index.node.mjs +58 -1
  11. package/dist/index.node.mjs.map +1 -1
  12. package/dist/query.browser.cjs +5 -52
  13. package/dist/query.browser.cjs.map +1 -1
  14. package/dist/query.browser.mjs +5 -52
  15. package/dist/query.browser.mjs.map +1 -1
  16. package/dist/query.native.mjs +5 -52
  17. package/dist/query.native.mjs.map +1 -1
  18. package/dist/query.node.cjs +5 -52
  19. package/dist/query.node.cjs.map +1 -1
  20. package/dist/query.node.mjs +5 -52
  21. package/dist/query.node.mjs.map +1 -1
  22. package/dist/types/index.d.ts +7 -0
  23. package/dist/types/index.d.ts.map +1 -1
  24. package/dist/types/query/useSubscriptionQuery.d.ts +5 -5
  25. package/dist/types/query/useSubscriptionQuery.d.ts.map +1 -1
  26. package/dist/types/query/useTrackedDataQuery.d.ts +2 -2
  27. package/dist/types/query/useTrackedDataQuery.d.ts.map +1 -1
  28. package/dist/types/useAirdrop.d.ts +40 -0
  29. package/dist/types/useAirdrop.d.ts.map +1 -0
  30. package/dist/types/useClient.d.ts +7 -6
  31. package/dist/types/useClient.d.ts.map +1 -1
  32. package/dist/types/useIdentity.d.ts +28 -0
  33. package/dist/types/useIdentity.d.ts.map +1 -0
  34. package/dist/types/usePayer.d.ts +27 -0
  35. package/dist/types/usePayer.d.ts.map +1 -0
  36. package/dist/types/usePlanTransaction.d.ts +25 -0
  37. package/dist/types/usePlanTransaction.d.ts.map +1 -0
  38. package/dist/types/usePlanTransactions.d.ts +26 -0
  39. package/dist/types/usePlanTransactions.d.ts.map +1 -0
  40. package/dist/types/useSendTransaction.d.ts +29 -0
  41. package/dist/types/useSendTransaction.d.ts.map +1 -0
  42. package/dist/types/useSendTransactions.d.ts +28 -0
  43. package/dist/types/useSendTransactions.d.ts.map +1 -0
  44. package/package.json +10 -10
  45. package/src/index.ts +7 -0
  46. package/src/query/__tests__/useRequestQuery-test.browser.tsx +2 -2
  47. package/src/query/__tests__/useSubscriptionQuery-test.browser.tsx +7 -9
  48. package/src/query/__tests__/useTrackedDataQuery-test.browser.tsx +3 -3
  49. package/src/query/useSubscriptionQuery.ts +9 -7
  50. package/src/query/useTrackedDataQuery.ts +9 -4
  51. package/src/swr/__tests__/useRequestSWR-test.browser.tsx +4 -6
  52. package/src/useAirdrop.ts +45 -0
  53. package/src/useClient.ts +7 -6
  54. package/src/useIdentity.ts +47 -0
  55. package/src/usePayer.ts +44 -0
  56. package/src/usePlanTransaction.ts +30 -0
  57. package/src/usePlanTransactions.ts +31 -0
  58. package/src/useSendTransaction.ts +34 -0
  59. package/src/useSendTransactions.ts +33 -0
  60. package/dist/types/query/bridgeStoreToAsyncIterable.d.ts +0 -39
  61. package/dist/types/query/bridgeStoreToAsyncIterable.d.ts.map +0 -1
  62. package/src/query/__tests__/bridgeStoreToAsyncIterable-test.ts +0 -188
  63. package/src/query/bridgeStoreToAsyncIterable.ts +0 -105
@@ -86,23 +86,23 @@ function makeStreamFactory<T>() {
86
86
  function createFakeStore<T>(): {
87
87
  connectCount: () => number;
88
88
  emit: (state: ReactiveState<T>) => void;
89
- resetCount: () => number;
90
89
  store: ReactiveStreamStore<T>;
91
90
  withSignalArg: () => AbortSignal | undefined;
92
91
  } {
93
92
  let state: ReactiveState<T> = { data: undefined, error: undefined, status: 'idle' };
94
93
  const listeners = new Set<() => void>();
95
94
  let connects = 0;
96
- let resets = 0;
97
95
  let withSignalArg: AbortSignal | undefined;
98
96
  const store: ReactiveStreamStore<T> = {
99
97
  connect: jest.fn().mockImplementation(() => {
100
98
  throw new Error('not implemented');
101
99
  }),
102
100
  getState: () => state,
103
- reset: () => {
104
- resets++;
105
- },
101
+ // The hook connects via `withSignal(signal).connect()` and relies on aborting that signal for
102
+ // teardown — it must never `reset()` the store. Throw so an accidental reset fails loudly.
103
+ reset: jest.fn().mockImplementation(() => {
104
+ throw new Error('not implemented');
105
+ }),
106
106
  subscribe: (callback: () => void) => {
107
107
  listeners.add(callback);
108
108
  return () => {
@@ -124,7 +124,6 @@ function createFakeStore<T>(): {
124
124
  state = next;
125
125
  listeners.forEach(l => l());
126
126
  },
127
- resetCount: () => resets,
128
127
  store,
129
128
  withSignalArg: () => withSignalArg,
130
129
  };
@@ -142,7 +141,6 @@ function makeStoreSource<T>() {
142
141
  };
143
142
  return {
144
143
  emit: (state: ReactiveState<T>) => latest!.emit(state),
145
- resetCount: () => latest!.resetCount(),
146
144
  source,
147
145
  withSignalArg: () => latest!.withSignalArg(),
148
146
  };
@@ -337,7 +335,7 @@ describe('useSubscriptionQuery', () => {
337
335
  await waitFor(() => expect(result.current.error).toBe(boom));
338
336
  });
339
337
 
340
- it('resets the store on unmount', async () => {
338
+ it('aborts the store connection on unmount', async () => {
341
339
  const src = makeStoreSource<number>();
342
340
  const { unmount } = renderHook(() => useSubscriptionQuery(['store-teardown'], src.source), {
343
341
  wrapper: createWrapper(),
@@ -345,7 +343,7 @@ describe('useSubscriptionQuery', () => {
345
343
  await waitFor(() => expect(src.withSignalArg()).toBeInstanceOf(AbortSignal));
346
344
 
347
345
  unmount();
348
- await waitFor(() => expect(src.resetCount()).toBeGreaterThan(0));
346
+ await waitFor(() => expect(src.withSignalArg()!.aborted).toBe(true));
349
347
  });
350
348
 
351
349
  it('does not connect when the source is null', async () => {
@@ -4,7 +4,7 @@ import {
4
4
  isSolanaError,
5
5
  ReactiveActionSource,
6
6
  ReactiveStreamSource,
7
- SOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERROR,
7
+ SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR,
8
8
  SolanaRpcResponse,
9
9
  } from '@solana/kit';
10
10
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -212,7 +212,7 @@ describe('useTrackedDataQuery', () => {
212
212
 
213
213
  await act(async () => fake.error(undefined));
214
214
  await waitFor(() =>
215
- expect(isSolanaError(result.current.error, SOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERROR)).toBe(
215
+ expect(isSolanaError(result.current.error, SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR)).toBe(
216
216
  true,
217
217
  ),
218
218
  );
@@ -293,7 +293,7 @@ describe('useTrackedDataQuery', () => {
293
293
  await waitFor(() => expect(fake.activeConnections()).toBe(1));
294
294
  });
295
295
 
296
- it('resets the store and aborts the connection on unmount', async () => {
296
+ it('aborts the connection on unmount', async () => {
297
297
  const fake = makeSpec();
298
298
  const { unmount } = renderHook(() => useTrackedDataQuery(['teardown'], fake.spec), { wrapper: makeWrapper() });
299
299
  await waitFor(() => expect(fake.activeConnections()).toBe(1));
@@ -1,4 +1,4 @@
1
- import type { ReactiveStreamSource } from '@solana/kit';
1
+ import { bridgeStoreToAsyncIterable, type ReactiveStreamSource } from '@solana/kit';
2
2
  import {
3
3
  experimental_streamedQuery,
4
4
  type QueryFunction,
@@ -12,7 +12,6 @@ import { useCallback } from 'react';
12
12
 
13
13
  import { useLatest } from '../useLatest';
14
14
  import type { UseRequestOptions } from '../useRequest';
15
- import { bridgeStoreToAsyncIterable } from './bridgeStoreToAsyncIterable';
16
15
 
17
16
  /**
18
17
  * Options accepted by {@link useSubscriptionQuery}: every option `@tanstack/react-query`'s `useQuery`
@@ -50,10 +49,10 @@ export type UseSubscriptionQueryOptions<T, TError = unknown, TData = T> = Omit<
50
49
  * Because the stream never settles, the query stays in `fetchStatus: 'fetching'` for the
51
50
  * subscription's whole life — `isFetching` is permanently `true` and `isLoading` flips false
52
51
  * after the first notification. Read `data` / `error` / `status` and ignore `isFetching` for
53
- * subscriptions. `result.refetch()` reconnects: TanStack aborts the old signal (ending the prior
54
- * iterable, which resets its store) and opens a fresh stream. Fire and forget — for a never-ending
55
- * stream the returned promise never resolves, so don't `await refetch()` (a click handler that does
56
- * will hang forever).
52
+ * subscriptions. `result.refetch()` reconnects: TanStack aborts the old signal (tearing the prior
53
+ * stream's connection down) and opens a fresh stream. Fire and forget — for a never-ending stream
54
+ * the returned promise never resolves, so don't `await refetch()` (a click handler that does will
55
+ * hang forever).
57
56
  *
58
57
  * Defaults, all overridable: `retry: false` (the underlying reactive store owns retry/backoff),
59
58
  * `staleTime: Infinity` and `refetchOnWindowFocus: false` (a focus revalidation must not tear down
@@ -133,7 +132,10 @@ export function useSubscriptionQuery<T, TError = unknown, TData = T>(
133
132
  if (typeof current === 'function') {
134
133
  return current(combinedSignal);
135
134
  }
136
- return bridgeStoreToAsyncIterable(current.reactiveStore(), combinedSignal);
135
+ const store = current.reactiveStore();
136
+ // reset not needed, the connection tears down and the store gets GCed after abort
137
+ store.withSignal(combinedSignal).connect();
138
+ return bridgeStoreToAsyncIterable(store, combinedSignal);
137
139
  },
138
140
  })(context),
139
141
  [getAbortSignalRef, sourceRef],
@@ -1,4 +1,8 @@
1
- import { createReactiveStoreWithInitialValueAndSlotTracking, type SolanaRpcResponse } from '@solana/kit';
1
+ import {
2
+ bridgeStoreToAsyncIterable,
3
+ createReactiveStoreWithInitialValueAndSlotTracking,
4
+ type SolanaRpcResponse,
5
+ } from '@solana/kit';
2
6
  import {
3
7
  experimental_streamedQuery,
4
8
  type QueryFunction,
@@ -14,7 +18,6 @@ import { useCallback } from 'react';
14
18
  import { useLatest } from '../useLatest';
15
19
  import type { UseRequestOptions } from '../useRequest';
16
20
  import type { TrackedDataSpec } from '../useTrackedData';
17
- import { bridgeStoreToAsyncIterable } from './bridgeStoreToAsyncIterable';
18
21
 
19
22
  /**
20
23
  * Options accepted by {@link useTrackedDataQuery}: every option `@tanstack/react-query`'s `useQuery`
@@ -53,8 +56,8 @@ export type UseTrackedDataQueryOptions<TItem, TError = unknown, TData = SolanaRp
53
56
  * Because the subscription never settles, the query stays in `fetchStatus: 'fetching'` for its whole
54
57
  * life — `isFetching` is permanently `true` and `isLoading` flips false after the first value. Read
55
58
  * `data` / `error` / `status` and ignore `isFetching`. `result.refetch()` reconnects: TanStack aborts
56
- * the old signal (ending the prior iterable, which resets its store) and re-runs both the initial RPC
57
- * fetch and the subscription. Fire and forget — for a never-ending stream the returned promise never
59
+ * the old signal (tearing the prior stream's connection down) and re-runs both the initial RPC fetch
60
+ * and the subscription. Fire and forget — for a never-ending stream the returned promise never
58
61
  * resolves, so don't `await refetch()`.
59
62
  *
60
63
  * Defaults, all overridable: `retry: false` (the underlying reactive store owns retry/backoff),
@@ -152,6 +155,8 @@ export function useTrackedDataQuery<
152
155
  // signal (when present) so aborting either tears the stream down.
153
156
  const combinedSignal = userSignal ? AbortSignal.any([signal, userSignal]) : signal;
154
157
  const store = createReactiveStoreWithInitialValueAndSlotTracking(current);
158
+ // reset not needed, the connection tears down and the store gets GCed after abort
159
+ store.withSignal(combinedSignal).connect();
155
160
  // Refuse any envelope older than the slot the cache already holds. A fresh store
156
161
  // (e.g. after a remount) tracks slots from scratch, so without this it could
157
162
  // overwrite the cached value with an older one from a lagging RPC node.
@@ -246,10 +246,9 @@ describe('useRequestSWR', () => {
246
246
  const withSignal = jest.fn(() => ({ dispatchAsync }));
247
247
  const source = stubActionSource<string>({ dispatchAsync, withSignal });
248
248
  const ctrl = new AbortController();
249
- renderHook(
250
- () => useRequestSWR(['source-signal-identity'], source, { getAbortSignal: () => ctrl.signal }),
251
- { wrapper },
252
- );
249
+ renderHook(() => useRequestSWR(['source-signal-identity'], source, { getAbortSignal: () => ctrl.signal }), {
250
+ wrapper,
251
+ });
253
252
  await waitFor(() => expect(withSignal).toHaveBeenCalledWith(ctrl.signal));
254
253
  });
255
254
 
@@ -321,8 +320,7 @@ describe('useRequestSWR', () => {
321
320
  reactiveStore: () => createReactiveActionStore<[], string>(() => Promise.resolve(value)),
322
321
  });
323
322
  const { result, rerender } = renderHook(
324
- ({ source }: { source: ReactiveActionSource<string> }) =>
325
- useRequestSWR(['source-latest'], source),
323
+ ({ source }: { source: ReactiveActionSource<string> }) => useRequestSWR(['source-latest'], source),
326
324
  { initialProps: { source: sourceFor('a') }, wrapper },
327
325
  );
328
326
  await waitFor(() => expect(result.current.data).toBe('a'));
@@ -0,0 +1,45 @@
1
+ import type { Address, ClientWithAirdrop, Lamports, Signature } from '@solana/kit';
2
+
3
+ import { type ActionResult, useAction } from './useAction';
4
+
5
+ /**
6
+ * Requests an airdrop of SOL to an address, as a reactive action.
7
+ *
8
+ * Wraps `client.airdrop` with {@link useAction}: each `dispatch(address, amount)` runs the airdrop
9
+ * with a fresh `AbortSignal` and tracks its lifecycle through React state. Calling `dispatch` again
10
+ * while a previous airdrop is in flight aborts the first. This is a great fit for a devnet or
11
+ * localnet "fund this account" button, where the `isRunning` / `data` / `error` tracking drives the
12
+ * UI directly.
13
+ *
14
+ * The airdrop capability is typically available on test networks (devnet, testnet) and local
15
+ * validators. Some implementations (e.g. LiteSVM) update balances directly without sending a
16
+ * transaction, in which case the resolved `data` is `undefined` rather than a {@link Signature}.
17
+ *
18
+ * @param client - A client with an airdrop plugin installed (`ClientWithAirdrop`).
19
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the recipient `address` and
20
+ * the `amount` of lamports, and resolve with the transaction {@link Signature}, or `undefined`
21
+ * when the airdrop was performed without a transaction.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { useAirdrop } from '@solana/react';
26
+ * import { lamports } from '@solana/kit';
27
+ *
28
+ * function AirdropButton({ client, address }) {
29
+ * const { dispatch, isRunning } = useAirdrop(client);
30
+ * return (
31
+ * <button disabled={isRunning} onClick={() => dispatch(address, lamports(1_000_000_000n))}>
32
+ * {isRunning ? 'Airdropping…' : 'Airdrop 1 SOL'}
33
+ * </button>
34
+ * );
35
+ * }
36
+ * ```
37
+ *
38
+ * @see {@link ActionResult}
39
+ * @see {@link useAction}
40
+ */
41
+ export function useAirdrop(
42
+ client: ClientWithAirdrop,
43
+ ): ActionResult<[address: Address, amount: Lamports], Signature | undefined> {
44
+ return useAction((abortSignal, address: Address, amount: Lamports) => client.airdrop(address, amount, abortSignal));
45
+ }
package/src/useClient.ts CHANGED
@@ -8,12 +8,13 @@ import { ClientContext } from './ClientProvider';
8
8
  * {@link SolanaError} with code {@link SOLANA_ERROR__REACT__MISSING_PROVIDER} if no provider is
9
9
  * mounted in the ancestor tree.
10
10
  *
11
- * Defaults to the base {@link Client} shape. Callers who know a specific plugin is installed may
12
- * widen the type via the generic — this is a pure cast with no runtime capability check, so reach
13
- * for {@link useClientCapability} when a missing plugin should fail loudly at mount instead of
14
- * surfacing later as `undefined`.
11
+ * Pass the shape of your client through the generic (typically the `AppClient` type you export
12
+ * alongside the client) so every installed capability is typed at the call site. This is a pure
13
+ * cast with no runtime capability check, so reach for {@link useClientCapability} when a missing
14
+ * plugin should fail loudly at mount instead of surfacing later as `undefined`.
15
15
  *
16
- * @typeParam TClient - The shape the client is expected to satisfy. Pure type assertion.
16
+ * @typeParam TClient - The shape the client is expected to satisfy. Pure type assertion, so this
17
+ * must always be supplied — the hook can't infer it from its (absent) arguments.
17
18
  *
18
19
  * @example
19
20
  * ```tsx
@@ -29,7 +30,7 @@ import { ClientContext } from './ClientProvider';
29
30
  * @see {@link ClientProvider}
30
31
  * @see {@link useClientCapability}
31
32
  */
32
- export function useClient<TClient extends object = object>(): Client<TClient> {
33
+ export function useClient<TClient extends object>(): Client<TClient> {
33
34
  const client = React.useContext(ClientContext);
34
35
  if (client == null) {
35
36
  throw new SolanaError(SOLANA_ERROR__REACT__MISSING_PROVIDER, { hookName: 'useClient' });
@@ -0,0 +1,47 @@
1
+ import type { ClientWithIdentity, ClientWithSubscribeToIdentity, TransactionSigner } from '@solana/kit';
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+
4
+ const NOOP_UNSUBSCRIBE = () => {};
5
+
6
+ /**
7
+ * Reads `client.identity` and re-renders whenever it changes, returning `undefined` while no
8
+ * identity is available.
9
+ *
10
+ * The identity is the {@link TransactionSigner} representing the wallet whose on-chain assets the
11
+ * application is acting upon.
12
+ *
13
+ * When the client advertises {@link ClientWithSubscribeToIdentity},
14
+ * this hook subscribes via `client.subscribeToIdentity` so the returned value always reflects the
15
+ * latest identity. For a client whose identity is fixed for its lifetime, the hook falls back to a
16
+ * no-op subscription and simply reads the value once.
17
+ *
18
+ * @param client - A client with an identity plugin installed. If it also advertises
19
+ * `subscribeToIdentity`, the hook tracks changes reactively.
20
+ * @returns The current `client.identity` signer, or `undefined` if no identity is currently
21
+ * available.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const identity = useIdentity(client);
26
+ * return <span>{identity ? `Signed in as ${identity.address}` : 'Signed out'}</span>;
27
+ * ```
28
+ *
29
+ * @see {@link usePayer}
30
+ */
31
+ export function useIdentity(
32
+ client: ClientWithIdentity & Partial<ClientWithSubscribeToIdentity>,
33
+ ): TransactionSigner | undefined {
34
+ const subscribe = useCallback(
35
+ (onStoreChange: () => void) =>
36
+ client.subscribeToIdentity ? client.subscribeToIdentity(onStoreChange) : NOOP_UNSUBSCRIBE,
37
+ [client],
38
+ );
39
+ const getSnapshot = useCallback((): TransactionSigner | undefined => {
40
+ try {
41
+ return client.identity;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }, [client]);
46
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
47
+ }
@@ -0,0 +1,44 @@
1
+ import type { ClientWithPayer, ClientWithSubscribeToPayer, TransactionSigner } from '@solana/kit';
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+
4
+ const NOOP_UNSUBSCRIBE = () => {};
5
+
6
+ /**
7
+ * Reads `client.payer` and re-renders whenever it changes, returning `undefined` while no payer is
8
+ * available.
9
+ *
10
+ * The payer is the {@link TransactionSigner} a client uses to sign and pay for transactions by
11
+ * default.
12
+ *
13
+ * When the client advertises {@link ClientWithSubscribeToPayer}, this hook subscribes via
14
+ * `client.subscribeToPayer` so the returned value always reflects the latest payer. For a client
15
+ * whose payer is fixed for its lifetime, the hook falls back to a no-op subscription and simply
16
+ * reads the value once.
17
+ *
18
+ * @param client - A client with a payer plugin installed. If it also advertises
19
+ * `subscribeToPayer`, the hook tracks changes reactively.
20
+ * @returns The current `client.payer` signer, or `undefined` if no payer is currently available.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const payer = usePayer(client);
25
+ * return <span>{payer ? `Paying with ${payer.address}` : 'No payer'}</span>;
26
+ * ```
27
+ *
28
+ * @see {@link useIdentity}
29
+ */
30
+ export function usePayer(client: ClientWithPayer & Partial<ClientWithSubscribeToPayer>): TransactionSigner | undefined {
31
+ const subscribe = useCallback(
32
+ (onStoreChange: () => void) =>
33
+ client.subscribeToPayer ? client.subscribeToPayer(onStoreChange) : NOOP_UNSUBSCRIBE,
34
+ [client],
35
+ );
36
+ const getSnapshot = useCallback((): TransactionSigner | undefined => {
37
+ try {
38
+ return client.payer;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }, [client]);
43
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
44
+ }
@@ -0,0 +1,30 @@
1
+ import type { ClientWithTransactionPlanning, InstructionPlanInput, SingleTransactionPlan } from '@solana/kit';
2
+
3
+ import { type ActionResult, useAction } from './useAction';
4
+
5
+ /**
6
+ * Plans a single transaction from an instruction input, as a reactive action.
7
+ *
8
+ * Wraps `client.planTransaction` with {@link useAction}: each `dispatch(input)` runs the plan with a
9
+ * fresh `AbortSignal` and tracks its lifecycle through React state. Calling `dispatch` again while a
10
+ * previous plan is in flight aborts the first. Use this when you expect all instructions to fit in a
11
+ * single transaction; reach for {@link usePlanTransactions} when they might need splitting.
12
+ *
13
+ * @param client - A client with a transaction-planning plugin installed.
14
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the
15
+ * {@link InstructionPlanInput} and resolve with the planned transaction message.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const { dispatch, data, isRunning } = usePlanTransaction(client);
20
+ * <button disabled={isRunning} onClick={() => dispatch(instructions)}>Plan</button>
21
+ * ```
22
+ *
23
+ * @see {@link usePlanTransactions}
24
+ * @see {@link useSendTransaction}
25
+ */
26
+ export function usePlanTransaction(
27
+ client: ClientWithTransactionPlanning,
28
+ ): ActionResult<[input: InstructionPlanInput], SingleTransactionPlan['message']> {
29
+ return useAction((abortSignal, input: InstructionPlanInput) => client.planTransaction(input, { abortSignal }));
30
+ }
@@ -0,0 +1,31 @@
1
+ import type { ClientWithTransactionPlanning, InstructionPlanInput, TransactionPlan } from '@solana/kit';
2
+
3
+ import { type ActionResult, useAction } from './useAction';
4
+
5
+ /**
6
+ * Plans one or more transactions from an instruction input, as a reactive action.
7
+ *
8
+ * Wraps `client.planTransactions` with {@link useAction}: each `dispatch(input)` runs the plan with a
9
+ * fresh `AbortSignal` and tracks its lifecycle through React state. Calling `dispatch` again while a
10
+ * previous plan is in flight aborts the first. Use this when the instructions might be split across
11
+ * multiple transactions due to size limits; reach for {@link usePlanTransaction} for the single-
12
+ * transaction case.
13
+ *
14
+ * @param client - A client with a transaction-planning plugin installed.
15
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the
16
+ * {@link InstructionPlanInput} and resolve with the full {@link TransactionPlan}.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const { dispatch, data, isRunning } = usePlanTransactions(client);
21
+ * <button disabled={isRunning} onClick={() => dispatch(instructions)}>Plan</button>
22
+ * ```
23
+ *
24
+ * @see {@link usePlanTransaction}
25
+ * @see {@link useSendTransactions}
26
+ */
27
+ export function usePlanTransactions(
28
+ client: ClientWithTransactionPlanning,
29
+ ): ActionResult<[input: InstructionPlanInput], TransactionPlan> {
30
+ return useAction((abortSignal, input: InstructionPlanInput) => client.planTransactions(input, { abortSignal }));
31
+ }
@@ -0,0 +1,34 @@
1
+ import type { ClientWithTransactionSending, SuccessfulSingleTransactionPlanResult } from '@solana/kit';
2
+
3
+ import { type ActionResult, useAction } from './useAction';
4
+
5
+ type SendTransactionInput = Parameters<ClientWithTransactionSending['sendTransaction']>[0];
6
+
7
+ /**
8
+ * Sends a single transaction to the network, as a reactive action.
9
+ *
10
+ * Wraps `client.sendTransaction` with {@link useAction}, which handles signing, submission, and
11
+ * confirmation. Each `dispatch(input)` runs with a fresh `AbortSignal` and tracks its lifecycle
12
+ * through React state; calling `dispatch` again while a previous send is in flight aborts the first.
13
+ * Accepts flexible input: instructions, an instruction plan, a single transaction message, or a
14
+ * single transaction plan. Use {@link useSendTransactions} when the work may span multiple
15
+ * transactions.
16
+ *
17
+ * @param client - A client with a transaction-sending plugin installed.
18
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the input and resolve with
19
+ * the {@link SuccessfulSingleTransactionPlanResult}.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const { dispatch, data, isRunning } = useSendTransaction(client);
24
+ * <button disabled={isRunning} onClick={() => dispatch(instructions)}>Send</button>
25
+ * ```
26
+ *
27
+ * @see {@link useSendTransactions}
28
+ * @see {@link usePlanTransaction}
29
+ */
30
+ export function useSendTransaction(
31
+ client: ClientWithTransactionSending,
32
+ ): ActionResult<[input: SendTransactionInput], SuccessfulSingleTransactionPlanResult> {
33
+ return useAction((abortSignal, input: SendTransactionInput) => client.sendTransaction(input, { abortSignal }));
34
+ }
@@ -0,0 +1,33 @@
1
+ import type { ClientWithTransactionSending, TransactionPlanResult } from '@solana/kit';
2
+
3
+ import { type ActionResult, useAction } from './useAction';
4
+
5
+ type SendTransactionsInput = Parameters<ClientWithTransactionSending['sendTransactions']>[0];
6
+
7
+ /**
8
+ * Sends one or more transactions to the network, as a reactive action.
9
+ *
10
+ * Wraps `client.sendTransactions` with {@link useAction}, which handles signing, submission, and
11
+ * confirmation. Each `dispatch(input)` runs with a fresh `AbortSignal` and tracks its lifecycle
12
+ * through React state; calling `dispatch` again while a previous send is in flight aborts the first.
13
+ * Accepts flexible input: instructions, an instruction plan, or a transaction plan. Use
14
+ * {@link useSendTransaction} for the single-transaction case.
15
+ *
16
+ * @param client - A client with a transaction-sending plugin installed.
17
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the input and resolve with
18
+ * the {@link TransactionPlanResult} for all transactions.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const { dispatch, data, isRunning } = useSendTransactions(client);
23
+ * <button disabled={isRunning} onClick={() => dispatch(instructions)}>Send</button>
24
+ * ```
25
+ *
26
+ * @see {@link useSendTransaction}
27
+ * @see {@link usePlanTransactions}
28
+ */
29
+ export function useSendTransactions(
30
+ client: ClientWithTransactionSending,
31
+ ): ActionResult<[input: SendTransactionsInput], TransactionPlanResult> {
32
+ return useAction((abortSignal, input: SendTransactionsInput) => client.sendTransactions(input, { abortSignal }));
33
+ }
@@ -1,39 +0,0 @@
1
- import { type ReactiveStreamStore } from '@solana/kit';
2
- /**
3
- * Bridges a {@link ReactiveStreamStore} into the `AsyncIterable` contract that TanStack Query's
4
- * `experimental_streamedQuery` consumes. The TanStack twin of `bridgeStoreToSwr`: where the SWR
5
- * bridge pushes each store update into a `next` callback, this one exposes a *pull*-based async
6
- * iterable, since `streamedQuery` drives the stream by `for await`-ing it.
7
- *
8
- * Subscribes to the store, `connect()`s it bound to `signal`, and yields its unified lifecycle:
9
- * - `loaded` → yields the value, unless an optional `shouldYield` predicate rejects it. Latest-wins:
10
- * if several notifications land between pulls, only the most recent unconsumed value is yielded (a
11
- * subscription consumer wants the freshest state, not a backlog).
12
- * - `error` → throws, so the `streamedQuery` queryFn rejects and the query surfaces `error`.
13
- * Substitutes a {@link SOLANA_ERROR__REACT__SUBSCRIPTION_CLOSED_WITHOUT_ERROR} sentinel when the
14
- * store reports an error with a nullish payload (mirrors `bridgeStoreToSwr`). An error takes
15
- * precedence over a buffered value: if a `loaded` value is still pending when an `error` arrives,
16
- * that value is dropped and the error propagates (once errored, stop yielding).
17
- * - `signal` aborts → ends the iterable cleanly (no error). An abort means teardown — unmount, a
18
- * superseding refetch, or a caller-supplied deadline — so the subscription stops rather than
19
- * failing.
20
- *
21
- * Whichever way the iterable ends — value exhaustion, error, or abort — the `finally` block
22
- * unsubscribes and `reset()`s the store, aborting the active connection. The store the caller hands
23
- * in is owned by this bridge for the iterable's lifetime.
24
- *
25
- * @typeParam T - The notification type emitted by the store.
26
- *
27
- * @param store - An idle stream store to drive.
28
- * @param signal - The abort signal to bind the connection to (TanStack's query-cancellation signal,
29
- * optionally combined with a caller-supplied one). Aborting it tears the stream down.
30
- * @param shouldYield - Optional gate run against each `loaded` value before it is yielded. Return
31
- * `false` to drop the value. When omitted, every loaded value is yielded.
32
- *
33
- * @returns An `AsyncIterable<T>` that yields each store value until the store errors or `signal`
34
- * aborts.
35
- *
36
- * @internal
37
- */
38
- export declare function bridgeStoreToAsyncIterable<T>(store: ReactiveStreamStore<T>, signal: AbortSignal, shouldYield?: (value: T) => boolean): AsyncIterable<T>;
39
- //# sourceMappingURL=bridgeStoreToAsyncIterable.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bridgeStoreToAsyncIterable.d.ts","sourceRoot":"","sources":["../../../src/query/bridgeStoreToAsyncIterable.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,mBAAmB,EAG3B,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EACxC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,EAC7B,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,GACpC,aAAa,CAAC,CAAC,CAAC,CA0DlB"}