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