@usebutr/core 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  //#region src/types/chain.d.ts
2
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.
3
+ * CAIP-2 shaped. butr never reads past these four fields, so consumers
4
+ * can extend the type structurally with logos, explorers and the like.
8
5
  */
9
6
  type ChainBase = {
10
7
  /** CAIP-2 identifier, e.g. "eip155:1", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" */
@@ -24,11 +21,9 @@ type Account = {
24
21
  walletAddress: string;
25
22
  };
26
23
  /**
27
- * Build butr's `Account` shape from a wallet address and a resolved
28
- * `ChainBase`. The composite id (`<chain>:<address>`) is what the
29
- * reducer uses to compare accounts across refreshes, so every adapter
30
- * must build accounts through this helper (or keep the format
31
- * byte-identical).
24
+ * The reducer compares accounts by the composite `<chain>:<address>`
25
+ * id, so every adapter must build accounts here or reproduce that
26
+ * format byte for byte.
32
27
  */
33
28
  declare const buildAccount: (address: string, chain: ChainBase) => Account;
34
29
  type Balance = {
@@ -44,24 +39,9 @@ type Balance = {
44
39
  //#endregion
45
40
  //#region src/types/capabilities.d.ts
46
41
  /**
47
- * Capability flags describing what an adapter can actually do at
48
- * runtime. Populated by each adapter (the auto-built ones derive
49
- * these from the underlying protocol's feature advertisements;
50
- * hand-rolled adapters declare them explicitly). Consumers branch on
51
- * these to gate UI affordances:
52
- *
53
- * ```tsx
54
- * {wallet.connector.capabilities.requestAccounts ? (
55
- * <button onClick={() => requestAccounts(wallet.connector.id)}>
56
- * Request more accounts
57
- * </button>
58
- * ) : null}
59
- * ```
60
- *
61
- * Each flag means "can this work right now," not "is the method
62
- * defined": `signMessage: false` means calling `signMessage()` would
63
- * reject; `switchChain: false` means switching is a no-op regardless
64
- * of which chain is passed.
42
+ * Each flag means "can this work right now", not "is the method
43
+ * defined": `signMessage: false` means the call would reject,
44
+ * `switchChain: false` means switching is a no-op for every chain.
65
45
  */
66
46
  type WalletCapabilities = {
67
47
  /** `getBalance` returns a real on-chain value (vs. a 0n placeholder). */
@@ -100,84 +80,44 @@ type WalletCapabilities = {
100
80
  //#endregion
101
81
  //#region src/types/platform.d.ts
102
82
  /**
103
- * Canonical list of supported chain platforms. The single runtime source
104
- * of truth: `ChainPlatform` is derived from it, and storage validators
105
- * build their allowlists from it, so the type and the runtime checks can
106
- * never drift (a missing platform here was why Polkadot connections failed
107
- * to persist).
83
+ * Single runtime source of truth: `ChainPlatform` and the storage
84
+ * validators' allowlists both derive from it. Omitting a platform here
85
+ * silently stops its connections from persisting.
108
86
  */
109
87
  declare const CHAIN_PLATFORMS: readonly ["evm", "svm", "sui", "bitcoin", "polkadot"];
110
88
  type ChainPlatform = (typeof CHAIN_PLATFORMS)[number];
111
89
  //#endregion
112
90
  //#region src/types/chains-by-platform.d.ts
113
91
  /**
114
- * Map of every chain platform to the list of chains the consumer wants
115
- * to expose to its UI (chain switcher, picker, etc).
116
- *
117
- * The platform key set is fixed by `ChainPlatform`. The value is the
118
- * chain list; empty when the consumer doesn't want to support that
119
- * platform's chains in this view. This is the only type that callers
120
- * write down; the values come from per-platform packages
121
- * (`EVM_CHAINS_LIST`, `SVM_CHAINS_LIST`, etc).
92
+ * Values come from the per-platform packages (`EVM_CHAINS_LIST`,
93
+ * `SVM_CHAINS_LIST`, …); an empty list opts that platform out of the
94
+ * consumer's chain UI.
122
95
  */
123
96
  type ChainsByPlatform = Readonly<Record<ChainPlatform, ReadonlyArray<ChainBase>>>;
124
97
  /**
125
- * Build a fully-populated `ChainsByPlatform` from a partial. Platforms
126
- * the consumer doesn't specify default to an empty list.
127
- *
128
- * Use this in apps that target one or two chain platforms; importing
129
- * only those packages keeps unused chain registries out of the bundle.
130
- * Apps that want every chain reach for `CHAINS_BY_PLATFORM` from
131
- * `@usebutr/wallets` instead.
132
- *
133
- * @example
134
- * // EVM-only app: Solana/Sui/Bitcoin tables never enter the bundle
135
- * import { EVM_CHAINS_LIST } from "@usebutr/evm";
136
- * import { buildChainsByPlatform } from "@usebutr/core";
137
- *
138
- * const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });
139
- *
140
- * @example
141
- * // Multi-chain app: pull from each package the app actually uses
142
- * import { EVM_CHAINS_LIST } from "@usebutr/evm";
143
- * import { SVM_CHAINS_LIST } from "@usebutr/svm";
144
- *
145
- * const chains = buildChainsByPlatform({
146
- * evm: EVM_CHAINS_LIST,
147
- * svm: SVM_CHAINS_LIST,
148
- * });
98
+ * Naming only the platforms an app targets keeps the other packages'
99
+ * chain registries out of its bundle. Apps that want all of them import
100
+ * `CHAINS_BY_PLATFORM` from `@usebutr/wallets`.
149
101
  */
150
- declare const buildChainsByPlatform: (partial: Partial<ChainsByPlatform>) => ChainsByPlatform;
102
+ declare const buildChainsByPlatform: (partial: Partial<ChainsByPlatform>) => {
103
+ bitcoin: readonly ChainBase[];
104
+ evm: readonly ChainBase[];
105
+ polkadot: readonly ChainBase[];
106
+ sui: readonly ChainBase[];
107
+ svm: readonly ChainBase[];
108
+ };
151
109
  //#endregion
152
110
  //#region src/types/connector.d.ts
153
111
  /**
154
- * Whether a wallet is currently usable from the user's environment.
155
- *
156
- * - `installed`: the wallet is available and `connect()` can be called.
157
- * - `loadable`: the wallet's SDK can be loaded on demand (e.g. WalletConnect
158
- * modal that pops a QR code without requiring a browser extension).
159
- * - `not-installed`: the wallet isn't reachable. Consumers typically render
160
- * a "download" affordance pointing at `meta.url`.
112
+ * Gate Connect on `installed` or `loadable`: a `loadable` wallet has no
113
+ * extension yet still connects (WalletConnect's QR modal). Only
114
+ * `not-installed` should degrade to a download link at `meta.url`.
161
115
  */
162
116
  type WalletAvailability = "installed" | "loadable" | "not-installed";
163
117
  /**
164
- * Events a connector can emit while connected. butr's runtime subscribes
165
- * via `Connector.subscribe?` after a successful `connect()` and dispatches
166
- * the equivalent reducer event:
167
- *
168
- * - `accountChanged` carries both the new active `account` AND the full
169
- * `accounts` array the wallet currently exposes. The runtime mirrors
170
- * that list verbatim into the pool entry. This handles two cases
171
- * uniformly:
172
- * - Multi-account wallets (MetaMask, Rabby, Brave): the user adds or
173
- * removes accounts from the dapp's permission set; the array grows
174
- * or shrinks to match.
175
- * - Single-account-exposure wallets (Phantom EVM/SVM, MetaMask Snap):
176
- * only the active account is ever in `accounts`; switching swaps it
177
- * in place rather than appending.
178
- * Also covers chain switches; the new chain lives inside `account.chain`.
179
- * - `disconnected` → `DISCONNECTED` (wallet has gone away externally:
180
- * user locked it, removed the extension, etc.).
118
+ * `accountChanged` must carry every account the wallet still exposes,
119
+ * not just the new active one: the runtime mirrors the array verbatim
120
+ * into the pool entry. Chain switches arrive as `account.chain`.
181
121
  */
182
122
  type ConnectorEvent = {
183
123
  account: Account;
@@ -200,16 +140,10 @@ type Connector<P extends ChainPlatform = ChainPlatform> = {
200
140
  * use one of the per-platform adapter types (`EvmAdapter`,
201
141
  * `SvmAdapter`, etc). */
202
142
  chainPlatform: P;
203
- /** Begin a connection request. Resolves when the wallet is connected,
204
- * rejects on user cancellation or other error.
205
- *
206
- * `opts.silent` requests a non-interactive reconnect to
207
- * already-authorized accounts; butr's mount-time hydration passes it
208
- * so a reload restores wallets without re-prompting (Wallet Standard
209
- * `standard:connect`'s `silent` input; the `eth_accounts` read on
210
- * EIP-1193). Adapters that can't reconnect without a prompt should
211
- * reject when `silent` is set rather than show UI; hydration treats
212
- * the rejection as a clean restore failure. */
143
+ /** `opts.silent` is hydration's non-interactive reconnect (Wallet
144
+ * Standard `standard:connect` silent input, `eth_accounts` on
145
+ * EIP-1193). An adapter that cannot honour it must reject instead of
146
+ * prompting; hydration reads that as a clean restore failure. */
213
147
  connect: (opts?: {
214
148
  silent?: boolean;
215
149
  }) => Promise<void>;
@@ -222,38 +156,23 @@ type Connector<P extends ChainPlatform = ChainPlatform> = {
222
156
  * show many accounts at once (MetaMask with multiple imports). If
223
157
  * omitted, butr defaults to `[await getAccount()]`. */
224
158
  getAccounts?: () => Promise<Array<Account>>;
225
- /** Optional. Wallet logo as a URL or data URI. Adapters built via butr's
226
- * auto-discovery (EIP-6963, Wallet Standard) populate this from the
227
- * wallet's announced metadata; hand-rolled adapters can leave it
228
- * unset and supply icons separately via `ConnectorMeta`.
229
- *
230
- * **Already sanitized on discovered adapters.** Discovery runs
231
- * `sanitizeIcon` at construction, so the value is either a trimmed,
232
- * non-empty string or `undefined`: never blank, never whitespace-led.
233
- * Render it directly, including into strict consumers like
234
- * `next/image`. No second `sanitizeIcon` call and no `icon !== ""`
235
- * guard is needed. Hand-rolled adapters set this field themselves and
236
- * own the guarantee. */
159
+ /** Discovery runs `sanitizeIcon` at construction, so on discovered
160
+ * adapters this is a trimmed non-empty string or `undefined`: render
161
+ * it straight into `next/image` with no re-sanitizing and no
162
+ * `icon !== ""` guard. Hand-rolled adapters own that guarantee. */
237
163
  icon?: string;
238
164
  /** Stable key: "metamask", "phantom", etc. Pool entries are keyed by this. */
239
165
  id: string;
240
166
  /** Human name: "MetaMask", "Phantom", etc. UI-facing only. */
241
167
  name: string;
242
- /** Optional. Ask the wallet to open its account-selection UI so the
243
- * user can expose additional accounts to this app. Implemented on
244
- * EIP-6963 wallets via `wallet_requestPermissions`; Wallet Standard
245
- * wallets generally leave this unset because the user enables more
246
- * accounts directly in the extension. Resolution doesn't include the
247
- * new accounts; call `getAccounts()` (or use butr's
248
- * `useRequestAccounts` hook, which refreshes the pool entry for
249
- * you). */
168
+ /** Opens the wallet's account-selection UI (`wallet_requestPermissions`
169
+ * on EIP-6963; usually unset on Wallet Standard). Resolution does not
170
+ * carry the new accounts: follow it with `getAccounts()`, or use the
171
+ * `useRequestAccounts` hook which refreshes the pool entry. */
250
172
  requestAccounts?: () => Promise<void>;
251
- /** Optional. Subscribe to wallet-side events (account swap, network swap,
252
- * external disconnect). butr's runtime calls this after a successful
253
- * `connect()`, and uses the returned function to unsubscribe on
254
- * disconnect / reset. Bridges native wallet events into the reducer so
255
- * consumers don't have to wire `accountsChanged` / `chainChanged`
256
- * themselves. */
173
+ /** Bridges native wallet events (`accountsChanged`, `chainChanged`, …)
174
+ * into the reducer. Only `ConnectorLifecycle` may call this, and it
175
+ * guarantees at most one live subscription per connector. */
257
176
  subscribe?: (listener: (event: ConnectorEvent) => void) => () => void;
258
177
  };
259
178
  type ConnectorMeta = {
@@ -275,6 +194,17 @@ type ConnectorMeta = {
275
194
  };
276
195
  //#endregion
277
196
  //#region src/types/wallet.d.ts
197
+ type SignInInput = {
198
+ readonly [key: string]: SignInValue;
199
+ };
200
+ type SignInValue = boolean | number | string | null | ReadonlyArray<SignInValue> | SignInInput;
201
+ type TransactionObject = {
202
+ readonly [key: string]: TransactionValue | undefined;
203
+ };
204
+ type TransactionMethod = (...args: ReadonlyArray<never>) => Promise<string>;
205
+ type TransactionValue = bigint | boolean | number | string | null | Uint8Array | ReadonlyArray<TransactionValue> | TransactionObject | TransactionMethod;
206
+ type TransactionInput = TransactionObject | string | Uint8Array;
207
+ type WalletSigner = object;
278
208
  /**
279
209
  * Methods every connected wallet supports regardless of chain. The
280
210
  * per-platform `Wallet` types extend this with their platform-specific
@@ -288,33 +218,24 @@ type WalletBase = {
288
218
  /** Returns a chain-specific signer. Consumers cast to the concrete
289
219
  * type via the `SignerForPlatform` registry (or directly to the
290
220
  * library shape they wrap: `WalletClient` on viem, etc.). */
291
- getSigner: () => Promise<unknown>;
221
+ getSigner: () => Promise<WalletSigner>;
292
222
  /** Look up the status of a previously-submitted transaction. */
293
223
  getTransactionReceipt: (tx: string) => Promise<{
294
224
  status: "Success" | "Error" | "Pending";
295
225
  }>;
296
- /** Submit a transaction on the wallet's currently-active chain.
297
- * Pass an `account` from `ConnectedWallet.accounts` to route the
298
- * transaction through a specific exposed address instead of the
299
- * wallet's currently-active one. EVM wallets honour this via
300
- * `tx.from`; Wallet Standard wallets via the feature's `account`
301
- * input. Omit for "use whichever the wallet picks." */
302
- sendTx: (tx: unknown, account?: Account) => Promise<string>;
226
+ /** `account` routes through a specific exposed address (EVM via
227
+ * `tx.from`, Wallet Standard via the feature's `account` input);
228
+ * omitting it lets the wallet pick. */
229
+ sendTx: (tx: TransactionInput, account?: Account) => Promise<string>;
303
230
  /** Submit a transaction targeting a specific chain. The optional
304
231
  * callback fires after the connector has switched chain (consumers
305
232
  * use this to re-enable UI). Pass an `account` to route through a
306
233
  * specific exposed address (see `sendTx`). */
307
- sendTxToChain: (tx: unknown, targetChainId: string, account?: Account, cb?: () => void) => Promise<string>;
234
+ sendTxToChain: (tx: TransactionInput, targetChainId: string, account?: Account, cb?: () => void) => Promise<string>;
308
235
  /**
309
- * Sign a message and return both the signature and the bytes the wallet
310
- * actually signed. Solana Wallet Standard wallets may prefix or re-encode
311
- * the message internally; verifiers must check the signature against
312
- * `signedMessage`, not the input bytes. EVM wallets echo the input.
313
- *
314
- * Pass an `account` to sign with a specific exposed address. EIP-1193
315
- * routes it through `personal_sign`'s address param; Wallet Standard
316
- * uses the feature's `account` input. Both support per-call signing
317
- * without changing the wallet's active account.
236
+ * Verify against `signedMessage`, not the input: Solana Wallet
237
+ * Standard wallets may prefix or re-encode it. `account` signs with a
238
+ * specific address without changing the wallet's active one.
318
239
  */
319
240
  signMessage: (msg: Uint8Array, account?: Account) => Promise<{
320
241
  signature: Uint8Array;
@@ -327,19 +248,15 @@ type WalletBase = {
327
248
  switchChain: (chain: ChainBase) => Promise<void>;
328
249
  };
329
250
  /**
330
- * EVM wallet surface. No `signIn` (Sign-In-With-Ethereum is an app-level
331
- * concern in this library, not a protocol method). No `signTransaction`:
332
- * EVM wallets sign-and-send via `eth_sendTransaction`; sign-only EVM
333
- * flows aren't exposed through this surface.
251
+ * No `signIn` (SIWE is app-level here, not a protocol method) and no
252
+ * `signTransaction`: EVM wallets sign and send in one step through
253
+ * `eth_sendTransaction`.
334
254
  */
335
255
  type EvmWallet = WalletBase;
336
256
  /**
337
- * Solana wallet surface. Adds:
338
- * - `signIn`: Sign-In-With-Solana (`solana:signIn`). Optional;
339
- * `capabilities.signIn` gates availability at runtime.
340
- * - `signTransaction`: sign-only path for wallets that advertise
341
- * `solana:signTransaction` but not `solana:signAndSendTransaction`.
342
- * Optional; `capabilities.signTransaction` gates availability.
257
+ * Both additions are optional at runtime; gate them on
258
+ * `capabilities.signIn` / `capabilities.signTransaction`, which mirror
259
+ * what the wallet actually advertises.
343
260
  */
344
261
  type SvmWallet = WalletBase & {
345
262
  /** Sign In With Solana (SIWS, `solana:signIn`). Authenticates the user
@@ -347,7 +264,7 @@ type SvmWallet = WalletBase & {
347
264
  * consumer can verify server-side. `input` is the SIWS message fields
348
265
  * (domain, statement, nonce, …); pass `{}` or omit for wallet
349
266
  * defaults. */
350
- signIn?: (input?: Record<string, unknown>) => Promise<{
267
+ signIn?: (input?: SignInInput) => Promise<{
351
268
  account: Account;
352
269
  signature: Uint8Array;
353
270
  signedMessage: Uint8Array;
@@ -355,7 +272,7 @@ type SvmWallet = WalletBase & {
355
272
  /** Sign a Solana transaction WITHOUT broadcasting it. butr ships no
356
273
  * RPC, so the consumer broadcasts the returned bytes via
357
274
  * `@solana/kit` / `@solana/web3.js` / etc. */
358
- signTransaction?: (tx: unknown, account?: Account) => Promise<Uint8Array>;
275
+ signTransaction?: (tx: TransactionInput, account?: Account) => Promise<Uint8Array>;
359
276
  };
360
277
  /**
361
278
  * Sui wallet surface. Adds optional `signTransaction` for the
@@ -363,24 +280,27 @@ type SvmWallet = WalletBase & {
363
280
  * consumer via `@mysten/sui`'s SuiClient.
364
281
  */
365
282
  type SuiWallet = WalletBase & {
366
- signTransaction?: (tx: unknown, account?: Account) => Promise<Uint8Array>;
283
+ /** Sign a Sui transaction WITHOUT executing it. Returns BOTH halves the
284
+ * chain requires: `SuiClient.executeTransactionBlock` needs
285
+ * `{ transactionBlock, signature }`, so a bare `Uint8Array` cannot express
286
+ * the result and a consumer holding one cannot tell which half they have. */
287
+ signTransaction?: (tx: TransactionInput, account?: Account) => Promise<{
288
+ bytes: Uint8Array;
289
+ signature: Uint8Array;
290
+ }>;
367
291
  };
368
292
  /**
369
- * Bitcoin wallet surface. `signTransaction` here is `bitcoin:signPsbt`
370
- * (sign-only PSBT path). Consumers pass `psbt.toBuffer()` bytes; the
371
- * wallet returns the signed PSBT bytes for the consumer to finalise /
372
- * broadcast through their own Esplora / Electrum client.
293
+ * `signTransaction` is `bitcoin:signPsbt`: pass `psbt.toBuffer()` bytes
294
+ * and get signed PSBT bytes back, to finalise and broadcast through
295
+ * your own Esplora or Electrum client.
373
296
  */
374
297
  type BitcoinWallet = WalletBase & {
375
- signTransaction?: (tx: unknown, account?: Account) => Promise<Uint8Array>;
298
+ signTransaction?: (tx: TransactionInput, account?: Account) => Promise<Uint8Array>;
376
299
  };
377
300
  /**
378
- * Polkadot/Substrate wallet surface. No standalone `signTransaction`:
379
- * building an extrinsic needs chain metadata (an RPC round-trip butr
380
- * doesn't ship), so transaction signing happens through the
381
- * `getSigner()` handoff; the consumer builds and submits with the
382
- * wallet's signer (e.g. polkadot-api). Message signing works via the
383
- * injected `signer.signRaw`. Same shape as `EvmWallet`.
301
+ * No standalone `signTransaction`: building an extrinsic needs chain
302
+ * metadata over RPC, which butr does not ship, so transaction signing
303
+ * goes through the `getSigner()` handoff.
384
304
  */
385
305
  type PolkadotWallet = WalletBase;
386
306
  /** Per-platform full adapter shapes: `Connector` + the platform's
@@ -392,22 +312,9 @@ type SuiAdapter = Connector<"sui"> & SuiWallet;
392
312
  type BitcoinAdapter = Connector<"bitcoin"> & BitcoinWallet;
393
313
  type PolkadotAdapter = Connector<"polkadot"> & PolkadotWallet;
394
314
  /**
395
- * Full adapter interface; discriminated union by `chainPlatform`.
396
- *
397
- * Narrow on `wallet.connector.chainPlatform === "svm"` (etc.) to gain
398
- * access to platform-specific methods like `signIn` (SVM) or
399
- * `signTransaction` (SVM / Sui / Bitcoin). Calling those methods on a
400
- * non-narrowed `WalletAdapter` is a TypeScript error; that's the
401
- * point. The discriminant carries the type-level fact "this method
402
- * doesn't exist on EVM" so consumers can't accidentally branch on
403
- * `capabilities.signIn` and call a method that EVM adapters don't
404
- * implement.
405
- *
406
- * Runtime gating via `capabilities` still matters for the methods that
407
- * are OPTIONAL within a platform (a Solana wallet might or might not
408
- * advertise `solana:signTransaction`). Capabilities narrow "wallet
409
- * supports this feature"; the discriminated union narrows "this
410
- * platform has this concept at all".
315
+ * The union narrows "this platform has the concept at all"; the
316
+ * `capabilities` flags narrow "this wallet supports it right now".
317
+ * Both gates are needed, and neither substitutes for the other.
411
318
  */
412
319
  type WalletAdapter = EvmAdapter | SvmAdapter | SuiAdapter | BitcoinAdapter | PolkadotAdapter;
413
320
  type ConnectedWallet = {
@@ -422,33 +329,15 @@ type ConnectedWallet = {
422
329
  //#endregion
423
330
  //#region src/types/discoverer.d.ts
424
331
  /**
425
- * Self-describing discovery descriptor for a single chain platform.
426
- *
427
- * Each platform package (`@usebutr/evm`, `@usebutr/svm`, `@usebutr/sui`,
428
- * `@usebutr/bitcoin`) exports one of these. The aggregator package
429
- * (`@usebutr/wallets`) composes them into `autoDiscovery()` without
430
- * needing to know per-platform defaults; the descriptor owns them.
431
- *
432
- * Adding a new chain platform means writing a `PlatformDiscoverer`
433
- * inside the new package and adding one import to the aggregator's
434
- * registry, which is keyed by `ChainPlatform`. The aggregator's logic
435
- * doesn't need to grow.
436
- *
437
- * Two parts:
438
- * - `subscribe`: the primary discovery channel (EIP-6963 / Wallet
439
- * Standard / etc).
440
- * - `fallback`: optional. The legacy-injected channel that should
441
- * only emit if the primary channel hasn't produced an adapter for
442
- * the same browser session by the settle deadline. `@usebutr/evm`
443
- * has one (window.ethereum); `@usebutr/bitcoin` has one
444
- * (window.unisat / sats-connect / window.btc); SVM and Sui don't.
332
+ * Each platform package exports exactly one; `@usebutr/wallets` composes
333
+ * them via a registry keyed by `ChainPlatform`, so a new chain adds a
334
+ * descriptor and a registry entry, not aggregator logic.
445
335
  */
446
336
  type PlatformDiscoverer = {
447
337
  /**
448
- * Optional legacy-injected fallback. Subscribes only when consumers
449
- * haven't disabled it. The hook receives `hasAnyPrimaryAdapter` so
450
- * the fallback can defer to standards-based discovery when an adapter
451
- * for the same wallet has already announced through the primary path.
338
+ * Legacy-injected channel (window.ethereum, window.unisat, …). Must
339
+ * consult `hasAnyPrimaryAdapter` and stay quiet when standards-based
340
+ * discovery already announced the same wallet, or it double-lists.
452
341
  */
453
342
  fallback?: {
454
343
  subscribe: (onAdapter: (adapter: WalletAdapter) => void, opts: {
@@ -461,16 +350,9 @@ type PlatformDiscoverer = {
461
350
  //#endregion
462
351
  //#region src/types/errors.d.ts
463
352
  /**
464
- * Tagged union of normalised connection errors.
465
- *
466
- * butr maps thrown values from connectors (which vary across wallet SDKs:
467
- * MetaMask uses EIP-1193 codes, Phantom throws stringly-typed errors,
468
- * embedded SDKs throw their own classes) into a small set of UX-meaningful
469
- * variants. Consumers branch on `kind` instead of regexing message strings.
470
- *
471
- * `message` is always present and human-readable. `cause` preserves the
472
- * original thrown value so callers can inspect raw connector errors when
473
- * the variant is `Unknown`.
353
+ * Wallet SDKs disagree on error shape (EIP-1193 codes, bare strings,
354
+ * bespoke classes), so consumers branch on `kind` rather than regexing
355
+ * messages. `cause` keeps the original value for the `Unknown` case.
474
356
  */
475
357
  type ConnectionError = {
476
358
  kind: "UserRejected";
@@ -493,25 +375,21 @@ type ConnectionError = {
493
375
  kind: "Timeout";
494
376
  message: string;
495
377
  } | {
496
- cause?: unknown;
378
+ cause?: ErrorCause;
497
379
  kind: "Unknown";
498
380
  message: string;
499
381
  };
500
382
  type ConnectionErrorKind = ConnectionError["kind"];
383
+ type ErrorCause = Error | string;
384
+ type CodedError = Error & {
385
+ code: number | string;
386
+ };
501
387
  /**
502
- * Normalise a thrown value into a `ConnectionError`.
503
- *
504
- * Recognises:
505
- * - butr's own `Error("Connection timeout")` (from the 90s connect timeout)
506
- * - butr's own `Error("Failed to get account")` (from the connect flow)
507
- * - EIP-1193 numeric `code` properties (`4001` → UserRejected,
508
- * `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected:
509
- * unauthorized / disconnected from all-or-one chains)
510
- * - common message substrings: "user rejected" / "user denied",
511
- * "locked", "chain", etc.
512
- * - anything else → `Unknown` with `cause` set to the original value.
388
+ * EIP-1193 codes: `4001` rejected, `-32002` pending, `4100`/`4900`/`4901`
389
+ * unauthorized or disconnected. Message-substring matching is the last
390
+ * resort for SDKs that ship no codes at all.
513
391
  */
514
- declare const mapConnectionError: (raw: unknown) => ConnectionError;
392
+ declare const mapConnectionError: (raw: ErrorCause) => ConnectionError;
515
393
  //#endregion
516
394
  //#region src/storage/persistence.d.ts
517
395
  type MaybePromise<T> = T | Promise<T>;
@@ -558,16 +436,9 @@ type WalletPersistence = {
558
436
  //#endregion
559
437
  //#region src/storage/snapshot.d.ts
560
438
  /**
561
- * Server-safe view of a butr-persisted session; everything you can
562
- * know about a user's connected wallets from the cookie payload alone,
563
- * without instantiating a `Connector`.
564
- *
565
- * Notably absent: the `Connector` instance. A wallet extension exists
566
- * only in the browser, so a server render can know *which* wallet was
567
- * connected and *what address* it held, but cannot dispatch
568
- * `signMessage`/`sendTransaction` on it. Splitting display from action
569
- * along this seam keeps the impossibility expressed in the types
570
- * rather than hidden inside a runtime check.
439
+ * Carries no `Connector` by design: a wallet extension exists only in
440
+ * the browser, so a server render can name the wallet and its address
441
+ * but can never dispatch on it.
571
442
  */
572
443
  type WalletSnapshot = {
573
444
  activeConnectorId: string | null;
@@ -587,53 +458,22 @@ type SnapshotOptions = {
587
458
  };
588
459
  declare const EMPTY_SNAPSHOT: WalletSnapshot;
589
460
  /**
590
- * Parse a cookie source into a server-safe `WalletSnapshot`.
591
- *
592
- * Pure, sync, no `document`, no React; runnable in any environment
593
- * (Server Component, route handler, edge middleware, even client
594
- * code). Pair with `createCookieStorageDriver({ initialCookies })`
595
- * and `<WalletManagerProvider initialSnapshot={…} />` to render a
596
- * connected shell server-side without a hydration flash.
597
- *
598
- * **Stale-snapshot semantics.** The snapshot reflects whatever the
599
- * browser most recently persisted. If the user has since uninstalled
600
- * the wallet, switched accounts, or disconnected in another tab, the
601
- * client-side hydration will reconcile reality and the live store
602
- * will diverge from the snapshot. Treat the snapshot as an
603
- * *optimistic* shell; accurate enough to avoid a paint flicker,
604
- * authoritative only after `useIsHydrated()` is true.
605
- *
606
- * **Inputs.** Accepts the three shapes Next.js / Express / Hono /
607
- * generic-Node cookie code naturally produces:
608
- * - A plain object: `{ "butr-pool": "{...}", … }`
609
- * - An array of `{ name, value }` (Next.js' `cookies().getAll()`)
610
- * - An iterable of `[name, value]` tuples
611
- *
612
- * Malformed entries are dropped with a `logWarn` (same policy as
613
- * `WalletStorage.getPool`); a cross-tab corruption shouldn't crash
614
- * the server render.
461
+ * Optimistic: only what the browser last persisted, so an uninstall or
462
+ * other-tab disconnect makes it stale. Authoritative once the entry
463
+ * leaves `reconnectingIds`; `isHydrated` is true from render one.
615
464
  */
616
465
  declare const readWalletSnapshot: (source: CookieSource, options?: SnapshotOptions) => WalletSnapshot;
617
466
  //#endregion
618
467
  //#region src/types/manager.d.ts
619
468
  /**
620
- * Outcome of butr's mount-time hydration pass. Passed to
621
- * `WalletManagerConfig.onHydrated`. Three buckets:
622
- *
623
- * - `restoredIds`: wallets that came back fully. Their pool entries
624
- * are live and consumers can use them immediately.
625
- * - `pendingIds`: wallets whose adapter wasn't registered yet
626
- * (auto-discovery's async warmup). The runtime retries each one
627
- * when discovery announces a matching id, so most of these will
628
- * restore within a few hundred ms of mount.
629
- * - `dropped`: wallets whose restore actually failed (connector
630
- * threw mid-flight). These have been removed from storage; consumer
631
- * UX can surface "Couldn't reconnect Phantom; connect again."
469
+ * `pendingIds` are not failures: discovery announces those adapters
470
+ * asynchronously and the runtime retries them, usually within a few
471
+ * hundred ms. Only `dropped` entries have been removed from storage.
632
472
  */
633
473
  type HydrationOutcome = {
634
474
  dropped: Array<{
635
475
  connectorId: string;
636
- reason: unknown;
476
+ reason: ErrorCause;
637
477
  }>;
638
478
  pendingIds: Array<string>;
639
479
  restoredIds: Array<string>;
@@ -644,67 +484,37 @@ type WalletManagerConfig = {
644
484
  /** Function to instantiate a connector by ID */
645
485
  createConnector: (id: string) => WalletAdapter | null;
646
486
  /**
647
- * Seed the store synchronously with persisted wallet state; typically
648
- * the return value of `readWalletSnapshot(cookies, { keyPrefix })`
649
- * called from a Server Component. When provided:
650
- * - `pool` is populated with `ConnectedWallet` entries whose
651
- * `connector` is a shadow adapter (see `createShadowAdapter`):
652
- * identity-only, all capabilities `false`, methods throw
653
- * `ShadowConnectorError` if called.
654
- * - `activeConnectorId` and `selection` are set from the snapshot.
655
- * - `isHydrated` flips `true` immediately on construction.
656
- * - Every seeded id appears in `reconnectingIds`; the background
657
- * silent-reconnect pass removes the id and replaces the pool
658
- * entry with a live adapter on success, or drops the entry on
659
- * failure.
660
- *
661
- * Pre-hydration UI renders from the snapshot's data (address,
662
- * accounts, chain, name, icon) without a flash. Action affordances
663
- * (sign, send) are naturally gated by the shadow's all-false
664
- * capabilities, or consumers can branch on `reconnectingIds`.
487
+ * Seeds the pool with shadow adapters and flips `isHydrated` true on
488
+ * construction, so identity renders without a flash but every seeded
489
+ * id sits in `reconnectingIds` until silent reconnect verifies it.
665
490
  */
666
491
  initialState?: WalletSnapshot;
667
492
  /** Called after a wallet is successfully connected */
668
493
  onConnect?: (wallet: ConnectedWallet) => void;
669
494
  /**
670
- * Called after a connection attempt fails (user rejected, wallet
671
- * locked, chain mismatch, timeout, …). Receives the normalised
672
- * `ConnectionError` plus the id of the connector that was being
673
- * connected. Useful for piping into observability tooling
674
- * (Sentry, OTel) without each consumer wiring `try/catch`s around
675
- * `connectWallet` themselves.
495
+ * Fires for every failed attempt (rejection, locked wallet, timeout,
496
+ * …), so observability can hook here instead of wrapping every
497
+ * `connectWallet` call.
676
498
  */
677
499
  onConnectError?: (error: ConnectionError, connectorId: string) => void;
678
500
  /** Called after a wallet is disconnected */
679
501
  onDisconnect?: (chainPlatform: ChainPlatform) => void;
680
- /**
681
- * Called once after butr's mount-time hydration finishes. Receives a
682
- * `HydrationOutcome` summarising which stored wallets were restored,
683
- * which are pending an adapter announcement, and which failed.
684
- * Useful for surfacing "Phantom couldn't be reconnected; try
685
- * again" UX or piping a metric to telemetry.
686
- */
502
+ /** Fires once, after the mount-time hydration pass. */
687
503
  onHydrated?: (outcome: HydrationOutcome) => void;
688
504
  /** Called after all wallets are reset (e.g., to clear auth tokens) */
689
505
  onReset?: () => void | Promise<void>;
690
506
  /**
691
- * Called when a connect attempt takes longer than
692
- * `slowConnectThresholdMs` (default 5_000) but hasn't yet resolved
693
- * or rejected. Fires at most once per connect attempt. Useful for
694
- * surfacing a "still trying, check your wallet" hint in the UI or
695
- * piping a slow-path metric to telemetry.
507
+ * Fires at most once per attempt, once it passes
508
+ * `slowConnectThresholdMs` without settling. The attempt keeps
509
+ * running; this is a hint, not a timeout.
696
510
  */
697
511
  onSlowConnect?: (connectorId: string) => void;
698
512
  /**
699
- * Called when a storage write fails. butr's persistence layer is
700
- * fire-and-forget by design (any individual write can fail without
701
- * breaking butr's reducer state), but the consumer might still want
702
- * to know; quota-exceeded errors, IndexedDB shutdown, cross-tab
703
- * conflicts, cookie size limits. `context` is a short string
704
- * describing which write failed (e.g. `"failed to persist pool"`).
705
- * The default behaviour when no callback is set is `console.warn`.
513
+ * Persistence is fire-and-forget: a failed write (quota, cookie size,
514
+ * cross-tab conflict) never breaks reducer state, it only surfaces
515
+ * here. Defaults to `console.warn`.
706
516
  */
707
- onStorageError?: (error: unknown, context: string) => void;
517
+ onStorageError?: (error: ErrorCause, context: string) => void;
708
518
  /** Threshold for `onSlowConnect`, in milliseconds. Defaults to 5_000. */
709
519
  slowConnectThresholdMs?: number;
710
520
  /** Optional custom persistence implementation (e.g., cookie-backed) */
@@ -715,55 +525,9 @@ type WalletManagerConfig = {
715
525
  //#endregion
716
526
  //#region src/types/signer.d.ts
717
527
  /**
718
- * Per-platform signer type registry.
719
- *
720
- * `Connector.getSigner()` returns `Promise<unknown>` because the actual
721
- * signer shape lives in the platform-specific packages: `@usebutr/evm`
722
- * returns an EIP-1193 provider, `@usebutr/svm` returns a Wallet
723
- * Standard wallet, `@usebutr/sui` returns the same Wallet Standard
724
- * wallet narrowed to Sui features, `@usebutr/bitcoin` returns either a
725
- * Wallet Standard wallet or an injected provider depending on which
726
- * adapter discovered it.
727
- *
728
- * Consumers cast the `unknown` to whichever signer their integration
729
- * library expects. This registry exists so the cast target is sourced
730
- * from one place; when a platform package renames its signer type,
731
- * consumer code keeps working through the registry without an explicit
732
- * patch.
733
- *
734
- * **How to extend.** Each platform package declares a module-augmentation
735
- * block that adds its key to this interface. For example,
736
- * `@usebutr/evm` ships:
737
- *
738
- * ```ts
739
- * declare module "@usebutr/core" {
740
- * interface SignerForPlatform {
741
- * evm: Eip1193Provider;
742
- * }
743
- * }
744
- * ```
745
- *
746
- * Consumers that import the EVM package get the typed entry for free;
747
- * the augmentation is transitive through TypeScript's structural
748
- * declaration merging.
749
- *
750
- * **Usage.**
751
- *
752
- * ```ts
753
- * import type { SignerForPlatform } from "@usebutr/core";
754
- *
755
- * const signer = (await wallet.connector.getSigner()) as SignerForPlatform["evm"];
756
- * ```
757
- *
758
- * The cast is still there: `getSigner` itself stays type-erased to
759
- * keep the cross-package boundary loose, but the cast target lives in
760
- * one canonical place.
761
- *
762
- * **Why not make `getSigner` generic.** Generic-on-platform `getSigner`
763
- * would require `Connector` to be a discriminated union by
764
- * `chainPlatform`, which is a public-API breaking change held for a
765
- * future major release. The registry is the small step you can take
766
- * today; the union is the larger step that makes the cast obsolete.
528
+ * Canonical cast target for `getSigner()`'s `unknown`, which stays
529
+ * type-erased to keep the cross-package boundary loose. Platform
530
+ * packages add their key by augmenting this interface.
767
531
  */
768
532
  interface SignerForPlatform {}
769
533
  /** Convenience alias for narrowing a single platform's signer type. */
@@ -771,34 +535,24 @@ type SignerOf<P extends keyof SignerForPlatform> = SignerForPlatform[P];
771
535
  //#endregion
772
536
  //#region src/wallet-source.d.ts
773
537
  /**
774
- * A discovery seam. Implementations call `onAdapter(adapter)` each time
775
- * they find a wallet and return an unsubscribe handle. `@usebutr/wallets`
776
- * composes EVM + SVM into a single `WalletSource`; third parties can
777
- * implement this type without depending on `@usebutr/wallets`.
538
+ * The discovery seam: implementable by third parties without depending
539
+ * on `@usebutr/wallets`, which itself just composes the per-platform
540
+ * sources into one.
778
541
  */
779
542
  type WalletSource = {
780
543
  subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void;
781
544
  };
782
545
  /**
783
- * Wrap a bare `subscribe` function (the exact shape of
784
- * `discoverEvmAdapters` / `discoverSvmAdapters`) into a `WalletSource`,
785
- * so an EVM-only app can do
786
- * `createWalletSource(discoverEvmAdapters)` without importing anything
787
- * protocol-bearing beyond `@usebutr/evm`.
546
+ * Takes the exact shape of `discoverEvmAdapters` and friends, so a
547
+ * single-platform app keeps `@usebutr/wallets` out of its bundle.
788
548
  */
789
549
  declare const createWalletSource: (subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void) => WalletSource;
790
550
  //#endregion
791
551
  //#region src/store/reducer.d.ts
792
552
  /**
793
- * Public connection-status enum. `state.connectionStatus` in the
794
- * reducer only takes the first four values ("idle" | "connecting" |
795
- * "success" | "error"); those track the user-initiated connect
796
- * attempt. The fifth value, `"reconnecting"`, is *derived* by
797
- * `useConnectionStatus` when the active wallet is still backed by a
798
- * shadow adapter (its id is in `state.reconnectingIds`). The
799
- * derivation lives in the React selector so the reducer can stay a
800
- * narrow state machine while the public API matches wagmi's
801
- * vocabulary.
553
+ * The reducer never writes `"reconnecting"`; `useConnectionStatus`
554
+ * derives it from `reconnectingIds` so the state machine stays narrow
555
+ * while the public vocabulary matches wagmi's.
802
556
  */
803
557
  type ConnectionStatus = "idle" | "connecting" | "success" | "error" | "reconnecting";
804
558
  type State = {
@@ -810,13 +564,9 @@ type State = {
810
564
  isUserDisconnected: boolean;
811
565
  pool: Map<string, ConnectedWallet>;
812
566
  /**
813
- * Connector IDs whose pool entry is currently backed by a shadow
814
- * adapter: created from `WalletManagerConfig.initialState` and
815
- * waiting for the live adapter to be announced (via discovery) and
816
- * the silent reconnect to succeed. The set shrinks as entries
817
- * upgrade (`HYDRATED` / `CONNECT_SUCCEEDED` clear matching ids) or
818
- * drop (`DISCONNECTED` / `RESET`). Empty for stores constructed
819
- * without `initialState`.
567
+ * Ids still backed by a shadow adapter from `initialState`. Empty
568
+ * without `initialState`; shrinks on `HYDRATED` / `CONNECT_SUCCEEDED`
569
+ * and on `DISCONNECTED` / `RESET`.
820
570
  */
821
571
  reconnectingIds: ReadonlySet<string>;
822
572
  selection: Map<ChainPlatform, string>;
@@ -824,40 +574,20 @@ type State = {
824
574
  //#endregion
825
575
  //#region src/store/shadow-adapter.d.ts
826
576
  /**
827
- * Error thrown when a method is called on a shadow adapter; the
828
- * placeholder `WalletAdapter` that the store seeds into the pool when
829
- * an `initialState` is provided (e.g. from a server-rendered cookie
830
- * snapshot). Shadow adapters carry the identity and account data of a
831
- * previously-connected wallet, but the live wallet extension hasn't
832
- * been verified yet; the silent reconnect happens asynchronously
833
- * after mount.
834
- *
835
- * UI code that gates affordances on `wallet.connector.capabilities.*`
836
- * never reaches a shadow method (capabilities are all `false`). Code
837
- * that calls through anyway hits this typed error, which is the
838
- * correct loud failure: the consumer ignored the capability gate.
839
- *
840
- * Consumers wanting to wait out the reconnecting window should branch
841
- * on whether `connectorId` is in `state.reconnectingIds`.
577
+ * Loud failure for consumers that called a wallet method without gating
578
+ * on `capabilities.*` (all `false` here) or `reconnectingIds`, before
579
+ * silent reconnect swapped in the live adapter.
842
580
  */
843
- declare class ShadowConnectorError extends Error {
581
+ declare class ShadowConnectorError extends Error implements CodedError {
844
582
  readonly code = "BUTR_RECONNECTING";
845
583
  readonly connectorId: string;
846
584
  readonly method: string;
847
585
  constructor(method: string, connectorId: string);
848
586
  }
849
587
  /**
850
- * Type guard. Returns true when an adapter is a placeholder created
851
- * by `createShadowAdapter`. Useful for the hydration coordinator
852
- * (which needs to know which pool entries still need upgrading) and
853
- * for consumers writing wagmi-style "is this connection verified yet"
854
- * checks without subscribing to `reconnectingIds` directly.
855
- *
856
- * Detection is structural: a shadow has all capabilities set to false.
857
- * Live adapters always advertise at least one capability (every wallet
858
- * surface includes `getBalance`, `signMessage`, `switchChain` as
859
- * required methods, and adapter constructors set their flags
860
- * accordingly).
588
+ * Structural detection: relies on every live adapter advertising at
589
+ * least one capability, since `getBalance`, `signMessage` and
590
+ * `switchChain` are required on all wallet surfaces.
861
591
  */
862
592
  declare const isShadowAdapter: (adapter: WalletAdapter) => boolean;
863
593
  //#endregion
@@ -886,22 +616,13 @@ type CookieDriverOptions = {
886
616
  */
887
617
  domain?: string;
888
618
  /**
889
- * Snapshot of cookies for the SSR pass; typically the result of
890
- * Next.js' `cookies()` (from `next/headers`) or a parsed
891
- * `req.headers.cookie`. Used only when `document` is unavailable.
892
- *
893
- * When provided, `getItem` reads from this snapshot during the
894
- * server render so the store sees the same persisted state the
895
- * client will see after hydration. Writes remain no-ops on the
896
- * server: emitting `Set-Cookie` has to be done by the framework
897
- * layer that owns the response.
619
+ * Server-side cookie snapshot (Next.js' `cookies()`, a parsed
620
+ * `req.headers.cookie`, …) read only when `document` is unavailable,
621
+ * so the SSR pass sees the state the client will hydrate with.
898
622
  */
899
623
  initialCookies?: InitialCookies;
900
624
  /**
901
- * Lifetime in seconds. Defaults to 30 days. Pass `undefined`
902
- * (the default) for "until the session ends", but note that
903
- * butr persists wallet state across sessions, so a finite
904
- * max-age is usually what consumers want.
625
+ * Lifetime in seconds. Defaults to 30 days.
905
626
  */
906
627
  maxAgeSeconds?: number;
907
628
  /**
@@ -921,34 +642,16 @@ type CookieDriverOptions = {
921
642
  secure?: boolean;
922
643
  };
923
644
  /**
924
- * Cookie-backed storage driver. Reads/writes `document.cookie`;
925
- * server-readable, survives reloads, scoped per `domain`/`path`.
926
- *
927
- * **When to use this:** SSR apps that need to know who's connected
928
- * during the server render (so they can stream the connected-wallet
929
- * UI without a client-side hydration flicker). Pass `initialCookies`
930
- * from a server-side cookie source (e.g. `cookies()` in Next.js'
931
- * `next/headers`, or a parsed `req.headers.cookie`) and the same
932
- * driver will serve those values during the server render and switch
933
- * to `document.cookie` once it mounts in the browser.
934
- *
935
- * **Trade-offs vs `localStorage`:** cookies travel with every
936
- * request, so they cost bytes on the wire. Keep the storage key
937
- * prefix short, and prefer this driver for the `persistent` slot
938
- * only: the `session` slot can stay in `sessionStorage` (which
939
- * cookies can't natively model anyway).
940
- *
941
- * **Server-side writes are no-ops.** Emitting `Set-Cookie` requires
942
- * access to the framework's response object, which a storage driver
943
- * shouldn't reach into. The store doesn't mutate persisted state
944
- * during the SSR pass anyway; writes only fire after client mount,
945
- * once `document.cookie` is reachable.
645
+ * Server-side writes are no-ops: `Set-Cookie` needs the framework's
646
+ * response object. Cookies ride every request, so use this driver for
647
+ * the `persistent` slot only and leave `session` on `sessionStorage`.
946
648
  */
947
649
  declare const createCookieStorageDriver: (options?: CookieDriverOptions) => StorageDriver;
948
650
  //#endregion
949
651
  //#region src/storage/wallet-storage.d.ts
950
652
  type StorageConfig = {
951
- keyPrefix: string;
653
+ /** Defaults to `"butr"`, matching `readWalletSnapshot`. */
654
+ keyPrefix?: string;
952
655
  /** Survives app restart. Defaults to localStorage on web. */
953
656
  persistent?: StorageDriver;
954
657
  /** Cleared on session end. Defaults to sessionStorage on web. */
@@ -962,27 +665,22 @@ declare class WalletStorage implements WalletPersistence {
962
665
  private readonly persistent;
963
666
  private readonly session;
964
667
  /**
965
- * Serializes pool-key mutations so concurrent fire-and-forget
966
- * writes can't interleave their read-modify-write phases. Without
967
- * this, two simultaneous `setPool` calls both read the pre-write
968
- * state, each merge their own entries, and whichever finishes last
969
- * overwrites the other's additions. Reads (`getPool`) don't enter
970
- * the queue; they observe whatever's currently in the driver.
668
+ * Serializes pool read-modify-writes; without it concurrent `setPool`
669
+ * calls drop each other's entries. Not reentrant, so a queued mutation
670
+ * must read via `readPool`: `getPool` re-enters and deadlocks it.
971
671
  */
972
672
  private poolMutationQueue;
973
673
  constructor(config: StorageConfig);
974
674
  /** Chain `fn` after the in-flight pool mutation. */
975
675
  private serializePoolMutation;
676
+ /** Read and decode the pool without touching the mutation queue. Safe to
677
+ * call from inside a queued mutation, unlike `getPool`. */
678
+ private readPool;
976
679
  getPool(): Promise<StoredPoolRecord>;
977
680
  /**
978
- * Upsert the in-memory pool into storage. Additive: entries in
979
- * `pool` are written; entries already in storage that aren't in
980
- * `pool` are kept. The in-memory pool reflects "what's live right
981
- * now", not "the complete list of remembered connections"; a
982
- * silent reconnect that fails on reload leaves the entry out of
983
- * the pool but the saved entry stays so the next load can retry.
984
- * Use `removePoolEntry` for explicit eviction (the user clicked
985
- * Disconnect) and `clearAll` for a full wipe (reset).
681
+ * Additive on purpose: a failed silent reconnect drops the entry from
682
+ * the live pool, and it must survive to be retried next load.
683
+ * Eviction goes through `removePoolEntry` or `clearAll`.
986
684
  */
987
685
  setPool(pool: Map<string, ConnectedWallet>): Promise<void>;
988
686
  removePoolEntry(connectorId: string): Promise<void>;
@@ -993,12 +691,9 @@ declare class WalletStorage implements WalletPersistence {
993
691
  setActiveConnectorId(connectorId: string | null): Promise<void>;
994
692
  clearAll(): Promise<void>;
995
693
  /**
996
- * Disconnect-intent tracking.
997
- *
998
- * Lives in the session driver: survives component remounts (unlike refs)
999
- * but clears when the session ends (unlike the persistent driver).
1000
- * Prevents auto-connect from firing immediately after a manual disconnect,
1001
- * while still allowing auto-connect on fresh sessions.
694
+ * Kept in the session driver so it survives remounts (unlike a ref)
695
+ * yet clears at session end (unlike the persistent driver): a manual
696
+ * disconnect must suppress auto-connect now, not forever.
1002
697
  */
1003
698
  isUserDisconnected(): Promise<boolean>;
1004
699
  markUserDisconnected(value: boolean): Promise<void>;
@@ -1032,38 +727,17 @@ declare const createWalletStore: (config: WalletManagerConfig) => import("zustan
1032
727
  //#endregion
1033
728
  //#region src/group-by-platform.d.ts
1034
729
  /**
1035
- * Bucket a flat list into per-platform groups.
1036
- *
1037
- * A multi-chain wallet announces one adapter per platform it speaks, so
1038
- * discovery hands back a flat list where a single brand appears several
1039
- * times. Any app targeting more than one chain has to bucket that list
1040
- * before rendering, and hand-rolled versions drift: butr's own demos
1041
- * carried a copy that reimplemented `CHAIN_PLATFORMS` as a local
1042
- * ordering constant.
1043
- *
1044
- * Keys are inserted in `CHAIN_PLATFORMS` order and platforms with no
1045
- * members are omitted, so the result is directly iterable for rendering
1046
- * (`[...groups]`) as well as addressable for lookup (`groups.get("evm")`)
1047
- * without a further emptiness filter.
1048
- *
1049
- * `getPlatform` exists because the discriminant sits at a different depth
1050
- * depending on the input: a discovered `WalletAdapter` carries
1051
- * `chainPlatform` at the top level, a pool entry nests it under
1052
- * `connector`. React consumers should reach for
1053
- * `useDiscoveredWalletsByPlatform` / `useConnectedWalletsByPlatform`
1054
- * instead, which bind the accessor for them.
730
+ * A multi-chain wallet announces one adapter per platform, so brands
731
+ * repeat in the flat list. Keys follow `CHAIN_PLATFORMS` order and empty
732
+ * platforms are omitted, so `[...groups]` needs no emptiness filter.
1055
733
  */
1056
734
  declare const groupByPlatform: <T>(items: ReadonlyArray<T>, getPlatform: (item: T) => ChainPlatform) => Map<ChainPlatform, Array<T>>;
1057
735
  //#endregion
1058
736
  //#region src/sign-in/sign-in-flow.d.ts
1059
737
  /**
1060
- * What the wallet produced, encoded for transport. Both byte fields are
1061
- * base64 because that is what survives `JSON.stringify` intact; the raw
1062
- * `Uint8Array`s are kept alongside for callers verifying in-process.
1063
- *
1064
- * Verify against `signedMessage`, not `message`. Solana Wallet Standard
1065
- * wallets may prefix or re-encode what they sign, so the bytes that carry
1066
- * the signature are the wallet's, not yours.
738
+ * Verify against `signedMessage`, not `message`: Solana Wallet Standard
739
+ * wallets may prefix or re-encode what they sign. Base64 mirrors exist
740
+ * because `Uint8Array` does not survive `JSON.stringify`.
1067
741
  */
1068
742
  type SignInResult = {
1069
743
  account: Account;
@@ -1086,15 +760,9 @@ type SignInMessageContext = {
1086
760
  };
1087
761
  type SignInFlowOptions = {
1088
762
  /**
1089
- * Compose the message to sign. Defaults to a plain
1090
- * `<address> signs in. Nonce: <nonce>` line.
1091
- *
1092
- * butr deliberately does not ship a wire format here: a message a
1093
- * server must parse is an authentication spec, and SIWE / SIWS already
1094
- * fill that role. Pass the formatter your backend expects.
1095
- *
1096
- * Unused on the SIWS path, where the wallet composes the message from
1097
- * the input fields.
763
+ * butr ships no wire format: a server-parsed message is an auth spec,
764
+ * and SIWE / SIWS already fill that role. Pass what your backend
765
+ * expects. Unused on the SIWS path, where the wallet composes it.
1098
766
  */
1099
767
  buildMessage?: (ctx: SignInMessageContext) => string;
1100
768
  /** Fetch a single-use nonce from your backend. */
@@ -1111,6 +779,9 @@ type SignInFlowOptions = {
1111
779
  /** Hand the signed result to your backend. Throw to fail the flow. */
1112
780
  verify: (result: SignInResult) => Promise<void>;
1113
781
  };
782
+ type SignInFlow = {
783
+ signIn: (wallet: ConnectedWallet, account?: Account) => Promise<SignInResult>;
784
+ };
1114
785
  /** Thrown before any wallet interaction when the wallet can't sign at
1115
786
  * all. Distinct from a rejection: nothing was asked of the user, so UI
1116
787
  * should say "this wallet can't sign in" rather than "you declined". */
@@ -1119,52 +790,17 @@ declare class SignInUnsupportedError extends Error {
1119
790
  constructor(connectorId: string);
1120
791
  }
1121
792
  /**
1122
- * Build a reusable sign-in flow: nonce, capability gate, signature,
1123
- * encoding, verification.
1124
- *
1125
- * Every wallet-auth app writes this same sequence, and the parts that
1126
- * are easy to get wrong are the ones that aren't about the app: which
1127
- * capability flag gates the attempt, whether a Solana wallet should take
1128
- * the SIWS path instead, and which bytes to base64 for the server (the
1129
- * wallet's `signedMessage`, not the input). This owns those; you own the
1130
- * nonce endpoint, the message format, and the verifier.
1131
- *
1132
- * ```ts
1133
- * const { signIn } = createSignInFlow({
1134
- * getNonce: () => fetch("/api/nonce").then((r) => r.text()),
1135
- * verify: (result) =>
1136
- * fetch("/api/verify", { body: JSON.stringify(result), method: "POST" }).then(() => {}),
1137
- * });
1138
- *
1139
- * await signIn(wallet);
1140
- * ```
1141
- *
1142
- * Solana wallets advertising `solana:signIn` take the SIWS path
1143
- * automatically, so `result.signedMessage` holds the wallet-composed
1144
- * SIWS statement and `result.message` is absent. Set
1145
- * `preferSignMessage` to opt out.
793
+ * Solana wallets advertising `solana:signIn` take the SIWS path, so
794
+ * `result.signedMessage` holds the wallet-composed statement and
795
+ * `result.message` is absent. `preferSignMessage` opts out.
1146
796
  */
1147
- declare const createSignInFlow: (options: SignInFlowOptions) => {
1148
- signIn: (wallet: ConnectedWallet, account?: Account) => Promise<SignInResult>;
1149
- };
797
+ declare const createSignInFlow: (options: SignInFlowOptions) => SignInFlow;
1150
798
  //#endregion
1151
799
  //#region src/wallet-equal.d.ts
1152
800
  /**
1153
- * Two wallet snapshots are equivalent for selector purposes iff they
1154
- * share the same adapter instance, active account address, and active
1155
- * account chain id. Used by `useStoreWithEqualityFn` consumers
1156
- * (active-wallet, selected-wallet, useWalletEntry) to suppress spurious
1157
- * re-renders when the underlying Map churns but the resolved entry
1158
- * hasn't changed.
1159
- *
1160
- * The adapter is compared by reference, not by `connector.id`: hydration
1161
- * replaces a shadow adapter with the live one under an unchanged id and
1162
- * address, so an id comparison reports "equal" and leaves consumers
1163
- * holding a placeholder whose every method throws ShadowConnectorError.
1164
- *
1165
- * Hoisted to its own module so the equivalence rule lives in one place;
1166
- * if we ever extend the snapshot (e.g. to consider `accounts.length`),
1167
- * every selector hook picks up the new rule for free.
801
+ * The adapter is compared by reference, not `connector.id`: hydration
802
+ * swaps a shadow adapter for the live one under an unchanged id, and an
803
+ * id check would strand consumers on the throwing placeholder.
1168
804
  */
1169
805
  declare const walletEqual: (a: ConnectedWallet | undefined, b: ConnectedWallet | undefined) => boolean;
1170
806
  //#endregion
@@ -1174,43 +810,17 @@ declare const logError: (...args: ReadonlyArray<unknown>) => void;
1174
810
  //#endregion
1175
811
  //#region src/sanitize-icon.d.ts
1176
812
  /**
1177
- * Normalize a wallet-announced icon string.
1178
- *
1179
- * Wallets announce their icon through external metadata; EIP-6963
1180
- * `providerInfo.icon`, Wallet Standard `wallet.icon`. That value is
1181
- * not under butr's control, and some wallets ship data-URI icons with
1182
- * surrounding whitespace (a newline left over from a pretty-printed
1183
- * manifest). Strict consumers reject it: Next.js's `<Image>` throws
1184
- * because `src` must not start with a control character.
1185
- *
1186
- * Trims surrounding whitespace and treats an all-whitespace (or empty)
1187
- * icon as absent, so consumers get either a usable string or
1188
- * `undefined`: never a blank or malformed one. `undefined` passes
1189
- * through untouched.
1190
- *
1191
- * **Discovery already applies this.** Every adapter butr discovers has
1192
- * had its icon sanitized at construction, so `Connector.icon` is safe to
1193
- * render as-is. Reach for this helper only when building adapter or
1194
- * `ConnectorMeta` metadata yourself from a source butr didn't produce.
813
+ * Some wallets announce data-URI icons wrapped in whitespace, which
814
+ * makes Next.js `<Image>` throw on a leading control character.
815
+ * Discovery already applies this; only hand-built metadata needs it.
1195
816
  */
1196
817
  declare const sanitizeIcon: (icon: string | undefined) => string | undefined;
1197
818
  //#endregion
1198
819
  //#region src/encoding/bytes.d.ts
1199
820
  /**
1200
- * Shared byte-encoding helpers for the connector packages.
1201
- *
1202
- * These functions sit directly on the signing/address path of every
1203
- * chain. They used to be hand-reimplemented in ~10 connector files; a
1204
- * single tested module removes the drift surface (a `padStart` omission
1205
- * or base64 variant mismatch corrupts signatures/addresses for one chain
1206
- * only, and the divergence is invisible because the copies look "the
1207
- * same").
1208
- *
1209
- * Hex prefixing diverges across chains, so the module exposes **explicit
1210
- * variants** rather than one function:
1211
- * - {@link bytesToHex} returns bare hex (Bitcoin, Ledger).
1212
- * - {@link bytesToHexPrefixed} returns `0x`-prefixed hex (EVM, Polkadot).
1213
- * - {@link hexToBytes} tolerantly strips an optional `0x` (all callers).
821
+ * Hex prefixing diverges by chain (bare for Bitcoin and Ledger, `0x` for
822
+ * EVM and Polkadot), so the variants stay explicit: these sit on the
823
+ * signing path, where a silent mismatch corrupts one chain's signatures.
1214
824
  */
1215
825
  /** Bare lowercase hex, no `0x` prefix (Bitcoin, Ledger). */
1216
826
  declare const bytesToHex: (bytes: Uint8Array) => string;
@@ -1239,5 +849,5 @@ declare const bytesToBase58: (bytes: Uint8Array) => string;
1239
849
  * rather than silently dropping them. */
1240
850
  declare const base58ToBytes: (input: string) => Uint8Array;
1241
851
  //#endregion
1242
- export { type Account, type Balance, type BitcoinAdapter, type BitcoinWallet, type BrowserStorageDrivers, CHAIN_PLATFORMS, type ChainBase, type ChainPlatform, type ChainsByPlatform, type ConnectedWallet, type ConnectionError, type ConnectionErrorKind, type ConnectionStatus, type Connector, type ConnectorEvent, type ConnectorMeta, type CookieDriverOptions, type CookieSource, EMPTY_SNAPSHOT, type EvmAdapter, type EvmWallet, type HydrationOutcome, type InitialCookies, type MaybePromise, type PlatformDiscoverer, type PolkadotAdapter, type PolkadotWallet, ShadowConnectorError, type SignInFlowOptions, type SignInMessageContext, type SignInResult, SignInUnsupportedError, type SignerForPlatform, type SignerOf, type SnapshotOptions, type StorageDriver, type StoredPoolEntry, type StoredPoolRecord, type StoredSelectionRecord, type SuiAdapter, type SuiWallet, type SvmAdapter, type SvmWallet, type WalletAdapter, type WalletAvailability, type WalletBase, type WalletCapabilities, type WalletManagerConfig, type WalletPersistence, type WalletSnapshot, type WalletSource, WalletStorage, type WalletStore, type WalletStoreState, base58ToBytes, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase58, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createSignInFlow, createWalletSource, createWalletStore, groupByPlatform, hexToBytes, isShadowAdapter, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
852
+ export { type Account, type Balance, type BitcoinAdapter, type BitcoinWallet, type BrowserStorageDrivers, CHAIN_PLATFORMS, type ChainBase, type ChainPlatform, type ChainsByPlatform, type ConnectedWallet, type ConnectionError, type ConnectionErrorKind, type ConnectionStatus, type Connector, type ConnectorEvent, type ConnectorMeta, type CookieDriverOptions, type CookieSource, EMPTY_SNAPSHOT, type EvmAdapter, type EvmWallet, type HydrationOutcome, type InitialCookies, type MaybePromise, type PlatformDiscoverer, type PolkadotAdapter, type PolkadotWallet, ShadowConnectorError, type SignInFlowOptions, type SignInInput, type SignInMessageContext, type SignInResult, SignInUnsupportedError, type SignInValue, type SignerForPlatform, type SignerOf, type SnapshotOptions, type StorageDriver, type StoredPoolEntry, type StoredPoolRecord, type StoredSelectionRecord, type SuiAdapter, type SuiWallet, type SvmAdapter, type SvmWallet, type TransactionInput, type TransactionMethod, type TransactionObject, type TransactionValue, type WalletAdapter, type WalletAvailability, type WalletBase, type WalletCapabilities, type WalletManagerConfig, type WalletPersistence, type WalletSigner, type WalletSnapshot, type WalletSource, WalletStorage, type WalletStore, type WalletStoreState, base58ToBytes, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase58, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createSignInFlow, createWalletSource, createWalletStore, groupByPlatform, hexToBytes, isShadowAdapter, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
1243
853
  //# sourceMappingURL=index.d.ts.map