@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/dist/index.js ADDED
@@ -0,0 +1,444 @@
1
+ import { a as toConnectionStatus, c as toError, n as CantonConnectProvider, o as ConnectCancelledError, r as useCantonConnectContext, s as toConnectError } from "./CantonConnectProvider-CJ1yWGXz.js";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { useSelector } from "@xstate/react";
4
+ //#region src/hooks/useConnect.ts
5
+ /**
6
+ * Connects the wallet and reports that transition. `connect` takes no argument: the picker chooses
7
+ * the wallet, so there is no mode to pass. Gate a pending face on `isPending` and
8
+ * session-dependent content on `useParty().party`, not on `isConnected`.
9
+ *
10
+ * @throws with no {@link CantonConnectProvider} above it, as every hook here does.
11
+ *
12
+ * @example
13
+ * const { connect, isPending } = useConnect()
14
+ * <button onClick={() => void connect().catch(() => undefined)} disabled={isPending}>
15
+ * Connect
16
+ * </button>
17
+ *
18
+ * @category Hooks
19
+ */
20
+ const useConnect = () => {
21
+ const { cancelConnect, connect, connection, resetConnectError } = useCantonConnectContext();
22
+ const status = useSelector(connection, toConnectionStatus);
23
+ const isPending = useSelector(connection, (snapshot) => snapshot.hasTag("connecting"));
24
+ const lastConnectError = useSelector(connection, (snapshot) => snapshot.context.lastConnectError);
25
+ const error = useMemo(() => lastConnectError === void 0 ? void 0 : toConnectError(lastConnectError), [lastConnectError]);
26
+ return {
27
+ cancelConnect,
28
+ connect,
29
+ isPending,
30
+ isConnected: status === "connected",
31
+ error,
32
+ reset: resetConnectError
33
+ };
34
+ };
35
+ //#endregion
36
+ //#region src/hooks/useDisconnect.ts
37
+ /**
38
+ * Disconnects the wallet and reports that transition. No `error`: a disconnect always settles, by
39
+ * the timeout if the wallet never answers.
40
+ *
41
+ * @throws with no {@link CantonConnectProvider} above it, as every hook here does.
42
+ *
43
+ * @example
44
+ * const { disconnect, isPending } = useDisconnect()
45
+ * <button onClick={() => void disconnect()} disabled={isPending}>
46
+ * Disconnect
47
+ * </button>
48
+ *
49
+ * @category Hooks
50
+ */
51
+ const useDisconnect = () => {
52
+ const { connection, disconnect } = useCantonConnectContext();
53
+ return {
54
+ disconnect,
55
+ isPending: useSelector(connection, toConnectionStatus) === "disconnecting"
56
+ };
57
+ };
58
+ //#endregion
59
+ //#region src/hooks/useTxFeed.ts
60
+ /** Tracks the SDK's `txChanged` pushes while a session is active, clearing on session end. */
61
+ const useTxFeed = (sdk, connection) => {
62
+ const [lastTx, setLastTx] = useState(void 0);
63
+ const sessionActive = useSelector(connection, (snapshot) => snapshot.matches("session"));
64
+ useEffect(() => {
65
+ if (!sessionActive) {
66
+ setLastTx(void 0);
67
+ return;
68
+ }
69
+ const onTx = (event) => {
70
+ setLastTx({
71
+ status: event.status,
72
+ commandId: event.commandId,
73
+ payload: "payload" in event ? event.payload : void 0
74
+ });
75
+ };
76
+ sdk.onTxChanged(onTx).catch(() => void 0);
77
+ return () => {
78
+ sdk.removeOnTxChanged(onTx).catch(() => void 0);
79
+ };
80
+ }, [sessionActive, sdk]);
81
+ return lastTx;
82
+ };
83
+ //#endregion
84
+ //#region src/hooks/useWalletCall.ts
85
+ /** The resting state, hoisted so a hook that never called keeps one identity across renders. */
86
+ const IDLE = {
87
+ isPending: false,
88
+ error: void 0
89
+ };
90
+ /** Throws when the wallet is disconnected or locked, the guard every SDK-calling hook shares. */
91
+ const assertUsable = (status, isLocked) => {
92
+ if (status !== "connected") throw new Error("wallet is not connected - call useConnect().connect() first");
93
+ if (isLocked) throw new Error("wallet is locked - unlock it in the wallet");
94
+ };
95
+ /** Throws when the session reports no party, which a connected one can. */
96
+ function assertPartyId(partyId) {
97
+ if (partyId === void 0) throw new Error("wallet reports no usable party - allocate one in the wallet");
98
+ }
99
+ /**
100
+ * Selects the session and wraps one SDK call with the connect, lock and party guards plus the
101
+ * pending/error bookkeeping `useExecute` and `useSignMessage` share.
102
+ */
103
+ const useWalletCall = () => {
104
+ const { connection } = useCantonConnectContext();
105
+ const sdk = useSelector(connection, (snapshot) => snapshot.context.sdk);
106
+ const partyId = useSelector(connection, (snapshot) => snapshot.context.party?.partyId);
107
+ const status = useSelector(connection, toConnectionStatus);
108
+ const isLocked = useSelector(connection, (snapshot) => snapshot.hasTag("unauthenticated"));
109
+ const [state, setState] = useState(IDLE);
110
+ const call = useCallback(async (run) => {
111
+ assertUsable(status, isLocked);
112
+ assertPartyId(partyId);
113
+ setState({
114
+ isPending: true,
115
+ error: void 0
116
+ });
117
+ try {
118
+ const result = await run(sdk, partyId);
119
+ setState(IDLE);
120
+ return result;
121
+ } catch (err) {
122
+ const error = toError(err);
123
+ setState({
124
+ isPending: false,
125
+ error
126
+ });
127
+ throw error;
128
+ }
129
+ }, [
130
+ isLocked,
131
+ partyId,
132
+ sdk,
133
+ status
134
+ ]);
135
+ const reset = useCallback(() => setState(IDLE), []);
136
+ return {
137
+ call,
138
+ isPending: state.isPending,
139
+ error: state.error,
140
+ reset,
141
+ connection,
142
+ sdk,
143
+ status,
144
+ isLocked
145
+ };
146
+ };
147
+ //#endregion
148
+ //#region src/hooks/useExecute.ts
149
+ /** Defaults `actAs` to the connected party, leaving a caller's own `actAs` untouched. */
150
+ const withActAs = (params, partyId) => params.actAs === void 0 ? {
151
+ ...params,
152
+ actAs: [partyId]
153
+ } : params;
154
+ /**
155
+ * Submits ledger commands and tracks the transaction in `lastTx`, fed by the SDK's `txChanged`
156
+ * event. `actAs` defaults to the party `useParty` reports, so a submit acts as the party the UI
157
+ * shows rather than the wallet's own primary.
158
+ *
159
+ * @throws with no {@link CantonConnectProvider} above it, and from `execute` where nothing is
160
+ * connected or no party is reported. A command that fails throws too, and lands in `error`.
161
+ *
162
+ * @example
163
+ * const { execute, lastTx } = useExecute()
164
+ * await execute({ commandId: 'claim-1', commands })
165
+ * lastTx?.status // 'pending' | 'signed' | 'executed' | 'failed'
166
+ *
167
+ * @category Hooks
168
+ */
169
+ const useExecute = () => {
170
+ const { call, isPending, error, reset, connection, sdk } = useWalletCall();
171
+ const lastTx = useTxFeed(sdk, connection);
172
+ return {
173
+ execute: useCallback((params) => call((walletSdk, actingPartyId) => walletSdk.prepareExecuteAndWait(withActAs(params, actingPartyId))), [call]),
174
+ lastTx,
175
+ isPending,
176
+ error,
177
+ reset
178
+ };
179
+ };
180
+ //#endregion
181
+ //#region src/hooks/useLedger.ts
182
+ /**
183
+ * Escape hatch for ledger reads `useExecute` and `useSignMessage` do not cover: the participant's
184
+ * JSON API, passed through untyped.
185
+ *
186
+ * @throws with no {@link CantonConnectProvider} above it, and from `ledgerApi` itself where nothing
187
+ * is connected, which `isReady` is there to check first.
188
+ *
189
+ * @example
190
+ * const { ledgerApi } = useLedger()
191
+ * await ledgerApi({ requestMethod: 'get', resource: '/v2/state/ledger-end' })
192
+ *
193
+ * @category Hooks
194
+ */
195
+ const useLedger = () => {
196
+ const { sdk, status, isLocked } = useWalletCall();
197
+ return {
198
+ ledgerApi: useCallback(async (params) => {
199
+ assertUsable(status, isLocked);
200
+ return await sdk.ledgerApi(params);
201
+ }, [
202
+ isLocked,
203
+ sdk,
204
+ status
205
+ ]),
206
+ isReady: status === "connected" && !isLocked
207
+ };
208
+ };
209
+ //#endregion
210
+ //#region src/hooks/useParty.ts
211
+ /**
212
+ * The connected account and status. `party` is `undefined` until a connect succeeds, and again
213
+ * whenever a restored session is locked.
214
+ *
215
+ * @throws with no {@link CantonConnectProvider} above it.
216
+ *
217
+ * @example
218
+ * const { party, isConnected } = useParty()
219
+ * isConnected && <span>{party?.partyId}</span>
220
+ *
221
+ * @category Hooks
222
+ */
223
+ const useParty = () => {
224
+ const { connection } = useCantonConnectContext();
225
+ const party = useSelector(connection, (snapshot) => snapshot.context.party);
226
+ const status = useSelector(connection, toConnectionStatus);
227
+ return {
228
+ party,
229
+ status,
230
+ isConnected: status === "connected"
231
+ };
232
+ };
233
+ //#endregion
234
+ //#region src/hooks/usePartyType.ts
235
+ const namespaceOf = (id) => /::(.+)$/.exec(id)?.[1];
236
+ const readParticipantNamespace = async (sdk) => {
237
+ const answer = await sdk.ledgerApi({
238
+ requestMethod: "get",
239
+ resource: "/v2/parties/participant-id"
240
+ });
241
+ const namespace = typeof answer.participantId === "string" ? namespaceOf(answer.participantId) : void 0;
242
+ if (namespace === void 0) throw new Error(`participant id not found in ${JSON.stringify(answer)}`);
243
+ return namespace;
244
+ };
245
+ /**
246
+ * Tells a local party from an external one when asked. Each `readPartyType` call is one
247
+ * `ledgerApi` read of the participant id, its namespace compared with the party's; nothing is
248
+ * cached, so hold the answer where several components need it. Reach for it before an action a
249
+ * local party cannot take, such as `signMessage`, which the reference gateway refuses.
250
+ *
251
+ * @throws with no {@link CantonConnectProvider} above it, and from `readPartyType` where nothing
252
+ * is connected or no party is reported, which `isReady` is there to check first.
253
+ *
254
+ * @example
255
+ * const { readPartyType } = usePartyType()
256
+ * if ((await readPartyType()) === 'local') {
257
+ * toast.error('This wallet cannot sign messages for a local party')
258
+ * }
259
+ *
260
+ * @category Hooks
261
+ */
262
+ const usePartyType = () => {
263
+ const { connection, sdk, status, isLocked } = useWalletCall();
264
+ const party = useSelector(connection, (snapshot) => snapshot.context.party);
265
+ return {
266
+ readPartyType: useCallback(async () => {
267
+ assertUsable(status, isLocked);
268
+ if (party === void 0) throw new Error("wallet reports no usable party - allocate one in the wallet");
269
+ const participantNamespace = await readParticipantNamespace(sdk);
270
+ return party.namespace === participantNamespace ? "local" : "external";
271
+ }, [
272
+ isLocked,
273
+ party,
274
+ sdk,
275
+ status
276
+ ]),
277
+ isReady: status === "connected" && !isLocked && party !== void 0
278
+ };
279
+ };
280
+ //#endregion
281
+ //#region src/hooks/useSignMessage.ts
282
+ /**
283
+ * Signs an arbitrary message with the connected wallet; the SDK owns the encoding.
284
+ *
285
+ * @throws with no {@link CantonConnectProvider} above it, and from `signMessage` where nothing is
286
+ * connected or no party is reported. A wallet refusal throws too, and lands in `error`.
287
+ *
288
+ * @example
289
+ * const { signMessage } = useSignMessage()
290
+ * const signed = await signMessage('Approve vesting claim')
291
+ *
292
+ * @category Hooks
293
+ */
294
+ const useSignMessage = () => {
295
+ const { call, isPending, error, reset: resetCall } = useWalletCall();
296
+ const [signature, setSignature] = useState(void 0);
297
+ return {
298
+ signMessage: useCallback(async (message) => {
299
+ setSignature(void 0);
300
+ const result = await call((walletSdk) => walletSdk.signMessage({ message }));
301
+ setSignature(result.signature);
302
+ return result.signature;
303
+ }, [call]),
304
+ signature,
305
+ isPending,
306
+ error,
307
+ reset: useCallback(() => {
308
+ setSignature(void 0);
309
+ resetCall();
310
+ }, [resetCall])
311
+ };
312
+ };
313
+ //#endregion
314
+ //#region src/hooks/useWalletStatus.ts
315
+ /**
316
+ * Reports the session and lock state from the wallet's own pushes. A wallet that disconnected on
317
+ * its own pushed the same thing as a lock, so `isLocked` cannot tell them apart.
318
+ *
319
+ * @throws with no {@link CantonConnectProvider} above it.
320
+ *
321
+ * @example
322
+ * const { isConnected, isLocked } = useWalletStatus()
323
+ * if (!isConnected) return <p>No session.</p>
324
+ * return isLocked ? <p>Unlock your wallet to continue.</p> : <p>Ready.</p>
325
+ *
326
+ * @category Hooks
327
+ */
328
+ const useWalletStatus = () => {
329
+ const { connection } = useCantonConnectContext();
330
+ return {
331
+ isLocked: useSelector(connection, (snapshot) => snapshot.hasTag("unauthenticated")),
332
+ isConnected: useSelector(connection, toConnectionStatus) === "connected"
333
+ };
334
+ };
335
+ //#endregion
336
+ //#region src/mock/mockAdapter.ts
337
+ const DEFAULT_PROVIDER_ID = "mock";
338
+ const DEFAULT_NAME = "Mock Wallet";
339
+ const DEFAULT_DESCRIPTION = "Mock wallet for dev and tests — no real signing, never a live wallet";
340
+ const MOCK_WALLET_STATUS = "allocated";
341
+ const MOCK_SIGNING_PROVIDER_ID = "mock";
342
+ const MOCK_PUBLIC_KEY = "mock-public-key";
343
+ /** The single mock account `createMockAdapter` reports when the caller supplies none. */
344
+ const defaultAccounts = (providerId) => [{ partyId: `${providerId}::1220abcd` }];
345
+ /** Shapes one `MockAccount` into the `Wallet` the mock adapter's `listAccounts` returns. */
346
+ const toWallet = (account, primary, networkId) => ({
347
+ primary,
348
+ partyId: account.partyId,
349
+ status: MOCK_WALLET_STATUS,
350
+ hint: account.name ?? account.partyId,
351
+ publicKey: account.publicKey ?? MOCK_PUBLIC_KEY,
352
+ namespace: account.partyId.split("::")[1] ?? account.partyId,
353
+ signingProviderId: MOCK_SIGNING_PROVIDER_ID,
354
+ ...networkId === void 0 ? {} : { networkId }
355
+ });
356
+ /**
357
+ * The adapter `createMockAdapter` returns: it announces itself like an installed wallet and
358
+ * answers the connect flow from canned accounts, with no wallet present.
359
+ */
360
+ var MockProviderAdapter = class {
361
+ providerId;
362
+ name = DEFAULT_NAME;
363
+ type = "browser";
364
+ wallets;
365
+ connected = false;
366
+ listenerMap = {};
367
+ constructor(options) {
368
+ this.providerId = options.id ?? DEFAULT_PROVIDER_ID;
369
+ const accounts = options.accounts ?? defaultAccounts(this.providerId);
370
+ this.wallets = accounts.map((account, index) => toWallet(account, index === 0, options.networkId));
371
+ }
372
+ getInfo() {
373
+ return {
374
+ providerId: this.providerId,
375
+ name: this.name,
376
+ type: this.type,
377
+ description: DEFAULT_DESCRIPTION
378
+ };
379
+ }
380
+ async detect() {
381
+ return true;
382
+ }
383
+ provider() {
384
+ return this;
385
+ }
386
+ teardown() {}
387
+ handlers = {
388
+ connect: () => {
389
+ this.connected = true;
390
+ return {
391
+ isConnected: true,
392
+ isNetworkConnected: true
393
+ };
394
+ },
395
+ disconnect: () => {
396
+ this.connected = false;
397
+ return null;
398
+ },
399
+ status: () => ({
400
+ provider: {
401
+ id: this.providerId,
402
+ providerType: this.type
403
+ },
404
+ connection: {
405
+ isConnected: this.connected,
406
+ isNetworkConnected: true
407
+ }
408
+ }),
409
+ listAccounts: () => this.wallets
410
+ };
411
+ async request(args) {
412
+ const handler = this.handlers[args.method];
413
+ if (handler === void 0) throw new Error(`mock adapter does not implement '${args.method}'`);
414
+ return handler();
415
+ }
416
+ on = (event, listener) => {
417
+ const listeners = this.listenerMap[event] ?? [];
418
+ listeners.push(listener);
419
+ this.listenerMap[event] = listeners;
420
+ return this;
421
+ };
422
+ emit = (event, ...args) => {
423
+ for (const listener of this.listenerMap[event] ?? []) listener(...args);
424
+ return true;
425
+ };
426
+ removeListener = (event, listenerToRemove) => {
427
+ this.listenerMap[event] = (this.listenerMap[event] ?? []).filter((listener) => listener !== listenerToRemove);
428
+ return this;
429
+ };
430
+ };
431
+ /**
432
+ * Answers the connect flow with canned data, so `CantonConnectProvider` runs with no wallet
433
+ * installed; pass it via `CantonConnectConfig.additionalAdapters`. Anything outside that flow
434
+ * throws naming the method; a canned result would be indistinguishable from a real one. Reach for
435
+ * `createFakeWallet` instead to exercise the SDK's real extension transport.
436
+ *
437
+ * @example
438
+ * const config = { appName: 'Vesting', additionalAdapters: [createMockAdapter()] }
439
+ *
440
+ * @category Utilities
441
+ */
442
+ const createMockAdapter = (options = {}) => new MockProviderAdapter(options);
443
+ //#endregion
444
+ export { CantonConnectProvider, ConnectCancelledError, createMockAdapter, useCantonConnectContext, useConnect, useDisconnect, useExecute, useLedger, useParty, usePartyType, useSignMessage, useWalletStatus };
@@ -0,0 +1,127 @@
1
+ import { a as Party, c as WalletSdk, r as ConnectionStatus } from "../types-Deu_03jh.js";
2
+ import { WalletPickerFn } from "@canton-network/dapp-sdk";
3
+ import { JSX, ReactNode } from "react";
4
+ //#region src/testing/autoPicker.d.ts
5
+ /**
6
+ * A `walletPicker` that selects with no UI, for tests and headless dev flows: the entry whose
7
+ * `providerId` matches `pick`, or the first discovered one.
8
+ *
9
+ * @throws when no discovered entry matches `pick`, so a test naming a wallet that never registered
10
+ * fails at the picker rather than at the connect.
11
+ *
12
+ * @example
13
+ * const config = { appName: 'Vesting', walletPicker: createAutoPicker('mock') }
14
+ *
15
+ * @category Utilities
16
+ */
17
+ declare const createAutoPicker: (pick?: string) => WalletPickerFn;
18
+ //#endregion
19
+ //#region src/testing/fakeSession.d.ts
20
+ /**
21
+ * Props for {@link FakeSessionProvider}. `status` starts the session mid-flight and `party` is
22
+ * what a connect resolves to; `readingAccounts` reaches the pending face over a live session, and
23
+ * `sdk` drives a hook's own pending, error and `reset()`, never what a wallet returns.
24
+ *
25
+ * @category Components
26
+ */
27
+ interface FakeSessionProviderProps {
28
+ children: ReactNode;
29
+ connectError?: Error;
30
+ isLocked?: boolean;
31
+ party?: Party;
32
+ readingAccounts?: boolean;
33
+ sdk?: Partial<WalletSdk>;
34
+ status?: ConnectionStatus;
35
+ }
36
+ /**
37
+ * Stands in for `CantonConnectProvider` with the session already in a given shape, so a component
38
+ * test asserts on markup without paying the SDK's discovery sleeps or its connect flow. The shape
39
+ * is a real `connectionMachine` actor rehydrated at the state the props ask for, so the hooks
40
+ * select from it exactly as they do in the app. `connect` and `disconnect` move the session, but
41
+ * reach for the real provider plus `createMockAdapter` to test connecting itself — the intermediate
42
+ * states here are not the SDK's.
43
+ *
44
+ * @example
45
+ * render(
46
+ * <FakeSessionProvider status="connected" party={party}>
47
+ * <ConnectButton />
48
+ * </FakeSessionProvider>,
49
+ * )
50
+ *
51
+ * @category Components
52
+ */
53
+ declare const FakeSessionProvider: ({ children, connectError, isLocked, party, readingAccounts, sdk, status: initialStatus }: FakeSessionProviderProps) => JSX.Element;
54
+ //#endregion
55
+ //#region src/testing/fakeWallet.d.ts
56
+ /**
57
+ * One account the fake wallet reports from `listAccounts`. Mark exactly one `primary`: that is the
58
+ * entry `selectPrimaryAccount` resolves to `Party`.
59
+ *
60
+ * @category Utilities
61
+ */
62
+ interface FakeWalletAccount {
63
+ partyId: string;
64
+ primary?: boolean;
65
+ name?: string;
66
+ publicKey?: string;
67
+ networkId?: string;
68
+ }
69
+ /**
70
+ * Wiring for {@link createFakeWallet}. `id` is announced to `window` and doubles as the postMessage
71
+ * target and the display name unless `target` or `name` override it, and `accounts` defaults to one
72
+ * account with `id` as its party prefix. `statusResponses` is `isConnected` per successive `status`
73
+ * call, last entry repeating, which is how a test restores a session and then reports it locked.
74
+ *
75
+ * @example
76
+ * const options: FakeWalletOptions = { id: 'mock', statusResponses: [true, false] }
77
+ *
78
+ * @category Utilities
79
+ */
80
+ interface FakeWalletOptions {
81
+ id: string;
82
+ name?: string;
83
+ target?: string;
84
+ accounts?: FakeWalletAccount[];
85
+ statusResponses?: boolean[];
86
+ }
87
+ /**
88
+ * Handles on a running fake wallet: `announce` re-announces it as if the extension had just loaded,
89
+ * `push` sends an unsolicited notification the way a real one does (`'statusChanged'`, say), and
90
+ * `dispose` removes the `window` listeners it installed, which every test must do in teardown.
91
+ *
92
+ * @category Utilities
93
+ */
94
+ interface FakeWallet {
95
+ announce: () => void;
96
+ push: (method: string, params: unknown) => void;
97
+ dispose: () => void;
98
+ }
99
+ /**
100
+ * A fake CIP-0103 extension wallet for tests. It speaks the real postMessage protocol, so it
101
+ * exercises the SDK's genuine `ExtensionAdapter` rather than a stub. Answers `connect`, `status`,
102
+ * `listAccounts` and `disconnect`; anything else rejects naming the method. Reach for
103
+ * `createMockAdapter` instead where the transport is not what is under test.
104
+ *
105
+ * @example
106
+ * const wallet = createFakeWallet({ id: 'mock' })
107
+ * wallet.push('statusChanged', { connection: { isConnected: false } })
108
+ * wallet.dispose()
109
+ *
110
+ * @category Utilities
111
+ */
112
+ declare const createFakeWallet: (options: FakeWalletOptions) => FakeWallet;
113
+ //#endregion
114
+ //#region src/testing/pause.d.ts
115
+ /**
116
+ * Real-timer sleep, which is how a suite awaits a promise-settling send; `pause(0)` flushes the
117
+ * pending macrotasks. Reach for it over `waitFor`, whose retry window turns a genuine red into a
118
+ * timeout rather than a failed assertion.
119
+ *
120
+ * @example
121
+ * await pause(0) // one macrotask on, so the send above has settled
122
+ *
123
+ * @category Utilities
124
+ */
125
+ declare const pause: (ms: number) => Promise<unknown>;
126
+ //#endregion
127
+ export { FakeSessionProvider, type FakeSessionProviderProps, type FakeWallet, type FakeWalletAccount, type FakeWalletOptions, createAutoPicker, createFakeWallet, pause };