@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.
- package/README.md +117 -39
- package/dist/index.browser.cjs +64 -0
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.mjs +58 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.native.mjs +58 -1
- package/dist/index.native.mjs.map +1 -1
- package/dist/index.node.cjs +64 -0
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.mjs +58 -1
- package/dist/index.node.mjs.map +1 -1
- package/dist/query.browser.cjs +5 -52
- package/dist/query.browser.cjs.map +1 -1
- package/dist/query.browser.mjs +5 -52
- package/dist/query.browser.mjs.map +1 -1
- package/dist/query.native.mjs +5 -52
- package/dist/query.native.mjs.map +1 -1
- package/dist/query.node.cjs +5 -52
- package/dist/query.node.cjs.map +1 -1
- package/dist/query.node.mjs +5 -52
- package/dist/query.node.mjs.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/query/useSubscriptionQuery.d.ts +5 -5
- package/dist/types/query/useSubscriptionQuery.d.ts.map +1 -1
- package/dist/types/query/useTrackedDataQuery.d.ts +2 -2
- package/dist/types/query/useTrackedDataQuery.d.ts.map +1 -1
- package/dist/types/useAirdrop.d.ts +40 -0
- package/dist/types/useAirdrop.d.ts.map +1 -0
- package/dist/types/useClient.d.ts +7 -6
- package/dist/types/useClient.d.ts.map +1 -1
- package/dist/types/useIdentity.d.ts +28 -0
- package/dist/types/useIdentity.d.ts.map +1 -0
- package/dist/types/usePayer.d.ts +27 -0
- package/dist/types/usePayer.d.ts.map +1 -0
- package/dist/types/usePlanTransaction.d.ts +25 -0
- package/dist/types/usePlanTransaction.d.ts.map +1 -0
- package/dist/types/usePlanTransactions.d.ts +26 -0
- package/dist/types/usePlanTransactions.d.ts.map +1 -0
- package/dist/types/useSendTransaction.d.ts +29 -0
- package/dist/types/useSendTransaction.d.ts.map +1 -0
- package/dist/types/useSendTransactions.d.ts +28 -0
- package/dist/types/useSendTransactions.d.ts.map +1 -0
- package/package.json +10 -10
- package/src/index.ts +7 -0
- package/src/query/__tests__/useRequestQuery-test.browser.tsx +2 -2
- package/src/query/__tests__/useSubscriptionQuery-test.browser.tsx +7 -9
- package/src/query/__tests__/useTrackedDataQuery-test.browser.tsx +3 -3
- package/src/query/useSubscriptionQuery.ts +9 -7
- package/src/query/useTrackedDataQuery.ts +9 -4
- package/src/swr/__tests__/useRequestSWR-test.browser.tsx +4 -6
- package/src/useAirdrop.ts +45 -0
- package/src/useClient.ts +7 -6
- package/src/useIdentity.ts +47 -0
- package/src/usePayer.ts +44 -0
- package/src/usePlanTransaction.ts +30 -0
- package/src/usePlanTransactions.ts +31 -0
- package/src/useSendTransaction.ts +34 -0
- package/src/useSendTransactions.ts +33 -0
- package/dist/types/query/bridgeStoreToAsyncIterable.d.ts +0 -39
- package/dist/types/query/bridgeStoreToAsyncIterable.d.ts.map +0 -1
- package/src/query/__tests__/bridgeStoreToAsyncIterable-test.ts +0 -188
- 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
|
-
|
|
104
|
-
|
|
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('
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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('
|
|
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
|
|
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 (
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
57
|
-
*
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* for {@link useClientCapability} when a missing
|
|
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
|
|
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
|
+
}
|
package/src/usePayer.ts
ADDED
|
@@ -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"}
|