@bootnodedev/canton-dappbooster 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,76 @@
1
+ # @bootnodedev/canton-dappbooster
2
+
3
+ Reusable UI components for Canton dApps — reading Canton identifiers (display, truncation,
4
+ copy-to-clipboard, explorer links) and entering them (validated party-id input). Amounts are the
5
+ other half: a token-amount field, plus the exact-decimal utilities under it, because a `number`
6
+ cannot carry a Canton amount without losing digits.
7
+
8
+ `src/index.ts` is the public API, and every export carries JSDoc that your editor will surface at
9
+ the call site and that is published at
10
+ [docs.dappbooster.cc](https://docs.dappbooster.cc/). The wallet
11
+ buttons sit behind the `/connect` sub-path instead, because they reach for the wallet session and so
12
+ pull in the Canton SDK. Authoring rules for
13
+ new components live in [`CLAUDE.md`](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/CLAUDE.md).
14
+
15
+ ## Scripts
16
+
17
+ | Script | What it does |
18
+ | --- | --- |
19
+ | `pnpm build` | tsdown → `dist/` (ESM `index.js` + `connect.js`, each with its `.d.ts`) |
20
+ | `pnpm test` | vitest (jsdom + Testing Library), against `src` |
21
+ | `pnpm typecheck` | `tsc --noEmit` |
22
+
23
+ ## Build & dev loop
24
+
25
+ tsdown builds ESM + `.d.ts` to `dist/`. Components carry no CSS, so `sideEffects: false` holds and
26
+ the bundle tree-shakes cleanly. `exports` carries a `development` condition → `src`, so Vite serves
27
+ source live in dev; `dist` is used for production and publish.
28
+
29
+ Consumers resolve source in dev and typecheck (no kit build needed); their production build resolves
30
+ `dist`. Build the kit first, or run `pnpm build` from the repo root, which builds workspaces in order.
31
+
32
+ React 19 only, peer and dev alike.
33
+
34
+ A consumer whose React resolves to a different copy than the kit's ends up with two Reacts in one
35
+ bundle, where hooks read a null dispatcher and every render throws. Only a production build shows
36
+ it, since the `development` condition resolves the kit to source. `resolve.dedupe` in the bundler
37
+ is the fix.
38
+
39
+ ## Styling: components carry none
40
+
41
+ Components (L2) ship zero styling opinion. Styling lives in the separate
42
+ [`@bootnodedev/canton-theme`](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-theme/README.md) package (L3), which consumers import explicitly:
43
+
44
+ ```ts
45
+ import '@bootnodedev/canton-theme/tokens.css'
46
+ import '@bootnodedev/canton-theme/default.css'
47
+ ```
48
+
49
+ The contract between the two is the DOM each component renders — not code. See
50
+ [`architecture.md`](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/architecture.md) for the seam and the reasoning.
51
+
52
+ ## Light / dark / system
53
+
54
+ The one styling-adjacent runtime this package does ship. `<ThemeProvider>` owns the mode and writes
55
+ `data-theme` to `<html>`, which is what the theme keys its dark values on; `useTheme()` reads and
56
+ sets it. No token names live here.
57
+
58
+ A reload flashes the page background before React applies the attribute, and this package ships
59
+ nothing to prevent it. [`architecture.md`](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/architecture.md) has the reasoning.
60
+
61
+ Client-only: the provider reads the OS preference as it mounts, so a server render throws.
62
+
63
+ ```tsx
64
+ import { ThemeProvider, useTheme } from '@bootnodedev/canton-dappbooster'
65
+
66
+ const App = () => (
67
+ <ThemeProvider>
68
+ <Page />
69
+ </ThemeProvider>
70
+ )
71
+
72
+ const ModeToggle = () => {
73
+ const { resolved, toggle } = useTheme()
74
+ return <button onClick={toggle}>{resolved === 'dark' ? 'Light' : 'Dark'}</button>
75
+ }
76
+ ```
@@ -0,0 +1,135 @@
1
+ import { t as Holding } from "./sumHoldings-BS0bHl7z.js";
2
+ import { ButtonHTMLAttributes, ComponentPropsWithRef, ReactElement } from "react";
3
+ //#region src/components/WalletButton/index.d.ts
4
+ /**
5
+ * Props for {@link WalletButton}. No `ref`: which element it lands on would depend on the session,
6
+ * so reach for the face you want instead.
7
+ *
8
+ * @category Components
9
+ */
10
+ type WalletButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
11
+ /**
12
+ * Follows the session, one button at a time: {@link CancelButton} while a connect is in flight,
13
+ * {@link DisconnectButton} once a session stands, {@link ConnectButton} otherwise. Each face is
14
+ * inert outside its own state, so whatever is on screen only ever does the thing it says.
15
+ *
16
+ * Swapping a face unmounts the focused button, so it hands focus to the one that took over.
17
+ *
18
+ * @example
19
+ * import { WalletButton } from '@bootnodedev/canton-dappbooster/connect'
20
+ *
21
+ * <WalletButton />
22
+ *
23
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
24
+ *
25
+ * @category Components
26
+ */
27
+ declare const WalletButton: (props: WalletButtonProps) => ReactElement;
28
+ //#endregion
29
+ //#region src/components/WalletButton/CancelButton.d.ts
30
+ /**
31
+ * Props for {@link CancelButton}.
32
+ *
33
+ * @category Components
34
+ */
35
+ type CancelButtonProps = ComponentPropsWithRef<"button">;
36
+ /**
37
+ * Abandons the connect attempt {@link ConnectButton} started, and is inert while there is none.
38
+ * Reach for it wherever a wallet prompt the user walked away from would otherwise leave the app
39
+ * pending forever. It spins while the attempt is in flight and keeps its accessible name on the
40
+ * action, so the wait is announced through the live region beside it rather than by renaming the
41
+ * button.
42
+ *
43
+ * @example
44
+ * import { CancelButton } from '@bootnodedev/canton-dappbooster/connect'
45
+ *
46
+ * <CancelButton />
47
+ *
48
+ * @example
49
+ * <CancelButton>{label}</CancelButton>
50
+ *
51
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
52
+ *
53
+ * @category Components
54
+ */
55
+ declare const CancelButton: ({ children, className, onClick, type, ...rest }: CancelButtonProps) => ReactElement;
56
+ //#endregion
57
+ //#region src/components/WalletButton/ConnectButton.d.ts
58
+ /**
59
+ * Props for {@link ConnectButton}.
60
+ *
61
+ * @category Components
62
+ */
63
+ type ConnectButtonProps = ComponentPropsWithRef<"button">;
64
+ /**
65
+ * Connect button. Can be customized. Inert while an attempt is in flight, so a click only ever
66
+ * connects; pair it with {@link CancelButton}, or take {@link WalletButton}, to let the user
67
+ * abandon one.
68
+ *
69
+ * @example
70
+ * import { ConnectButton } from '@bootnodedev/canton-dappbooster/connect'
71
+ *
72
+ * <ConnectButton />
73
+ * <ConnectButton>{label}</ConnectButton>
74
+ *
75
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
76
+ *
77
+ * @category Components
78
+ */
79
+ declare const ConnectButton: ({ children, className, onClick, type, ...rest }: ConnectButtonProps) => ReactElement;
80
+ //#endregion
81
+ //#region src/components/WalletButton/DisconnectButton.d.ts
82
+ /**
83
+ * Props for {@link DisconnectButton}.
84
+ *
85
+ * @category Components
86
+ */
87
+ type DisconnectButtonProps = ComponentPropsWithRef<"button">;
88
+ /**
89
+ * Disconnect button. Can be customized.
90
+ *
91
+ * @example
92
+ * import { DisconnectButton } from '@bootnodedev/canton-dappbooster/connect'
93
+ *
94
+ * <DisconnectButton />
95
+ *
96
+ * @example
97
+ * <DisconnectButton onClick={(event) => { event.preventDefault(); toggleMenu() }}>
98
+ * {label}
99
+ * </DisconnectButton>
100
+ *
101
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
102
+ *
103
+ * @category Components
104
+ */
105
+ declare const DisconnectButton: ({ children, className, onClick, type, ...rest }: DisconnectButtonProps) => ReactElement;
106
+ //#endregion
107
+ //#region src/hooks/useHoldings.d.ts
108
+ /**
109
+ * Return shape of {@link useHoldings}. `holdings` is `undefined` until the first read answers, and
110
+ * again whenever one fails, so an empty array means the party holds nothing.
111
+ *
112
+ * @category Hooks
113
+ */
114
+ interface UseHoldingsResult {
115
+ error: Error | undefined;
116
+ holdings: readonly Holding[] | undefined;
117
+ isLoading: boolean;
118
+ refetch: () => void;
119
+ }
120
+ /**
121
+ * Every standard holding the connected party owns, one entry per contract, read again whenever the
122
+ * party changes. Imported from `@bootnodedev/canton-dappbooster/connect`. Pair it with
123
+ * {@link sumHoldings} to get one row per instrument, which is what a token list wants.
124
+ *
125
+ * @throws with no `<CantonConnectProvider>` above it. A failed read lands in `error` instead.
126
+ *
127
+ * @example
128
+ * const { holdings } = useHoldings()
129
+ * const tokens = sumHoldings(holdings ?? [])
130
+ *
131
+ * @category Hooks
132
+ */
133
+ declare const useHoldings: () => UseHoldingsResult;
134
+ //#endregion
135
+ export { CancelButton, type CancelButtonProps, ConnectButton, type ConnectButtonProps, DisconnectButton, type DisconnectButtonProps, type UseHoldingsResult, WalletButton, type WalletButtonProps, useHoldings };
@@ -0,0 +1,296 @@
1
+ import { i as cx, n as valueAt, r as SR_ONLY } from "./json-CVZFwEw1.js";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useConnect, useDisconnect, useLedger, useParty, useWalletStatus } from "@bootnodedev/canton-connect";
5
+ //#region src/components/WalletButton/anatomy.ts
6
+ const cancelAnatomy = {
7
+ parts: {
8
+ root: "cnc-cancel-button",
9
+ spinner: "cnc-cancel-button__spinner",
10
+ status: "cnc-cancel-button__status"
11
+ },
12
+ states: { pending: "data-pending" }
13
+ };
14
+ const connectAnatomy = {
15
+ parts: {
16
+ root: "cnc-connect-button",
17
+ spinner: "cnc-connect-button__spinner"
18
+ },
19
+ states: { pending: "data-pending" }
20
+ };
21
+ const disconnectAnatomy = { parts: { root: "cnc-disconnect-button" } };
22
+ //#endregion
23
+ //#region src/components/WalletButton/composeAction.ts
24
+ const composeAction = (onClick, action) => (event) => {
25
+ onClick?.(event);
26
+ if (event.defaultPrevented) return;
27
+ Promise.resolve(action()).catch(() => void 0);
28
+ };
29
+ //#endregion
30
+ //#region src/components/WalletButton/CancelButton.tsx
31
+ /**
32
+ * Abandons the connect attempt {@link ConnectButton} started, and is inert while there is none.
33
+ * Reach for it wherever a wallet prompt the user walked away from would otherwise leave the app
34
+ * pending forever. It spins while the attempt is in flight and keeps its accessible name on the
35
+ * action, so the wait is announced through the live region beside it rather than by renaming the
36
+ * button.
37
+ *
38
+ * @example
39
+ * import { CancelButton } from '@bootnodedev/canton-dappbooster/connect'
40
+ *
41
+ * <CancelButton />
42
+ *
43
+ * @example
44
+ * <CancelButton>{label}</CancelButton>
45
+ *
46
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
47
+ *
48
+ * @category Components
49
+ */
50
+ const CancelButton = ({ children, className, onClick, type = "button", ...rest }) => {
51
+ const { cancelConnect, isPending } = useConnect();
52
+ const [painted, setPainted] = useState(false);
53
+ useEffect(() => setPainted(true), []);
54
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
55
+ ...rest,
56
+ "aria-disabled": !isPending || void 0,
57
+ className: cx(cancelAnatomy.parts.root, className),
58
+ onClick: isPending ? composeAction(onClick, cancelConnect) : (event) => event.preventDefault(),
59
+ type,
60
+ [cancelAnatomy.states.pending]: isPending || void 0,
61
+ children: [isPending && /* @__PURE__ */ jsx("span", {
62
+ "aria-hidden": "true",
63
+ className: cancelAnatomy.parts.spinner
64
+ }), children ?? "Cancel"]
65
+ }), /* @__PURE__ */ jsx("span", {
66
+ className: cancelAnatomy.parts.status,
67
+ role: "status",
68
+ style: SR_ONLY,
69
+ children: painted && isPending ? "Connecting…" : ""
70
+ })] });
71
+ };
72
+ //#endregion
73
+ //#region src/components/WalletButton/ConnectButton.tsx
74
+ /**
75
+ * Connect button. Can be customized. Inert while an attempt is in flight, so a click only ever
76
+ * connects; pair it with {@link CancelButton}, or take {@link WalletButton}, to let the user
77
+ * abandon one.
78
+ *
79
+ * @example
80
+ * import { ConnectButton } from '@bootnodedev/canton-dappbooster/connect'
81
+ *
82
+ * <ConnectButton />
83
+ * <ConnectButton>{label}</ConnectButton>
84
+ *
85
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
86
+ *
87
+ * @category Components
88
+ */
89
+ const ConnectButton = ({ children, className, onClick, type = "button", ...rest }) => {
90
+ const { connect, isPending } = useConnect();
91
+ return /* @__PURE__ */ jsxs("button", {
92
+ ...rest,
93
+ "aria-disabled": isPending || void 0,
94
+ className: cx(connectAnatomy.parts.root, className),
95
+ onClick: isPending ? (event) => event.preventDefault() : composeAction(onClick, connect),
96
+ type,
97
+ [connectAnatomy.states.pending]: isPending || void 0,
98
+ children: [isPending && /* @__PURE__ */ jsx("span", {
99
+ "aria-hidden": "true",
100
+ className: connectAnatomy.parts.spinner
101
+ }), children ?? (isPending ? "Connecting…" : "Connect wallet")]
102
+ });
103
+ };
104
+ //#endregion
105
+ //#region src/components/WalletButton/DisconnectButton.tsx
106
+ /**
107
+ * Disconnect button. Can be customized.
108
+ *
109
+ * @example
110
+ * import { DisconnectButton } from '@bootnodedev/canton-dappbooster/connect'
111
+ *
112
+ * <DisconnectButton />
113
+ *
114
+ * @example
115
+ * <DisconnectButton onClick={(event) => { event.preventDefault(); toggleMenu() }}>
116
+ * {label}
117
+ * </DisconnectButton>
118
+ *
119
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
120
+ *
121
+ * @category Components
122
+ */
123
+ const DisconnectButton = ({ children, className, onClick, type = "button", ...rest }) => {
124
+ const { disconnect } = useDisconnect();
125
+ const handleClick = composeAction(onClick, disconnect);
126
+ return /* @__PURE__ */ jsx("button", {
127
+ ...rest,
128
+ className: cx(disconnectAnatomy.parts.root, className),
129
+ onClick: handleClick,
130
+ type,
131
+ children: children ?? "Disconnect"
132
+ });
133
+ };
134
+ //#endregion
135
+ //#region src/components/WalletButton/index.tsx
136
+ const faceFor = (isPending, isConnected) => {
137
+ if (isPending) return "cancel";
138
+ if (isConnected) return "disconnect";
139
+ return "connect";
140
+ };
141
+ /**
142
+ * Follows the session, one button at a time: {@link CancelButton} while a connect is in flight,
143
+ * {@link DisconnectButton} once a session stands, {@link ConnectButton} otherwise. Each face is
144
+ * inert outside its own state, so whatever is on screen only ever does the thing it says.
145
+ *
146
+ * Swapping a face unmounts the focused button, so it hands focus to the one that took over.
147
+ *
148
+ * @example
149
+ * import { WalletButton } from '@bootnodedev/canton-dappbooster/connect'
150
+ *
151
+ * <WalletButton />
152
+ *
153
+ * @see [anatomy.ts](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-dappbooster/src/components/WalletButton/anatomy.ts) for the part classes and state attributes the theme selects.
154
+ *
155
+ * @category Components
156
+ */
157
+ const WalletButton = (props) => {
158
+ const { isConnected } = useWalletStatus();
159
+ const { isPending } = useConnect();
160
+ const face = faceFor(isPending, isConnected);
161
+ const button = useRef(null);
162
+ const previous = useRef(face);
163
+ useEffect(() => {
164
+ const swapped = previous.current !== face;
165
+ previous.current = face;
166
+ if (swapped && document.activeElement === document.body) button.current?.focus();
167
+ }, [face]);
168
+ if (face === "cancel") return /* @__PURE__ */ jsx(CancelButton, {
169
+ ...props,
170
+ ref: button
171
+ });
172
+ if (face === "disconnect") return /* @__PURE__ */ jsx(DisconnectButton, {
173
+ ...props,
174
+ ref: button
175
+ });
176
+ return /* @__PURE__ */ jsx(ConnectButton, {
177
+ ...props,
178
+ ref: button
179
+ });
180
+ };
181
+ //#endregion
182
+ //#region src/hooks/useHoldings.ts
183
+ const HOLDING_INTERFACE = "#splice-api-token-holding-v1:Splice.Api.Token.HoldingV1:Holding";
184
+ const holdingFromView = (view) => {
185
+ const value = valueAt(view, "viewValue");
186
+ const admin = valueAt(value, "instrumentId", "admin");
187
+ const id = valueAt(value, "instrumentId", "id");
188
+ const amount = valueAt(value, "amount");
189
+ if (typeof admin !== "string" || typeof id !== "string" || typeof amount !== "string") return;
190
+ const lock = valueAt(value, "lock");
191
+ return {
192
+ amount,
193
+ instrumentId: {
194
+ admin,
195
+ id
196
+ },
197
+ isLocked: lock !== null && lock !== void 0
198
+ };
199
+ };
200
+ const holdingsFromAcsRows = (rows) => {
201
+ if (!Array.isArray(rows)) return [];
202
+ return rows.flatMap((row) => {
203
+ const views = valueAt(row, "contractEntry", "JsActiveContract", "createdEvent", "interfaceViews");
204
+ if (!Array.isArray(views)) return [];
205
+ return views.flatMap((view) => holdingFromView(view) ?? []);
206
+ });
207
+ };
208
+ const acsRequest = (partyId, offset) => ({
209
+ requestMethod: "post",
210
+ resource: "/v2/state/active-contracts",
211
+ body: {
212
+ filter: { filtersByParty: { [partyId]: { cumulative: [{ identifierFilter: { InterfaceFilter: { value: {
213
+ interfaceId: HOLDING_INTERFACE,
214
+ includeInterfaceView: true
215
+ } } } }] } } },
216
+ activeAtOffset: offset,
217
+ verbose: true
218
+ }
219
+ });
220
+ const readPartyHoldings = async (ledgerApi, partyId) => {
221
+ const end = await ledgerApi({
222
+ requestMethod: "get",
223
+ resource: "/v2/state/ledger-end"
224
+ });
225
+ const offset = valueAt(end, "offset");
226
+ if (typeof offset !== "string" && typeof offset !== "number") throw new Error("the ledger did not return an offset");
227
+ return holdingsFromAcsRows(await ledgerApi(acsRequest(partyId, offset)));
228
+ };
229
+ const IDLE = {
230
+ error: void 0,
231
+ holdings: void 0,
232
+ isLoading: false
233
+ };
234
+ const LOADING = {
235
+ error: void 0,
236
+ holdings: void 0,
237
+ isLoading: true
238
+ };
239
+ const toError = (value) => value instanceof Error ? value : new Error(String(value));
240
+ /**
241
+ * Every standard holding the connected party owns, one entry per contract, read again whenever the
242
+ * party changes. Imported from `@bootnodedev/canton-dappbooster/connect`. Pair it with
243
+ * {@link sumHoldings} to get one row per instrument, which is what a token list wants.
244
+ *
245
+ * @throws with no `<CantonConnectProvider>` above it. A failed read lands in `error` instead.
246
+ *
247
+ * @example
248
+ * const { holdings } = useHoldings()
249
+ * const tokens = sumHoldings(holdings ?? [])
250
+ *
251
+ * @category Hooks
252
+ */
253
+ const useHoldings = () => {
254
+ const { ledgerApi, isReady } = useLedger();
255
+ const { party } = useParty();
256
+ const partyId = party?.partyId;
257
+ const [state, setState] = useState(IDLE);
258
+ const newest = useRef(0);
259
+ const load = useCallback(() => {
260
+ if (!isReady || partyId === void 0) {
261
+ setState(IDLE);
262
+ return;
263
+ }
264
+ newest.current += 1;
265
+ const request = newest.current;
266
+ setState(LOADING);
267
+ readPartyHoldings(ledgerApi, partyId).then((holdings) => {
268
+ if (request === newest.current) setState({
269
+ error: void 0,
270
+ holdings,
271
+ isLoading: false
272
+ });
273
+ }, (err) => {
274
+ if (request === newest.current) setState({
275
+ ...IDLE,
276
+ error: toError(err)
277
+ });
278
+ });
279
+ }, [
280
+ isReady,
281
+ ledgerApi,
282
+ partyId
283
+ ]);
284
+ useEffect(() => {
285
+ load();
286
+ return () => {
287
+ newest.current += 1;
288
+ };
289
+ }, [load]);
290
+ return {
291
+ ...state,
292
+ refetch: load
293
+ };
294
+ };
295
+ //#endregion
296
+ export { CancelButton, ConnectButton, DisconnectButton, WalletButton, useHoldings };