@usearete/react 0.46.0 → 0.47.0

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/dist/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
- import React, { createContext, useRef, useCallback, useEffect, useContext, useState, useSyncExternalStore, useMemo } from 'react';
2
- import { Arete, getTransactionFailureOutcome, OperationExecutionError, unwrapOperationExecutionError, InstructionError, compareProgramIdentity, serializeViewKey, canonicalQueryKey, AreteError, getStackRuntimeExtensions } from '@usearete/sdk';
3
- export { Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OperationCallbackError, OperationExecutionError, ProcessedSlotTimeoutError, QueryStore, ReadRequestError, SubscriptionRegistry, TransactionExecutionError, buildInstruction, canonicalQueryKey, createInstructionExecutor, createInstructionHandler, createPublicKeySeed, createSeed, derivePda, executeInstruction, formatProgramError, formatRawToUi, getTransactionFailureOutcome, isErrorFrame, isHostedAreteEndpoint, isSnapshotFrame, isSubscribedFrame, isUnsubscribedFrame, isValidFrame, normalizeSubscriptionQuery, parseFrame, parseFrameFromBlob, parseInstructionError, resolveAccounts, safeToRawAmount, serializeInstructionData, serializeViewKey, stringifyBigints, toRawAmount, unwrapOperationExecutionError, validateAccountResolution, withPrograms } from '@usearete/sdk';
1
+ import React, { createContext, useRef, useCallback, useEffect, useContext, useState, useMemo, useSyncExternalStore } from 'react';
2
+ import { Arete, createSession, getTransactionFailureOutcome, OperationExecutionError, unwrapOperationExecutionError, InstructionError, compareProgramIdentity, serializeViewKey, canonicalQueryKey, AreteError, getStackRuntimeExtensions, createPlatformHttpClient } from '@usearete/sdk';
3
+ export { Arete, AreteError, ConnectionManager, DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, FrameProcessor, InstructionError, MemoryAdapter, OperationCallbackError, OperationExecutionError, ProcessedSlotTimeoutError, QueryStore, ReadRequestError, SubscriptionRegistry, TransactionExecutionError, buildInstruction, canonicalQueryKey, createInstructionExecutor, createInstructionHandler, createPlatformHttpClient, createPublicKeySeed, createSeed, derivePda, executeInstruction, formatProgramError, formatRawToUi, getTransactionFailureOutcome, isErrorFrame, isHostedAreteEndpoint, isSnapshotFrame, isSubscribedFrame, isUnsubscribedFrame, isValidFrame, normalizeSubscriptionQuery, parseFrame, parseFrameFromBlob, parseInstructionError, resolveAccounts, safeToRawAmount, serializeInstructionData, serializeViewKey, stringifyBigints, toRawAmount, unwrapOperationExecutionError, validateAccountResolution, withPrograms } from '@usearete/sdk';
4
4
  import { create } from 'zustand';
5
5
 
6
6
  const DEFAULT_FLUSH_INTERVAL_MS = 16;
@@ -379,6 +379,10 @@ function AreteProvider({ children, ...config }) {
379
379
  };
380
380
  return (React.createElement(AreteContext.Provider, { value: value }, children));
381
381
  }
