@bootnodedev/canton-connect 0.3.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.
@@ -0,0 +1,777 @@
1
+ import { DappSDK, WalletConnectAdapter } from "@canton-network/dapp-sdk";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
3
+ import { assign, enqueueActions, fromCallback, fromPromise, setup, waitFor } from "xstate";
4
+ import { useActorRef } from "@xstate/react";
5
+ import { WALLET_DISABLED_REASON } from "@canton-network/core-types";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/CantonConnectProvider/adapters.ts
8
+ /** Builds the extra adapters for the SDK: WalletConnect when configured, plus any passed in. */
9
+ const buildAdditionalAdapters = (config, networkId) => {
10
+ const adapters = [...config.additionalAdapters ?? []];
11
+ if (config.walletConnectProjectId !== void 0 && config.walletConnectProjectId !== "") adapters.push(WalletConnectAdapter.create({
12
+ projectId: config.walletConnectProjectId,
13
+ chainId: networkId,
14
+ metadata: {
15
+ name: config.appName,
16
+ description: config.appDescription ?? config.appName,
17
+ url: config.appUrl ?? (typeof window === "undefined" ? "" : window.location.origin),
18
+ icons: []
19
+ }
20
+ }));
21
+ return adapters;
22
+ };
23
+ //#endregion
24
+ //#region src/connectError.ts
25
+ const PICKER_DISMISSED = "User closed the wallet picker";
26
+ /**
27
+ * Raised by `guardedConnect` when it settles a connect the SDK left pending. The SDK's own
28
+ * `connect()` is still running underneath, so whoever catches this retires the `DappSDK`.
29
+ *
30
+ * @example
31
+ * if (err instanceof PickerClosedError) discardSdk()
32
+ *
33
+ * @internal
34
+ */
35
+ var PickerClosedError = class extends Error {
36
+ constructor() {
37
+ super(PICKER_DISMISSED);
38
+ this.name = "PickerClosedError";
39
+ }
40
+ };
41
+ /**
42
+ * Raised by the connect actor when its early init rejects, so the machine can tell an init
43
+ * failure from a connect one: the SDK caches the rejection on the instance forever, and only a
44
+ * replacement instance can genuinely retry. The SDK's own error rides in `cause`.
45
+ *
46
+ * @example
47
+ * if (err instanceof InitFailedError) retireSdk()
48
+ */
49
+ var InitFailedError = class extends Error {
50
+ constructor(cause) {
51
+ super("DappSDK.init() failed", { cause });
52
+ this.name = "InitFailedError";
53
+ }
54
+ };
55
+ /**
56
+ * A connect the user walked away from: the picker was closed rather than a wallet failing.
57
+ *
58
+ * @example
59
+ * if (error !== undefined && !(error instanceof ConnectCancelledError)) {
60
+ * toast.error(error.message)
61
+ * }
62
+ *
63
+ * @category Errors
64
+ */
65
+ var ConnectCancelledError = class extends Error {
66
+ constructor(cause) {
67
+ super("Wallet connection cancelled", { cause });
68
+ this.name = "ConnectCancelledError";
69
+ }
70
+ };
71
+ /** Whether a rejection carries a string `message`, as a JSON-RPC error object does. */
72
+ const hasMessage = (cause) => typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string";
73
+ /**
74
+ * Hands `cause` back as an `Error`, wrapping what a wallet answered with over JSON-RPC.
75
+ *
76
+ * @example
77
+ * const error = toError(await sdk.signMessage(params).catch((cause: unknown) => cause))
78
+ * error.cause // the wallet's `{ code, message }` when that is what it sent
79
+ */
80
+ const toError = (cause) => {
81
+ if (cause instanceof Error) return cause;
82
+ return new Error(hasMessage(cause) ? cause.message : String(cause), { cause });
83
+ };
84
+ /** Classifies what `sdk.connect()` threw, so the cancel path is decided once. */
85
+ const toConnectError = (cause) => {
86
+ if (cause instanceof ConnectCancelledError) return cause;
87
+ return cause instanceof Error && cause.message === PICKER_DISMISSED ? new ConnectCancelledError(cause) : toError(cause);
88
+ };
89
+ //#endregion
90
+ //#region src/CantonConnectProvider/useConnectBridge.ts
91
+ /**
92
+ * Turns the machine's `connect` event into a promise. The machine's own tags say when an attempt
93
+ * has been answered, and its context carries what to raise.
94
+ */
95
+ const useConnectBridge = (actorRef) => {
96
+ return useCallback(async () => {
97
+ actorRef.send({ type: "connect" });
98
+ const settled = await waitFor(actorRef, (snapshot) => snapshot.hasTag("connect.settled") || snapshot.hasTag("connect.failed") || snapshot.hasTag("connect.cancelled"));
99
+ if (settled.hasTag("connect.settled")) return;
100
+ if (settled.hasTag("connect.cancelled")) throw new ConnectCancelledError();
101
+ throw toConnectError(settled.context.lastConnectError);
102
+ }, [actorRef]);
103
+ };
104
+ //#endregion
105
+ //#region src/walletAccount.ts
106
+ /** Whether one raw account entry still has ledger rights to act as a party. */
107
+ const isUsable = (account) => {
108
+ if (account.status === "initialized" || account.status === "removed") return false;
109
+ if (account.disabled === true) return account.reason === WALLET_DISABLED_REASON.NO_SIGNING_PROVIDER_MATCHED;
110
+ return true;
111
+ };
112
+ /** Filters a raw account list down to the ones still usable as a party. */
113
+ const selectUsableAccounts = (accounts) => accounts.filter(isUsable);
114
+ /** Picks the account flagged `primary`, falling back to the first if none is. */
115
+ const selectPrimaryAccount = (accounts) => accounts.find((a) => a.primary) ?? accounts[0];
116
+ /** Maps one raw account entry to the public `Party` shape the hooks expose. */
117
+ const toParty = (account, fallbackNetworkId) => ({
118
+ partyId: account.partyId,
119
+ networkId: account.networkId ?? fallbackNetworkId,
120
+ namespace: account.namespace,
121
+ signingProviderId: account.signingProviderId,
122
+ ...account.hint === void 0 ? {} : { name: account.hint },
123
+ ...account.publicKey === void 0 ? {} : { publicKey: account.publicKey }
124
+ });
125
+ //#endregion
126
+ //#region src/machine/accountsActors.ts
127
+ /** Narrows a raw wallet-account list to the primary usable party, or none. */
128
+ const toWalletAccounts = (accounts, networkId) => {
129
+ const primary = selectPrimaryAccount(selectUsableAccounts(accounts));
130
+ return { party: primary === void 0 ? void 0 : toParty(primary, networkId) };
131
+ };
132
+ /** Reads the wallet's account list once and resolves the primary usable party. */
133
+ const readAccounts = fromPromise(async ({ input: { sdk, networkId } }) => toWalletAccounts(await sdk.listAccounts(), networkId));
134
+ /** Forwards the wallet's own account-change pushes into the machine as `accounts.changed`. */
135
+ const accountsEvents = fromCallback(({ sendBack, input: { sdk, networkId } }) => {
136
+ const listener = (accounts) => {
137
+ sendBack({
138
+ type: "accounts.changed",
139
+ accounts: toWalletAccounts(accounts, networkId)
140
+ });
141
+ };
142
+ sdk.onAccountsChanged(listener).catch(() => {});
143
+ return () => {
144
+ sdk.removeOnAccountsChanged(listener).catch(() => {});
145
+ };
146
+ });
147
+ //#endregion
148
+ //#region src/machine/accountsMachine.ts
149
+ /**
150
+ * Reads the connected party once, then follows the wallet's own `accounts.changed` pushes.
151
+ * Invoked as `connectionMachine`'s `accounts` child while a session is authenticated.
152
+ */
153
+ const accountsMachine = setup({
154
+ actors: {
155
+ readAccounts,
156
+ accountsEvents
157
+ },
158
+ actions: {
159
+ applyAccounts: assign((_, params) => ({
160
+ ...params.accounts,
161
+ error: void 0
162
+ })),
163
+ assignError: assign((_, params) => ({ error: params.error }))
164
+ },
165
+ types: {
166
+ context: {},
167
+ events: {},
168
+ input: {}
169
+ }
170
+ }).createMachine({
171
+ context: ({ input }) => ({
172
+ ...input,
173
+ party: void 0,
174
+ error: void 0
175
+ }),
176
+ id: "accounts",
177
+ initial: "reading",
178
+ invoke: {
179
+ src: "accountsEvents",
180
+ input: ({ context: { sdk, networkId } }) => ({
181
+ sdk,
182
+ networkId
183
+ })
184
+ },
185
+ on: { "accounts.changed": {
186
+ target: ".ready",
187
+ actions: {
188
+ type: "applyAccounts",
189
+ params: ({ event: { accounts } }) => ({ accounts })
190
+ }
191
+ } },
192
+ states: {
193
+ reading: { invoke: {
194
+ src: "readAccounts",
195
+ input: ({ context: { sdk, networkId } }) => ({
196
+ sdk,
197
+ networkId
198
+ }),
199
+ onDone: {
200
+ target: "ready",
201
+ actions: {
202
+ type: "applyAccounts",
203
+ params: ({ event: { output } }) => ({ accounts: output })
204
+ }
205
+ },
206
+ onError: {
207
+ target: "unavailable",
208
+ actions: {
209
+ type: "assignError",
210
+ params: ({ event: { error } }) => ({ error })
211
+ }
212
+ }
213
+ } },
214
+ ready: {},
215
+ unavailable: {}
216
+ }
217
+ });
218
+ //#endregion
219
+ //#region src/guardedConnect.ts
220
+ const POLL_MS = 400;
221
+ let nativeOpen;
222
+ let inFlight = 0;
223
+ let opened;
224
+ /** Swaps in a `window.open` wrapper that captures the next popup, then restores itself. */
225
+ const borrowOpen = () => {
226
+ if (nativeOpen !== void 0) return;
227
+ const native = window.open;
228
+ nativeOpen = native;
229
+ window.open = (...args) => {
230
+ const popup = native.apply(window, args);
231
+ if (popup) {
232
+ opened = popup;
233
+ window.open = native;
234
+ nativeOpen = void 0;
235
+ }
236
+ return popup;
237
+ };
238
+ };
239
+ /** Restores the native `window.open`, but only once no guarded connect is still in flight. */
240
+ const returnOpen = () => {
241
+ if (nativeOpen === void 0 || inFlight > 0) return;
242
+ window.open = nativeOpen;
243
+ nativeOpen = void 0;
244
+ };
245
+ /** Posts the SDK picker's own result message, so its pending listener resolves as abandoned. */
246
+ const settleAbandonedConnect = () => {
247
+ window.postMessage({
248
+ messageType: "SPLICE_WALLET_PICKER_RESULT",
249
+ providerId: "abandoned",
250
+ walletType: "browser"
251
+ }, window.location.origin);
252
+ };
253
+ /** Rejects when the caller abandons the connect, closing the picker window on the way out. */
254
+ const abandonOn = (signal) => new Promise((_, reject) => {
255
+ if (signal === void 0) return;
256
+ const abandon = () => {
257
+ if (opened?.closed === false) opened.close();
258
+ if (inFlight === 1) settleAbandonedConnect();
259
+ reject(new ConnectCancelledError());
260
+ };
261
+ if (signal.aborted) {
262
+ abandon();
263
+ return;
264
+ }
265
+ signal.addEventListener("abort", abandon, { once: true });
266
+ });
267
+ /** Reports the wallet type from the picker's result message; call the return value to stop. */
268
+ const watchForPick = (picked) => {
269
+ const listener = (event) => {
270
+ if (event.origin === window.location.origin && event.data?.messageType === "SPLICE_WALLET_PICKER_RESULT") picked(event.data.walletType);
271
+ };
272
+ window.addEventListener("message", listener);
273
+ return () => window.removeEventListener("message", listener);
274
+ };
275
+ /**
276
+ * `sdk.connect()` with a watchdog on the popup the SDK opens. Call it only when no
277
+ * `CantonConnectConfig.walletPicker` is set; a consumer's picker owns its own surface.
278
+ *
279
+ * @example
280
+ * const result = await guardedConnect(sdk)
281
+ * if (!result.isConnected) throw new Error(result.reason)
282
+ */
283
+ const guardedConnect = (sdk, signal) => {
284
+ if (typeof window === "undefined") return sdk.connect();
285
+ inFlight += 1;
286
+ borrowOpen();
287
+ let seen = opened;
288
+ let watched = opened?.closed === false ? opened : void 0;
289
+ let poll;
290
+ let picked = false;
291
+ const unwatchPick = watchForPick((walletType) => {
292
+ picked = walletType === "browser";
293
+ });
294
+ const dismissed = new Promise((_resolve, reject) => {
295
+ poll = setInterval(() => {
296
+ if (opened !== seen) {
297
+ seen = opened;
298
+ watched = opened;
299
+ }
300
+ if (!picked && watched?.closed === true) {
301
+ if (inFlight === 1) settleAbandonedConnect();
302
+ reject(new PickerClosedError());
303
+ }
304
+ }, POLL_MS);
305
+ });
306
+ return Promise.race([
307
+ sdk.connect(),
308
+ dismissed,
309
+ abandonOn(signal)
310
+ ]).finally(() => {
311
+ clearInterval(poll);
312
+ unwatchPick();
313
+ inFlight -= 1;
314
+ returnOpen();
315
+ });
316
+ };
317
+ //#endregion
318
+ //#region src/machine/connectionActors.ts
319
+ /** The in-flight or settled `init()` per sdk instance. */
320
+ const initializations = /* @__PURE__ */ new WeakMap();
321
+ /** Runs `sdk.init()` once per sdk instance and caches the result for every later caller. */
322
+ const ensureInit = ({ sdk, initOptions }) => {
323
+ const started = initializations.get(sdk);
324
+ if (started !== void 0) return started;
325
+ const initialization = sdk.init({
326
+ defaultAdapters: [],
327
+ ...initOptions
328
+ });
329
+ initializations.set(sdk, initialization);
330
+ initialization.catch((error) => {
331
+ console.error("canton-connect: DappSDK.init() failed, so no wallet can be discovered or connected. This is usually a bad adapter config — check `additionalAdapters` and `walletConnectProjectId` on CantonConnectConfig.", error);
332
+ });
333
+ return initialization;
334
+ };
335
+ /** Reads the wallet's status and returns it only when a session is standing, else null. */
336
+ const standingSession = async (sdk) => {
337
+ const status = await sdk.status().catch(() => null);
338
+ return status?.connection?.isConnected ? { connection: status.connection } : null;
339
+ };
340
+ /** Resolves once the wallet answers, rejecting with the wallet's own error. */
341
+ const connect = fromPromise(async ({ input, signal }) => {
342
+ const { sdk, guardPicker } = input;
343
+ try {
344
+ await ensureInit(input);
345
+ } catch (error) {
346
+ throw new InitFailedError(error);
347
+ }
348
+ try {
349
+ const connection = await (guardPicker ? guardedConnect(sdk, signal) : sdk.connect());
350
+ const walletAnswer = { connection };
351
+ if (connection.isConnected) return walletAnswer;
352
+ return await standingSession(sdk) ?? walletAnswer;
353
+ } catch (error) {
354
+ if (error instanceof PickerClosedError || error instanceof ConnectCancelledError) throw error;
355
+ const recovered = await standingSession(sdk);
356
+ if (recovered !== null) return recovered;
357
+ throw error;
358
+ }
359
+ });
360
+ /** Asks the wallet to end the session; resolves once it acknowledges. */
361
+ const disconnect = fromPromise(({ input }) => input.sdk.disconnect());
362
+ /** Boots the sdk through `ensureInit`, so `initializing` and `retiring` share one cache. */
363
+ const init = fromPromise(({ input }) => ensureInit(input));
364
+ /** Reads the wallet's status and recovers a standing session without opening the picker. */
365
+ const restore = fromPromise(async ({ input }) => {
366
+ try {
367
+ const { connection } = await input.sdk.status();
368
+ if (connection === void 0) throw new Error("status answered without a connection");
369
+ return { connection };
370
+ } catch (error) {
371
+ console.debug("canton-connect: no session restored", error);
372
+ throw error;
373
+ }
374
+ });
375
+ /** Forwards the wallet's own status pushes into the machine as `wallet.statusChanged`. */
376
+ const walletEvents = fromCallback(({ sendBack, input: { sdk } }) => {
377
+ const listener = ({ connection }) => {
378
+ if (connection === void 0) return;
379
+ sendBack({
380
+ type: "wallet.statusChanged",
381
+ status: { connection }
382
+ });
383
+ };
384
+ sdk.onStatusChanged(listener).catch(() => {});
385
+ return () => {
386
+ sdk.removeOnStatusChanged(listener).catch(() => {});
387
+ };
388
+ });
389
+ //#endregion
390
+ //#region src/machine/connectionMachine.ts
391
+ const DISCONNECT_TIMEOUT_MS = 1e4;
392
+ /** Reduces the machine's internal states to the five-value `ConnectionStatus` hooks expose. */
393
+ const toConnectionStatus = (snapshot) => {
394
+ if (snapshot.matches("connecting")) return "connecting";
395
+ if (snapshot.matches({ retiring: "changing" }) || snapshot.matches({ restoring: "changing" })) return "connecting";
396
+ if (snapshot.matches("session")) return "connected";
397
+ if (snapshot.matches("idle") || snapshot.matches("restoring") || snapshot.matches("initializing")) return "idle";
398
+ if (snapshot.matches("disconnecting")) return "disconnecting";
399
+ return "disconnected";
400
+ };
401
+ /** The transition an authenticated wallet answer takes, shared by `connect` and `restore`. */
402
+ const landAuthenticated = {
403
+ guard: {
404
+ type: "isAuthenticated",
405
+ params: ({ event: { output } }) => ({ connection: output.connection })
406
+ },
407
+ target: "#connection.session.authenticated"
408
+ };
409
+ /** The exit from `disconnecting`, success and failure alike: nothing overlaps a disconnect. */
410
+ const afterDisconnect = { target: "disconnected" };
411
+ /** The exit `disconnecting` takes when the wallet never answers, on a replacement sdk. */
412
+ const afterSilentDisconnect = {
413
+ actions: { type: "retireSdk" },
414
+ target: "disconnected"
415
+ };
416
+ /** The `init` invoke shared by `initializing` and `retiring`. Each caller resumes in a different
417
+ * `restoring` state, so it names the `onDone` target. */
418
+ const bootSdk = (onDone) => ({
419
+ src: "init",
420
+ input: ({ context }) => ({
421
+ sdk: context.sdk,
422
+ initOptions: context.initOptions
423
+ }),
424
+ onDone: { target: onDone },
425
+ onError: {
426
+ target: "#connection.failure",
427
+ actions: [{
428
+ type: "assignError",
429
+ params: ({ event: { error } }) => ({ error })
430
+ }, { type: "retireSdk" }]
431
+ }
432
+ });
433
+ /** The `connect` invoke. The caller names which `retiring` variant a closed picker lands in, so
434
+ * a wallet change stays one through the retirement. */
435
+ const askWallet = (retiringTarget) => ({
436
+ src: "connect",
437
+ input: ({ context }) => ({
438
+ sdk: context.sdk,
439
+ initOptions: context.initOptions,
440
+ guardPicker: context.guardPicker
441
+ }),
442
+ onDone: [landAuthenticated, {
443
+ target: "#connection.failure",
444
+ actions: {
445
+ type: "assignDeclined",
446
+ params: ({ event: { output } }) => ({ connection: output.connection })
447
+ }
448
+ }],
449
+ onError: [
450
+ {
451
+ guard: {
452
+ type: "isPickerClosed",
453
+ params: ({ event: { error } }) => ({ error })
454
+ },
455
+ actions: { type: "retireSdk" },
456
+ target: retiringTarget
457
+ },
458
+ {
459
+ guard: {
460
+ type: "isInitFailed",
461
+ params: ({ event: { error } }) => ({ error })
462
+ },
463
+ target: "#connection.failure",
464
+ actions: [{
465
+ type: "assignError",
466
+ params: ({ event: { error } }) => ({ error: error instanceof InitFailedError ? error.cause : error })
467
+ }, { type: "retireSdk" }]
468
+ },
469
+ {
470
+ target: "#connection.failure",
471
+ actions: {
472
+ type: "assignError",
473
+ params: ({ event: { error } }) => ({ error })
474
+ }
475
+ }
476
+ ]
477
+ });
478
+ /** A connect attempt and the two exits that keep no session: the picker closing, and a cancel. */
479
+ const connectAttempt = (retiringTarget) => ({
480
+ invoke: askWallet(retiringTarget),
481
+ on: { "connect.cancel": {
482
+ actions: { type: "retireSdk" },
483
+ target: retiringTarget
484
+ } }
485
+ });
486
+ /**
487
+ * The lifecycle itself: what a connect, a restore, a lock and a disconnect mean, and the tags the
488
+ * bridges and hooks read off them. `CantonConnectProvider` runs it; reach for it directly only to
489
+ * drive a session in a test.
490
+ *
491
+ * @category Types
492
+ */
493
+ const connectionMachine = setup({
494
+ actors: {
495
+ connect,
496
+ disconnect,
497
+ init,
498
+ restore,
499
+ walletEvents,
500
+ accounts: accountsMachine
501
+ },
502
+ actions: {
503
+ assignError: assign((_, params) => ({ lastConnectError: params.error })),
504
+ assignDeclined: assign((_, params) => ({ lastConnectError: new Error(params.connection.reason ?? params.connection.networkReason ?? "wallet declined connection") })),
505
+ forgetError: assign({ lastConnectError: void 0 }),
506
+ retireSdk: enqueueActions(({ enqueue }) => {
507
+ enqueue(({ context }) => {
508
+ Object.assign(context.sdk, { walletPicker: () => Promise.reject(new ConnectCancelledError()) });
509
+ });
510
+ enqueue.assign({ sdk: ({ context }) => context.createSdk() });
511
+ })
512
+ },
513
+ guards: {
514
+ isAuthenticated: (_, params) => params.connection?.isConnected === true,
515
+ isPickerClosed: (_, params) => params.error instanceof PickerClosedError,
516
+ isInitFailed: (_, params) => params.error instanceof InitFailedError
517
+ },
518
+ delays: { disconnectTimeout: DISCONNECT_TIMEOUT_MS },
519
+ types: {
520
+ children: {},
521
+ context: {},
522
+ input: {},
523
+ tags: {},
524
+ events: {}
525
+ }
526
+ }).createMachine({
527
+ context: ({ input }) => ({
528
+ ...input,
529
+ sdk: input.createSdk(),
530
+ lastConnectError: void 0,
531
+ party: void 0
532
+ }),
533
+ id: "connection",
534
+ initial: "idle",
535
+ on: { "connectError.reset": { actions: { type: "forgetError" } } },
536
+ states: {
537
+ idle: {
538
+ tags: ["disconnect.settled"],
539
+ on: {
540
+ connect: { target: "connecting" },
541
+ restore: { target: "initializing" }
542
+ }
543
+ },
544
+ disconnected: {
545
+ tags: ["connect.cancelled", "disconnect.settled"],
546
+ entry: { type: "forgetError" },
547
+ on: {
548
+ connect: { target: "connecting" },
549
+ restore: { target: "initializing" }
550
+ }
551
+ },
552
+ connecting: {
553
+ tags: ["connecting"],
554
+ entry: { type: "forgetError" },
555
+ initial: "new",
556
+ states: {
557
+ new: connectAttempt("#connection.retiring.new"),
558
+ changing: connectAttempt("#connection.retiring.changing")
559
+ },
560
+ on: { disconnect: { target: "disconnecting" } }
561
+ },
562
+ session: {
563
+ initial: "unauthenticated",
564
+ invoke: {
565
+ src: "walletEvents",
566
+ input: ({ context }) => ({ sdk: context.sdk })
567
+ },
568
+ states: {
569
+ authenticated: {
570
+ initial: "reading",
571
+ exit: assign({ party: void 0 }),
572
+ invoke: {
573
+ src: "accounts",
574
+ id: "accounts",
575
+ input: ({ context }) => ({
576
+ sdk: context.sdk,
577
+ networkId: context.networkId
578
+ }),
579
+ onSnapshot: [{
580
+ guard: ({ event }) => event.snapshot.matches("ready"),
581
+ target: ".ready",
582
+ actions: assign(({ event }) => ({
583
+ party: event.snapshot.context.party,
584
+ lastConnectError: void 0
585
+ }))
586
+ }, {
587
+ guard: ({ event }) => event.snapshot.matches("unavailable"),
588
+ target: ".unavailable",
589
+ actions: assign(({ event }) => ({ lastConnectError: event.snapshot.context.error }))
590
+ }]
591
+ },
592
+ states: {
593
+ reading: {
594
+ tags: ["connecting"],
595
+ on: { "connect.cancel": { target: "#connection.disconnecting" } }
596
+ },
597
+ ready: { tags: ["connect.settled"] },
598
+ unavailable: { tags: ["connect.failed"] }
599
+ },
600
+ on: { "wallet.statusChanged": [{ guard: {
601
+ type: "isAuthenticated",
602
+ params: ({ event: { status } }) => ({ connection: status.connection })
603
+ } }, { target: "unauthenticated" }] }
604
+ },
605
+ unauthenticated: {
606
+ tags: ["connect.settled", "unauthenticated"],
607
+ on: { "wallet.statusChanged": {
608
+ guard: {
609
+ type: "isAuthenticated",
610
+ params: ({ event: { status } }) => ({ connection: status.connection })
611
+ },
612
+ target: "authenticated"
613
+ } }
614
+ }
615
+ },
616
+ on: {
617
+ connect: { target: "connecting.changing" },
618
+ disconnect: { target: "disconnecting" },
619
+ restore: { target: "initializing" }
620
+ }
621
+ },
622
+ failure: {
623
+ tags: ["connect.failed"],
624
+ on: {
625
+ connect: { target: "connecting" },
626
+ disconnect: { target: "disconnecting" },
627
+ restore: { target: "initializing" }
628
+ }
629
+ },
630
+ retiring: {
631
+ tags: ["connect.cancelled"],
632
+ entry: { type: "forgetError" },
633
+ initial: "new",
634
+ states: {
635
+ new: { invoke: bootSdk("#connection.restoring.new") },
636
+ changing: { invoke: bootSdk("#connection.restoring.changing") }
637
+ },
638
+ on: {
639
+ connect: { target: "connecting" },
640
+ disconnect: { target: "disconnecting" }
641
+ }
642
+ },
643
+ restoring: {
644
+ initial: "new",
645
+ states: {
646
+ new: {},
647
+ changing: {}
648
+ },
649
+ invoke: {
650
+ src: "restore",
651
+ input: ({ context }) => ({ sdk: context.sdk }),
652
+ onDone: [landAuthenticated, { target: "disconnected" }],
653
+ onError: { target: "disconnected" }
654
+ },
655
+ on: {
656
+ connect: { target: "connecting" },
657
+ disconnect: { target: "disconnecting" }
658
+ }
659
+ },
660
+ initializing: {
661
+ invoke: bootSdk("#connection.restoring.new"),
662
+ on: {
663
+ connect: { target: "connecting" },
664
+ disconnect: { target: "disconnecting" }
665
+ }
666
+ },
667
+ disconnecting: {
668
+ entry: { type: "forgetError" },
669
+ invoke: {
670
+ src: "disconnect",
671
+ input: ({ context }) => ({ sdk: context.sdk }),
672
+ onDone: afterDisconnect,
673
+ onError: afterDisconnect
674
+ },
675
+ after: { disconnectTimeout: afterSilentDisconnect }
676
+ }
677
+ }
678
+ });
679
+ //#endregion
680
+ //#region src/CantonConnectProvider/useConnectionActor.ts
681
+ /** Creates the connection actor and sends `restore` once it starts, so a prior session resumes. */
682
+ const useConnectionActor = (input) => {
683
+ const actorRef = useActorRef(connectionMachine, { input });
684
+ useEffect(() => {
685
+ actorRef.send({ type: "restore" });
686
+ }, [actorRef]);
687
+ return actorRef;
688
+ };
689
+ //#endregion
690
+ //#region src/CantonConnectProvider/useDisconnectBridge.ts
691
+ /** Resolves once the wallet has been asked, whichever state the machine started from. */
692
+ const useDisconnectBridge = (actorRef) => useCallback(async () => {
693
+ actorRef.send({ type: "disconnect" });
694
+ await waitFor(actorRef, (snapshot) => snapshot.hasTag("disconnect.settled"));
695
+ }, [actorRef]);
696
+ //#endregion
697
+ //#region src/CantonConnectProvider/index.tsx
698
+ const CantonConnectContext = createContext(void 0);
699
+ /**
700
+ * The whole context in one read, and the escape hatch behind every other hook here. Reach for a
701
+ * narrower hook unless a component needs several slices at once; this one hands back the config,
702
+ * the connection to select off, and the three actions.
703
+ *
704
+ * @throws with no {@link CantonConnectProvider} above it.
705
+ *
706
+ * @example
707
+ * const { config, connection } = useCantonConnectContext()
708
+ * const snapshot = connection.getSnapshot()
709
+ *
710
+ * @category Hooks
711
+ */
712
+ const useCantonConnectContext = () => {
713
+ const ctx = useContext(CantonConnectContext);
714
+ if (ctx === void 0) throw new Error("canton-connect hooks must be used inside a <CantonConnectProvider>");
715
+ return ctx;
716
+ };
717
+ /**
718
+ * Hands the connection machine what it needs to build its own `DappSDK`, and publishes the actor
719
+ * that goes through the states, plus the two bridges that drive it. Nothing here selects: a
720
+ * provider that pre-selected the whole session re-rendered every consumer on every tick of it.
721
+ * The hooks mirror wagmi's naming, not its TanStack Query result shapes.
722
+ *
723
+ * @example
724
+ * <CantonConnectProvider config={{ appName: 'Vesting', networkId: 'canton:local' }}>
725
+ * <App />
726
+ * </CantonConnectProvider>
727
+ *
728
+ * @category Components
729
+ */
730
+ const CantonConnectProvider = ({ config, children }) => {
731
+ const networkId = config.networkId ?? "canton:local";
732
+ const additionalAdapters = useMemo(() => buildAdditionalAdapters({
733
+ appName: config.appName,
734
+ appDescription: config.appDescription,
735
+ appUrl: config.appUrl,
736
+ walletConnectProjectId: config.walletConnectProjectId,
737
+ additionalAdapters: config.additionalAdapters
738
+ }, networkId), [
739
+ config.appName,
740
+ config.appDescription,
741
+ config.appUrl,
742
+ config.walletConnectProjectId,
743
+ config.additionalAdapters,
744
+ networkId
745
+ ]);
746
+ const actorRef = useConnectionActor({
747
+ createSdk: () => new DappSDK({ walletPicker: config.walletPicker }),
748
+ initOptions: { additionalAdapters },
749
+ guardPicker: config.walletPicker === void 0,
750
+ networkId
751
+ });
752
+ const resetConnectError = useCallback(() => actorRef.send({ type: "connectError.reset" }), [actorRef]);
753
+ const cancelConnect = useCallback(() => actorRef.send({ type: "connect.cancel" }), [actorRef]);
754
+ const connect = useConnectBridge(actorRef);
755
+ const disconnect = useDisconnectBridge(actorRef);
756
+ const value = useMemo(() => ({
757
+ config,
758
+ connection: actorRef,
759
+ connect,
760
+ cancelConnect,
761
+ disconnect,
762
+ resetConnectError
763
+ }), [
764
+ config,
765
+ actorRef,
766
+ connect,
767
+ cancelConnect,
768
+ disconnect,
769
+ resetConnectError
770
+ ]);
771
+ return /* @__PURE__ */ jsx(CantonConnectContext.Provider, {
772
+ value,
773
+ children
774
+ });
775
+ };
776
+ //#endregion
777
+ export { toConnectionStatus as a, toError as c, connectionMachine as i, CantonConnectProvider as n, ConnectCancelledError as o, useCantonConnectContext as r, toConnectError as s, CantonConnectContext as t };