@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/README.md +162 -1
- package/dist/index.cjs +909 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +423 -5
- package/dist/index.esm.js +900 -31
- package/dist/index.esm.js.map +1 -1
- package/dist/testing.d.ts +9 -1
- package/dist/wallet-standard.cjs +129 -0
- package/dist/wallet-standard.cjs.map +1 -0
- package/dist/wallet-standard.d.ts +73 -0
- package/dist/wallet-standard.esm.js +120 -0
- package/dist/wallet-standard.esm.js.map +1 -0
- package/package.json +30 -3
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useRef, useCallback, useEffect, useContext, useState,
|
|
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
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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(
|
|
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
|
|
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
|