@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.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/CantonConnectProvider-CJ1yWGXz.js +777 -0
- package/dist/index.d.ts +342 -0
- package/dist/index.js +444 -0
- package/dist/testing/index.d.ts +127 -0
- package/dist/testing/index.js +291 -0
- package/dist/types-Deu_03jh.d.ts +385 -0
- package/package.json +86 -0
|
@@ -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 };
|