@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BootNode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # canton-connect
2
+
3
+ wagmi-style React hooks for connecting Canton dApps to CIP-0103 wallets, over
4
+ `@canton-network/dapp-sdk`'s `DappSDK` facade.
5
+
6
+ ## Why
7
+
8
+ `dapp-sdk` handles wallet discovery, the connect picker, the session and every transport (browser
9
+ extension, WalletConnect, remote gateway), but ships no React hooks. This package adds that layer.
10
+ A consumer never calls the SDK: it installs it as a peer, and the hooks take the SDK's own parameter
11
+ types.
12
+
13
+ [`@partylayer/react`](https://partylayer.xyz) is the alternative, built over its own wallet
14
+ adapters. This one wraps Digital Asset's official SDK, the dependency these dApps already carry, and
15
+ stays thin enough to delete. How the hook results relate to wagmi's is in
16
+ [coming-from-wagmi.md](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-connect/coming-from-wagmi.md).
17
+
18
+ ## Why a state machine
19
+
20
+ The connection lifecycle looks like four states (idle, connecting, connected,
21
+ disconnected) and isn't. The hard part isn't holding state, it's canceling work
22
+ when the state that started it is gone: a picker the user abandoned, a lock
23
+ that races the account read, a connect asked for mid-disconnect, a wallet that
24
+ answers late or never. Handled one at a time these were five separate races
25
+ (#76). A state machine folds them into one model: a state's invoked work is
26
+ canceled when the state is left, and the combinations that used to be bugs
27
+ (connected with no party, an error beside a live session) are states that name
28
+ their case: `session.unauthenticated`, `session.authenticated.unavailable`.
29
+
30
+ Most of that weight works around `@canton-network/dapp-sdk` gaps, not domain
31
+ complexity (below). When those close upstream, this layer collapses to its floor
32
+ (idle-vs-disconnected, the account-read states, the CIP-0103 lock/disconnect
33
+ ambiguity), small enough that a lighter store wins on bundle size. A spike
34
+ reimplementing it on zustand confirmed this: full behavioral parity, but the win
35
+ is bundle size and one fewer dependency, not less logic, and it points to
36
+ switching only once those gaps close. Until then, the machine earns its cost.
37
+
38
+ | dapp-sdk gap | what it costs us | gone when |
39
+ |---|---|---|
40
+ | `connect()` can't be aborted; a closed popup hangs it (#49) | `guardedConnect`, `settleAbandonedConnect`, `PickerClosedError`, the `retiring` state, `retireSdk`'s picker swap | `connect(signal)` truly aborts |
41
+ | `init()` caches a rejected promise forever | `retireSdk`, the `retiring` state, `InitFailedError` | `init()` retries after a failure |
42
+ | `disconnect()` has no timeout (#105) | `DISCONNECT_TIMEOUT_MS`, and `retireSdk` when it fires | `disconnect()` times out itself |
43
+ | lock and wallet-side disconnect are one push | `session.unauthenticated`, party-dropped-on-lock | CIP-0103 separates them (spec, not SDK) |
44
+
45
+ ## Status
46
+
47
+ Consumed by `dapp/frontend` in this repo as a workspace package. Not published (`private: true`).
48
+
49
+ ## Install
50
+
51
+ Peers a consumer installs beside it: `@canton-network/dapp-sdk`, `@canton-network/core-types`,
52
+ `react` 19 and `@walletconnect/sign-client`. The last is declared optional, but `dapp-sdk` imports
53
+ it statically at the top of its bundle (checked on 1.5.1), so it has to be present whether or not
54
+ you set `walletConnectProjectId`. Only the session is lazy: `SignClient.init()` runs when a pairing
55
+ starts, not at import.
56
+
57
+ ## Usage
58
+
59
+ ```tsx
60
+ import {
61
+ CantonConnectProvider,
62
+ useConnect,
63
+ useParty,
64
+ useWalletStatus,
65
+ useSignMessage,
66
+ useExecute,
67
+ useLedger,
68
+ } from '@bootnodedev/canton-connect'
69
+
70
+ function App() {
71
+ return (
72
+ <CantonConnectProvider config={{ appName: 'My dApp', networkId: 'canton:local' }}>
73
+ <Dapp />
74
+ </CantonConnectProvider>
75
+ )
76
+ }
77
+
78
+ function Dapp() {
79
+ const { connect, isPending, isConnected, error } = useConnect()
80
+ const { party } = useParty()
81
+ const { isLocked } = useWalletStatus()
82
+ const { signMessage } = useSignMessage()
83
+ const { execute } = useExecute()
84
+ const { ledgerApi } = useLedger()
85
+
86
+ if (!isConnected) {
87
+ return (
88
+ <div>
89
+ <button onClick={() => connect().catch(() => undefined)} disabled={isPending}>
90
+ Connect
91
+ </button>
92
+ {error !== undefined && <p>{error.message}</p>}
93
+ </div>
94
+ )
95
+ }
96
+
97
+ if (isLocked) {
98
+ return <p>Wallet locked. Unlock it to continue.</p>
99
+ }
100
+
101
+ // ... your dApp: party.partyId, signMessage(text), execute(params), ledgerApi(params)
102
+ }
103
+ ```
104
+
105
+ `connect()` opens the SDK's wallet picker, a popup by default. There is no mode argument: the picker
106
+ is what chooses the wallet. Dismissing it rejects with `ConnectCancelledError`, which you filter by
107
+ `instanceof`, never by message. Whether `error` records it as well depends on which side saw
108
+ the close, so do not gate on that.
109
+
110
+ `signMessage`, `execute` and `ledgerApi` refuse with no session, and refuse again while the wallet
111
+ reports it is not authenticated; that is `isLocked`, and it happens after a successful connect.
112
+ `signMessage` and `execute` also refuse with no party, which `ledgerApi` does not need. The
113
+ reference gateway also refuses `signMessage` for a local party; `usePartyType().readPartyType()`
114
+ tells local from external when you ask. The SDK's status carries one `isConnected` flag, so a lock
115
+ and a wallet-side disconnect look the same
116
+ here. `useLedger().isReady` covers both, and `useParty().party` is `undefined` for the duration:
117
+ gate session content on the party, and use `isLocked` only to explain why it went away.
118
+
119
+ ## Reference
120
+
121
+ Every hook and every config field is documented in JSDoc, which your editor surfaces at the call
122
+ site and which is published at
123
+ [docs.dappbooster.cc](https://docs.dappbooster.cc/). Start at
124
+ `CantonConnectProvider` and `CantonConnectConfig`.
125
+
126
+ ## Testing helpers
127
+
128
+ - `createMockAdapter()`, from the package root: a `ProviderAdapter` answering `connect`,
129
+ `disconnect`, `status` and `listAccounts` with no wallet installed, so a dApp or a test can
130
+ connect and show a party. Anything else throws, naming the method.
131
+ - From `@bootnodedev/canton-connect/testing`: `createFakeWallet()`, a CIP-0103 extension driven over
132
+ `postMessage` that exercises the SDK's real announce and detect path; `createAutoPicker()`, a
133
+ headless picker so `connect()` runs without a popup; `FakeSessionProvider`, the context rehydrated
134
+ at an asked-for session with no SDK behind it; and `pause(ms)`, a real-timer sleep.
135
+
136
+ ```tsx
137
+ import { CantonConnectProvider, createMockAdapter } from '@bootnodedev/canton-connect'
138
+ import { createAutoPicker } from '@bootnodedev/canton-connect/testing'
139
+
140
+ const config = {
141
+ appName: 'My dApp',
142
+ additionalAdapters: [createMockAdapter()],
143
+ walletPicker: createAutoPicker(),
144
+ }
145
+ ```
146
+
147
+ ## Architecture
148
+
149
+ [`architecture.md`](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-connect/architecture.md)
150
+ maps the seams; its `architecture/` chapters carry the connection machine and the popup close guard.
151
+
152
+ ## Testing
153
+
154
+ `pnpm test`: vitest + jsdom + Testing Library. `pnpm coverage` runs the same suite under v8, with
155
+ `testing/`, `mock/` and the barrel excluded.