@usebutr/core 0.1.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,745 @@
1
+ //#region src/types/chain.d.ts
2
+ /**
3
+ * Minimal chain shape that butr needs to function.
4
+ * Follows the CAIP-2 chain identifier standard.
5
+ *
6
+ * Consumers extend this with app-specific fields (logos, block explorers, etc.)
7
+ * via structural typing — butr never inspects beyond these 4 fields.
8
+ */
9
+ type ChainBase = {
10
+ /** CAIP-2 identifier, e.g. "eip155:1", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" */id: string; /** Human-readable name, e.g. "Ethereum", "Solana" */
11
+ name: string; /** CAIP-2 namespace, e.g. "eip155", "solana" */
12
+ namespace: string; /** CAIP-2 reference, e.g. "1", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" */
13
+ reference: string;
14
+ };
15
+ //#endregion
16
+ //#region src/storage/persistence.d.ts
17
+ type MaybePromise<T> = T | Promise<T>;
18
+ /** Low-level key/value driver. Sync on web (localStorage, MMKV),
19
+ * async on React Native (AsyncStorage). */
20
+ type StorageDriver = {
21
+ getItem: (key: string) => MaybePromise<string | null>;
22
+ removeItem: (key: string) => MaybePromise<void>;
23
+ setItem: (key: string, value: string) => MaybePromise<void>;
24
+ };
25
+ type StoredPoolEntry = {
26
+ account: Account;
27
+ /** All known accounts on the wallet at last persist. Always contains
28
+ * `account`. */
29
+ accounts: Array<Account>;
30
+ chainPlatform: ChainPlatform;
31
+ connectorId: string;
32
+ };
33
+ type StoredPoolRecord = Partial<Record<string, StoredPoolEntry>>;
34
+ type StoredSelectionRecord = Partial<Record<ChainPlatform, string>>;
35
+ type WalletPersistence = {
36
+ clearAll(): Promise<void>;
37
+ clearPool(): Promise<void>;
38
+ getActiveConnectorId(): Promise<string | null>;
39
+ getPool(): Promise<StoredPoolRecord>;
40
+ getSelection(): Promise<StoredSelectionRecord>;
41
+ isUserDisconnected(): Promise<boolean>;
42
+ markUserDisconnected(value: boolean): Promise<void>;
43
+ removePoolEntry(connectorId: string): Promise<void>;
44
+ setActiveConnectorId(connectorId: string | null): Promise<void>;
45
+ setPool(pool: Map<string, ConnectedWallet>): Promise<void>;
46
+ setSelection(selection: Map<ChainPlatform, string>): Promise<void>;
47
+ };
48
+ //#endregion
49
+ //#region src/types/errors.d.ts
50
+ /**
51
+ * Tagged union of normalised connection errors.
52
+ *
53
+ * butr maps thrown values from connectors (which vary across wallet SDKs:
54
+ * MetaMask uses EIP-1193 codes, Phantom throws stringly-typed errors,
55
+ * embedded SDKs throw their own classes) into a small set of UX-meaningful
56
+ * variants. Consumers branch on `kind` instead of regexing message strings.
57
+ *
58
+ * `message` is always present and human-readable. `cause` preserves the
59
+ * original thrown value so callers can inspect raw connector errors when
60
+ * the variant is `Unknown`.
61
+ */
62
+ type ConnectionError = {
63
+ kind: "UserRejected";
64
+ message: string;
65
+ } | {
66
+ kind: "RequestPending";
67
+ message: string;
68
+ } | {
69
+ kind: "WalletLocked";
70
+ message: string;
71
+ } | {
72
+ actualChain?: string;
73
+ expectedChain?: string;
74
+ kind: "ChainMismatch";
75
+ message: string;
76
+ } | {
77
+ kind: "NotConnected";
78
+ message: string;
79
+ } | {
80
+ kind: "Timeout";
81
+ message: string;
82
+ } | {
83
+ cause?: unknown;
84
+ kind: "Unknown";
85
+ message: string;
86
+ };
87
+ type ConnectionErrorKind = ConnectionError["kind"];
88
+ /**
89
+ * Normalise a thrown value into a `ConnectionError`.
90
+ *
91
+ * Recognises:
92
+ * - butr's own `Error("Connection timeout")` (from the 90s connect timeout)
93
+ * - butr's own `Error("Failed to get account")` (from the connect flow)
94
+ * - EIP-1193 numeric `code` properties (`4001` → UserRejected,
95
+ * `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected —
96
+ * unauthorized / disconnected from all-or-one chains)
97
+ * - common message substrings: "user rejected" / "user denied",
98
+ * "locked", "chain", etc.
99
+ * - anything else → `Unknown` with `cause` set to the original value.
100
+ */
101
+ declare const mapConnectionError: (raw: unknown) => ConnectionError;
102
+ //#endregion
103
+ //#region src/types/wallet.d.ts
104
+ type ChainPlatform = "evm" | "svm" | "sui" | "bitcoin";
105
+ type Account = {
106
+ chain: ChainBase;
107
+ id: string;
108
+ walletAddress: string;
109
+ };
110
+ type Balance = {
111
+ /** Token decimals (e.g. 9 for SOL, 18 for ETH) */decimals: number; /** Human-readable string, trimmed of trailing zeros */
112
+ formatted: string; /** Token symbol (e.g. "SOL", "ETH") */
113
+ symbol: string; /** Raw integer amount */
114
+ value: bigint;
115
+ };
116
+ /**
117
+ * Whether a wallet is currently usable from the user's environment.
118
+ *
119
+ * - `installed` — the wallet is available and `connect()` can be called.
120
+ * - `loadable` — the wallet's SDK can be loaded on demand (e.g. WalletConnect
121
+ * modal that pops a QR code without requiring a browser extension).
122
+ * - `not-installed` — the wallet isn't reachable. Consumers typically render
123
+ * a "download" affordance pointing at `meta.url`.
124
+ */
125
+ type WalletAvailability = "installed" | "loadable" | "not-installed";
126
+ /**
127
+ * Events a connector can emit while connected. butr's runtime subscribes
128
+ * via `Connector.subscribe?` after a successful `connect()` and dispatches
129
+ * the equivalent reducer event:
130
+ *
131
+ * - `accountChanged` carries both the new active `account` AND the full
132
+ * `accounts` array the wallet currently exposes. The runtime mirrors
133
+ * that list verbatim into the pool entry. This handles two cases
134
+ * uniformly:
135
+ * - Multi-account wallets (MetaMask, Rabby, Brave): the user adds or
136
+ * removes accounts from the dapp's permission set; the array grows
137
+ * or shrinks to match.
138
+ * - Single-account-exposure wallets (Phantom EVM/SVM, MetaMask Snap):
139
+ * only the active account is ever in `accounts`; switching swaps it
140
+ * in place rather than appending.
141
+ * Also covers chain switches — the new chain lives inside `account.chain`.
142
+ * - `disconnected` → `DISCONNECTED` (wallet has gone away externally:
143
+ * user locked it, removed the extension, etc.).
144
+ */
145
+ type ConnectorEvent = {
146
+ account: Account;
147
+ accounts: Array<Account>;
148
+ type: "accountChanged";
149
+ } | {
150
+ type: "disconnected";
151
+ };
152
+ /**
153
+ * Capability flags describing what an adapter can actually do at
154
+ * runtime. Populated by each adapter (the auto-built ones derive
155
+ * these from the underlying protocol's feature advertisements;
156
+ * hand-rolled adapters declare them explicitly). Consumers branch on
157
+ * these to gate UI affordances:
158
+ *
159
+ * ```tsx
160
+ * {wallet.connector.capabilities.requestAccounts ? (
161
+ * <button onClick={() => requestAccounts(wallet.connector.id)}>
162
+ * Request more accounts
163
+ * </button>
164
+ * ) : null}
165
+ * ```
166
+ *
167
+ * Each flag means "can this work right now," not "is the method
168
+ * defined": `signMessage: false` means calling `signMessage()` would
169
+ * reject; `switchChain: false` means switching is a no-op regardless
170
+ * of which chain is passed.
171
+ */
172
+ type WalletCapabilities = {
173
+ /** `getBalance` returns a real on-chain value (vs. a 0n placeholder). */getBalance: boolean; /** `getTransactionReceipt` returns a real RPC response. */
174
+ getTransactionReceipt: boolean;
175
+ /** Calling `requestAccounts` will actually do something — either
176
+ * prompt the user (EIP-2255) or refresh the exposed list. */
177
+ requestAccounts: boolean;
178
+ /** `sendTx` / `sendTxToChain` will work. False for SVM wallets that
179
+ * don't advertise `solana:signAndSendTransaction`. */
180
+ sendTransaction: boolean;
181
+ /** `signIn` works — Sign In With Solana (`solana:signIn`). True only
182
+ * for SVM wallets advertising the feature. */
183
+ signIn: boolean;
184
+ /** `signMessage` will work. False for SVM wallets that don't
185
+ * advertise `solana:signMessage`. */
186
+ signMessage: boolean;
187
+ /** `signTransaction` returns a signed-but-unbroadcast transaction the
188
+ * consumer submits with their own RPC client. True only for SVM
189
+ * wallets advertising `solana:signTransaction`; butr ships no RPC so
190
+ * it can't broadcast the result itself. EVM/hardware adapters leave
191
+ * this `false` (no `signTransaction` method). */
192
+ signTransaction: boolean; /** Wallet emits account/chain change events that butr can bridge. */
193
+ subscribe: boolean;
194
+ /** `switchAccount` is real. Almost always `false` for auto adapters
195
+ * — neither protocol exposes silent account switch. Hand-rolled
196
+ * adapters with custom transports may set it `true`. */
197
+ switchAccount: boolean;
198
+ /** `switchChain` routes subsequent calls through the new chain. EVM:
199
+ * true via `wallet_switchEthereumChain`. SVM: true (local state +
200
+ * per-call `chain` input) when more than one chain is advertised. */
201
+ switchChain: boolean;
202
+ };
203
+ /**
204
+ * Orchestration interface — what `butr` actually calls during the
205
+ * connect / disconnect / hydrate flow. This is the contract `butr`
206
+ * cares about; everything else on `WalletAdapter` is consumer-facing.
207
+ */
208
+ type Connector = {
209
+ /** Runtime capability flags — see `WalletCapabilities`. Read these
210
+ * to gate UI affordances rather than probing for method existence. */
211
+ capabilities: WalletCapabilities;
212
+ chainPlatform: ChainPlatform;
213
+ /** Begin a connection request. Resolves when the wallet is connected,
214
+ * rejects on user cancellation or other error.
215
+ *
216
+ * `opts.silent` requests a non-interactive reconnect to
217
+ * already-authorized accounts — butr's mount-time hydration passes it
218
+ * so a reload restores wallets without re-prompting (Wallet Standard
219
+ * `standard:connect`'s `silent` input; the `eth_accounts` read on
220
+ * EIP-1193). Adapters that can't reconnect without a prompt should
221
+ * reject when `silent` is set rather than show UI; hydration treats
222
+ * the rejection as a clean restore failure. */
223
+ connect(opts?: {
224
+ silent?: boolean;
225
+ }): Promise<void>; /** Optional teardown. butr calls this on disconnect, error recovery, and reset. */
226
+ disconnect?(): Promise<void>;
227
+ /** Read the currently-active account. butr uses this to populate the pool
228
+ * after a successful `connect()` and during hydration. */
229
+ getAccount(): Promise<Account | null>;
230
+ /** Optional. List every account the wallet exposes. Some browser wallets
231
+ * show many accounts at once (MetaMask with multiple imports). If
232
+ * omitted, butr defaults to `[await getAccount()]`. */
233
+ getAccounts?(): Promise<Array<Account>>;
234
+ /** Optional. Wallet logo as a URL or data URI. Adapters built via butr's
235
+ * auto-discovery (EIP-6963, Wallet Standard) populate this from the
236
+ * wallet's announced metadata; hand-rolled adapters can leave it
237
+ * unset and supply icons separately via `ConnectorMeta`. */
238
+ icon?: string; /** Stable key: "metamask", "phantom", etc. Pool entries are keyed by this. */
239
+ id: string; /** Human name: "MetaMask", "Phantom", etc. UI-facing only. */
240
+ name: string;
241
+ /** Optional. Ask the wallet to open its account-selection UI so the
242
+ * user can expose additional accounts to this app. Implemented on
243
+ * EIP-6963 wallets via `wallet_requestPermissions`; Wallet Standard
244
+ * wallets generally leave this unset because the user enables more
245
+ * accounts directly in the extension. Resolution doesn't include the
246
+ * new accounts — call `getAccounts()` (or use butr's
247
+ * `useRequestAccounts` hook, which refreshes the pool entry for
248
+ * you). */
249
+ requestAccounts?(): Promise<void>;
250
+ /** Optional. Subscribe to wallet-side events (account swap, network swap,
251
+ * external disconnect). butr's runtime calls this after a successful
252
+ * `connect()`, and uses the returned function to unsubscribe on
253
+ * disconnect / reset. Bridges native wallet events into the reducer so
254
+ * consumers don't have to wire `accountsChanged` / `chainChanged`
255
+ * themselves. */
256
+ subscribe?(listener: (event: ConnectorEvent) => void): () => void;
257
+ };
258
+ /**
259
+ * Capability interface — what consumers call on a connected wallet
260
+ * (signing, sending, balance lookups). `butr` never invokes these
261
+ * itself; they exist solely to give consumers a typed surface to talk
262
+ * to a connected wallet through.
263
+ */
264
+ type Wallet = {
265
+ /** Read a token balance. `mint` is optional; the connector decides
266
+ * what "no mint" means for its chain (native ETH on EVM, native SOL
267
+ * on Solana, etc.). */
268
+ getBalance(mint?: string): Promise<Balance>;
269
+ /** Returns a chain-specific signer. Consumers cast to the concrete
270
+ * type (e.g. WalletClient on viem, AnchorProvider on Solana). */
271
+ getSigner(): Promise<unknown>; /** Look up the status of a previously-submitted transaction. */
272
+ getTransactionReceipt(tx: string): Promise<{
273
+ status: "Success" | "Error" | "Pending";
274
+ }>;
275
+ /** Submit a transaction on the wallet's currently-active chain.
276
+ * Pass an `account` from `ConnectedWallet.accounts` to route the
277
+ * transaction through a specific exposed address instead of the
278
+ * wallet's currently-active one. EVM wallets honour this via
279
+ * `tx.from`; Wallet Standard wallets via the feature's `account`
280
+ * input. Omit for "use whichever the wallet picks." */
281
+ sendTx(tx: unknown, account?: Account): Promise<string>;
282
+ /** Submit a transaction targeting a specific chain. The optional
283
+ * callback fires after the connector has switched chain (consumers
284
+ * use this to re-enable UI). Pass an `account` to route through a
285
+ * specific exposed address (see `sendTx`). */
286
+ sendTxToChain(tx: unknown, targetChainId: string, account?: Account, cb?: () => void): Promise<string>;
287
+ /**
288
+ * Optional. Sign In With Solana (SIWS, `solana:signIn`). Authenticates
289
+ * the user and returns the connected account plus the signed
290
+ * statement so the consumer can verify it server-side. `input` is the
291
+ * SIWS message fields (domain, statement, nonce, …) — pass `{}` or
292
+ * omit for wallet defaults. Present only when `capabilities.signIn`
293
+ * is true.
294
+ */
295
+ signIn?(input?: Record<string, unknown>): Promise<{
296
+ account: Account;
297
+ signature: Uint8Array;
298
+ signedMessage: Uint8Array;
299
+ }>;
300
+ /**
301
+ * Sign a message and return both the signature and the bytes the wallet
302
+ * actually signed. Solana Wallet Standard wallets may prefix or re-encode
303
+ * the message internally; verifiers must check the signature against
304
+ * `signedMessage`, not the input bytes. EVM wallets echo the input.
305
+ *
306
+ * Pass an `account` to sign with a specific exposed address. EIP-1193
307
+ * routes it through `personal_sign`'s address param; Wallet Standard
308
+ * uses the feature's `account` input. Both support per-call signing
309
+ * without changing the wallet's active account.
310
+ */
311
+ signMessage(msg: Uint8Array, account?: Account): Promise<{
312
+ signature: Uint8Array;
313
+ signedMessage: Uint8Array;
314
+ }>;
315
+ /**
316
+ * Optional. Sign a transaction WITHOUT broadcasting it, returning the
317
+ * signed transaction bytes. For Solana Wallet Standard wallets that
318
+ * advertise `solana:signTransaction` but not
319
+ * `solana:signAndSendTransaction` (sign-only wallets). butr ships no
320
+ * RPC, so the consumer broadcasts the returned bytes with their own
321
+ * client. Present only when `capabilities.signTransaction` is true.
322
+ */
323
+ signTransaction?(tx: unknown, account?: Account): Promise<Uint8Array>;
324
+ /** Switch to a different account on the same wallet (some wallets
325
+ * expose multiple accounts simultaneously). */
326
+ switchAccount?(address: string): Promise<void>; /** Switch the wallet's active chain. */
327
+ switchChain(chain: ChainBase): Promise<void>;
328
+ };
329
+ /**
330
+ * Full interface every connector implementation must fulfill.
331
+ * Equivalent to `Connector & Wallet`. The split is documentary —
332
+ * it makes the orchestration / capability seam explicit:
333
+ * - `Connector` is what butr calls.
334
+ * - `Wallet` is what your app code calls on a connected wallet.
335
+ */
336
+ type WalletAdapter = Connector & Wallet;
337
+ type ConnectedWallet = {
338
+ /** Currently-active account on this wallet. */account: Account;
339
+ /** All accounts known on this wallet at the time of connect/refresh.
340
+ * Always contains at least `account`. Populated from `getAccounts()`
341
+ * if the connector implements it; otherwise `[account]`. */
342
+ accounts: Array<Account>;
343
+ connector: WalletAdapter;
344
+ };
345
+ type ConnectorMeta = {
346
+ /** Optional. Sync probe that reports whether the wallet is currently
347
+ * available. Defaults to `"installed"` when omitted. Consumers call
348
+ * this at render time to gate the "Connect" button. */
349
+ availability?: () => WalletAvailability;
350
+ chainPlatform: ChainPlatform; /** Optional image URL or data URI for wallet selection UIs. */
351
+ icon?: string;
352
+ id: string;
353
+ name: string;
354
+ /** Optional. Where to send users who don't have this wallet (download
355
+ * page, app store link, etc.). */
356
+ url?: string;
357
+ };
358
+ /**
359
+ * Outcome of butr's mount-time hydration pass. Passed to
360
+ * `WalletManagerConfig.onHydrated`. Three buckets:
361
+ *
362
+ * - `restoredIds` — wallets that came back fully. Their pool entries
363
+ * are live and consumers can use them immediately.
364
+ * - `pendingIds` — wallets whose adapter wasn't registered yet
365
+ * (auto-discovery's async warmup). The runtime retries each one
366
+ * when discovery announces a matching id, so most of these will
367
+ * restore within a few hundred ms of mount.
368
+ * - `dropped` — wallets whose restore actually failed (connector
369
+ * threw mid-flight). These have been removed from storage; consumer
370
+ * UX can surface "Couldn't reconnect Phantom — connect again."
371
+ */
372
+ type HydrationOutcome = {
373
+ dropped: Array<{
374
+ connectorId: string;
375
+ reason: unknown;
376
+ }>;
377
+ pendingIds: Array<string>;
378
+ restoredIds: Array<string>;
379
+ };
380
+ type WalletManagerConfig = {
381
+ /** Available connector metadata */connectors: Array<ConnectorMeta>; /** Function to instantiate a connector by ID */
382
+ createConnector: (id: string) => WalletAdapter | null; /** Called after a wallet is successfully connected */
383
+ onConnect?: (wallet: ConnectedWallet) => void;
384
+ /**
385
+ * Called after a connection attempt fails (user rejected, wallet
386
+ * locked, chain mismatch, timeout, …). Receives the normalised
387
+ * `ConnectionError` plus the id of the connector that was being
388
+ * connected. Useful for piping into observability tooling
389
+ * (Sentry, OTel) without each consumer wiring `try/catch`s around
390
+ * `connectWallet` themselves.
391
+ */
392
+ onConnectError?: (error: ConnectionError, connectorId: string) => void; /** Called after a wallet is disconnected */
393
+ onDisconnect?: (chainPlatform: ChainPlatform) => void;
394
+ /**
395
+ * Called once after butr's mount-time hydration finishes. Receives a
396
+ * `HydrationOutcome` summarising which stored wallets were restored,
397
+ * which are pending an adapter announcement, and which failed.
398
+ * Useful for surfacing "Phantom couldn't be reconnected — try
399
+ * again" UX or piping a metric to telemetry.
400
+ */
401
+ onHydrated?: (outcome: HydrationOutcome) => void; /** Called after all wallets are reset (e.g., to clear auth tokens) */
402
+ onReset?: () => void | Promise<void>;
403
+ /**
404
+ * Called when a connect attempt takes longer than
405
+ * `slowConnectThresholdMs` (default 5_000) but hasn't yet resolved
406
+ * or rejected. Fires at most once per connect attempt. Useful for
407
+ * surfacing a "still trying — check your wallet" hint in the UI or
408
+ * piping a slow-path metric to telemetry.
409
+ */
410
+ onSlowConnect?: (connectorId: string) => void;
411
+ /**
412
+ * Called when a storage write fails. butr's persistence layer is
413
+ * fire-and-forget by design (any individual write can fail without
414
+ * breaking butr's reducer state), but the consumer might still want
415
+ * to know — quota-exceeded errors, IndexedDB shutdown, cross-tab
416
+ * conflicts, cookie size limits. `context` is a short string
417
+ * describing which write failed (e.g. `"failed to persist pool"`).
418
+ * The default behaviour when no callback is set is `console.warn`.
419
+ */
420
+ onStorageError?: (error: unknown, context: string) => void; /** Threshold for `onSlowConnect`, in milliseconds. Defaults to 5_000. */
421
+ slowConnectThresholdMs?: number; /** Optional custom persistence implementation (e.g., cookie-backed) */
422
+ storage?: WalletPersistence; /** Storage key prefix for localStorage */
423
+ storageKeyPrefix?: string;
424
+ };
425
+ //#endregion
426
+ //#region src/types/chains-by-platform.d.ts
427
+ /**
428
+ * Map of every chain platform to the list of chains the consumer wants
429
+ * to expose to its UI (chain switcher, picker, etc).
430
+ *
431
+ * The platform key set is fixed by `ChainPlatform`. The value is the
432
+ * chain list — empty when the consumer doesn't want to support that
433
+ * platform's chains in this view. This is the only type that callers
434
+ * write down; the values come from per-platform packages
435
+ * (`EVM_CHAINS_LIST`, `SVM_CHAINS_LIST`, etc).
436
+ */
437
+ type ChainsByPlatform = Readonly<Record<ChainPlatform, ReadonlyArray<ChainBase>>>;
438
+ /**
439
+ * Build a fully-populated `ChainsByPlatform` from a partial. Platforms
440
+ * the consumer doesn't specify default to an empty list.
441
+ *
442
+ * Use this in apps that target one or two chain platforms — importing
443
+ * only those packages keeps unused chain registries out of the bundle.
444
+ * Apps that want every chain reach for `CHAINS_BY_PLATFORM` from
445
+ * `@usebutr/wallets` instead.
446
+ *
447
+ * @example
448
+ * // EVM-only app — Solana/Sui/Bitcoin tables never enter the bundle
449
+ * import { EVM_CHAINS_LIST } from "@usebutr/evm";
450
+ * import { buildChainsByPlatform } from "@usebutr/core";
451
+ *
452
+ * const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });
453
+ *
454
+ * @example
455
+ * // Multi-chain app — pull from each package the app actually uses
456
+ * import { EVM_CHAINS_LIST } from "@usebutr/evm";
457
+ * import { SVM_CHAINS_LIST } from "@usebutr/svm";
458
+ *
459
+ * const chains = buildChainsByPlatform({
460
+ * evm: EVM_CHAINS_LIST,
461
+ * svm: SVM_CHAINS_LIST,
462
+ * });
463
+ */
464
+ declare const buildChainsByPlatform: (partial: Partial<ChainsByPlatform>) => ChainsByPlatform;
465
+ //#endregion
466
+ //#region src/types/discoverer.d.ts
467
+ /**
468
+ * Self-describing discovery descriptor for a single chain platform.
469
+ *
470
+ * Each platform package (`@usebutr/evm`, `@usebutr/svm`, `@usebutr/sui`,
471
+ * `@usebutr/bitcoin`) exports one of these. The aggregator package
472
+ * (`@usebutr/wallets`) composes them into `autoDiscovery()` without
473
+ * needing to know per-platform defaults — the descriptor owns them.
474
+ *
475
+ * Adding a new chain platform means writing a `PlatformDiscoverer`
476
+ * inside the new package and adding one import to the aggregator's
477
+ * registry. The aggregator's logic doesn't need to grow.
478
+ *
479
+ * Two parts:
480
+ * - `subscribe` — the primary discovery channel (EIP-6963 / Wallet
481
+ * Standard / etc).
482
+ * - `fallback` — optional. The legacy-injected channel that should
483
+ * only emit if the primary channel hasn't produced an adapter for
484
+ * the same browser session by the settle deadline. `@usebutr/evm`
485
+ * has one (window.ethereum); `@usebutr/bitcoin` has one
486
+ * (window.unisat / sats-connect / window.btc); SVM and Sui don't.
487
+ */
488
+ type PlatformDiscoverer = {
489
+ /**
490
+ * Optional legacy-injected fallback. Subscribes only when consumers
491
+ * haven't disabled it. The hook receives `hasAnyPrimaryAdapter` so
492
+ * the fallback can defer to standards-based discovery when an adapter
493
+ * for the same wallet has already announced through the primary path.
494
+ */
495
+ fallback?: {
496
+ subscribe: (onAdapter: (adapter: WalletAdapter) => void, opts: {
497
+ hasAnyPrimaryAdapter: () => boolean;
498
+ }) => () => void;
499
+ }; /** Stable platform identifier. Used by the aggregator for keying. */
500
+ platform: ChainPlatform; /** Primary discovery subscription. */
501
+ subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void;
502
+ };
503
+ //#endregion
504
+ //#region src/types/signer.d.ts
505
+ /**
506
+ * Per-platform signer type registry.
507
+ *
508
+ * `Connector.getSigner()` returns `Promise<unknown>` because the actual
509
+ * signer shape lives in the platform-specific packages — `@usebutr/evm`
510
+ * returns an EIP-1193 provider, `@usebutr/svm` returns a Wallet
511
+ * Standard wallet, `@usebutr/sui` returns the same Wallet Standard
512
+ * wallet narrowed to Sui features, `@usebutr/bitcoin` returns either a
513
+ * Wallet Standard wallet or an injected provider depending on which
514
+ * adapter discovered it.
515
+ *
516
+ * Consumers cast the `unknown` to whichever signer their integration
517
+ * library expects. This registry exists so the cast target is sourced
518
+ * from one place — when a platform package renames its signer type,
519
+ * consumer code keeps working through the registry without an explicit
520
+ * patch.
521
+ *
522
+ * **How to extend.** Each platform package declares a module-augmentation
523
+ * block that adds its key to this interface. For example,
524
+ * `@usebutr/evm` ships:
525
+ *
526
+ * ```ts
527
+ * declare module "@usebutr/core" {
528
+ * interface SignerForPlatform {
529
+ * evm: Eip1193Provider;
530
+ * }
531
+ * }
532
+ * ```
533
+ *
534
+ * Consumers that import the EVM package get the typed entry for free;
535
+ * the augmentation is transitive through TypeScript's structural
536
+ * declaration merging.
537
+ *
538
+ * **Usage.**
539
+ *
540
+ * ```ts
541
+ * import type { SignerForPlatform } from "@usebutr/core";
542
+ *
543
+ * const signer = (await wallet.connector.getSigner()) as SignerForPlatform["evm"];
544
+ * ```
545
+ *
546
+ * The cast is still there — `getSigner` itself stays type-erased to
547
+ * keep the cross-package boundary loose — but the cast target lives in
548
+ * one canonical place.
549
+ *
550
+ * **Why not make `getSigner` generic.** Generic-on-platform `getSigner`
551
+ * would require `Connector` to be a discriminated union by
552
+ * `chainPlatform`, which is a public-API breaking change held for a
553
+ * future major release. The registry is the small step you can take
554
+ * today; the union is the larger step that makes the cast obsolete.
555
+ */
556
+ interface SignerForPlatform {}
557
+ /** Convenience alias for narrowing a single platform's signer type. */
558
+ type SignerOf<P extends keyof SignerForPlatform> = SignerForPlatform[P];
559
+ //#endregion
560
+ //#region src/wallet-source.d.ts
561
+ /**
562
+ * A discovery seam. Implementations call `onAdapter(adapter)` each time
563
+ * they find a wallet and return an unsubscribe handle. `@usebutr/wallets`
564
+ * composes EVM + SVM into a single `WalletSource`; third parties can
565
+ * implement this type without depending on `@usebutr/wallets`.
566
+ */
567
+ type WalletSource = {
568
+ subscribe(onAdapter: (adapter: WalletAdapter) => void): () => void;
569
+ };
570
+ /**
571
+ * Wrap a bare `subscribe` function (the exact shape of
572
+ * `discoverEvmAdapters` / `discoverSvmAdapters`) into a `WalletSource`,
573
+ * so an EVM-only app can do
574
+ * `createWalletSource(discoverEvmAdapters)` without importing anything
575
+ * protocol-bearing beyond `@usebutr/evm`.
576
+ */
577
+ declare const createWalletSource: (subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void) => WalletSource;
578
+ //#endregion
579
+ //#region src/store/reducer.d.ts
580
+ type ConnectionStatus = "idle" | "connecting" | "success" | "error";
581
+ type State = {
582
+ activeConnectorId: string | null;
583
+ connectingConnectorId: string | null;
584
+ connectionError: ConnectionError | null;
585
+ connectionStatus: ConnectionStatus;
586
+ isHydrated: boolean;
587
+ isUserDisconnected: boolean;
588
+ pool: Map<string, ConnectedWallet>;
589
+ selection: Map<ChainPlatform, string>;
590
+ };
591
+ /**
592
+ * Lifecycle events: hydration, full reset, and the explicit
593
+ * user-disconnected flag the runtime sets to suppress eager reconnect.
594
+ */
595
+ //#endregion
596
+ //#region src/storage/browser-storage-driver.d.ts
597
+ type BrowserStorageDrivers = {
598
+ /** Survives app restart (localStorage on web, AsyncStorage/MMKV on RN). */persistent: StorageDriver; /** Cleared when the app session ends (sessionStorage on web, in-memory on RN). */
599
+ session: StorageDriver;
600
+ };
601
+ declare const createMemoryStorageDriver: () => StorageDriver;
602
+ /**
603
+ * Default storage drivers for web. `persistent` wraps `localStorage`,
604
+ * `session` wraps `sessionStorage`. Both fall back to in-memory maps
605
+ * when the global isn't available (SSR, non-browser environments).
606
+ */
607
+ declare const createBrowserStorageDriver: () => BrowserStorageDrivers;
608
+ //#endregion
609
+ //#region src/storage/cookie-storage-driver.d.ts
610
+ type CookieDriverOptions = {
611
+ /**
612
+ * Cookie `domain` attribute. Omit to scope to the current host.
613
+ * Set to a parent domain (e.g. `.example.com`) to share state
614
+ * across subdomains.
615
+ */
616
+ domain?: string;
617
+ /**
618
+ * Lifetime in seconds. Defaults to 30 days. Pass `undefined`
619
+ * (the default) for "until the session ends" — but note that
620
+ * butr persists wallet state across sessions, so a finite
621
+ * max-age is usually what consumers want.
622
+ */
623
+ maxAgeSeconds?: number;
624
+ /**
625
+ * URL path the cookie applies to. Defaults to `/` (whole origin).
626
+ */
627
+ path?: string;
628
+ /**
629
+ * `SameSite` attribute. Defaults to `"lax"` — restrictive enough
630
+ * to avoid CSRF on cross-origin POSTs, permissive enough for
631
+ * top-level navigations.
632
+ */
633
+ sameSite?: "lax" | "strict" | "none";
634
+ /**
635
+ * `Secure` attribute. Defaults to `true` (cookies only sent over
636
+ * HTTPS). Force `false` for local development on plain `http://`.
637
+ */
638
+ secure?: boolean;
639
+ };
640
+ /**
641
+ * Cookie-backed storage driver. Reads/writes `document.cookie` —
642
+ * server-readable, survives reloads, scoped per `domain`/`path`.
643
+ *
644
+ * **When to use this:** SSR apps that need to know who's connected
645
+ * during the server render (so they can stream the connected-wallet
646
+ * UI without a client-side hydration flicker). The server can read
647
+ * the same cookies via `req.headers.cookie` and feed them into
648
+ * `WalletStorage` ahead of mount.
649
+ *
650
+ * **Trade-offs vs `localStorage`:** cookies travel with every
651
+ * request, so they cost bytes on the wire. Keep the storage key
652
+ * prefix short, and prefer this driver for the `persistent` slot
653
+ * only — the `session` slot can stay in `sessionStorage` (which
654
+ * cookies can't natively model anyway).
655
+ *
656
+ * **No-op on the server.** If `document` is undefined the driver
657
+ * returns a read-only stub that always resolves to `null` for
658
+ * `getItem` and ignores writes. The caller is expected to hydrate
659
+ * server-side state by reading cookies from the request header
660
+ * directly and seeding `WalletStorage` with an in-memory driver
661
+ * built from that snapshot.
662
+ */
663
+ declare const createCookieStorageDriver: (options?: CookieDriverOptions) => StorageDriver;
664
+ //#endregion
665
+ //#region src/storage/wallet-storage.d.ts
666
+ type StorageConfig = {
667
+ keyPrefix: string; /** Survives app restart. Defaults to localStorage on web. */
668
+ persistent?: StorageDriver; /** Cleared on session end. Defaults to sessionStorage on web. */
669
+ session?: StorageDriver;
670
+ };
671
+ declare class WalletStorage implements WalletPersistence {
672
+ private poolKey;
673
+ private selectionKey;
674
+ private activeKey;
675
+ private userDisconnectedKey;
676
+ private persistent;
677
+ private session;
678
+ constructor(config: StorageConfig);
679
+ getPool(): Promise<StoredPoolRecord>;
680
+ setPool(pool: Map<string, ConnectedWallet>): Promise<void>;
681
+ removePoolEntry(connectorId: string): Promise<void>;
682
+ clearPool(): Promise<void>;
683
+ getSelection(): Promise<StoredSelectionRecord>;
684
+ setSelection(selection: Map<ChainPlatform, string>): Promise<void>;
685
+ getActiveConnectorId(): Promise<string | null>;
686
+ setActiveConnectorId(connectorId: string | null): Promise<void>;
687
+ clearAll(): Promise<void>;
688
+ /**
689
+ * Disconnect-intent tracking.
690
+ *
691
+ * Lives in the session driver: survives component remounts (unlike refs)
692
+ * but clears when the session ends (unlike the persistent driver).
693
+ * Prevents auto-connect from firing immediately after a manual disconnect,
694
+ * while still allowing auto-connect on fresh sessions.
695
+ */
696
+ isUserDisconnected(): Promise<boolean>;
697
+ markUserDisconnected(value: boolean): Promise<void>;
698
+ }
699
+ //#endregion
700
+ //#region src/store/wallet-store.d.ts
701
+ type ExtractState<S> = S extends {
702
+ getState: () => infer T;
703
+ } ? T : never;
704
+ type RuntimeMembers = {
705
+ _config: WalletManagerConfig;
706
+ _storage: WalletPersistence;
707
+ connectWallet: (connectorId: string, onSuccess?: (wallet: ConnectedWallet) => void, onError?: (error: Error) => void) => Promise<void>;
708
+ disconnectWallet: (connectorId: string) => void;
709
+ getConnectorInstance: (id: string) => ReturnType<WalletManagerConfig["createConnector"]>;
710
+ hydrateWallets: () => Promise<void>;
711
+ refreshWallet: (connectorId: string) => void;
712
+ requestAccounts: (connectorId: string) => Promise<void>;
713
+ reset: () => void;
714
+ resetConnectionStatus: () => void;
715
+ setActiveConnector: (connectorId: string | null) => void;
716
+ setConnectionError: (error: ConnectionError | null) => void;
717
+ setSelection: (chainPlatform: ChainPlatform, connectorId: string | null) => void;
718
+ setUserDisconnected: (value: boolean) => void;
719
+ tryRestoreFromPending: (connectorId: string) => Promise<void>;
720
+ updateWalletAccount: (connectorId: string, account: Account) => void;
721
+ };
722
+ type WalletStore = ReturnType<typeof createWalletStore>;
723
+ type WalletStoreState = ExtractState<WalletStore>;
724
+ declare const createWalletStore: (config: WalletManagerConfig) => import("zustand/vanilla").StoreApi<State & RuntimeMembers>;
725
+ //#endregion
726
+ //#region src/wallet-equal.d.ts
727
+ /**
728
+ * Two wallet snapshots are equivalent for selector purposes iff they
729
+ * share connectorId, active account address, and active account chain
730
+ * id. Used by `useStoreWithEqualityFn` consumers (active-wallet,
731
+ * selected-wallet, useWalletEntry) to suppress spurious re-renders
732
+ * when the underlying Map churns but the resolved entry hasn't changed.
733
+ *
734
+ * Hoisted to its own module so the equivalence rule lives in one place
735
+ * — if we ever extend the snapshot (e.g. to consider `accounts.length`),
736
+ * every selector hook picks up the new rule for free.
737
+ */
738
+ declare const walletEqual: (a: ConnectedWallet | undefined, b: ConnectedWallet | undefined) => boolean;
739
+ //#endregion
740
+ //#region src/logger.d.ts
741
+ declare const logWarn: (...args: ReadonlyArray<unknown>) => void;
742
+ declare const logError: (...args: ReadonlyArray<unknown>) => void;
743
+ //#endregion
744
+ export { type Account, type Balance, type BrowserStorageDrivers, type ChainBase, type ChainPlatform, type ChainsByPlatform, type ConnectedWallet, type ConnectionError, type ConnectionErrorKind, type ConnectionStatus, type Connector, type ConnectorEvent, type ConnectorMeta, type CookieDriverOptions, type HydrationOutcome, type MaybePromise, type PlatformDiscoverer, type SignerForPlatform, type SignerOf, type StorageDriver, type StoredPoolEntry, type StoredPoolRecord, type StoredSelectionRecord, type Wallet, type WalletAdapter, type WalletAvailability, type WalletCapabilities, type WalletManagerConfig, type WalletPersistence, type WalletSource, WalletStorage, type WalletStore, type WalletStoreState, buildChainsByPlatform, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, logError, logWarn, mapConnectionError, walletEqual };
745
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/chain.ts","../src/storage/persistence.ts","../src/types/errors.ts","../src/types/wallet.ts","../src/types/chains-by-platform.ts","../src/types/discoverer.ts","../src/types/signer.ts","../src/wallet-source.ts","../src/store/reducer.ts","../src/storage/browser-storage-driver.ts","../src/storage/cookie-storage-driver.ts","../src/storage/wallet-storage.ts","../src/store/wallet-store.ts","../src/wallet-equal.ts","../src/logger.ts"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{createStore as e}from"zustand/vanilla";const t=e=>({bitcoin:e.bitcoin??[],evm:e.evm??[],sui:e.sui??[],svm:e.svm??[]}),n=e=>{if(e instanceof Error){let t=e.message,n=t.toLowerCase();if(t===`Connection timeout`)return{kind:`Timeout`,message:t};if(t===`Failed to get account`||n.includes(`not connected`))return{kind:`NotConnected`,message:t};let r=e.code;return r===4001?{kind:`UserRejected`,message:t}:r===-32002?{kind:`RequestPending`,message:t}:r===4100||r===4900||r===4901?{kind:`NotConnected`,message:t}:n.includes(`user rejected`)||n.includes(`user denied`)?{kind:`UserRejected`,message:t}:n.includes(`locked`)?{kind:`WalletLocked`,message:t}:n.includes(`chain`)&&(n.includes(`mismatch`)||n.includes(`unsupported`))?{kind:`ChainMismatch`,message:t}:{cause:e,kind:`Unknown`,message:t}}return typeof e==`string`?{kind:`Unknown`,message:e}:{cause:e,kind:`Unknown`,message:`Connection failed`}},r=e=>({subscribe:e}),i=(...e)=>{console.warn(...e)},a=(...e)=>{console.error(...e)},o=e=>{try{return typeof globalThis<`u`&&globalThis[e]!==void 0}catch{return!1}},s=()=>{let e=new Map;return{getItem(t){return e.get(t)??null},removeItem(t){e.delete(t)},setItem(t,n){e.set(t,n)}}},c=e=>{if(!o(e))return s();let t=globalThis[e];return{getItem(e){try{return t.getItem(e)}catch{return null}},removeItem(e){try{t.removeItem(e)}catch{}},setItem(e,n){try{t.setItem(e,n)}catch{}}}},l=()=>({persistent:c(`localStorage`),session:c(`sessionStorage`)}),u=e=>encodeURIComponent(e),d=e=>decodeURIComponent(e),f=()=>{let e=new Map;if(typeof document>`u`)return e;let t=document.cookie;if(!t)return e;for(let n of t.split(`;`)){let t=n.trim();if(!t)continue;let r=t.indexOf(`=`);if(r===-1)continue;let i=t.slice(0,r),a=t.slice(r+1);try{e.set(i,d(a))}catch{}}return e},p=(e,t,n,r=!1)=>{let i=[`${e}=${u(t)}`,`Path=${n.path??`/`}`];if(n.domain&&i.push(`Domain=${n.domain}`),r)i.push(`Max-Age=0`);else{let e=n.maxAgeSeconds??2592e3;i.push(`Max-Age=${e}`)}return i.push(`SameSite=${n.sameSite??`lax`}`),(n.secure??!0)&&i.push(`Secure`),i.join(`; `)},m=(e={})=>typeof document>`u`?{getItem(){return null},removeItem(){},setItem(){}}:{getItem(e){return f().get(e)??null},removeItem(t){document.cookie=p(t,``,e,!0)},setItem(t,n){document.cookie=p(t,n,e)}},h=new Set([`evm`,`svm`,`sui`,`bitcoin`]),g=e=>{if(!e||typeof e!=`object`)return!1;let t=e;return!(typeof t.walletAddress!=`string`||typeof t.id!=`string`||!t.chain||typeof t.chain!=`object`)},_=(e,t)=>{if(!t||typeof t!=`object`)return!1;let n=t;return!(typeof n.connectorId!=`string`||n.connectorId!==e||typeof n.chainPlatform!=`string`||!h.has(n.chainPlatform)||!g(n.account)||n.accounts!==void 0&&(!Array.isArray(n.accounts)||!n.accounts.every(g)))};var v=class{poolKey;selectionKey;activeKey;userDisconnectedKey;persistent;session;constructor(e){if(this.poolKey=`${e.keyPrefix}-pool`,this.selectionKey=`${e.keyPrefix}-selection`,this.activeKey=`${e.keyPrefix}-active`,this.userDisconnectedKey=`${e.keyPrefix}-user-disconnected`,e.persistent&&e.session)this.persistent=e.persistent,this.session=e.session;else{let t=l();this.persistent=e.persistent??t.persistent,this.session=e.session??t.session}}async getPool(){try{let e=await this.persistent.getItem(this.poolKey);if(!e)return{};let t=JSON.parse(e);if(!t||typeof t!=`object`)return await this.clearPool(),{};let n={};for(let[e,r]of Object.entries(t))_(e,r)?n[e]={...r,accounts:r.accounts??[r.account]}:i(`[butr] dropping invalid pool entry for ${e}`);return n}catch(e){return i(`[butr] failed to parse pool from storage:`,e),await this.clearPool(),{}}}async setPool(e){try{let t={};for(let[n,r]of e){let e={account:r.account,accounts:r.accounts,chainPlatform:r.connector.chainPlatform,connectorId:n};if(!_(n,e))throw Error(`[butr] refusing to persist invalid pool entry for ${n}`);t[n]=e}await this.persistent.setItem(this.poolKey,JSON.stringify(t))}catch(e){i(`[butr] failed to persist pool:`,e)}}async removePoolEntry(e){try{let t=await this.getPool();if(t[e]){let{[e]:n,...r}=t;await this.persistent.setItem(this.poolKey,JSON.stringify(r))}}catch(t){i(`[butr] failed to remove pool entry ${e}:`,t)}}async clearPool(){await this.persistent.removeItem(this.poolKey)}async getSelection(){try{let e=await this.persistent.getItem(this.selectionKey);if(!e)return{};let t=JSON.parse(e);if(!t||typeof t!=`object`)return{};let n={};for(let[e,r]of Object.entries(t))h.has(e)&&typeof r==`string`&&r.length>0&&(n[e]=r);return n}catch(e){return i(`[butr] failed to parse selection from storage:`,e),{}}}async setSelection(e){try{let t={};for(let[n,r]of e)t[n]=r;await this.persistent.setItem(this.selectionKey,JSON.stringify(t))}catch(e){i(`[butr] failed to persist selection:`,e)}}async getActiveConnectorId(){try{let e=await this.persistent.getItem(this.activeKey);return e&&e.length>0?e:null}catch{return null}}async setActiveConnectorId(e){try{await(e===null?this.persistent.removeItem(this.activeKey):this.persistent.setItem(this.activeKey,e))}catch(e){i(`[butr] failed to persist active connector id:`,e)}}async clearAll(){await Promise.all([this.persistent.removeItem(this.poolKey),this.persistent.removeItem(this.selectionKey),this.persistent.removeItem(this.activeKey)])}async isUserDisconnected(){try{return await this.session.getItem(this.userDisconnectedKey)===`true`}catch{return!1}}async markUserDisconnected(e){try{await(e?this.session.setItem(this.userDisconnectedKey,`true`):this.session.removeItem(this.userDisconnectedKey))}catch{}}};const y=e=>{let t=new Map,n=e=>{let n=t.get(e);if(n){try{n()}catch(e){i(`[butr] unsubscribe threw:`,e)}t.delete(e)}};return{attach:(r,a)=>{if(a.subscribe){n(r);try{let i=a.subscribe(t=>{switch(t.type){case`accountChanged`:e.onAccountChanged(r,t.accounts,t.account);break;case`disconnected`:n(r),e.onDisconnected(r,a.chainPlatform);break;default:}});t.set(r,i)}catch(e){i(`[butr] subscribe failed for ${r}:`,e)}}},detach:n,detachAll:()=>{let e=[];for(let n of t.keys())e.push(n);for(let t of e)n(t)}}},b=async(e,t,n)=>{if(e.getAccounts){let t=await e.getAccounts().catch(()=>null);if(t&&t.length>0)return t}return t.length>0?[...t]:[n]},x=async(e,t,n)=>{try{await n.connect({silent:!0});let r=await n.getAccount()||t.account;return{connectorId:e,entry:{account:r,accounts:await b(n,t.accounts,r),connector:n},kind:`ok`}}catch(t){return{connectorId:e,error:t,kind:`fail`}}},S=(e,t,n)=>{let r=new Map,i=t=>{(async()=>{try{await e.removePoolEntry(t)}catch(e){n(`failed to remove broken entry`,e)}})()};return{drainPending:async e=>{let n=r.get(e);if(!n)return null;let a=t(e);if(!a)return null;let o=await x(e,n,a);return r.delete(e),o.kind===`fail`&&i(e),o},hydrate:async()=>{let[n,a,o,s]=await Promise.all([e.getPool(),e.getSelection(),e.getActiveConnectorId(),e.isUserDisconnected()]),c=new Map,l=[];r.clear();let u=[];for(let[e,i]of Object.entries(n)){if(!i)continue;let n=t(e);if(!n){r.set(e,i);continue}u.push(x(e,i,n))}let d=await Promise.all(u);for(let e of d){if(e.kind===`ok`){c.set(e.connectorId,e.entry);continue}l.push({connectorId:e.connectorId,reason:e.error}),i(e.connectorId)}let f=new Map;for(let[e,t]of Object.entries(a))t&&c.has(t)&&f.set(e,t);for(let[e,t]of c){let n=t.connector.chainPlatform;f.has(n)||f.set(n,e)}let p=null;return o&&c.has(o)?p=o:c.size>0&&(p=c.keys().next().value??null),{activeConnectorId:p,dropped:l,isUserDisconnected:s,pendingIds:[...r.keys()],pool:c,selection:f}},pendingIds:()=>[...r.keys()]}},C={activeConnectorId:null,connectingConnectorId:null,connectionError:null,connectionStatus:`idle`,isHydrated:!1,isUserDisconnected:!1,pool:new Map,selection:new Map},w=(e,t)=>{for(let[n,r]of e)if(r.connector.chainPlatform===t)return n},T=(e,t)=>{switch(t.type){case`HYDRATED`:{let n=new Map(e.pool);for(let[e,r]of t.pool)n.set(e,r);let r=new Map(e.selection);for(let[e,n]of t.selection)r.set(e,n);let i=null;return t.activeConnectorId&&n.has(t.activeConnectorId)?i=t.activeConnectorId:e.activeConnectorId&&n.has(e.activeConnectorId)?i=e.activeConnectorId:n.size>0&&(i=n.keys().next().value??null),{...e,activeConnectorId:i,isHydrated:!0,isUserDisconnected:t.isUserDisconnected,pool:n,selection:r}}case`USER_DISCONNECTED_SET`:return{...e,isUserDisconnected:t.value};case`CONNECT_STARTED`:return{...e,connectingConnectorId:t.connectorId,connectionError:null,connectionStatus:`connecting`};case`CONNECT_SUCCEEDED`:{let{connectorId:n,entry:r}=t,i=e.pool.get(n);if(i&&i.account.walletAddress.toLowerCase()===r.account.walletAddress.toLowerCase())return{...e,activeConnectorId:n,connectingConnectorId:null,connectionStatus:`success`};let a=new Map([...e.pool,[n,r]]),o=new Map([...e.selection,[r.connector.chainPlatform,n]]);return{...e,activeConnectorId:n,connectingConnectorId:null,connectionStatus:`success`,pool:a,selection:o}}case`CONNECT_FAILED`:return{...e,connectingConnectorId:null,connectionError:t.error,connectionStatus:`error`};case`DISCONNECTED`:{let n=e.pool.get(t.connectorId);if(!n)return e;let r=new Map(e.pool);r.delete(t.connectorId);let i=new Map(e.selection),a=n.connector.chainPlatform;if(i.get(a)===t.connectorId){i.delete(a);let e=w(r,a);e&&i.set(a,e)}let o=r.keys().next(),s=e.activeConnectorId===t.connectorId?o.value??null:e.activeConnectorId;return{...e,activeConnectorId:s,pool:r,selection:i}}case`ACCOUNTS_REFRESHED`:{let n=e.pool.get(t.connectorId);if(!n)return e;let r;r=t.active?t.active:t.accounts.some(e=>e.walletAddress===n.account.walletAddress&&e.chain.id===n.account.chain.id)?n.account:t.accounts[0]??n.account;let i=r.walletAddress===n.account.walletAddress&&r.chain.id===n.account.chain.id,a=t.accounts.length===n.accounts.length&&t.accounts.every((e,t)=>e.walletAddress===n.accounts[t]?.walletAddress);if(i&&a)return e;let o={...n,account:r,accounts:[...t.accounts]},s=new Map([...e.pool,[t.connectorId,o]]);return{...e,pool:s}}case`WALLET_REFRESHED`:{let n=e.pool.get(t.connectorId);if(!n)return e;let r=new Map([...e.pool,[t.connectorId,{...n}]]);return{...e,pool:r}}case`ACTIVE_CHANGED`:return{...e,activeConnectorId:t.connectorId};case`SELECTION_CHANGED`:{let n=new Map(e.selection);return t.connectorId===null?n.delete(t.chainPlatform):n.set(t.chainPlatform,t.connectorId),{...e,selection:n}}case`STATUS_RESET`:return{...e,connectingConnectorId:null,connectionError:null,connectionStatus:`idle`};case`ERROR_SET`:return{...e,connectionError:t.error,connectionStatus:t.error?`error`:`idle`};case`RESET`:return{...C,isHydrated:e.isHydrated,isUserDisconnected:!0};default:return e}},E=async(e,t)=>{try{await e()}catch(e){t(e)}},D=e=>e.toLowerCase(),O=t=>{let r=t.storageKeyPrefix||`butr`,o=t.storage||new v({keyPrefix:r}),s=e=>n=>{if(t.onStorageError){try{t.onStorageError(n,e)}catch(e){i(`[butr] onStorageError threw:`,e)}return}i(`[butr] ${e}:`,n)},c=S(o,t.createConnector,(e,t)=>s(e)(t));return e()((e,r)=>{let l=t=>{e(e=>T(e,t),!1)},u=()=>{E(()=>o.setPool(r().pool),s(`failed to persist pool`))},d=(e,t,n)=>{l({accounts:t,active:n,connectorId:e,type:`ACCOUNTS_REFRESHED`}),u()},f=()=>{E(()=>o.setSelection(r().selection),s(`failed to persist selection`))},p=()=>{E(()=>o.setActiveConnectorId(r().activeConnectorId),s(`failed to persist active connector`))},m=y({onAccountChanged:(e,t,n)=>{d(e,[...t],n)},onDisconnected:(e,n)=>{l({connectorId:e,type:`DISCONNECTED`}),r().pool.size===0?E(()=>o.clearAll(),s(`failed to clear storage`)):(u(),f(),p()),t.onDisconnect?.(n)}});return{...C,_config:t,_storage:o,connectWallet:async(e,a,o)=>{r().setUserDisconnected(!1);let s=t.createConnector(e);if(!s)throw Error(`Failed to create connector for ${e}`);l({connectorId:e,type:`CONNECT_STARTED`});let c=t.slowConnectThresholdMs??5e3,d=t.onSlowConnect?setTimeout(()=>{try{t.onSlowConnect?.(e)}catch(e){i(`[butr] onSlowConnect threw:`,e)}},c):null;try{let n=s.connect();n.catch(()=>{}),await Promise.race([n,new Promise((e,t)=>{setTimeout(()=>{t(Error(`Connection timeout`))},9e4)})]);let r=await s.getAccount();if(!r)throw Error(`Failed to get account`);let i=s.getAccounts?await s.getAccounts().catch(()=>null):null,o={account:r,accounts:i&&i.length>0?i:[r],connector:s};l({connectorId:e,entry:o,type:`CONNECT_SUCCEEDED`}),m.attach(e,s),u(),f(),p(),t.onConnect?.(o),a?.(o)}catch(r){let a=n(r);l({error:a,type:`CONNECT_FAILED`});try{await s.disconnect?.()}catch(e){i(`[butr] disconnect during error recovery failed:`,e)}try{t.onConnectError?.(a,e)}catch(e){i(`[butr] onConnectError threw:`,e)}throw o?.(r),r}finally{d&&clearTimeout(d)}},disconnectWallet:e=>{r().setUserDisconnected(!0);let n=r();if(!n.isHydrated)return;let i=n.pool.get(e);if(!i)return;m.detach(e),E(()=>i.connector.disconnect?.()??Promise.resolve(),a);let c=i.connector.chainPlatform;l({connectorId:e,type:`DISCONNECTED`}),r().pool.size===0?E(()=>o.clearAll(),s(`failed to clear storage`)):(u(),f(),p()),t.onDisconnect?.(c)},getConnectorInstance:e=>t.createConnector(e),hydrateWallets:async()=>{let e=await c.hydrate();l({activeConnectorId:e.activeConnectorId,isUserDisconnected:e.isUserDisconnected,pool:e.pool,selection:e.selection,type:`HYDRATED`});for(let[t,n]of e.pool)m.attach(t,n.connector);if(f(),p(),Promise.all(c.pendingIds().map(e=>r().tryRestoreFromPending(e))),t.onHydrated)try{t.onHydrated({dropped:e.dropped,pendingIds:[...e.pendingIds],restoredIds:[...e.pool.keys()]})}catch(e){i(`[butr] onHydrated threw:`,e)}else if(e.dropped.length>0)for(let{connectorId:t,reason:n}of e.dropped)i(`[butr] failed to restore connector ${t}:`,n)},refreshWallet:e=>{l({connectorId:e,type:`WALLET_REFRESHED`})},requestAccounts:async e=>{let t=r().pool.get(e);if(!t)return;await t.connector.requestAccounts?.();let n=t.connector.getAccounts?await t.connector.getAccounts().catch(()=>null):null;!n||n.length===0||d(e,n)},reset:()=>{r().setUserDisconnected(!0);let e=r();if(e.isHydrated){m.detachAll();for(let t of e.pool.values())E(()=>t.connector.disconnect?.()??Promise.resolve(),a);if(E(()=>o.clearAll(),s(`failed to clear storage`)),t.onReset){let e=t.onReset;E(()=>Promise.resolve(e()),a)}l({type:`RESET`})}},resetConnectionStatus:()=>{l({type:`STATUS_RESET`})},setActiveConnector:e=>{l({connectorId:e,type:`ACTIVE_CHANGED`}),p()},setConnectionError:e=>{l({error:e,type:`ERROR_SET`})},setSelection:(e,t)=>{l({chainPlatform:e,connectorId:t,type:`SELECTION_CHANGED`}),f()},setUserDisconnected:e=>{l({type:`USER_DISCONNECTED_SET`,value:e}),E(()=>o.markUserDisconnected(e),s(`failed to persist disconnect intent`))},tryRestoreFromPending:async e=>{let n=await c.drainPending(e);if(n){if(n.kind===`fail`){i(`[butr] late restore failed for ${e}:`,n.error);return}l({connectorId:e,entry:n.entry,type:`CONNECT_SUCCEEDED`}),m.attach(e,n.entry.connector),u(),f(),p(),t.onConnect?.(n.entry)}},updateWalletAccount:(e,t)=>{let n=r().pool.get(e);if(!n)return;let i=t.chain,a=n.accounts.map(e=>e.chain.id===i.id?e:{chain:i,id:`${i.id}:${D(e.walletAddress)}`,walletAddress:e.walletAddress});d(e,a.some(e=>D(e.walletAddress)===D(t.walletAddress))?a:[...a,t],t)}}})},k=(e,t)=>e===t?!0:!e||!t?!1:e.connector.id===t.connector.id&&e.account.walletAddress===t.account.walletAddress&&e.account.chain.id===t.account.chain.id;export{v as WalletStorage,t as buildChainsByPlatform,l as createBrowserStorageDriver,m as createCookieStorageDriver,s as createMemoryStorageDriver,r as createWalletSource,O as createWalletStore,a as logError,i as logWarn,n as mapConnectionError,k as walletEqual};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/types/chains-by-platform.ts","../src/types/errors.ts","../src/wallet-source.ts","../src/logger.ts","../src/storage/browser-storage-driver.ts","../src/storage/cookie-storage-driver.ts","../src/storage/wallet-storage.ts","../src/store/connector-lifecycle.ts","../src/store/hydration.ts","../src/store/reducer.ts","../src/store/wallet-store-helpers.ts","../src/store/wallet-store.ts","../src/wallet-equal.ts"],"sourcesContent":["import type { ChainBase } from \"./chain\";\nimport type { ChainPlatform } from \"./wallet\";\n\n/**\n * Map of every chain platform to the list of chains the consumer wants\n * to expose to its UI (chain switcher, picker, etc).\n *\n * The platform key set is fixed by `ChainPlatform`. The value is the\n * chain list — empty when the consumer doesn't want to support that\n * platform's chains in this view. This is the only type that callers\n * write down; the values come from per-platform packages\n * (`EVM_CHAINS_LIST`, `SVM_CHAINS_LIST`, etc).\n */\ntype ChainsByPlatform = Readonly<Record<ChainPlatform, ReadonlyArray<ChainBase>>>;\n\n/**\n * Build a fully-populated `ChainsByPlatform` from a partial. Platforms\n * the consumer doesn't specify default to an empty list.\n *\n * Use this in apps that target one or two chain platforms — importing\n * only those packages keeps unused chain registries out of the bundle.\n * Apps that want every chain reach for `CHAINS_BY_PLATFORM` from\n * `@usebutr/wallets` instead.\n *\n * @example\n * // EVM-only app — Solana/Sui/Bitcoin tables never enter the bundle\n * import { EVM_CHAINS_LIST } from \"@usebutr/evm\";\n * import { buildChainsByPlatform } from \"@usebutr/core\";\n *\n * const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });\n *\n * @example\n * // Multi-chain app — pull from each package the app actually uses\n * import { EVM_CHAINS_LIST } from \"@usebutr/evm\";\n * import { SVM_CHAINS_LIST } from \"@usebutr/svm\";\n *\n * const chains = buildChainsByPlatform({\n * evm: EVM_CHAINS_LIST,\n * svm: SVM_CHAINS_LIST,\n * });\n */\nconst buildChainsByPlatform = (partial: Partial<ChainsByPlatform>): ChainsByPlatform => ({\n bitcoin: partial.bitcoin ?? [],\n evm: partial.evm ?? [],\n sui: partial.sui ?? [],\n svm: partial.svm ?? [],\n});\n\nexport type { ChainsByPlatform };\nexport { buildChainsByPlatform };\n","/**\n * Tagged union of normalised connection errors.\n *\n * butr maps thrown values from connectors (which vary across wallet SDKs:\n * MetaMask uses EIP-1193 codes, Phantom throws stringly-typed errors,\n * embedded SDKs throw their own classes) into a small set of UX-meaningful\n * variants. Consumers branch on `kind` instead of regexing message strings.\n *\n * `message` is always present and human-readable. `cause` preserves the\n * original thrown value so callers can inspect raw connector errors when\n * the variant is `Unknown`.\n */\ntype ConnectionError =\n | { kind: \"UserRejected\"; message: string }\n | { kind: \"RequestPending\"; message: string }\n | { kind: \"WalletLocked\"; message: string }\n | { actualChain?: string; expectedChain?: string; kind: \"ChainMismatch\"; message: string }\n | { kind: \"NotConnected\"; message: string }\n | { kind: \"Timeout\"; message: string }\n | { cause?: unknown; kind: \"Unknown\"; message: string };\n\ntype ConnectionErrorKind = ConnectionError[\"kind\"];\n\n/**\n * Normalise a thrown value into a `ConnectionError`.\n *\n * Recognises:\n * - butr's own `Error(\"Connection timeout\")` (from the 90s connect timeout)\n * - butr's own `Error(\"Failed to get account\")` (from the connect flow)\n * - EIP-1193 numeric `code` properties (`4001` → UserRejected,\n * `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected —\n * unauthorized / disconnected from all-or-one chains)\n * - common message substrings: \"user rejected\" / \"user denied\",\n * \"locked\", \"chain\", etc.\n * - anything else → `Unknown` with `cause` set to the original value.\n */\nconst mapConnectionError = (raw: unknown): ConnectionError => {\n if (raw instanceof Error) {\n const message = raw.message;\n const lower = message.toLowerCase();\n\n if (message === \"Connection timeout\") {\n return { kind: \"Timeout\", message };\n }\n if (message === \"Failed to get account\" || lower.includes(\"not connected\")) {\n return { kind: \"NotConnected\", message };\n }\n\n const code = (raw as { code?: unknown }).code;\n if (code === 4001) {\n return { kind: \"UserRejected\", message };\n }\n if (code === -32_002) {\n return { kind: \"RequestPending\", message };\n }\n // EIP-1193: 4100 unauthorized, 4900 disconnected (all chains),\n // 4901 disconnected (requested chain). All mean \"no usable session\"\n // from butr's perspective → NotConnected.\n if (code === 4100 || code === 4900 || code === 4901) {\n return { kind: \"NotConnected\", message };\n }\n\n if (lower.includes(\"user rejected\") || lower.includes(\"user denied\")) {\n return { kind: \"UserRejected\", message };\n }\n if (lower.includes(\"locked\")) {\n return { kind: \"WalletLocked\", message };\n }\n if (lower.includes(\"chain\") && (lower.includes(\"mismatch\") || lower.includes(\"unsupported\"))) {\n return { kind: \"ChainMismatch\", message };\n }\n\n return { cause: raw, kind: \"Unknown\", message };\n }\n\n if (typeof raw === \"string\") {\n return { kind: \"Unknown\", message: raw };\n }\n\n return { cause: raw, kind: \"Unknown\", message: \"Connection failed\" };\n};\n\nexport type { ConnectionError, ConnectionErrorKind };\nexport { mapConnectionError };\n","import type { WalletAdapter } from \"./types\";\n\n/**\n * A discovery seam. Implementations call `onAdapter(adapter)` each time\n * they find a wallet and return an unsubscribe handle. `@usebutr/wallets`\n * composes EVM + SVM into a single `WalletSource`; third parties can\n * implement this type without depending on `@usebutr/wallets`.\n */\ntype WalletSource = {\n subscribe(onAdapter: (adapter: WalletAdapter) => void): () => void;\n};\n\n/**\n * Wrap a bare `subscribe` function (the exact shape of\n * `discoverEvmAdapters` / `discoverSvmAdapters`) into a `WalletSource`,\n * so an EVM-only app can do\n * `createWalletSource(discoverEvmAdapters)` without importing anything\n * protocol-bearing beyond `@usebutr/evm`.\n */\nconst createWalletSource = (\n subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void,\n): WalletSource => ({ subscribe });\n\nexport type { WalletSource };\nexport { createWalletSource };\n","/* eslint-disable no-console -- central logging sink; the one sanctioned console use */\nconst logWarn = (...args: ReadonlyArray<unknown>): void => {\n console.warn(...args);\n};\n\nconst logError = (...args: ReadonlyArray<unknown>): void => {\n console.error(...args);\n};\n/* eslint-enable no-console */\n\nexport { logError, logWarn };\n","import type { StorageDriver } from \"./persistence\";\n\ntype BrowserStorageDrivers = {\n /** Survives app restart (localStorage on web, AsyncStorage/MMKV on RN). */\n persistent: StorageDriver;\n /** Cleared when the app session ends (sessionStorage on web, in-memory on RN). */\n session: StorageDriver;\n};\n\ntype WebStorageKind = \"localStorage\" | \"sessionStorage\";\n\nconst hasWebStorage = (kind: WebStorageKind): boolean => {\n try {\n return (\n typeof globalThis !== \"undefined\" &&\n (globalThis as Record<string, unknown>)[kind] !== undefined\n );\n } catch {\n return false;\n }\n};\n\nconst createMemoryStorageDriver = (): StorageDriver => {\n const memory = new Map<string, string>();\n return {\n getItem(key) {\n return memory.get(key) ?? null;\n },\n removeItem(key) {\n memory.delete(key);\n },\n setItem(key, value) {\n memory.set(key, value);\n },\n };\n};\n\nconst createWebStorageDriver = (kind: WebStorageKind): StorageDriver => {\n if (!hasWebStorage(kind)) {\n return createMemoryStorageDriver();\n }\n\n const storage = (globalThis as unknown as Record<WebStorageKind, Storage>)[kind];\n\n return {\n getItem(key) {\n try {\n return storage.getItem(key);\n } catch {\n return null;\n }\n },\n removeItem(key) {\n try {\n storage.removeItem(key);\n } catch {\n // ignore\n }\n },\n setItem(key, value) {\n try {\n storage.setItem(key, value);\n } catch {\n // ignore quota/security errors\n }\n },\n };\n};\n\n/**\n * Default storage drivers for web. `persistent` wraps `localStorage`,\n * `session` wraps `sessionStorage`. Both fall back to in-memory maps\n * when the global isn't available (SSR, non-browser environments).\n */\nconst createBrowserStorageDriver = (): BrowserStorageDrivers => ({\n persistent: createWebStorageDriver(\"localStorage\"),\n session: createWebStorageDriver(\"sessionStorage\"),\n});\n\nexport type { BrowserStorageDrivers };\nexport { createBrowserStorageDriver, createMemoryStorageDriver };\n","import type { StorageDriver } from \"./persistence\";\n\ntype CookieDriverOptions = {\n /**\n * Cookie `domain` attribute. Omit to scope to the current host.\n * Set to a parent domain (e.g. `.example.com`) to share state\n * across subdomains.\n */\n domain?: string;\n /**\n * Lifetime in seconds. Defaults to 30 days. Pass `undefined`\n * (the default) for \"until the session ends\" — but note that\n * butr persists wallet state across sessions, so a finite\n * max-age is usually what consumers want.\n */\n maxAgeSeconds?: number;\n /**\n * URL path the cookie applies to. Defaults to `/` (whole origin).\n */\n path?: string;\n /**\n * `SameSite` attribute. Defaults to `\"lax\"` — restrictive enough\n * to avoid CSRF on cross-origin POSTs, permissive enough for\n * top-level navigations.\n */\n sameSite?: \"lax\" | \"strict\" | \"none\";\n /**\n * `Secure` attribute. Defaults to `true` (cookies only sent over\n * HTTPS). Force `false` for local development on plain `http://`.\n */\n secure?: boolean;\n};\n\nconst DEFAULT_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;\n\nconst encode = (value: string): string => encodeURIComponent(value);\nconst decode = (value: string): string => decodeURIComponent(value);\n\nconst readCookies = (): Map<string, string> => {\n const out = new Map<string, string>();\n if (typeof document === \"undefined\") {\n return out;\n }\n const cookieString = document.cookie;\n if (!cookieString) {\n return out;\n }\n for (const segment of cookieString.split(\";\")) {\n const trimmed = segment.trim();\n if (!trimmed) {\n continue;\n }\n const eq = trimmed.indexOf(\"=\");\n if (eq === -1) {\n continue;\n }\n const name = trimmed.slice(0, eq);\n const raw = trimmed.slice(eq + 1);\n try {\n out.set(name, decode(raw));\n } catch {\n // Malformed entry; skip rather than crash hydration.\n }\n }\n return out;\n};\n\nconst buildCookieString = (\n name: string,\n value: string,\n options: CookieDriverOptions,\n expired = false,\n): string => {\n const parts: Array<string> = [`${name}=${encode(value)}`, `Path=${options.path ?? \"/\"}`];\n if (options.domain) {\n parts.push(`Domain=${options.domain}`);\n }\n if (expired) {\n parts.push(\"Max-Age=0\");\n } else {\n const maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;\n parts.push(`Max-Age=${maxAge}`);\n }\n parts.push(`SameSite=${options.sameSite ?? \"lax\"}`);\n if (options.secure ?? true) {\n parts.push(\"Secure\");\n }\n return parts.join(\"; \");\n};\n\n/**\n * Cookie-backed storage driver. Reads/writes `document.cookie` —\n * server-readable, survives reloads, scoped per `domain`/`path`.\n *\n * **When to use this:** SSR apps that need to know who's connected\n * during the server render (so they can stream the connected-wallet\n * UI without a client-side hydration flicker). The server can read\n * the same cookies via `req.headers.cookie` and feed them into\n * `WalletStorage` ahead of mount.\n *\n * **Trade-offs vs `localStorage`:** cookies travel with every\n * request, so they cost bytes on the wire. Keep the storage key\n * prefix short, and prefer this driver for the `persistent` slot\n * only — the `session` slot can stay in `sessionStorage` (which\n * cookies can't natively model anyway).\n *\n * **No-op on the server.** If `document` is undefined the driver\n * returns a read-only stub that always resolves to `null` for\n * `getItem` and ignores writes. The caller is expected to hydrate\n * server-side state by reading cookies from the request header\n * directly and seeding `WalletStorage` with an in-memory driver\n * built from that snapshot.\n */\nconst createCookieStorageDriver = (options: CookieDriverOptions = {}): StorageDriver => {\n if (typeof document === \"undefined\") {\n return {\n getItem() {\n return null;\n },\n removeItem() {\n // server-side no-op\n },\n setItem() {\n // server-side no-op\n },\n };\n }\n return {\n getItem(key) {\n return readCookies().get(key) ?? null;\n },\n removeItem(key) {\n // eslint-disable-next-line unicorn/no-document-cookie -- cookie storage driver is the document.cookie abstraction by design\n document.cookie = buildCookieString(key, \"\", options, true);\n },\n setItem(key, value) {\n // eslint-disable-next-line unicorn/no-document-cookie -- cookie storage driver is the document.cookie abstraction by design\n document.cookie = buildCookieString(key, value, options);\n },\n };\n};\n\nexport type { CookieDriverOptions };\nexport { createCookieStorageDriver };\n","import { logWarn } from \"../logger\";\nimport type { ChainPlatform, ConnectedWallet } from \"../types\";\n\nimport { createBrowserStorageDriver } from \"./browser-storage-driver\";\nimport type {\n StorageDriver,\n StoredPoolEntry,\n StoredPoolRecord,\n StoredSelectionRecord,\n WalletPersistence,\n} from \"./persistence\";\n\nconst VALID_CHAIN_PLATFORMS = new Set<ChainPlatform>([\"evm\", \"svm\", \"sui\", \"bitcoin\"]);\n\ntype StorageConfig = {\n keyPrefix: string;\n /** Survives app restart. Defaults to localStorage on web. */\n persistent?: StorageDriver;\n /** Cleared on session end. Defaults to sessionStorage on web. */\n session?: StorageDriver;\n};\n\nconst isValidAccount = (value: unknown): boolean => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n const account = value as Record<string, unknown>;\n if (typeof account.walletAddress !== \"string\" || typeof account.id !== \"string\") {\n return false;\n }\n if (!account.chain || typeof account.chain !== \"object\") {\n return false;\n }\n return true;\n};\n\n/**\n * Structural validator for a serialized pool entry.\n *\n * Used in two directions:\n * - On read (`getPool`): malformed entries from storage are warned\n * and dropped — legacy / cross-tab corruption shouldn't crash the\n * consumer.\n * - On write (`setPool`): the runtime's reducer state is the source\n * of truth, and a malformed write would indicate a programming\n * error inside butr. Throw rather than silently corrupt storage.\n *\n * Same validator either way — keeping the two paths symmetric means\n * \"what's storable\" is a single fact.\n */\nconst isValidPoolEntry = (key: string, value: unknown): value is StoredPoolEntry => {\n if (!value || typeof value !== \"object\") {\n return false;\n }\n const entry = value as Record<string, unknown>;\n if (typeof entry.connectorId !== \"string\" || entry.connectorId !== key) {\n return false;\n }\n if (typeof entry.chainPlatform !== \"string\") {\n return false;\n }\n if (!VALID_CHAIN_PLATFORMS.has(entry.chainPlatform as ChainPlatform)) {\n return false;\n }\n if (!isValidAccount(entry.account)) {\n return false;\n }\n // `accounts` is optional during read for backward compatibility; if\n // present, every entry must be a valid Account.\n if (\n entry.accounts !== undefined &&\n (!Array.isArray(entry.accounts) || !entry.accounts.every(isValidAccount))\n ) {\n return false;\n }\n return true;\n};\n\nclass WalletStorage implements WalletPersistence {\n private poolKey: string;\n private selectionKey: string;\n private activeKey: string;\n private userDisconnectedKey: string;\n private persistent: StorageDriver;\n private session: StorageDriver;\n\n constructor(config: StorageConfig) {\n this.poolKey = `${config.keyPrefix}-pool`;\n this.selectionKey = `${config.keyPrefix}-selection`;\n this.activeKey = `${config.keyPrefix}-active`;\n this.userDisconnectedKey = `${config.keyPrefix}-user-disconnected`;\n\n if (config.persistent && config.session) {\n this.persistent = config.persistent;\n this.session = config.session;\n } else {\n const defaults = createBrowserStorageDriver();\n this.persistent = config.persistent ?? defaults.persistent;\n this.session = config.session ?? defaults.session;\n }\n }\n\n // ---- Pool ----\n\n async getPool(): Promise<StoredPoolRecord> {\n try {\n const stored = await this.persistent.getItem(this.poolKey);\n if (!stored) {\n return {};\n }\n const parsed: unknown = JSON.parse(stored);\n if (!parsed || typeof parsed !== \"object\") {\n await this.clearPool();\n return {};\n }\n const result: StoredPoolRecord = {};\n for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {\n if (isValidPoolEntry(key, value)) {\n // Default `accounts` to `[account]` for legacy entries that\n // predate the multi-account field.\n result[key] = {\n ...value,\n accounts: value.accounts ?? [value.account],\n };\n } else {\n logWarn(`[butr] dropping invalid pool entry for ${key}`);\n }\n }\n return result;\n } catch (error) {\n logWarn(\"[butr] failed to parse pool from storage:\", error);\n await this.clearPool();\n return {};\n }\n }\n\n async setPool(pool: Map<string, ConnectedWallet>): Promise<void> {\n try {\n const serializable: StoredPoolRecord = {};\n for (const [connectorId, wallet] of pool) {\n const entry: StoredPoolEntry = {\n account: wallet.account,\n accounts: wallet.accounts,\n chainPlatform: wallet.connector.chainPlatform,\n connectorId,\n };\n // The runtime's reducer state is the source of truth — a\n // malformed entry here means a programming error inside butr,\n // not data drift. Throw loudly so the bug surfaces at the\n // write site rather than silently corrupting storage and\n // re-emerging as a \"wallet didn't restore\" puzzle on the next\n // page load.\n if (!isValidPoolEntry(connectorId, entry)) {\n throw new Error(`[butr] refusing to persist invalid pool entry for ${connectorId}`);\n }\n serializable[connectorId] = entry;\n }\n await this.persistent.setItem(this.poolKey, JSON.stringify(serializable));\n } catch (error) {\n logWarn(\"[butr] failed to persist pool:\", error);\n }\n }\n\n async removePoolEntry(connectorId: string): Promise<void> {\n try {\n const stored = await this.getPool();\n if (stored[connectorId]) {\n const { [connectorId]: _, ...remaining } = stored;\n await this.persistent.setItem(this.poolKey, JSON.stringify(remaining));\n }\n } catch (error) {\n logWarn(`[butr] failed to remove pool entry ${connectorId}:`, error);\n }\n }\n\n async clearPool(): Promise<void> {\n await this.persistent.removeItem(this.poolKey);\n }\n\n // ---- Selection ----\n\n async getSelection(): Promise<StoredSelectionRecord> {\n try {\n const stored = await this.persistent.getItem(this.selectionKey);\n if (!stored) {\n return {};\n }\n const parsed: unknown = JSON.parse(stored);\n if (!parsed || typeof parsed !== \"object\") {\n return {};\n }\n const result: StoredSelectionRecord = {};\n for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {\n if (\n VALID_CHAIN_PLATFORMS.has(key as ChainPlatform) &&\n typeof value === \"string\" &&\n value.length > 0\n ) {\n result[key as ChainPlatform] = value;\n }\n }\n return result;\n } catch (error) {\n logWarn(\"[butr] failed to parse selection from storage:\", error);\n return {};\n }\n }\n\n async setSelection(selection: Map<ChainPlatform, string>): Promise<void> {\n try {\n const serializable: StoredSelectionRecord = {};\n for (const [platform, connectorId] of selection) {\n serializable[platform] = connectorId;\n }\n await this.persistent.setItem(this.selectionKey, JSON.stringify(serializable));\n } catch (error) {\n logWarn(\"[butr] failed to persist selection:\", error);\n }\n }\n\n // ---- Active connector ----\n\n async getActiveConnectorId(): Promise<string | null> {\n try {\n const value = await this.persistent.getItem(this.activeKey);\n return value && value.length > 0 ? value : null;\n } catch {\n return null;\n }\n }\n\n async setActiveConnectorId(connectorId: string | null): Promise<void> {\n try {\n await (connectorId === null\n ? this.persistent.removeItem(this.activeKey)\n : this.persistent.setItem(this.activeKey, connectorId));\n } catch (error) {\n logWarn(\"[butr] failed to persist active connector id:\", error);\n }\n }\n\n // ---- Bulk clear ----\n\n async clearAll(): Promise<void> {\n // Three independent removeItem calls — run them concurrently\n // instead of awaiting in sequence. Drivers with async I/O\n // (AsyncStorage, MMKV, IndexedDB-wrapped) benefit; sync drivers\n // (localStorage, memory) just collapse to immediate resolves.\n await Promise.all([\n this.persistent.removeItem(this.poolKey),\n this.persistent.removeItem(this.selectionKey),\n this.persistent.removeItem(this.activeKey),\n ]);\n }\n\n // ---- Disconnect intent ----\n\n /**\n * Disconnect-intent tracking.\n *\n * Lives in the session driver: survives component remounts (unlike refs)\n * but clears when the session ends (unlike the persistent driver).\n * Prevents auto-connect from firing immediately after a manual disconnect,\n * while still allowing auto-connect on fresh sessions.\n */\n async isUserDisconnected(): Promise<boolean> {\n try {\n const value = await this.session.getItem(this.userDisconnectedKey);\n return value === \"true\";\n } catch {\n return false;\n }\n }\n\n async markUserDisconnected(value: boolean): Promise<void> {\n try {\n await (value\n ? this.session.setItem(this.userDisconnectedKey, \"true\")\n : this.session.removeItem(this.userDisconnectedKey));\n } catch {\n // ignore\n }\n }\n}\n\nexport { WalletStorage };\n","import { logWarn } from \"../logger\";\nimport type { Account, ChainPlatform, Connector } from \"../types\";\n\n/**\n * Side-effect callbacks the lifecycle bridge invokes when the\n * underlying wallet emits an event. The bridge owns event-to-action\n * mapping and the unsubscribe handle; the runtime owns what those\n * actions actually do (dispatch reducer events, persist, notify\n * consumer callbacks).\n */\ntype LifecycleHandlers = {\n /** Wallet exposed a new accounts list (or active account swap). The\n * bridge forwards the full array — single-account wallets included.\n * `active` is the address the wallet picked as current. */\n onAccountChanged: (\n connectorId: string,\n accounts: ReadonlyArray<Account>,\n active: Account,\n ) => void;\n /** Wallet disconnected externally (user locked, extension removed,\n * WC session ended on the relay). The bridge has already cleared\n * the subscription for `connectorId` by the time this fires. */\n onDisconnected: (connectorId: string, chainPlatform: ChainPlatform) => void;\n};\n\n/**\n * Owns the \"exactly one subscription per connector\" invariant plus the\n * event-to-handler choreography. Replaces three previously-scattered\n * call sites in the runtime (`hydrate`, `tryRestoreFromPending`,\n * `connectWallet`) and the matching teardown sites\n * (`disconnectWallet`, `reset`, the disconnect-event handler).\n */\ntype ConnectorLifecycle = {\n /**\n * Subscribe to the connector's events. Idempotent — calling `attach`\n * twice for the same `connectorId` detaches the previous subscription\n * before installing the new one, preserving the \"exactly one\"\n * invariant. No-op for connectors without a `subscribe` method.\n */\n attach: (connectorId: string, connector: Connector) => void;\n /** Detach a single connector. Safe to call when no subscription is\n * registered. */\n detach: (connectorId: string) => void;\n /** Detach every active subscription. Used during `reset`. */\n detachAll: () => void;\n};\n\nconst createConnectorLifecycle = (handlers: LifecycleHandlers): ConnectorLifecycle => {\n // Private to the bridge — the runtime never reads or writes this Map.\n const unsubscribers = new Map<string, () => void>();\n\n const detach = (connectorId: string) => {\n const unsub = unsubscribers.get(connectorId);\n if (!unsub) {\n return;\n }\n try {\n unsub();\n } catch (error: unknown) {\n logWarn(\"[butr] unsubscribe threw:\", error);\n }\n unsubscribers.delete(connectorId);\n };\n\n return {\n attach: (connectorId, connector) => {\n if (!connector.subscribe) {\n return;\n }\n // Idempotent install — drop any prior subscription first so a\n // re-attach doesn't leak listeners on the wallet side.\n detach(connectorId);\n try {\n const unsub = connector.subscribe((event) => {\n switch (event.type) {\n case \"accountChanged\": {\n handlers.onAccountChanged(connectorId, event.accounts, event.account);\n break;\n }\n case \"disconnected\": {\n // Clear the subscription before notifying so the handler\n // sees a clean state and can't re-enter via dispatch\n // chains.\n detach(connectorId);\n handlers.onDisconnected(connectorId, connector.chainPlatform);\n break;\n }\n default: {\n const exhaustiveCheck: never = event;\n void exhaustiveCheck;\n }\n }\n });\n unsubscribers.set(connectorId, unsub);\n } catch (error: unknown) {\n logWarn(`[butr] subscribe failed for ${connectorId}:`, error);\n }\n },\n\n detach,\n\n detachAll: () => {\n // Snapshot keys before iterating because `detach` mutates the Map.\n const ids: Array<string> = [];\n for (const id of unsubscribers.keys()) {\n ids.push(id);\n }\n for (const id of ids) {\n detach(id);\n }\n },\n };\n};\n\nexport type { ConnectorLifecycle, LifecycleHandlers };\nexport { createConnectorLifecycle };\n","import type { StoredPoolEntry, WalletPersistence } from \"../storage\";\nimport type {\n Account,\n ChainPlatform,\n ConnectedWallet,\n Connector,\n WalletManagerConfig,\n} from \"../types\";\n\n/**\n * Restore outcome for a single pool entry. `kind: \"ok\"` carries the\n * fully-formed `ConnectedWallet`. `kind: \"fail\"` carries the raw error\n * so the caller can route it to telemetry.\n */\ntype RestoreOutcome =\n | { connectorId: string; entry: ConnectedWallet; kind: \"ok\" }\n | { connectorId: string; error: unknown; kind: \"fail\" };\n\n/**\n * Snapshot returned by `HydrationCoordinator.hydrate()` — everything\n * the reducer needs to compose a `HYDRATED` event plus the metadata the\n * runtime uses to wire subscriptions and surface the outcome.\n */\ntype HydrateResult = {\n activeConnectorId: string | null;\n /** Entries whose restore actually failed (connect() rejected, the\n * connector threw mid-flight). The coordinator has already removed\n * these from storage so a reload won't re-try the same broken entry. */\n dropped: Array<{ connectorId: string; reason: unknown }>;\n isUserDisconnected: boolean;\n /** Entries parked because `createConnector(id)` returned `null` at\n * hydration time (typical for Wallet Standard adapters announced\n * asynchronously). The coordinator keeps an internal record of these\n * and resolves them when the runtime later calls `drainPending`. */\n pendingIds: Array<string>;\n pool: Map<string, ConnectedWallet>;\n selection: Map<ChainPlatform, string>;\n};\n\ntype HydrationCoordinator = {\n /**\n * Restore a parked entry whose adapter has now become available.\n * Returns `null` if no entry is parked under `connectorId`. Returns an\n * `ok` outcome with the new `ConnectedWallet`, or a `fail` outcome\n * with the error (storage cleanup is handled internally).\n */\n drainPending(connectorId: string): Promise<RestoreOutcome | null>;\n /**\n * Read storage, instantiate every adapter that's currently\n * registered, restore in parallel, park the rest. After the returned\n * promise resolves, the coordinator holds an internal pending queue\n * indexed by `connectorId`. Resolve queued entries via `drainPending`.\n */\n hydrate(): Promise<HydrateResult>;\n /**\n * Snapshot of currently-parked ids. The runtime uses this once\n * immediately after `hydrate()` resolves to drain entries whose\n * adapter announced *during* the hydration window (the discovery race\n * we used to resolve manually inside the store).\n */\n pendingIds(): Array<string>;\n};\n\ntype StorageErrorReporter = (context: string, error: unknown) => void;\n\n/** Pick the right `accounts` list: fresh from the connector if it\n * exposes `getAccounts`, otherwise the persisted list, otherwise just\n * the active account. */\nconst resolveHydratedAccounts = async (\n connector: Connector,\n storedAccounts: ReadonlyArray<Account>,\n fallbackAccount: Account,\n): Promise<Array<Account>> => {\n if (connector.getAccounts) {\n const fresh = await connector.getAccounts().catch(() => null);\n if (fresh && fresh.length > 0) {\n return fresh;\n }\n }\n if (storedAccounts.length > 0) {\n return [...storedAccounts];\n }\n return [fallbackAccount];\n};\n\nconst restoreOneEntry = async (\n connectorId: string,\n entry: { account: Account; accounts: ReadonlyArray<Account> },\n connector: Connector,\n): Promise<RestoreOutcome> => {\n try {\n // Eager restore: ask the wallet to reconnect to already-authorized\n // accounts without prompting (Wallet Standard `silent`, EIP-1193\n // `eth_accounts`). A wallet that can't do this silently rejects, and\n // the entry is dropped as a clean restore failure rather than the\n // user being surprised by a connect popup on page load.\n await connector.connect({ silent: true });\n const freshAccount = await connector.getAccount();\n const accountToUse = freshAccount || entry.account;\n const accounts = await resolveHydratedAccounts(connector, entry.accounts, accountToUse);\n return {\n connectorId,\n entry: {\n account: accountToUse,\n accounts,\n connector: connector as ConnectedWallet[\"connector\"],\n },\n kind: \"ok\",\n };\n } catch (error) {\n return { connectorId, error, kind: \"fail\" };\n }\n};\n\nconst createHydrationCoordinator = (\n storage: WalletPersistence,\n createConnector: WalletManagerConfig[\"createConnector\"],\n reportStorageError: StorageErrorReporter,\n): HydrationCoordinator => {\n // Private to the coordinator — the runtime never touches the queue\n // directly. Storing the raw `StoredPoolEntry` is enough; the entry\n // gets re-restored through the same `restoreOneEntry` path used by\n // the eager pass, so the two code paths can't drift.\n const pending = new Map<string, StoredPoolEntry>();\n\n const removeBrokenEntry = (connectorId: string) => {\n void (async () => {\n try {\n await storage.removePoolEntry(connectorId);\n } catch (error: unknown) {\n reportStorageError(\"failed to remove broken entry\", error);\n }\n })();\n };\n\n return {\n drainPending: async (connectorId) => {\n const entry = pending.get(connectorId);\n if (!entry) {\n return null;\n }\n const connector = createConnector(connectorId);\n if (!connector) {\n // Adapter still not available — leave the entry parked.\n return null;\n }\n const outcome = await restoreOneEntry(connectorId, entry, connector);\n pending.delete(connectorId);\n if (outcome.kind === \"fail\") {\n removeBrokenEntry(connectorId);\n }\n return outcome;\n },\n\n hydrate: async () => {\n const [storedPool, storedSelection, storedActive, userDisconnected] = await Promise.all([\n storage.getPool(),\n storage.getSelection(),\n storage.getActiveConnectorId(),\n storage.isUserDisconnected(),\n ]);\n\n const pool = new Map<string, ConnectedWallet>();\n const dropped: Array<{ connectorId: string; reason: unknown }> = [];\n\n // Clear pending from any prior hydrate() in the same coordinator\n // instance (e.g. tests that rehydrate). The runtime only calls\n // hydrate() once per provider mount, but defending against the\n // multi-call case is cheap.\n pending.clear();\n\n // Build the restore-task list eagerly. A null factory result\n // parks the entry; an instantiated connector enters the parallel\n // restore pool.\n const tasks: Array<Promise<RestoreOutcome>> = [];\n for (const [connectorId, entry] of Object.entries(storedPool)) {\n if (!entry) {\n continue;\n }\n const connector = createConnector(connectorId);\n if (!connector) {\n pending.set(connectorId, entry);\n continue;\n }\n tasks.push(restoreOneEntry(connectorId, entry, connector));\n }\n\n const outcomes = await Promise.all(tasks);\n for (const outcome of outcomes) {\n if (outcome.kind === \"ok\") {\n pool.set(outcome.connectorId, outcome.entry);\n continue;\n }\n dropped.push({ connectorId: outcome.connectorId, reason: outcome.error });\n removeBrokenEntry(outcome.connectorId);\n }\n\n // Reconcile selection: drop stale entries, fill platforms with\n // a pool member but no stored selection.\n const selection = new Map<ChainPlatform, string>();\n for (const [platform, connectorId] of Object.entries(storedSelection)) {\n if (connectorId && pool.has(connectorId)) {\n selection.set(platform as ChainPlatform, connectorId);\n }\n }\n for (const [connectorId, wallet] of pool) {\n const platform = wallet.connector.chainPlatform;\n if (!selection.has(platform)) {\n selection.set(platform, connectorId);\n }\n }\n\n let activeConnectorId: string | null = null;\n if (storedActive && pool.has(storedActive)) {\n activeConnectorId = storedActive;\n } else if (pool.size > 0) {\n activeConnectorId = pool.keys().next().value ?? null;\n }\n\n return {\n activeConnectorId,\n dropped,\n isUserDisconnected: userDisconnected,\n pendingIds: [...pending.keys()],\n pool,\n selection,\n };\n },\n\n pendingIds: () => [...pending.keys()],\n };\n};\n\nexport type { HydrateResult, HydrationCoordinator, RestoreOutcome, StorageErrorReporter };\nexport { createHydrationCoordinator };\n","import type { Account, ChainPlatform, ConnectedWallet } from \"../types\";\nimport type { ConnectionError } from \"../types/errors\";\n\ntype ConnectionStatus = \"idle\" | \"connecting\" | \"success\" | \"error\";\n\ntype State = {\n activeConnectorId: string | null;\n connectingConnectorId: string | null;\n connectionError: ConnectionError | null;\n connectionStatus: ConnectionStatus;\n isHydrated: boolean;\n isUserDisconnected: boolean;\n pool: Map<string, ConnectedWallet>;\n selection: Map<ChainPlatform, string>;\n};\n\n/**\n * Lifecycle events: hydration, full reset, and the explicit\n * user-disconnected flag the runtime sets to suppress eager reconnect.\n */\ntype LifecycleEvent =\n | {\n activeConnectorId: string | null;\n isUserDisconnected: boolean;\n pool: Map<string, ConnectedWallet>;\n selection: Map<ChainPlatform, string>;\n type: \"HYDRATED\";\n }\n | { type: \"RESET\" }\n | { type: \"USER_DISCONNECTED_SET\"; value: boolean };\n\n/**\n * Connection status events: the transitions of the in-flight connect\n * attempt — start, success/failure, status/error reset. None of these\n * mutate the pool directly; `CONNECT_SUCCEEDED` is the bridge to\n * `PoolMutationEvent`.\n */\ntype ConnectionStatusEvent =\n | { connectorId: string; type: \"CONNECT_STARTED\" }\n | { connectorId: string; entry: ConnectedWallet; type: \"CONNECT_SUCCEEDED\" }\n | { error: ConnectionError; type: \"CONNECT_FAILED\" }\n | { error: ConnectionError | null; type: \"ERROR_SET\" }\n | { type: \"STATUS_RESET\" };\n\n/**\n * Pool-mutation events: changes to the set of connected wallets and\n * their per-wallet account lists. `DISCONNECTED` removes an entry;\n * the refresh events update an existing one in-place.\n */\ntype PoolMutationEvent =\n | {\n /** When set, becomes the new active account. Used by paths that\n * know which account should be active (wallet event, manual\n * update). When unset, the reducer preserves the current active\n * if it's still in `accounts`, else picks `accounts[0]`. */\n accounts: Array<Account>;\n active?: Account;\n connectorId: string;\n type: \"ACCOUNTS_REFRESHED\";\n }\n | { connectorId: string; type: \"DISCONNECTED\" }\n | { connectorId: string; type: \"WALLET_REFRESHED\" };\n\n/**\n * Selection events: which connector is active, and which connector\n * serves each chain platform. Pure pointer updates — no pool mutation.\n */\ntype SelectionEvent =\n | { chainPlatform: ChainPlatform; connectorId: string | null; type: \"SELECTION_CHANGED\" }\n | { connectorId: string | null; type: \"ACTIVE_CHANGED\" };\n\n/**\n * The single event type the reducer dispatches on. Grouped above by\n * concern so a reader sees the map before the cases — adding an event\n * means picking the group it belongs to.\n */\ntype Event = ConnectionStatusEvent | LifecycleEvent | PoolMutationEvent | SelectionEvent;\n\nconst initialState: State = {\n activeConnectorId: null,\n connectingConnectorId: null,\n connectionError: null,\n connectionStatus: \"idle\",\n isHydrated: false,\n isUserDisconnected: false,\n pool: new Map(),\n selection: new Map(),\n};\n\n/** Find any connector in the pool serving a given platform. */\nconst findConnectorForPlatform = (\n pool: Map<string, ConnectedWallet>,\n chainPlatform: ChainPlatform,\n): string | undefined => {\n for (const [id, wallet] of pool) {\n if (wallet.connector.chainPlatform === chainPlatform) {\n return id;\n }\n }\n return undefined;\n};\n\n/** Pure reducer: every state transition is expressed as `(state, event) => state`.\n * No I/O, no async, no side effects. Side effects live in the runtime. */\nconst reducer = (state: State, event: Event): State => {\n switch (event.type) {\n case \"HYDRATED\": {\n // Merge instead of replace. CONNECT_SUCCEEDED can fire during the\n // late-restore window — discovery announces an adapter, the runtime\n // calls tryRestoreFromPending, drainPending succeeds, the reducer\n // adds the entry to state.pool — all BEFORE hydrate's outer await\n // resolves and HYDRATED dispatches. A wholesale replace at that\n // point wipes the freshly-restored late entry. Merging keeps both\n // the eager-restored entries (event.pool) and the late-restored\n // entries (state.pool).\n const pool = new Map<string, ConnectedWallet>(state.pool);\n for (const [id, entry] of event.pool) {\n pool.set(id, entry);\n }\n const selection = new Map<ChainPlatform, string>(state.selection);\n for (const [platform, id] of event.selection) {\n selection.set(platform, id);\n }\n // Prefer hydrate's activeConnectorId when valid in the merged\n // pool (matches the pre-reload selection). Fall back to any late-\n // restore-driven active if hydrate's choice didn't restore eagerly.\n let activeConnectorId: string | null = null;\n if (event.activeConnectorId && pool.has(event.activeConnectorId)) {\n activeConnectorId = event.activeConnectorId;\n } else if (state.activeConnectorId && pool.has(state.activeConnectorId)) {\n activeConnectorId = state.activeConnectorId;\n } else if (pool.size > 0) {\n activeConnectorId = pool.keys().next().value ?? null;\n }\n return {\n ...state,\n activeConnectorId,\n isHydrated: true,\n isUserDisconnected: event.isUserDisconnected,\n pool,\n selection,\n };\n }\n\n case \"USER_DISCONNECTED_SET\": {\n return { ...state, isUserDisconnected: event.value };\n }\n\n case \"CONNECT_STARTED\": {\n return {\n ...state,\n connectingConnectorId: event.connectorId,\n connectionError: null,\n connectionStatus: \"connecting\",\n };\n }\n\n case \"CONNECT_SUCCEEDED\": {\n const { connectorId, entry } = event;\n const existing = state.pool.get(connectorId);\n\n // Same connector + same address: only flip the status, keep object refs stable\n // so reactive selectors don't churn.\n if (\n existing &&\n existing.account.walletAddress.toLowerCase() === entry.account.walletAddress.toLowerCase()\n ) {\n return {\n ...state,\n activeConnectorId: connectorId,\n connectingConnectorId: null,\n connectionStatus: \"success\",\n };\n }\n\n const newPool = new Map([...state.pool, [connectorId, entry] as const]);\n const newSelection = new Map([\n ...state.selection,\n [entry.connector.chainPlatform, connectorId] as const,\n ]);\n\n return {\n ...state,\n activeConnectorId: connectorId,\n connectingConnectorId: null,\n connectionStatus: \"success\",\n pool: newPool,\n selection: newSelection,\n };\n }\n\n case \"CONNECT_FAILED\": {\n return {\n ...state,\n connectingConnectorId: null,\n connectionError: event.error,\n connectionStatus: \"error\",\n };\n }\n\n case \"DISCONNECTED\": {\n const wallet = state.pool.get(event.connectorId);\n if (!wallet) {\n return state;\n }\n\n const newPool = new Map(state.pool);\n newPool.delete(event.connectorId);\n\n const newSelection = new Map(state.selection);\n const platform = wallet.connector.chainPlatform;\n if (newSelection.get(platform) === event.connectorId) {\n newSelection.delete(platform);\n const fallback = findConnectorForPlatform(newPool, platform);\n if (fallback) {\n newSelection.set(platform, fallback);\n }\n }\n\n const firstId = newPool.keys().next();\n const activeConnectorId =\n state.activeConnectorId === event.connectorId\n ? (firstId.value ?? null)\n : state.activeConnectorId;\n\n return {\n ...state,\n activeConnectorId,\n pool: newPool,\n selection: newSelection,\n };\n }\n\n case \"ACCOUNTS_REFRESHED\": {\n const wallet = state.pool.get(event.connectorId);\n if (!wallet) {\n return state;\n }\n // Pick the new active address. Three policies in priority order:\n // 1. Explicit `event.active` (wallet event or manual update knows\n // what should be active).\n // 2. Preserve current active if it's still in the new list.\n // 3. Fall back to `accounts[0]` so the pool entry doesn't end up\n // pointing at an address the wallet no longer exposes.\n let nextAccount: Account;\n if (event.active) {\n nextAccount = event.active;\n } else {\n const stillHasCurrent = event.accounts.some(\n (a) =>\n a.walletAddress === wallet.account.walletAddress &&\n a.chain.id === wallet.account.chain.id,\n );\n nextAccount = stillHasCurrent ? wallet.account : (event.accounts[0] ?? wallet.account);\n }\n // No-op short-circuit: when the wallet event echoes the current\n // active and the accounts array hasn't changed shape, skip the\n // Map clone so `useSyncExternalStore` subscribers don't re-render.\n // (Cheap heuristic — equality by reference for `accounts` would\n // miss content changes; we compare by length + address set.)\n const sameAccount =\n nextAccount.walletAddress === wallet.account.walletAddress &&\n nextAccount.chain.id === wallet.account.chain.id;\n const sameAccountsShape =\n event.accounts.length === wallet.accounts.length &&\n event.accounts.every((a, i) => a.walletAddress === wallet.accounts[i]?.walletAddress);\n if (sameAccount && sameAccountsShape) {\n return state;\n }\n const updated: ConnectedWallet = {\n ...wallet,\n account: nextAccount,\n accounts: [...event.accounts],\n };\n const newPool = new Map([...state.pool, [event.connectorId, updated] as const]);\n return { ...state, pool: newPool };\n }\n\n case \"WALLET_REFRESHED\": {\n const wallet = state.pool.get(event.connectorId);\n if (!wallet) {\n return state;\n }\n const newPool = new Map([...state.pool, [event.connectorId, { ...wallet }] as const]);\n return { ...state, pool: newPool };\n }\n\n case \"ACTIVE_CHANGED\": {\n return { ...state, activeConnectorId: event.connectorId };\n }\n\n case \"SELECTION_CHANGED\": {\n const newSelection = new Map(state.selection);\n if (event.connectorId === null) {\n newSelection.delete(event.chainPlatform);\n } else {\n newSelection.set(event.chainPlatform, event.connectorId);\n }\n return { ...state, selection: newSelection };\n }\n\n case \"STATUS_RESET\": {\n return {\n ...state,\n connectingConnectorId: null,\n connectionError: null,\n connectionStatus: \"idle\",\n };\n }\n\n case \"ERROR_SET\": {\n return {\n ...state,\n connectionError: event.error,\n connectionStatus: event.error ? \"error\" : \"idle\",\n };\n }\n\n case \"RESET\": {\n return {\n ...initialState,\n isHydrated: state.isHydrated,\n isUserDisconnected: true,\n };\n }\n\n default: {\n // Exhaustiveness check — TS errors if a new event type is added without a case.\n const exhaustiveCheck: never = event;\n void exhaustiveCheck;\n return state;\n }\n }\n};\n\nexport type {\n ConnectionStatus,\n ConnectionStatusEvent,\n Event,\n LifecycleEvent,\n PoolMutationEvent,\n SelectionEvent,\n State,\n};\nexport { initialState, reducer };\n","/** Run an async operation fire-and-forget, calling onError if it throws. */\nconst run = async (fn: () => Promise<void>, onError: (e: unknown) => void): Promise<void> => {\n try {\n await fn();\n } catch (error) {\n onError(error);\n }\n};\n\nexport { run };\n","import { createStore } from \"zustand/vanilla\";\n\nimport { logError, logWarn } from \"../logger\";\nimport { WalletStorage } from \"../storage\";\nimport type { WalletPersistence } from \"../storage\";\nimport type { Account, ChainPlatform, ConnectedWallet, WalletManagerConfig } from \"../types\";\nimport type { ConnectionError } from \"../types/errors\";\nimport { mapConnectionError } from \"../types/errors\";\n\nimport { createConnectorLifecycle } from \"./connector-lifecycle\";\nimport { createHydrationCoordinator } from \"./hydration\";\nimport { type Event, type State, initialState, reducer } from \"./reducer\";\nimport { run } from \"./wallet-store-helpers\";\n\nconst CONNECT_TIMEOUT_MS = 90_000;\nconst DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5000;\n\nconst normaliseAddress = (addr: string): string => addr.toLowerCase();\n\ntype ExtractState<S> = S extends { getState: () => infer T } ? T : never;\n\ntype RuntimeMembers = {\n _config: WalletManagerConfig;\n _storage: WalletPersistence;\n connectWallet: (\n connectorId: string,\n onSuccess?: (wallet: ConnectedWallet) => void,\n onError?: (error: Error) => void,\n ) => Promise<void>;\n disconnectWallet: (connectorId: string) => void;\n getConnectorInstance: (id: string) => ReturnType<WalletManagerConfig[\"createConnector\"]>;\n hydrateWallets: () => Promise<void>;\n refreshWallet: (connectorId: string) => void;\n requestAccounts: (connectorId: string) => Promise<void>;\n reset: () => void;\n resetConnectionStatus: () => void;\n setActiveConnector: (connectorId: string | null) => void;\n setConnectionError: (error: ConnectionError | null) => void;\n setSelection: (chainPlatform: ChainPlatform, connectorId: string | null) => void;\n setUserDisconnected: (value: boolean) => void;\n tryRestoreFromPending: (connectorId: string) => Promise<void>;\n updateWalletAccount: (connectorId: string, account: Account) => void;\n};\n\ntype WalletStore = ReturnType<typeof createWalletStore>;\ntype WalletStoreState = ExtractState<WalletStore>;\n\nconst createWalletStore = (config: WalletManagerConfig) => {\n const storageKeyPrefix = config.storageKeyPrefix || \"butr\";\n const storage = config.storage || new WalletStorage({ keyPrefix: storageKeyPrefix });\n\n // Storage-error handler. Default: `console.warn` (preserves the\n // pre-`onStorageError` behaviour). Consumer-provided callback routes\n // failures through their telemetry pipeline.\n const reportStorageError = (context: string) => (error: unknown) => {\n if (config.onStorageError) {\n try {\n config.onStorageError(error, context);\n } catch (cbError: unknown) {\n logWarn(\"[butr] onStorageError threw:\", cbError);\n }\n return;\n }\n logWarn(`[butr] ${context}:`, error);\n };\n\n // Owns the full restore lifecycle: read storage, instantiate adapters,\n // restore in parallel, park entries whose adapter wasn't registered\n // yet, drain parked entries when discovery announces a matching id.\n // `pendingRestores` lives inside the coordinator — the runtime no\n // longer manages the queue directly.\n const hydration = createHydrationCoordinator(storage, config.createConnector, (context, error) =>\n reportStorageError(context)(error),\n );\n\n return createStore<State & RuntimeMembers>()((set, get) => {\n const dispatch = (event: Event) => {\n set((prev) => reducer(prev, event), false);\n };\n\n // Fire-and-forget storage writes. Each persister is independent\n // — `persistPool` only writes the pool, `persistSelection` only\n // writes the selection, etc. Some events update one slot\n // (`SELECTION_CHANGED` → `persistSelection`), others update all\n // three (`CONNECT_SUCCEEDED`, hydration). When multiple\n // persisters fire from the same call site, they race —\n // intentionally. Each storage key is durable on its own; the\n // order of resolution doesn't affect correctness. Don't\n // collapse these into a single `persistAll()` helper without\n // first reading the call sites — many of them only need one.\n const persistPool = () => {\n void run(() => storage.setPool(get().pool), reportStorageError(\"failed to persist pool\"));\n };\n\n // Single entry point for \"the pool entry's exposed accounts\n // changed.\" Three call sites use it: the wallet-event bridge\n // (`accountChanged`), the consumer-facing `updateWalletAccount`\n // action, and the `requestAccounts` action. `active` is set when\n // the caller knows which address is now active (wallet event,\n // manual update); omitted when the caller just refreshed the\n // list and wants the reducer's preserve-or-fallback logic.\n const refreshPoolEntry = (connectorId: string, accounts: Array<Account>, active?: Account) => {\n dispatch({ accounts, active, connectorId, type: \"ACCOUNTS_REFRESHED\" });\n persistPool();\n };\n\n const persistSelection = () => {\n void run(\n () => storage.setSelection(get().selection),\n reportStorageError(\"failed to persist selection\"),\n );\n };\n\n const persistActive = () => {\n void run(\n () => storage.setActiveConnectorId(get().activeConnectorId),\n reportStorageError(\"failed to persist active connector\"),\n );\n };\n\n // Owns the \"exactly one subscription per connector\" invariant,\n // the event-to-handler choreography, and the unsubscribers Map.\n // Attach is idempotent: the runtime calls it after every\n // restore / connect without first checking whether a prior\n // subscription exists.\n const lifecycle = createConnectorLifecycle({\n onAccountChanged: (connectorId, accounts, active) => {\n // Full-replace the accounts array so the pool entry mirrors\n // the wallet's current exposure. Drops the previously-active\n // address on single-account wallets (Phantom EVM/SVM,\n // MetaMask Snap); preserves the multi-account list on\n // MetaMask, Rabby, etc. The wallet tells us which is active.\n refreshPoolEntry(connectorId, [...accounts], active);\n },\n onDisconnected: (connectorId, chainPlatform) => {\n // Mirror an external disconnect into the reducer. Don't call\n // connector.disconnect — the wallet already did the work; the\n // lifecycle bridge has already cleared the subscription. Just\n // dispatch, persist, notify.\n dispatch({ connectorId, type: \"DISCONNECTED\" });\n if (get().pool.size === 0) {\n void run(() => storage.clearAll(), reportStorageError(\"failed to clear storage\"));\n } else {\n persistPool();\n persistSelection();\n persistActive();\n }\n config.onDisconnect?.(chainPlatform);\n },\n });\n\n return {\n ...initialState,\n _config: config,\n _storage: storage,\n connectWallet: async (connectorId, onSuccess, onError) => {\n get().setUserDisconnected(false);\n\n const connector = config.createConnector(connectorId);\n if (!connector) {\n throw new Error(`Failed to create connector for ${connectorId}`);\n }\n\n dispatch({ connectorId, type: \"CONNECT_STARTED\" });\n\n // Slow-connect telemetry. Fires once if the wallet doesn't\n // resolve within the configured threshold. Cleared in finally.\n const slowThreshold = config.slowConnectThresholdMs ?? DEFAULT_SLOW_CONNECT_THRESHOLD_MS;\n const slowTimer = config.onSlowConnect\n ? setTimeout(() => {\n try {\n config.onSlowConnect?.(connectorId);\n } catch (cbError: unknown) {\n logWarn(\"[butr] onSlowConnect threw:\", cbError);\n }\n }, slowThreshold)\n : null;\n\n try {\n const connectPromise = connector.connect();\n // oxlint-disable-next-line promise/prefer-await-to-then -- suppress unhandled rejection; we await via Promise.race below\n void connectPromise.catch(() => {});\n await Promise.race([\n connectPromise,\n new Promise((_, reject) => {\n setTimeout(() => {\n reject(new Error(\"Connection timeout\"));\n }, CONNECT_TIMEOUT_MS);\n }),\n ]);\n\n const account = await connector.getAccount();\n if (!account) {\n throw new Error(\"Failed to get account\");\n }\n\n // Pull all known accounts when the connector supports it.\n const fetchedAccounts = connector.getAccounts\n ? await connector.getAccounts().catch(() => null)\n : null;\n const accounts =\n fetchedAccounts && fetchedAccounts.length > 0 ? fetchedAccounts : [account];\n\n const entry: ConnectedWallet = { account, accounts, connector };\n dispatch({ connectorId, entry, type: \"CONNECT_SUCCEEDED\" });\n\n lifecycle.attach(connectorId, connector);\n\n persistPool();\n persistSelection();\n persistActive();\n\n config.onConnect?.(entry);\n onSuccess?.(entry);\n } catch (error) {\n const normalised = mapConnectionError(error);\n dispatch({ error: normalised, type: \"CONNECT_FAILED\" });\n try {\n await connector.disconnect?.();\n } catch (disconnectError: unknown) {\n logWarn(\"[butr] disconnect during error recovery failed:\", disconnectError);\n }\n try {\n config.onConnectError?.(normalised, connectorId);\n } catch (cbError: unknown) {\n logWarn(\"[butr] onConnectError threw:\", cbError);\n }\n onError?.(error as Error);\n throw error;\n } finally {\n if (slowTimer) {\n clearTimeout(slowTimer);\n }\n }\n },\n\n disconnectWallet: (connectorId) => {\n get().setUserDisconnected(true);\n\n const state = get();\n if (!state.isHydrated) {\n return;\n }\n const wallet = state.pool.get(connectorId);\n if (!wallet) {\n return;\n }\n\n lifecycle.detach(connectorId);\n\n void run(() => wallet.connector.disconnect?.() ?? Promise.resolve(), logError);\n\n const platform = wallet.connector.chainPlatform;\n dispatch({ connectorId, type: \"DISCONNECTED\" });\n\n if (get().pool.size === 0) {\n void run(() => storage.clearAll(), reportStorageError(\"failed to clear storage\"));\n } else {\n persistPool();\n persistSelection();\n persistActive();\n }\n\n config.onDisconnect?.(platform);\n },\n\n getConnectorInstance: (id) => config.createConnector(id),\n\n hydrateWallets: async () => {\n const result = await hydration.hydrate();\n dispatch({\n activeConnectorId: result.activeConnectorId,\n isUserDisconnected: result.isUserDisconnected,\n pool: result.pool,\n selection: result.selection,\n type: \"HYDRATED\",\n });\n // Wire up wallet-side event subscriptions for every restored\n // connector so account/chain swaps after a refresh keep the\n // reducer in sync without consumer effort.\n for (const [connectorId, wallet] of result.pool) {\n lifecycle.attach(connectorId, wallet.connector);\n }\n // Persist any reconciled values back so future loads stay consistent.\n persistSelection();\n persistActive();\n // Drain: catch the race where a Wallet Standard adapter\n // announced BEFORE hydration finished populating its queue.\n // Without this, the discovery callback's\n // `tryRestoreFromPending(adapter.id)` would have hit the\n // empty queue and silently returned, leaving SVM wallets\n // un-restored on reload. The coordinator's `pendingIds()`\n // is the post-hydrate snapshot; each id resolves\n // independently. Late-restore failures surface per-entry\n // inside `tryRestoreFromPending`.\n void Promise.all(hydration.pendingIds().map((id) => get().tryRestoreFromPending(id)));\n // Surface the hydration outcome. Three buckets: restored,\n // pending (waiting for adapter announcement), dropped\n // (genuine restore failures). The default `console.warn`\n // for dropped entries is replaced by this callback when\n // set — consumers route to telemetry / UX hints.\n if (config.onHydrated) {\n try {\n config.onHydrated({\n dropped: result.dropped,\n pendingIds: [...result.pendingIds],\n restoredIds: [...result.pool.keys()],\n });\n } catch (cbError: unknown) {\n logWarn(\"[butr] onHydrated threw:\", cbError);\n }\n } else if (result.dropped.length > 0) {\n // Preserve the pre-`onHydrated` console.warn behaviour\n // when no callback is set, so silent drops still log.\n for (const { connectorId, reason } of result.dropped) {\n logWarn(`[butr] failed to restore connector ${connectorId}:`, reason);\n }\n }\n },\n refreshWallet: (connectorId) => {\n dispatch({ connectorId, type: \"WALLET_REFRESHED\" });\n },\n\n requestAccounts: async (connectorId) => {\n const entry = get().pool.get(connectorId);\n if (!entry) {\n return;\n }\n // Ask the wallet to open its picker (if supported). EVM\n // wallets honour this via `wallet_requestPermissions`;\n // Wallet Standard wallets typically have no equivalent\n // RPC, so the call resolves immediately and the refresh\n // below simply re-reads the current list.\n await entry.connector.requestAccounts?.();\n const fresh = entry.connector.getAccounts\n ? await entry.connector.getAccounts().catch(() => null)\n : null;\n if (!fresh || fresh.length === 0) {\n return;\n }\n // No `active` — the reducer preserves the current active\n // if it's still in the refreshed list, else picks [0].\n refreshPoolEntry(connectorId, fresh);\n },\n\n reset: () => {\n get().setUserDisconnected(true);\n\n const state = get();\n if (!state.isHydrated) {\n return;\n }\n\n lifecycle.detachAll();\n\n for (const wallet of state.pool.values()) {\n void run(() => wallet.connector.disconnect?.() ?? Promise.resolve(), logError);\n }\n\n void run(() => storage.clearAll(), reportStorageError(\"failed to clear storage\"));\n\n if (config.onReset) {\n const onReset = config.onReset;\n void run(() => Promise.resolve(onReset()), logError);\n }\n\n dispatch({ type: \"RESET\" });\n },\n\n resetConnectionStatus: () => {\n dispatch({ type: \"STATUS_RESET\" });\n },\n\n setActiveConnector: (connectorId) => {\n dispatch({ connectorId, type: \"ACTIVE_CHANGED\" });\n persistActive();\n },\n\n setConnectionError: (error) => {\n dispatch({ error, type: \"ERROR_SET\" });\n },\n\n setSelection: (chainPlatform, connectorId) => {\n dispatch({ chainPlatform, connectorId, type: \"SELECTION_CHANGED\" });\n persistSelection();\n },\n\n setUserDisconnected: (value: boolean) => {\n dispatch({ type: \"USER_DISCONNECTED_SET\", value });\n void run(\n () => storage.markUserDisconnected(value),\n reportStorageError(\"failed to persist disconnect intent\"),\n );\n },\n\n tryRestoreFromPending: async (connectorId) => {\n // Note: deliberately NOT gating on `isUserDisconnected`.\n // The eager hydration path doesn't check that flag either —\n // stored entries restore on reload regardless of whether\n // the user's most recent action was a disconnect. The\n // late-restore path mirrors that policy for symmetry.\n const outcome = await hydration.drainPending(connectorId);\n if (!outcome) {\n return;\n }\n if (outcome.kind === \"fail\") {\n logWarn(`[butr] late restore failed for ${connectorId}:`, outcome.error);\n return;\n }\n // Reuse the reducer's connect-success path so selection /\n // active reconciliation matches a normal connect flow.\n dispatch({ connectorId, entry: outcome.entry, type: \"CONNECT_SUCCEEDED\" });\n lifecycle.attach(connectorId, outcome.entry.connector);\n persistPool();\n persistSelection();\n persistActive();\n config.onConnect?.(outcome.entry);\n },\n\n updateWalletAccount: (connectorId, account) => {\n // Manual variant of the wallet-event path. Builds the\n // next accounts list (dedupe-by-address + chain refresh\n // since chain is wallet-level) and routes through\n // ACCOUNTS_REFRESHED so there's one reducer case for\n // \"the wallet's exposure changed.\"\n const entry = get().pool.get(connectorId);\n if (!entry) {\n return;\n }\n const newChain = account.chain;\n const remapped = entry.accounts.map((a) =>\n a.chain.id === newChain.id\n ? a\n : {\n chain: newChain,\n id: `${newChain.id}:${normaliseAddress(a.walletAddress)}`,\n walletAddress: a.walletAddress,\n },\n );\n const seen = remapped.some(\n (a) => normaliseAddress(a.walletAddress) === normaliseAddress(account.walletAddress),\n );\n const accounts = seen ? remapped : [...remapped, account];\n refreshPoolEntry(connectorId, accounts, account);\n },\n };\n });\n};\n\nexport type { WalletStore, WalletStoreState };\nexport { createWalletStore };\n","import type { ConnectedWallet } from \"./types\";\n\n/**\n * Two wallet snapshots are equivalent for selector purposes iff they\n * share connectorId, active account address, and active account chain\n * id. Used by `useStoreWithEqualityFn` consumers (active-wallet,\n * selected-wallet, useWalletEntry) to suppress spurious re-renders\n * when the underlying Map churns but the resolved entry hasn't changed.\n *\n * Hoisted to its own module so the equivalence rule lives in one place\n * — if we ever extend the snapshot (e.g. to consider `accounts.length`),\n * every selector hook picks up the new rule for free.\n */\nconst walletEqual = (a: ConnectedWallet | undefined, b: ConnectedWallet | undefined): boolean => {\n if (a === b) {\n return true;\n }\n if (!a || !b) {\n return false;\n }\n return (\n a.connector.id === b.connector.id &&\n a.account.walletAddress === b.account.walletAddress &&\n a.account.chain.id === b.account.chain.id\n );\n};\n\nexport { walletEqual };\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@usebutr/core",
3
+ "version": "0.1.0",
4
+ "description": "Core types, store, storage, and discovery seam for butr. No React, no protocols.",
5
+ "license": "MIT",
6
+ "author": "Pedro Filho <pedro@filho.me>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pedroapfilho/usebutr.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^6.0.3",
29
+ "vitest": "^4.1.6",
30
+ "zustand": "^5.0.0",
31
+ "@repo/config-vitest": "0.0.0",
32
+ "@repo/typescript-config": "0.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "zustand": ">=4.0.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "clean": "rm -rf dist",
40
+ "dev": "tsdown --watch",
41
+ "lint": "oxlint src",
42
+ "test": "vitest run",
43
+ "test:coverage": "vitest run --coverage",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit"
45
+ }
46
+ }