382
+ /** The enclosing `AreteProvider`'s context, or `null` outside one. */
383
+ function useOptionalAreteContext() {
384
+ return useContext(AreteContext);
385
+ }
382
386
  function useAreteContext() {
383
387
  const context = useContext(AreteContext);
384
388
  if (!context) {
@@ -421,6 +425,276 @@ function useConnectionState(stack, options) {
421
425
  return state;
422
426
  }
423
427
 
428
+ const AreteSessionContext = createContext(null);
429
+ /**
430
+ * `ready()` rejects with this when its `AreteSessionProvider` was removed
431
+ * before the session opened (or is called after the provider is gone): no
432
+ * session will come. `code` is `provider_unmounted`.
433
+ */
434
+ class AreteSessionUnavailableError extends Error {
435
+ constructor(message = 'The AreteSessionProvider was unmounted before its session opened') {
436
+ super(message);
437
+ this.code = 'provider_unmounted';
438
+ this.name = 'AreteSessionUnavailableError';
439
+ }
440
+ }
441
+ /** Whether `error` is an {@link AreteSessionUnavailableError}. */
442
+ function isAreteSessionUnavailableError(error) {
443
+ return error instanceof AreteSessionUnavailableError;
444
+ }
445
+ function createDeferred() {
446
+ let resolvePromise;
447
+ let rejectPromise;
448
+ const promise = new Promise((resolve, reject) => {
449
+ resolvePromise = resolve;
450
+ rejectPromise = reject;
451
+ });
452
+ // A failure nobody awaited is reported through `status`, not as an
453
+ // unhandled rejection.
454
+ promise.catch(() => undefined);
455
+ const deferred = {
456
+ promise,
457
+ settled: false,
458
+ resolve(value) {
459
+ if (deferred.settled)
460
+ return;
461
+ deferred.settled = true;
462
+ resolvePromise(value);
463
+ },
464
+ reject(error) {
465
+ if (deferred.settled)
466
+ return;
467
+ deferred.settled = true;
468
+ rejectPromise(error);
469
+ },
470
+ };
471
+ return deferred;
472
+ }
473
+ function isPlainObject(value) {
474
+ if (typeof value !== 'object' || value === null)
475
+ return false;
476
+ const prototype = Object.getPrototypeOf(value);
477
+ return prototype === Object.prototype || prototype === null;
478
+ }
479
+ /**
480
+ * Equal by identity, or plain objects with the same keys whose values are
481
+ * equal to `depth` levels. Arrays, functions and class instances compare by
482
+ * identity.
483
+ */
484
+ function sameShape(left, right, depth) {
485
+ if (Object.is(left, right))
486
+ return true;
487
+ if (depth === 0 || !isPlainObject(left) || !isPlainObject(right))
488
+ return false;
489
+ const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
490
+ const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
491
+ return leftKeys.length === rightKeys.length
492
+ && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && sameShape(left[key], right[key], depth - 1));
493
+ }
494
+ /**
495
+ * `value`, or the previous one while it has the same shape: two levels of
496
+ * plain objects, so `options={{ apiUrl, auth: { tokenKind: 'api', … } }}`
497
+ * written inline does not reopen the session on every render.
498
+ */
499
+ function useSameShape(value) {
500
+ const ref = useRef(value);
501
+ if (!sameShape(ref.current, value, 2))
502
+ ref.current = value;
503
+ return ref.current;
504
+ }
505
+ function toError(value) {
506
+ return value instanceof Error ? value : new Error(String(value));
507
+ }
508
+ const openSession = createSession;
509
+ /**
510
+ * One Arete session (`createSession`) for everything below it, read with
511
+ * {@link useAreteSession}.
512
+ *
513
+ * ```tsx
514
+ * const SQUADS = { programs: { squads: SQUADS_V4_PROGRAM } };
515
+ * const auth = { tokenKind: 'api', tokenEndpoint: '/api/arete-token' } as const;
516
+ *
517
+ * <AreteSessionProvider definition={SQUADS} options={{ auth }} wallet={wallet}>
518
+ * <App />
519
+ * </AreteSessionProvider>;
520
+ *
521
+ * // below it
522
+ * const { session } = useAreteSession<typeof SQUADS>();
523
+ * session?.programs.squads; // typed
524
+ * ```
525
+ *
526
+ * Given `definition`, the provider opens the session when it mounts and
527
+ * closes it when it unmounts. `options.auth` and `options.fetch` default to
528
+ * the enclosing `AreteProvider`'s. When the definition or the options
529
+ * change, the old session is closed at once and a new one is opened:
530
+ * `session` is `null` until it is ready, so nothing runs against a session
531
+ * opened with credentials that no longer apply. Options are compared by
532
+ * shape two levels deep (an inline `auth` object with the same fields is the
533
+ * same auth); functions such as `getToken` or `fetch` compare by identity,
534
+ * so keep them stable. The wallet never reopens the session: changing
535
+ * `wallet` calls `session.setWallet`.
536
+ *
537
+ * Given `session`, the provider only shares it: the app opened it and closes
538
+ * it. `wallet`, when passed, is set on it the same way.
539
+ */
540
+ function AreteSessionProvider(props) {
541
+ const arete = useOptionalAreteContext();
542
+ const isExternal = props.definition === undefined;
543
+ const externalSession = isExternal ? props.session ?? null : null;
544
+ const definition = useSameShape(props.definition);
545
+ const given = (props.options ?? {});
546
+ const inheritedAuth = arete?.config.auth;
547
+ const inheritedFetch = arete?.config.fetch;
548
+ const options = useSameShape({
549
+ ...given,
550
+ ...(given.auth === undefined && inheritedAuth ? { auth: inheritedAuth } : {}),
551
+ ...(given.fetch === undefined && inheritedFetch ? { fetch: inheritedFetch } : {}),
552
+ });
553
+ const [attempt, setAttempt] = useState(0);
554
+ const key = useMemo(() => ({ definition, options, external: externalSession, attempt }), [definition, options, externalSession, attempt]);
555
+ const keyRef = useRef(key);
556
+ keyRef.current = key;
557
+ const walletRef = useRef(props.wallet);
558
+ walletRef.current = props.wallet;
559
+ const syncsWallet = !isExternal || 'wallet' in props;
560
+ const deferredsRef = useRef(new WeakMap());
561
+ // Every waiter that may still be pending, so unmounting can settle them.
562
+ const pendingRef = useRef(new Set());
563
+ const unmountedRef = useRef(false);
564
+ const deferredFor = useCallback((forKey) => {
565
+ let deferred = deferredsRef.current.get(forKey);
566
+ if (!deferred) {
567
+ deferred = createDeferred();
568
+ if (unmountedRef.current)
569
+ deferred.reject(new AreteSessionUnavailableError());
570
+ deferredsRef.current.set(forKey, deferred);
571
+ for (const pending of pendingRef.current) {
572
+ if (pending.settled)
573
+ pendingRef.current.delete(pending);
574
+ }
575
+ if (!deferred.settled)
576
+ pendingRef.current.add(deferred);
577
+ }
578
+ return deferred;
579
+ }, []);
580
+ // Removed for good, nothing will open: the waiters fail instead of
581
+ // hanging. The check waits a microtask because StrictMode (and a tree
582
+ // that is hidden and shown again) runs this cleanup and then mounts the
583
+ // same provider again, which keeps the waiters for the session it opens.
584
+ const mountedRef = useRef(false);
585
+ useEffect(() => {
586
+ mountedRef.current = true;
587
+ unmountedRef.current = false;
588
+ const waiters = pendingRef.current;
589
+ return () => {
590
+ mountedRef.current = false;
591
+ void Promise.resolve().then(() => {
592
+ if (mountedRef.current)
593
+ return;
594
+ unmountedRef.current = true;
595
+ const pending = [...waiters];
596
+ waiters.clear();
597
+ // Mounted again later (a hidden tree shown again), `ready()` waits
598
+ // for the session opened then rather than repeating this failure.
599
+ deferredsRef.current = new WeakMap();
600
+ for (const deferred of pending)
601
+ deferred.reject(new AreteSessionUnavailableError());
602
+ });
603
+ };
604
+ }, []);
605
+ const [opened, setOpened] = useState(null);
606
+ // A caller waiting on the previous key's session gets the next one.
607
+ const previousKeyRef = useRef(null);
608
+ useEffect(() => {
609
+ const previous = previousKeyRef.current;
610
+ previousKeyRef.current = key;
611
+ if (previous && previous !== key) {
612
+ const waiting = deferredsRef.current.get(previous);
613
+ if (waiting && !waiting.settled)
614
+ waiting.resolve(deferredFor(key).promise);
615
+ }
616
+ }, [key, deferredFor]);
617
+ useEffect(() => {
618
+ if (key.external)
619
+ deferredFor(key).resolve(key.external);
620
+ }, [key, deferredFor]);
621
+ useEffect(() => {
622
+ if (!key.definition)
623
+ return undefined;
624
+ let active = true;
625
+ let session = null;
626
+ const deferred = deferredFor(key);
627
+ const wallet = walletRef.current;
628
+ openSession(key.definition, { ...key.options, ...(wallet ? { wallet } : {}) }).then((openedSession) => {
629
+ if (!active) {
630
+ openedSession.close();
631
+ return;
632
+ }
633
+ session = openedSession;
634
+ // The wallet may have changed while the session was opening.
635
+ if (openedSession.wallet !== walletRef.current)
636
+ openedSession.setWallet(walletRef.current);
637
+ deferred.resolve(openedSession);
638
+ setOpened({ key, session: openedSession, error: null });
639
+ }, (cause) => {
640
+ if (!active)
641
+ return;
642
+ const error = toError(cause);
643
+ deferred.reject(error);
644
+ setOpened({ key, session: null, error });
645
+ });
646
+ return () => {
647
+ active = false;
648
+ session?.close();
649
+ };
650
+ }, [key, deferredFor]);
651
+ let session = null;
652
+ let error = null;
653
+ let status = 'opening';
654
+ if (isExternal) {
655
+ session = externalSession;
656
+ status = session ? 'open' : 'opening';
657
+ }
658
+ else if (opened?.key === key) {
659
+ session = opened.session;
660
+ error = opened.error;
661
+ status = error ? 'error' : 'open';
662
+ }
663
+ const { wallet } = props;
664
+ useEffect(() => {
665
+ if (session && syncsWallet && session.wallet !== wallet)
666
+ session.setWallet(wallet);
667
+ }, [session, syncsWallet, wallet]);
668
+ const ready = useCallback(() => deferredFor(keyRef.current).promise, [deferredFor]);
669
+ const retry = useCallback(() => {
670
+ setAttempt((current) => current + 1);
671
+ }, []);
672
+ const value = useMemo(() => ({ session, status, error, ready, retry }), [session, status, error, ready, retry]);
673
+ return (React.createElement(AreteSessionContext.Provider, { value: value }, props.children));
674
+ }
675
+ /**
676
+ * The nearest {@link AreteSessionProvider}'s session. Pass the definition
677
+ * (or the session type) for typed members:
678
+ *
679
+ * ```tsx
680
+ * const { session, status, ready } = useAreteSession<typeof SQUADS>();
681
+ * const squads = session?.programs.squads;
682
+ * ```
683
+ *
684
+ * Throws outside an `AreteSessionProvider`.
685
+ */
686
+ function useAreteSession() {
687
+ const value = useContext(AreteSessionContext);
688
+ if (!value) {
689
+ throw new Error('useAreteSession must be used within AreteSessionProvider');
690
+ }
691
+ return value;
692
+ }
693
+ /** The nearest `AreteSessionProvider`'s value, or `null` outside one. */
694
+ function useOptionalAreteSession() {
695
+ return useContext(AreteSessionContext);
696
+ }
697
+
424
698
  function isMarkedDefaultReconciliation(value) {
425
699
  return (typeof value === 'function'
426
700
  && value.areteDefaultReconciliation === true
@@ -1662,12 +1936,17 @@ function useQuery(client, query, snapshotEnabled, enabled, initialData) {
1662
1936
  return current;
1663
1937
  }
1664
1938
  catch (value) {
1665
- state.fallback = {
1666
- ...state.fallback,
1667
- isLoading: false,
1668
- isRefreshing: false,
1669
- error: queryError(value, 'Subscription snapshot failed'),
1670
- };
1939
+ // A lease whose client was disconnected under it (its owner closed
1940
+ // it) throws on every read: report the error once and keep returning
1941
+ // the same snapshot, so React sees a stable value.
1942
+ if (!state.fallback.error) {
1943
+ state.fallback = {
1944
+ ...state.fallback,
1945
+ isLoading: false,
1946
+ isRefreshing: false,
1947
+ error: queryError(value, 'Subscription snapshot failed'),
1948
+ };
1949
+ }
1671
1950
  return state.fallback;
1672
1951
  }
1673
1952
  // state is replaced whenever either listed identity changes.
@@ -1878,12 +2157,588 @@ function createListViewHook(viewDef, client) {
1878
2157
  return { use, useOne, refresh };
1879
2158
  }
1880
2159
 
2160
+ /** The typed failure of a {@link useOperationMutation} run. */
2161
+ class OperationMutationError extends Error {
2162
+ constructor(input) {
2163
+ super(input.message);
2164
+ this.name = 'OperationMutationError';
2165
+ this.kind = input.kind;
2166
+ this.cause = input.cause;
2167
+ this.failure = input.failure ?? null;
2168
+ this.prepared = input.prepared ?? null;
2169
+ this.completedSignatures = [...(input.completedSignatures ?? [])];
2170
+ this.signature = this.failure && 'signature' in this.failure
2171
+ ? this.failure.signature
2172
+ : undefined;
2173
+ this.programError = this.failure?.status === 'chain-failed'
2174
+ ? this.failure.programError
2175
+ : undefined;
2176
+ }
2177
+ }
2178
+ /** The `cause` chain of `error`, outermost first. */
2179
+ function causeChain(error) {
2180
+ const chain = [];
2181
+ let current = error;
2182
+ while (current !== undefined && current !== null && chain.length < 10 && !chain.includes(current)) {
2183
+ chain.push(current);
2184
+ current = typeof current === 'object' ? current.cause : undefined;
2185
+ }
2186
+ return chain;
2187
+ }
2188
+ /** Error codes wallets report a declined prompt with (EIP-1193 4001 and kin). */
2189
+ const USER_REJECTION_CODES = [4001, 'ACTION_REJECTED', 'USER_REJECTED'];
2190
+ /**
2191
+ * Whether `error` (or anything in its `cause` chain) is a wallet prompt the
2192
+ * user declined or closed. Wallets report this in a few ways: EIP-1193 code
2193
+ * 4001 (Phantom, Solflare), a `...RejectedError`, or the message.
2194
+ */
2195
+ function isWalletRejection(error) {
2196
+ return causeChain(error).some((item) => {
2197
+ if (typeof item !== 'object' || item === null)
2198
+ return false;
2199
+ const { code, name, message } = item;
2200
+ if (USER_REJECTION_CODES.includes(code))
2201
+ return true;
2202
+ if (name === 'WalletStandardSignerError')
2203
+ return code === 'user_rejected';
2204
+ if (typeof name === 'string' && /rejected/i.test(name))
2205
+ return true;
2206
+ return typeof message === 'string'
2207
+ && /user (rejected|declined|denied|cancel)|rejected the request|request (was )?(rejected|declined)/i
2208
+ .test(message);
2209
+ });
2210
+ }
2211
+ /** The `code` of a `WalletStandardSignerError` in the cause chain, if any. */
2212
+ function walletSignerErrorCode(error) {
2213
+ for (const item of causeChain(error)) {
2214
+ if (item instanceof Error
2215
+ && item.name === 'WalletStandardSignerError'
2216
+ && typeof item.code === 'string') {
2217
+ return item.code;
2218
+ }
2219
+ }
2220
+ return null;
2221
+ }
2222
+ function messageOf(error) {
2223
+ if (error instanceof Error && error.message)
2224
+ return error.message;
2225
+ if (typeof error === 'string' && error)
2226
+ return error;
2227
+ return 'The operation failed';
2228
+ }
2229
+ /**
2230
+ * Classify an execution failure. Exported so code outside the hook (an
2231
+ * imperative `session.execute`) can show the same messages.
2232
+ */
2233
+ function toOperationMutationError(error, prepared = null, completedSignatures = []) {
2234
+ if (error instanceof OperationMutationError)
2235
+ return error;
2236
+ const failure = getTransactionFailureOutcome(error);
2237
+ const completed = error instanceof OperationExecutionError
2238
+ ? error.completedReceipts.map((receipt) => receipt.signature)
2239
+ : completedSignatures;
2240
+ const make = (kind) => new OperationMutationError({
2241
+ kind,
2242
+ message: messageOf(error),
2243
+ cause: error,
2244
+ failure,
2245
+ prepared,
2246
+ completedSignatures: completed,
2247
+ });
2248
+ if (failure?.status === 'chain-failed')
2249
+ return make('chain-failed');
2250
+ if (failure?.status === 'submitted-unknown')
2251
+ return make('submitted-unknown');
2252
+ switch (walletSignerErrorCode(error)) {
2253
+ case 'wallet_returned_invalid_transaction':
2254
+ return make('wallet-modified');
2255
+ case 'sign_transaction_unsupported':
2256
+ return make('wallet-feature-unsupported');
2257
+ case 'chain_unsupported':
2258
+ return make('wallet-chain-unsupported');
2259
+ case 'transaction_version_unsupported':
2260
+ return make('wallet-version-unsupported');
2261
+ case 'user_rejected':
2262
+ return make('wallet-rejected');
2263
+ }
2264
+ if (isWalletRejection(error))
2265
+ return make('wallet-rejected');
2266
+ return make('not-submitted');
2267
+ }
2268
+ function hasActivity(wallet) {
2269
+ return typeof wallet?.onActivity === 'function';
2270
+ }
2271
+ /**
2272
+ * The wallet as one run hands it to the executor: every `signAndSend` it
2273
+ * makes carries `listener` as that send's `onActivity`, after any listener
2274
+ * the caller's send options already set.
2275
+ */
2276
+ function withSendActivity(wallet, listener) {
2277
+ const signAndSend = (instructions, options, ...rest) => {
2278
+ const own = options?.onActivity;
2279
+ const onActivity = typeof own === 'function'
2280
+ ? (activity) => {
2281
+ try {
2282
+ own(activity);
2283
+ }
2284
+ finally {
2285
+ listener(activity);
2286
+ }
2287
+ }
2288
+ : listener;
2289
+ return wallet.signAndSend(instructions, { ...(options ?? {}), onActivity }, ...rest);
2290
+ };
2291
+ return new Proxy(wallet, {
2292
+ get(target, property) {
2293
+ if (property === 'signAndSend')
2294
+ return signAndSend;
2295
+ const value = Reflect.get(target, property, target);
2296
+ return typeof value === 'function' ? value.bind(target) : value;
2297
+ },
2298
+ });
2299
+ }
2300
+ function isPreparedWithSigners(value) {
2301
+ return typeof value === 'object'
2302
+ && value !== null
2303
+ && 'operation' in value
2304
+ && !('kind' in value && 'plan' in value);
2305
+ }
2306
+ async function resolveExecutor(source) {
2307
+ if (!source)
2308
+ return null;
2309
+ if (typeof source === 'function')
2310
+ return (await source()) ?? null;
2311
+ return source;
2312
+ }
2313
+ const IDLE_STATE = {
2314
+ status: 'idle',
2315
+ prepared: null,
2316
+ result: undefined,
2317
+ error: null,
2318
+ signatures: [],
2319
+ transactionIndex: null,
2320
+ };
2321
+ /**
2322
+ * Run a prepared instruction, transaction or flow with a wallet and report
2323
+ * where it is: idle → preparing → signing → sending → confirming → done (or
2324
+ * error). Works with anything that has `execute(prepared, options)`, a
2325
+ * connected client or a session, so it needs no stack:
2326
+ *
2327
+ * ```tsx
2328
+ * const createMultisig = useOperationMutation(
2329
+ * async (input: CreateInput) => {
2330
+ * const createKey = await generateKeyPairSigner();
2331
+ * return {
2332
+ * operation: await squads.flows.arete.createMultisig.prepare({ ...input, createKey: createKey.address }),
2333
+ * signers: [createKey],
2334
+ * };
2335
+ * },
2336
+ * { executor: session, wallet },
2337
+ * );
2338
+ * // Inside an AreteSessionProvider, `executor` can be left out.
2339
+ * // createMultisig.mutate(input); createMultisig.isAwaitingWallet → "Approve in your wallet"
2340
+ * ```
2341
+ *
2342
+ * Signers returned by `prepare`, given to the hook and given to the run are
2343
+ * all passed to `execute`. A run without a wallet fails with `not-ready`
2344
+ * before `prepare` is called; set `allowExecutorWallet` to sign with the
2345
+ * executor's own wallet instead. A second run while one is in progress rejects
2346
+ * with `busy` and leaves the first alone, so a double click never opens two
2347
+ * wallet prompts. "In progress" is exactly `isBusy`: a run ends when its
2348
+ * `done` or `error` status is published, before `onSuccess`/`onError` are
2349
+ * awaited, so a button disabled by `isBusy` is never enabled while `run()`
2350
+ * would refuse it. A run started during those callbacks is a new run; the
2351
+ * earlier run's `run()` promise still settles after its callback.
2352
+ */
2353
+ function useOperationMutation(prepare, options) {
2354
+ const [state, setState] = useState(IDLE_STATE);
2355
+ const sessionContext = useOptionalAreteSession();
2356
+ const prepareRef = useRef(prepare);
2357
+ const optionsRef = useRef(options);
2358
+ const sessionReadyRef = useRef(sessionContext?.ready);
2359
+ prepareRef.current = prepare;
2360
+ optionsRef.current = options;
2361
+ sessionReadyRef.current = sessionContext?.ready;
2362
+ const generationRef = useRef(0);
2363
+ // The run that holds the lock, if any. A run releases only its own lock,
2364
+ // so a run that finishes its callbacks late never frees a newer run's.
2365
+ const runningRef = useRef(null);
2366
+ const mountedRef = useRef(true);
2367
+ useEffect(() => {
2368
+ mountedRef.current = true;
2369
+ return () => {
2370
+ mountedRef.current = false;
2371
+ generationRef.current += 1;
2372
+ };
2373
+ }, []);
2374
+ const run = useCallback(async (input, runOptions = {}) => {
2375
+ if (runningRef.current) {
2376
+ throw new OperationMutationError({
2377
+ kind: 'busy',
2378
+ message: 'This operation is already running',
2379
+ cause: undefined,
2380
+ });
2381
+ }
2382
+ const lock = {};
2383
+ runningRef.current = lock;
2384
+ const release = () => {
2385
+ if (runningRef.current === lock)
2386
+ runningRef.current = null;
2387
+ };
2388
+ const generation = ++generationRef.current;
2389
+ const hookOptions = optionsRef.current;
2390
+ let lastStatus = 'idle';
2391
+ let transactionIndex = null;
2392
+ const update = (patch) => {
2393
+ if (!mountedRef.current || generation !== generationRef.current)
2394
+ return;
2395
+ if (patch.transactionIndex !== undefined)
2396
+ transactionIndex = patch.transactionIndex;
2397
+ setState((current) => ({ ...current, ...patch }));
2398
+ if (patch.status && patch.status !== lastStatus) {
2399
+ lastStatus = patch.status;
2400
+ try {
2401
+ hookOptions.onStatusChange?.(patch.status, { transactionIndex });
2402
+ }
2403
+ catch {
2404
+ // Observational only.
2405
+ }
2406
+ }
2407
+ };
2408
+ let prepared = null;
2409
+ const signatures = [];
2410
+ const fail = async (error) => {
2411
+ // The run is over once `error` is published: `isBusy` turns false and
2412
+ // the hook takes the next run, even while `onError` is still pending.
2413
+ release();
2414
+ update({ status: 'error', error, prepared, signatures: [...signatures] });
2415
+ try {
2416
+ await hookOptions.onError?.(error);
2417
+ }
2418
+ catch {
2419
+ // The callback observes the failure; it does not replace it.
2420
+ }
2421
+ return error;
2422
+ };
2423
+ setState(IDLE_STATE);
2424
+ update({ status: 'preparing' });
2425
+ try {
2426
+ let executor;
2427
+ const source = hookOptions.executor !== undefined
2428
+ ? hookOptions.executor
2429
+ : sessionReadyRef.current ?? null;
2430
+ try {
2431
+ executor = await resolveExecutor(source);
2432
+ }
2433
+ catch (cause) {
2434
+ throw await fail(new OperationMutationError({
2435
+ kind: 'not-ready', message: messageOf(cause), cause,
2436
+ }));
2437
+ }
2438
+ if (!executor) {
2439
+ throw await fail(new OperationMutationError({
2440
+ kind: 'not-ready',
2441
+ message: 'Nothing to execute the operation with yet',
2442
+ cause: undefined,
2443
+ }));
2444
+ }
2445
+ // No wallet, no run: nothing is prepared, nothing is signed.
2446
+ const wallet = runOptions.wallet
2447
+ ?? hookOptions.wallet
2448
+ ?? (hookOptions.allowExecutorWallet
2449
+ ? executor.wallet
2450
+ : undefined);
2451
+ if (!wallet) {
2452
+ throw await fail(new OperationMutationError({
2453
+ kind: 'not-ready',
2454
+ message: 'Connect a wallet to run this operation',
2455
+ cause: undefined,
2456
+ }));
2457
+ }
2458
+ let extraSigners = [];
2459
+ try {
2460
+ const built = await prepareRef.current(input);
2461
+ if (isPreparedWithSigners(built)) {
2462
+ prepared = built.operation;
2463
+ extraSigners = built.signers ?? [];
2464
+ }
2465
+ else {
2466
+ prepared = built;
2467
+ }
2468
+ }
2469
+ catch (cause) {
2470
+ throw await fail(new OperationMutationError({
2471
+ kind: 'prepare-failed', message: messageOf(cause), cause,
2472
+ }));
2473
+ }
2474
+ update({ prepared });
2475
+ // A wallet that reports its activity says when its prompt opens; until
2476
+ // then the transaction is still being built (blockhash, budgets). The
2477
+ // run listens to its own sends only, and only until they settle, so
2478
+ // another run on the same wallet, or one that starts while this run's
2479
+ // callbacks are pending, cannot move this run's status.
2480
+ const reportsActivity = hasActivity(wallet);
2481
+ let listening = reportsActivity;
2482
+ const runWallet = reportsActivity
2483
+ ? withSendActivity(wallet, (activity) => {
2484
+ if (!listening)
2485
+ return;
2486
+ if (activity.stage === 'signing' || activity.stage === 'sending'
2487
+ || activity.stage === 'confirming') {
2488
+ update({ status: activity.stage });
2489
+ }
2490
+ })
2491
+ : wallet;
2492
+ const signers = [
2493
+ ...(hookOptions.signers ?? []),
2494
+ ...extraSigners,
2495
+ ...(runOptions.signers ?? []),
2496
+ ];
2497
+ const send = runOptions.send ?? hookOptions.send;
2498
+ let result;
2499
+ try {
2500
+ result = await executor.execute(prepared, {
2501
+ wallet: runWallet,
2502
+ ...(signers.length > 0 ? { signers } : {}),
2503
+ ...(send ? { send } : {}),
2504
+ onTransactionStart: (event) => {
2505
+ update({
2506
+ status: reportsActivity ? 'preparing' : 'signing',
2507
+ transactionIndex: event.transactionIndex,
2508
+ });
2509
+ },
2510
+ onTransactionSuccess: (event) => {
2511
+ signatures.push(event.receipt.signature);
2512
+ update({ signatures: [...signatures] });
2513
+ },
2514
+ });
2515
+ }
2516
+ catch (cause) {
2517
+ listening = false;
2518
+ throw await fail(toOperationMutationError(cause, prepared, signatures));
2519
+ }
2520
+ listening = false;
2521
+ // As in `fail`: `done` releases the lock, so `isBusy` and `run()` agree
2522
+ // while `onSuccess` runs. Activity listening has already stopped, so a
2523
+ // run started from (or during) the callback cannot move this one.
2524
+ release();
2525
+ update({
2526
+ status: 'done',
2527
+ result,
2528
+ signatures: [...result.signatures],
2529
+ transactionIndex: null,
2530
+ });
2531
+ try {
2532
+ await hookOptions.onSuccess?.(result, prepared);
2533
+ }
2534
+ catch {
2535
+ // The operation confirmed; a failing callback does not undo that.
2536
+ }
2537
+ return result;
2538
+ }
2539
+ finally {
2540
+ release();
2541
+ }
2542
+ }, []);
2543
+ const mutate = useCallback((input, runOptions) => {
2544
+ void run(input, runOptions).catch(() => undefined);
2545
+ }, [run]);
2546
+ const reset = useCallback(() => {
2547
+ if (runningRef.current)
2548
+ return;
2549
+ generationRef.current += 1;
2550
+ setState(IDLE_STATE);
2551
+ }, []);
2552
+ const busy = state.status === 'preparing' || state.status === 'signing'
2553
+ || state.status === 'sending' || state.status === 'confirming';
2554
+ return {
2555
+ run,
2556
+ mutate,
2557
+ reset,
2558
+ status: state.status,
2559
+ isBusy: busy,
2560
+ isAwaitingWallet: state.status === 'signing',
2561
+ prepared: state.prepared,
2562
+ artifacts: state.prepared?.artifacts,
2563
+ result: state.result,
2564
+ error: state.error,
2565
+ signatures: state.signatures,
2566
+ transactionIndex: state.transactionIndex,
2567
+ transactionCount: state.prepared ? state.prepared.plan.transactions.length : null,
2568
+ };
2569
+ }
2570
+
2571
+ function isStackClient(value) {
2572
+ return typeof value === 'object'
2573
+ && value !== null
2574
+ && typeof value.getSubscriptionRegistry === 'function'
2575
+ && typeof value.onConnectionStateChange === 'function';
2576
+ }
2577
+ function isMemberOptions(value) {
2578
+ const stack = value?.stack;
2579
+ return typeof stack === 'object' && stack !== null && 'views' in stack;
2580
+ }
2581
+ function zustandStoreOf(client) {
2582
+ return client?.store?.store ?? null;
2583
+ }
2584
+ /**
2585
+ * `useArete` for a client the caller (usually a session) connected and
2586
+ * owns. It reads the member's own connection and store: nothing connects,
2587
+ * and nothing is disconnected when the component unmounts or the member is
2588
+ * replaced. A `null` member reads as the connecting state `useArete(stack)`
2589
+ * reports before its client exists.
2590
+ */
2591
+ function useMemberArete(member, options) {
2592
+ const client = member;
2593
+ const stack = client?.stack ?? options?.stack;
2594
+ if (!stack) {
2595
+ throw new Error('useArete(member) could not read the stack definition of the member it was given. '
2596
+ + 'Pass it as useArete(member, { stack }).');
2597
+ }
2598
+ // Mutations wait for the streamed slot they landed in only when the member
2599
+ // was connected over the socket.
2600
+ const transport = client?.transport;
2601
+ const placeholderPrograms = options?.programs;
2602
+ const sessionContext = useOptionalAreteSession();
2603
+ const [connection, setConnection] = useState(() => ({
2604
+ client,
2605
+ state: client?.connectionState ?? 'disconnected',
2606
+ error: null,
2607
+ socketIssue: null,
2608
+ }));
2609
+ // A new member (a reopened session) never shows the previous one's state.
2610
+ const current = connection.client === client
2611
+ ? connection
2612
+ : { client, state: client?.connectionState ?? 'disconnected', error: null, socketIssue: null };
2613
+ useEffect(() => {
2614
+ if (!client) {
2615
+ setConnection((previous) => previous.client === null
2616
+ ? previous
2617
+ : { client: null, state: 'disconnected', error: null, socketIssue: null });
2618
+ return undefined;
2619
+ }
2620
+ const update = (next) => {
2621
+ setConnection((previous) => {
2622
+ const base = previous.client === client
2623
+ ? previous
2624
+ : { client, state: client.connectionState, error: null, socketIssue: null };
2625
+ return { ...base, ...next };
2626
+ });
2627
+ };
2628
+ update({ state: client.connectionState });
2629
+ const unsubscribeState = client.onConnectionStateChange((state, stateError) => {
2630
+ if (state === 'error') {
2631
+ update({ state, error: normalizeConnectionError(stateError, 'Arete connection entered an error state') });
2632
+ }
2633
+ else if (state === 'connected') {
2634
+ update({ state, error: null, socketIssue: null });
2635
+ }
2636
+ else {
2637
+ update({ state });
2638
+ }
2639
+ });
2640
+ const unsubscribeIssue = client.onSocketIssue((socketIssue) => {
2641
+ update({ socketIssue });
2642
+ });
2643
+ // Only listeners are removed: the member's connection is its owner's.
2644
+ return () => {
2645
+ unsubscribeIssue();
2646
+ unsubscribeState();
2647
+ };
2648
+ }, [client]);
2649
+ const views = useMemo(() => buildViewHooks(stack, client), [stack, client]);
2650
+ const programs = useMemo(() => {
2651
+ if (!client) {
2652
+ return buildDisconnectedProgramHooks(stack.programs, placeholderPrograms);
2653
+ }
2654
+ return buildProgramHookInterfaces(client.programs, client, useInstructionMutation, { defaultReconciliation: transport !== 'http' });
2655
+ }, [client, stack, transport, placeholderPrograms]);
2656
+ const connectedRead = (client?.read ?? null);
2657
+ const readArgCounts = getStackRuntimeExtensions(stack)?.readArgCounts;
2658
+ const read = useMemo(() => buildReadInterfaces(connectedRead, readArgCounts), [connectedRead, readArgCounts]);
2659
+ const sessionStatus = sessionContext?.status;
2660
+ const sessionError = sessionContext?.error ?? null;
2661
+ const sessionRetry = sessionContext?.retry;
2662
+ const sessionFailed = !client && sessionStatus === 'error';
2663
+ const retry = useCallback(async () => {
2664
+ // The member's connection belongs to its session: retrying reopens a
2665
+ // session that failed to open, and does nothing otherwise.
2666
+ if (sessionFailed)
2667
+ sessionRetry?.();
2668
+ }, [sessionFailed, sessionRetry]);
2669
+ const status = client
2670
+ ? current.error && current.state !== 'connected' ? 'error' : current.state
2671
+ : sessionFailed ? 'error' : 'connecting';
2672
+ const error = client ? current.error : sessionFailed ? sessionError : null;
2673
+ const extension = (name) => client?.[name]
2674
+ ?? stack[name];
2675
+ return {
2676
+ views,
2677
+ queries: client?.queries ?? {},
2678
+ programs,
2679
+ chain: client?.chain ?? null,
2680
+ zustandStore: zustandStoreOf(client),
2681
+ client,
2682
+ read,
2683
+ reads: read,
2684
+ addresses: extension('addresses'),
2685
+ constants: extension('constants'),
2686
+ defaults: extension('defaults'),
2687
+ math: extension('math'),
2688
+ connectionState: client ? current.state : 'disconnected',
2689
+ status,
2690
+ isConnected: Boolean(client) && current.state === 'connected',
2691
+ isLoading: !client && !sessionFailed,
2692
+ canRetry: sessionFailed,
2693
+ error,
2694
+ socketIssue: client ? current.socketIssue : null,
2695
+ retry,
2696
+ };
2697
+ }
2698
+ function buildViewHooks(stack, client) {
2699
+ const result = {};
2700
+ for (const [viewName, viewGroup] of Object.entries(stack.views)) {
2701
+ result[viewName] = {};
2702
+ if (typeof viewGroup === 'object' && viewGroup !== null) {
2703
+ for (const [subViewName, viewDef] of Object.entries(viewGroup)) {
2704
+ if (!viewDef || typeof viewDef !== 'object' || !('mode' in viewDef))
2705
+ continue;
2706
+ if (viewDef.mode === 'state') {
2707
+ result[viewName][subViewName] = createStateViewHook(viewDef, client);
2708
+ }
2709
+ else if (viewDef.mode === 'list') {
2710
+ result[viewName][subViewName] = createListViewHook(viewDef, client);
2711
+ }
2712
+ }
2713
+ }
2714
+ }
2715
+ return result;
2716
+ }
1881
2717
  function normalizeConnectionError(value, fallback) {
1882
2718
  if (value instanceof Error)
1883
2719
  return value;
1884
2720
  return new Error(typeof value === 'string' && value.length > 0 ? value : fallback);
1885
2721
  }
1886
- function useArete(stack,
2722
+ function useArete(stackOrMember,
2723
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2724
+ options
2725
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2726
+ ) {
2727
+ // A call site keeps one form for its lifetime (a stack definition, or a
2728
+ // session member that may be null while the session opens), so the hooks
2729
+ // either branch calls stay in the same order.
2730
+ if (isStackClient(stackOrMember)) {
2731
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- one form per call site
2732
+ return useMemberArete(stackOrMember, options);
2733
+ }
2734
+ if (stackOrMember == null && isMemberOptions(options)) {
2735
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- one form per call site
2736
+ return useMemberArete(null, options);
2737
+ }
2738
+ // eslint-disable-next-line react-hooks/rules-of-hooks -- one form per call site
2739
+ return useStackArete((stackOrMember ?? undefined), options);
2740
+ }
2741
+ function useStackArete(stack,
1887
2742
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1888
2743
  options
1889
2744
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2046,25 +2901,7 @@ options
2046
2901
  retryPromiseRef.current = { lookupKey, promise: tracked };
2047
2902
  return tracked;
2048
2903
  }, [lookupKey, lookupOptions, retryClient, resolvedStack]);
2049
- const views = useMemo(() => {
2050
- const result = {};
2051
- for (const [viewName, viewGroup] of Object.entries(resolvedStack.views)) {
2052
- result[viewName] = {};
2053
- if (typeof viewGroup === 'object' && viewGroup !== null) {
2054
- for (const [subViewName, viewDef] of Object.entries(viewGroup)) {
2055
- if (!viewDef || typeof viewDef !== 'object' || !('mode' in viewDef))
2056
- continue;
2057
- if (viewDef.mode === 'state') {
2058
- result[viewName][subViewName] = createStateViewHook(viewDef, client);
2059
- }
2060
- else if (viewDef.mode === 'list') {
2061
- result[viewName][subViewName] = createListViewHook(viewDef, client);
2062
- }
2063
- }
2064
- }
2065
- }
2066
- return result;
2067
- }, [resolvedStack, client]);
2904
+ const views = useMemo(() => buildViewHooks(resolvedStack, client), [resolvedStack, client]);
2068
2905
  const programs = useMemo(() => {
2069
2906
  if (!client) {
2070
2907
  // Placeholder hooks so components can render (and keep hook order)
@@ -2091,7 +2928,7 @@ options
2091
2928
  queries: (client?.queries ?? {}),
2092
2929
  programs: programs,
2093
2930
  chain: client?.chain ?? null,
2094
- zustandStore: client?.store?.store ?? null,
2931
+ zustandStore: zustandStoreOf(client),
2095
2932
  client,
2096
2933
  read,
2097
2934
  reads: read,
@@ -2171,6 +3008,38 @@ function summarizeStatuses(sources) {
2171
3008
  /** @deprecated Use summarizeStatuses; it also accepts connection and read results. */
2172
3009
  const summarizeViews = summarizeStatuses;
2173
3010
 
3011
+ /**
3012
+ * A client for the Arete API's account routes, authenticated with the
3013
+ * provider's `auth` (and `fetch`). With `auth.tokenKind: 'api'`, the same
3014
+ * credential the provider mints its stack and gateway sessions with, so one
3015
+ * login drives both:
3016
+ *
3017
+ * ```tsx
3018
+ * <AreteProvider auth={{ tokenKind: 'api', tokenEndpoint: '/api/arete-token' }}>
3019
+ * …
3020
+ * </AreteProvider>
3021
+ * // in a component
3022
+ * const http = usePlatformHttpClient({ apiUrl });
3023
+ * const squads = useMemo(() => createSquadsClient({ http }), [http]);
3024
+ * ```
3025
+ *
3026
+ * The client is stable while `apiUrl`, `auth` and `fetch` keep their
3027
+ * identity, so pass a memoized (or module-level) `auth` object. Throws
3028
+ * during render when the auth carries no API credential, like
3029
+ * `createPlatformHttpClient`.
3030
+ */
3031
+ function usePlatformHttpClient(options = {}) {
3032
+ const { config } = useAreteContext();
3033
+ const auth = options.auth ?? config.auth;
3034
+ const fetchImpl = config.fetch;
3035
+ const { apiUrl } = options;
3036
+ return useMemo(() => createPlatformHttpClient({
3037
+ ...(apiUrl !== undefined ? { apiUrl } : {}),
3038
+ ...(auth ? { auth } : {}),
3039
+ ...(fetchImpl ? { fetch: fetchImpl } : {}),
3040
+ }), [apiUrl, auth, fetchImpl]);
3041
+ }
3042
+
2174
3043
  const chainIds = new WeakMap();
2175
3044
  let nextChainId = 1;
2176
3045
  function chainKey(chain) {
@@ -2386,5 +3255,5 @@ function useProtocolEvents(client, instanceId, options) {
2386
3255
  }, [client, instanceId, enabled]);
2387
3256
  }
2388
3257
 
2389
- export { AreteProvider, DEFAULT_RECONCILIATION_TIMEOUT_MS, ZustandAdapter, createAreteReact, createProcessedSlotReconciliation, reconcileProcessedSlot, summarizeStatuses, summarizeViews, useAllowedSteps, useArete, useAreteContext, useAsyncRead, useConnectionState, useInstructionMutation, useNativeBalance, useProtocolEvents, useProtocolInstance, useProtocolParticipant, useReconcileProcessedSlot, useTokenBalance, useWalletActions };
3258
+ export { AreteProvider, AreteSessionProvider, AreteSessionUnavailableError, DEFAULT_RECONCILIATION_TIMEOUT_MS, OperationMutationError, ZustandAdapter, createAreteReact, createProcessedSlotReconciliation, isAreteSessionUnavailableError, isWalletRejection, reconcileProcessedSlot, summarizeStatuses, summarizeViews, toOperationMutationError, useAllowedSteps, useArete, useAreteContext, useAreteSession, useAsyncRead, useConnectionState, useInstructionMutation, useNativeBalance, useOperationMutation, usePlatformHttpClient, useProtocolEvents, useProtocolInstance, useProtocolParticipant, useReconcileProcessedSlot, useTokenBalance, useWalletActions };
2390
3259
  //# sourceMappingURL=index.esm.js.map