@usebutr/core 0.5.0 → 1.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.
package/dist/index.d.ts CHANGED
@@ -17,172 +17,7 @@ type ChainBase = {
17
17
  reference: string;
18
18
  };
19
19
  //#endregion
20
- //#region src/storage/persistence.d.ts
21
- type MaybePromise<T> = T | Promise<T>;
22
- /** Low-level key/value driver. Sync on web (localStorage, MMKV),
23
- * async on React Native (AsyncStorage). */
24
- type StorageDriver = {
25
- getItem: (key: string) => MaybePromise<string | null>;
26
- removeItem: (key: string) => MaybePromise<void>;
27
- setItem: (key: string, value: string) => MaybePromise<void>;
28
- };
29
- type StoredPoolEntry = {
30
- account: Account;
31
- /** All known accounts on the wallet at last persist. Always contains
32
- * `account`. */
33
- accounts: Array<Account>;
34
- chainPlatform: ChainPlatform;
35
- connectorId: string;
36
- /** Wallet icon URL or data-URI captured at persist time. Lets the
37
- * shadow adapter render the same icon the live adapter will when
38
- * the store is seeded from a snapshot. Omitted when the adapter
39
- * itself has no icon. */
40
- icon?: string;
41
- /** Human-facing wallet name (e.g. "MetaMask") captured at persist
42
- * time. Required so the shadow adapter renders the same identity
43
- * the live adapter will; no "metamask" → "MetaMask" swap at the
44
- * hydration boundary. */
45
- name: string;
46
- };
47
- type StoredPoolRecord = Partial<Record<string, StoredPoolEntry>>;
48
- type StoredSelectionRecord = Partial<Record<ChainPlatform, string>>;
49
- type WalletPersistence = {
50
- clearAll: () => Promise<void>;
51
- clearPool: () => Promise<void>;
52
- getActiveConnectorId: () => Promise<string | null>;
53
- getPool: () => Promise<StoredPoolRecord>;
54
- getSelection: () => Promise<StoredSelectionRecord>;
55
- isUserDisconnected: () => Promise<boolean>;
56
- markUserDisconnected: (value: boolean) => Promise<void>;
57
- removePoolEntry: (connectorId: string) => Promise<void>;
58
- setActiveConnectorId: (connectorId: string | null) => Promise<void>;
59
- setPool: (pool: Map<string, ConnectedWallet>) => Promise<void>;
60
- setSelection: (selection: Map<ChainPlatform, string>) => Promise<void>;
61
- };
62
- //#endregion
63
- //#region src/storage/snapshot.d.ts
64
- /**
65
- * Server-safe view of a butr-persisted session; everything you can
66
- * know about a user's connected wallets from the cookie payload alone,
67
- * without instantiating a `Connector`.
68
- *
69
- * Notably absent: the `Connector` instance. A wallet extension exists
70
- * only in the browser, so a server render can know *which* wallet was
71
- * connected and *what address* it held, but cannot dispatch
72
- * `signMessage`/`sendTransaction` on it. Splitting display from action
73
- * along this seam keeps the impossibility expressed in the types
74
- * rather than hidden inside a runtime check.
75
- */
76
- type WalletSnapshot = {
77
- activeConnectorId: string | null;
78
- pool: StoredPoolRecord;
79
- selection: StoredSelectionRecord;
80
- };
81
- type CookieSource = Iterable<{
82
- name: string;
83
- value: string;
84
- }> | Iterable<[string, string]> | Readonly<Record<string, string>>;
85
- type SnapshotOptions = {
86
- /**
87
- * Same prefix passed to `WalletManagerProvider` / `WalletStorage`.
88
- * Defaults to `"butr"` to match the library default.
89
- */
90
- keyPrefix?: string;
91
- };
92
- declare const EMPTY_SNAPSHOT: WalletSnapshot;
93
- /**
94
- * Parse a cookie source into a server-safe `WalletSnapshot`.
95
- *
96
- * Pure, sync, no `document`, no React; runnable in any environment
97
- * (Server Component, route handler, edge middleware, even client
98
- * code). Pair with `createCookieStorageDriver({ initialCookies })`
99
- * and `<WalletManagerProvider initialSnapshot={…} />` to render a
100
- * connected shell server-side without a hydration flash.
101
- *
102
- * **Stale-snapshot semantics.** The snapshot reflects whatever the
103
- * browser most recently persisted. If the user has since uninstalled
104
- * the wallet, switched accounts, or disconnected in another tab, the
105
- * client-side hydration will reconcile reality and the live store
106
- * will diverge from the snapshot. Treat the snapshot as an
107
- * *optimistic* shell; accurate enough to avoid a paint flicker,
108
- * authoritative only after `useIsHydrated()` is true.
109
- *
110
- * **Inputs.** Accepts the three shapes Next.js / Express / Hono /
111
- * generic-Node cookie code naturally produces:
112
- * - A plain object: `{ "butr-pool": "{...}", … }`
113
- * - An array of `{ name, value }` (Next.js' `cookies().getAll()`)
114
- * - An iterable of `[name, value]` tuples
115
- *
116
- * Malformed entries are dropped with a `logWarn` (same policy as
117
- * `WalletStorage.getPool`); a cross-tab corruption shouldn't crash
118
- * the server render.
119
- */
120
- declare const readWalletSnapshot: (source: CookieSource, options?: SnapshotOptions) => WalletSnapshot;
121
- //#endregion
122
- //#region src/types/errors.d.ts
123
- /**
124
- * Tagged union of normalised connection errors.
125
- *
126
- * butr maps thrown values from connectors (which vary across wallet SDKs:
127
- * MetaMask uses EIP-1193 codes, Phantom throws stringly-typed errors,
128
- * embedded SDKs throw their own classes) into a small set of UX-meaningful
129
- * variants. Consumers branch on `kind` instead of regexing message strings.
130
- *
131
- * `message` is always present and human-readable. `cause` preserves the
132
- * original thrown value so callers can inspect raw connector errors when
133
- * the variant is `Unknown`.
134
- */
135
- type ConnectionError = {
136
- kind: "UserRejected";
137
- message: string;
138
- } | {
139
- kind: "RequestPending";
140
- message: string;
141
- } | {
142
- kind: "WalletLocked";
143
- message: string;
144
- } | {
145
- actualChain?: string;
146
- expectedChain?: string;
147
- kind: "ChainMismatch";
148
- message: string;
149
- } | {
150
- kind: "NotConnected";
151
- message: string;
152
- } | {
153
- kind: "Timeout";
154
- message: string;
155
- } | {
156
- cause?: unknown;
157
- kind: "Unknown";
158
- message: string;
159
- };
160
- type ConnectionErrorKind = ConnectionError["kind"];
161
- /**
162
- * Normalise a thrown value into a `ConnectionError`.
163
- *
164
- * Recognises:
165
- * - butr's own `Error("Connection timeout")` (from the 90s connect timeout)
166
- * - butr's own `Error("Failed to get account")` (from the connect flow)
167
- * - EIP-1193 numeric `code` properties (`4001` → UserRejected,
168
- * `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected:
169
- * unauthorized / disconnected from all-or-one chains)
170
- * - common message substrings: "user rejected" / "user denied",
171
- * "locked", "chain", etc.
172
- * - anything else → `Unknown` with `cause` set to the original value.
173
- */
174
- declare const mapConnectionError: (raw: unknown) => ConnectionError;
175
- //#endregion
176
- //#region src/types/wallet.d.ts
177
- /**
178
- * Canonical list of supported chain platforms. The single runtime source
179
- * of truth: `ChainPlatform` is derived from it, and storage validators
180
- * build their allowlists from it, so the type and the runtime checks can
181
- * never drift (a missing platform here was why Polkadot connections failed
182
- * to persist).
183
- */
184
- declare const CHAIN_PLATFORMS: readonly ["evm", "svm", "sui", "bitcoin", "polkadot"];
185
- type ChainPlatform = (typeof CHAIN_PLATFORMS)[number];
20
+ //#region src/types/account.d.ts
186
21
  type Account = {
187
22
  chain: ChainBase;
188
23
  id: string;
@@ -206,42 +41,8 @@ type Balance = {
206
41
  /** Raw integer amount */
207
42
  value: bigint;
208
43
  };
209
- /**
210
- * Whether a wallet is currently usable from the user's environment.
211
- *
212
- * - `installed`: the wallet is available and `connect()` can be called.
213
- * - `loadable`: the wallet's SDK can be loaded on demand (e.g. WalletConnect
214
- * modal that pops a QR code without requiring a browser extension).
215
- * - `not-installed`: the wallet isn't reachable. Consumers typically render
216
- * a "download" affordance pointing at `meta.url`.
217
- */
218
- type WalletAvailability = "installed" | "loadable" | "not-installed";
219
- /**
220
- * Events a connector can emit while connected. butr's runtime subscribes
221
- * via `Connector.subscribe?` after a successful `connect()` and dispatches
222
- * the equivalent reducer event:
223
- *
224
- * - `accountChanged` carries both the new active `account` AND the full
225
- * `accounts` array the wallet currently exposes. The runtime mirrors
226
- * that list verbatim into the pool entry. This handles two cases
227
- * uniformly:
228
- * - Multi-account wallets (MetaMask, Rabby, Brave): the user adds or
229
- * removes accounts from the dapp's permission set; the array grows
230
- * or shrinks to match.
231
- * - Single-account-exposure wallets (Phantom EVM/SVM, MetaMask Snap):
232
- * only the active account is ever in `accounts`; switching swaps it
233
- * in place rather than appending.
234
- * Also covers chain switches; the new chain lives inside `account.chain`.
235
- * - `disconnected` → `DISCONNECTED` (wallet has gone away externally:
236
- * user locked it, removed the extension, etc.).
237
- */
238
- type ConnectorEvent = {
239
- account: Account;
240
- accounts: Array<Account>;
241
- type: "accountChanged";
242
- } | {
243
- type: "disconnected";
244
- };
44
+ //#endregion
45
+ //#region src/types/capabilities.d.ts
245
46
  /**
246
47
  * Capability flags describing what an adapter can actually do at
247
48
  * runtime. Populated by each adapter (the auto-built ones derive
@@ -296,6 +97,95 @@ type WalletCapabilities = {
296
97
  * per-call `chain` input) when more than one chain is advertised. */
297
98
  switchChain: boolean;
298
99
  };
100
+ //#endregion
101
+ //#region src/types/platform.d.ts
102
+ /**
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).
108
+ */
109
+ declare const CHAIN_PLATFORMS: readonly ["evm", "svm", "sui", "bitcoin", "polkadot"];
110
+ type ChainPlatform = (typeof CHAIN_PLATFORMS)[number];
111
+ //#endregion
112
+ //#region src/types/chains-by-platform.d.ts
113
+ /**
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).
122
+ */
123
+ type ChainsByPlatform = Readonly<Record<ChainPlatform, ReadonlyArray<ChainBase>>>;
124
+ /**
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
+ * });
149
+ */
150
+ declare const buildChainsByPlatform: (partial: Partial<ChainsByPlatform>) => ChainsByPlatform;
151
+ //#endregion
152
+ //#region src/types/connector.d.ts
153
+ /**
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`.
161
+ */
162
+ type WalletAvailability = "installed" | "loadable" | "not-installed";
163
+ /**
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.).
181
+ */
182
+ type ConnectorEvent = {
183
+ account: Account;
184
+ accounts: Array<Account>;
185
+ type: "accountChanged";
186
+ } | {
187
+ type: "disconnected";
188
+ };
299
189
  /**
300
190
  * Orchestration interface: what `butr` actually calls during the
301
191
  * connect / disconnect / hydrate flow. This is the contract `butr`
@@ -335,7 +225,15 @@ type Connector<P extends ChainPlatform = ChainPlatform> = {
335
225
  /** Optional. Wallet logo as a URL or data URI. Adapters built via butr's
336
226
  * auto-discovery (EIP-6963, Wallet Standard) populate this from the
337
227
  * wallet's announced metadata; hand-rolled adapters can leave it
338
- * unset and supply icons separately via `ConnectorMeta`. */
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. */
339
237
  icon?: string;
340
238
  /** Stable key: "metamask", "phantom", etc. Pool entries are keyed by this. */
341
239
  id: string;
@@ -358,6 +256,25 @@ type Connector<P extends ChainPlatform = ChainPlatform> = {
358
256
  * themselves. */
359
257
  subscribe?: (listener: (event: ConnectorEvent) => void) => () => void;
360
258
  };
259
+ type ConnectorMeta = {
260
+ /** Optional. Sync probe that reports whether the wallet is currently
261
+ * available. Defaults to `"installed"` when omitted. Consumers call
262
+ * this at render time to gate the "Connect" button. */
263
+ availability?: () => WalletAvailability;
264
+ chainPlatform: ChainPlatform;
265
+ /** Optional image URL or data URI for wallet selection UIs. Unlike
266
+ * `Connector.icon`, this one is consumer-supplied and butr does not
267
+ * sanitize it; run it through `sanitizeIcon` if the value came from
268
+ * wallet metadata rather than your own assets. */
269
+ icon?: string;
270
+ id: string;
271
+ name: string;
272
+ /** Optional. Where to send users who don't have this wallet (download
273
+ * page, app store link, etc.). */
274
+ url?: string;
275
+ };
276
+ //#endregion
277
+ //#region src/types/wallet.d.ts
361
278
  /**
362
279
  * Methods every connected wallet supports regardless of chain. The
363
280
  * per-platform `Wallet` types extend this with their platform-specific
@@ -475,50 +392,230 @@ type SuiAdapter = Connector<"sui"> & SuiWallet;
475
392
  type BitcoinAdapter = Connector<"bitcoin"> & BitcoinWallet;
476
393
  type PolkadotAdapter = Connector<"polkadot"> & PolkadotWallet;
477
394
  /**
478
- * Full adapter interface; discriminated union by `chainPlatform`.
479
- *
480
- * Narrow on `wallet.connector.chainPlatform === "svm"` (etc.) to gain
481
- * access to platform-specific methods like `signIn` (SVM) or
482
- * `signTransaction` (SVM / Sui / Bitcoin). Calling those methods on a
483
- * non-narrowed `WalletAdapter` is a TypeScript error; that's the
484
- * point. The discriminant carries the type-level fact "this method
485
- * doesn't exist on EVM" so consumers can't accidentally branch on
486
- * `capabilities.signIn` and call a method that EVM adapters don't
487
- * implement.
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".
411
+ */
412
+ type WalletAdapter = EvmAdapter | SvmAdapter | SuiAdapter | BitcoinAdapter | PolkadotAdapter;
413
+ type ConnectedWallet = {
414
+ /** Currently-active account on this wallet. */
415
+ account: Account;
416
+ /** All accounts known on this wallet at the time of connect/refresh.
417
+ * Always contains at least `account`. Populated from `getAccounts()`
418
+ * if the connector implements it; otherwise `[account]`. */
419
+ accounts: Array<Account>;
420
+ connector: WalletAdapter;
421
+ };
422
+ //#endregion
423
+ //#region src/types/discoverer.d.ts
424
+ /**
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.
445
+ */
446
+ type PlatformDiscoverer = {
447
+ /**
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.
452
+ */
453
+ fallback?: {
454
+ subscribe: (onAdapter: (adapter: WalletAdapter) => void, opts: {
455
+ hasAnyPrimaryAdapter: () => boolean;
456
+ }) => () => void;
457
+ };
458
+ /** Primary discovery subscription. */
459
+ subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void;
460
+ };
461
+ //#endregion
462
+ //#region src/types/errors.d.ts
463
+ /**
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`.
474
+ */
475
+ type ConnectionError = {
476
+ kind: "UserRejected";
477
+ message: string;
478
+ } | {
479
+ kind: "RequestPending";
480
+ message: string;
481
+ } | {
482
+ kind: "WalletLocked";
483
+ message: string;
484
+ } | {
485
+ actualChain?: string;
486
+ expectedChain?: string;
487
+ kind: "ChainMismatch";
488
+ message: string;
489
+ } | {
490
+ kind: "NotConnected";
491
+ message: string;
492
+ } | {
493
+ kind: "Timeout";
494
+ message: string;
495
+ } | {
496
+ cause?: unknown;
497
+ kind: "Unknown";
498
+ message: string;
499
+ };
500
+ type ConnectionErrorKind = ConnectionError["kind"];
501
+ /**
502
+ * Normalise a thrown value into a `ConnectionError`.
488
503
  *
489
- * Runtime gating via `capabilities` still matters for the methods that
490
- * are OPTIONAL within a platform (a Solana wallet might or might not
491
- * advertise `solana:signTransaction`). Capabilities narrow "wallet
492
- * supports this feature"; the discriminated union narrows "this
493
- * platform has this concept at all".
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.
494
513
  */
495
- type WalletAdapter = EvmAdapter | SvmAdapter | SuiAdapter | BitcoinAdapter | PolkadotAdapter;
496
- /** Deprecated. Use one of the per-platform `*Wallet` types or
497
- * `WalletBase`. Retained as an alias so existing names resolve. */
498
- type Wallet = WalletBase;
499
- type ConnectedWallet = {
500
- /** Currently-active account on this wallet. */
514
+ declare const mapConnectionError: (raw: unknown) => ConnectionError;
515
+ //#endregion
516
+ //#region src/storage/persistence.d.ts
517
+ type MaybePromise<T> = T | Promise<T>;
518
+ /** Low-level key/value driver. Sync on web (localStorage, MMKV),
519
+ * async on React Native (AsyncStorage). */
520
+ type StorageDriver = {
521
+ getItem: (key: string) => MaybePromise<string | null>;
522
+ removeItem: (key: string) => MaybePromise<void>;
523
+ setItem: (key: string, value: string) => MaybePromise<void>;
524
+ };
525
+ type StoredPoolEntry = {
501
526
  account: Account;
502
- /** All accounts known on this wallet at the time of connect/refresh.
503
- * Always contains at least `account`. Populated from `getAccounts()`
504
- * if the connector implements it; otherwise `[account]`. */
527
+ /** All known accounts on the wallet at last persist. Always contains
528
+ * `account`. */
505
529
  accounts: Array<Account>;
506
- connector: WalletAdapter;
507
- };
508
- type ConnectorMeta = {
509
- /** Optional. Sync probe that reports whether the wallet is currently
510
- * available. Defaults to `"installed"` when omitted. Consumers call
511
- * this at render time to gate the "Connect" button. */
512
- availability?: () => WalletAvailability;
513
530
  chainPlatform: ChainPlatform;
514
- /** Optional image URL or data URI for wallet selection UIs. */
531
+ connectorId: string;
532
+ /** Wallet icon URL or data-URI captured at persist time. Lets the
533
+ * shadow adapter render the same icon the live adapter will when
534
+ * the store is seeded from a snapshot. Omitted when the adapter
535
+ * itself has no icon. */
515
536
  icon?: string;
516
- id: string;
537
+ /** Human-facing wallet name (e.g. "MetaMask") captured at persist
538
+ * time. Required so the shadow adapter renders the same identity
539
+ * the live adapter will; no "metamask" → "MetaMask" swap at the
540
+ * hydration boundary. */
517
541
  name: string;
518
- /** Optional. Where to send users who don't have this wallet (download
519
- * page, app store link, etc.). */
520
- url?: string;
521
542
  };
543
+ type StoredPoolRecord = Partial<Record<string, StoredPoolEntry>>;
544
+ type StoredSelectionRecord = Partial<Record<ChainPlatform, string>>;
545
+ type WalletPersistence = {
546
+ clearAll: () => Promise<void>;
547
+ clearPool: () => Promise<void>;
548
+ getActiveConnectorId: () => Promise<string | null>;
549
+ getPool: () => Promise<StoredPoolRecord>;
550
+ getSelection: () => Promise<StoredSelectionRecord>;
551
+ isUserDisconnected: () => Promise<boolean>;
552
+ markUserDisconnected: (value: boolean) => Promise<void>;
553
+ removePoolEntry: (connectorId: string) => Promise<void>;
554
+ setActiveConnectorId: (connectorId: string | null) => Promise<void>;
555
+ setPool: (pool: Map<string, ConnectedWallet>) => Promise<void>;
556
+ setSelection: (selection: Map<ChainPlatform, string>) => Promise<void>;
557
+ };
558
+ //#endregion
559
+ //#region src/storage/snapshot.d.ts
560
+ /**
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.
571
+ */
572
+ type WalletSnapshot = {
573
+ activeConnectorId: string | null;
574
+ pool: StoredPoolRecord;
575
+ selection: StoredSelectionRecord;
576
+ };
577
+ type CookieSource = Iterable<{
578
+ name: string;
579
+ value: string;
580
+ }> | Iterable<[string, string]> | Readonly<Record<string, string>>;
581
+ type SnapshotOptions = {
582
+ /**
583
+ * Same prefix passed to `WalletManagerProvider` / `WalletStorage`.
584
+ * Defaults to `"butr"` to match the library default.
585
+ */
586
+ keyPrefix?: string;
587
+ };
588
+ declare const EMPTY_SNAPSHOT: WalletSnapshot;
589
+ /**
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.
615
+ */
616
+ declare const readWalletSnapshot: (source: CookieSource, options?: SnapshotOptions) => WalletSnapshot;
617
+ //#endregion
618
+ //#region src/types/manager.d.ts
522
619
  /**
523
620
  * Outcome of butr's mount-time hydration pass. Passed to
524
621
  * `WalletManagerConfig.onHydrated`. Three buckets:
@@ -616,86 +713,6 @@ type WalletManagerConfig = {
616
713
  storageKeyPrefix?: string;
617
714
  };
618
715
  //#endregion
619
- //#region src/types/chains-by-platform.d.ts
620
- /**
621
- * Map of every chain platform to the list of chains the consumer wants
622
- * to expose to its UI (chain switcher, picker, etc).
623
- *
624
- * The platform key set is fixed by `ChainPlatform`. The value is the
625
- * chain list; empty when the consumer doesn't want to support that
626
- * platform's chains in this view. This is the only type that callers
627
- * write down; the values come from per-platform packages
628
- * (`EVM_CHAINS_LIST`, `SVM_CHAINS_LIST`, etc).
629
- */
630
- type ChainsByPlatform = Readonly<Record<ChainPlatform, ReadonlyArray<ChainBase>>>;
631
- /**
632
- * Build a fully-populated `ChainsByPlatform` from a partial. Platforms
633
- * the consumer doesn't specify default to an empty list.
634
- *
635
- * Use this in apps that target one or two chain platforms; importing
636
- * only those packages keeps unused chain registries out of the bundle.
637
- * Apps that want every chain reach for `CHAINS_BY_PLATFORM` from
638
- * `@usebutr/wallets` instead.
639
- *
640
- * @example
641
- * // EVM-only app: Solana/Sui/Bitcoin tables never enter the bundle
642
- * import { EVM_CHAINS_LIST } from "@usebutr/evm";
643
- * import { buildChainsByPlatform } from "@usebutr/core";
644
- *
645
- * const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });
646
- *
647
- * @example
648
- * // Multi-chain app: pull from each package the app actually uses
649
- * import { EVM_CHAINS_LIST } from "@usebutr/evm";
650
- * import { SVM_CHAINS_LIST } from "@usebutr/svm";
651
- *
652
- * const chains = buildChainsByPlatform({
653
- * evm: EVM_CHAINS_LIST,
654
- * svm: SVM_CHAINS_LIST,
655
- * });
656
- */
657
- declare const buildChainsByPlatform: (partial: Partial<ChainsByPlatform>) => ChainsByPlatform;
658
- //#endregion
659
- //#region src/types/discoverer.d.ts
660
- /**
661
- * Self-describing discovery descriptor for a single chain platform.
662
- *
663
- * Each platform package (`@usebutr/evm`, `@usebutr/svm`, `@usebutr/sui`,
664
- * `@usebutr/bitcoin`) exports one of these. The aggregator package
665
- * (`@usebutr/wallets`) composes them into `autoDiscovery()` without
666
- * needing to know per-platform defaults; the descriptor owns them.
667
- *
668
- * Adding a new chain platform means writing a `PlatformDiscoverer`
669
- * inside the new package and adding one import to the aggregator's
670
- * registry. The aggregator's logic doesn't need to grow.
671
- *
672
- * Two parts:
673
- * - `subscribe`: the primary discovery channel (EIP-6963 / Wallet
674
- * Standard / etc).
675
- * - `fallback`: optional. The legacy-injected channel that should
676
- * only emit if the primary channel hasn't produced an adapter for
677
- * the same browser session by the settle deadline. `@usebutr/evm`
678
- * has one (window.ethereum); `@usebutr/bitcoin` has one
679
- * (window.unisat / sats-connect / window.btc); SVM and Sui don't.
680
- */
681
- type PlatformDiscoverer = {
682
- /**
683
- * Optional legacy-injected fallback. Subscribes only when consumers
684
- * haven't disabled it. The hook receives `hasAnyPrimaryAdapter` so
685
- * the fallback can defer to standards-based discovery when an adapter
686
- * for the same wallet has already announced through the primary path.
687
- */
688
- fallback?: {
689
- subscribe: (onAdapter: (adapter: WalletAdapter) => void, opts: {
690
- hasAnyPrimaryAdapter: () => boolean;
691
- }) => () => void;
692
- };
693
- /** Stable platform identifier. Used by the aggregator for keying. */
694
- platform: ChainPlatform;
695
- /** Primary discovery subscription. */
696
- subscribe: (onAdapter: (adapter: WalletAdapter) => void) => () => void;
697
- };
698
- //#endregion
699
716
  //#region src/types/signer.d.ts
700
717
  /**
701
718
  * Per-platform signer type registry.
@@ -1013,6 +1030,124 @@ type WalletStore = ReturnType<typeof createWalletStore>;
1013
1030
  type WalletStoreState = ExtractState<WalletStore>;
1014
1031
  declare const createWalletStore: (config: WalletManagerConfig) => import("zustand/vanilla").StoreApi<State & RuntimeMembers>;
1015
1032
  //#endregion
1033
+ //#region src/group-by-platform.d.ts
1034
+ /**
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.
1055
+ */
1056
+ declare const groupByPlatform: <T>(items: ReadonlyArray<T>, getPlatform: (item: T) => ChainPlatform) => Map<ChainPlatform, Array<T>>;
1057
+ //#endregion
1058
+ //#region src/sign-in/sign-in-flow.d.ts
1059
+ /**
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.
1067
+ */
1068
+ type SignInResult = {
1069
+ account: Account;
1070
+ /** The message handed to the wallet. Absent on the SIWS path, where the
1071
+ * wallet composes the message itself. */
1072
+ message?: string;
1073
+ nonce: string;
1074
+ signature: Uint8Array;
1075
+ /** Base64 of `signature`. */
1076
+ signatureBase64: string;
1077
+ signedMessage: Uint8Array;
1078
+ /** Base64 of `signedMessage`; verify against this. */
1079
+ signedMessageBase64: string;
1080
+ wallet: ConnectedWallet;
1081
+ };
1082
+ type SignInMessageContext = {
1083
+ account: Account;
1084
+ nonce: string;
1085
+ wallet: ConnectedWallet;
1086
+ };
1087
+ type SignInFlowOptions = {
1088
+ /**
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.
1098
+ */
1099
+ buildMessage?: (ctx: SignInMessageContext) => string;
1100
+ /** Fetch a single-use nonce from your backend. */
1101
+ getNonce: (ctx: {
1102
+ account: Account;
1103
+ wallet: ConnectedWallet;
1104
+ }) => Promise<string>;
1105
+ /**
1106
+ * Skip the Sign In With Solana path even on wallets that advertise it,
1107
+ * forcing every platform down the same `signMessage` route. Useful when
1108
+ * one backend verifier has to handle every chain identically.
1109
+ */
1110
+ preferSignMessage?: boolean;
1111
+ /** Hand the signed result to your backend. Throw to fail the flow. */
1112
+ verify: (result: SignInResult) => Promise<void>;
1113
+ };
1114
+ /** Thrown before any wallet interaction when the wallet can't sign at
1115
+ * all. Distinct from a rejection: nothing was asked of the user, so UI
1116
+ * should say "this wallet can't sign in" rather than "you declined". */
1117
+ declare class SignInUnsupportedError extends Error {
1118
+ readonly connectorId: string;
1119
+ constructor(connectorId: string);
1120
+ }
1121
+ /**
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.
1146
+ */
1147
+ declare const createSignInFlow: (options: SignInFlowOptions) => {
1148
+ signIn: (wallet: ConnectedWallet, account?: Account) => Promise<SignInResult>;
1149
+ };
1150
+ //#endregion
1016
1151
  //#region src/wallet-equal.d.ts
1017
1152
  /**
1018
1153
  * Two wallet snapshots are equivalent for selector purposes iff they
@@ -1052,6 +1187,11 @@ declare const logError: (...args: ReadonlyArray<unknown>) => void;
1052
1187
  * icon as absent, so consumers get either a usable string or
1053
1188
  * `undefined`: never a blank or malformed one. `undefined` passes
1054
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.
1055
1195
  */
1056
1196
  declare const sanitizeIcon: (icon: string | undefined) => string | undefined;
1057
1197
  //#endregion
@@ -1089,6 +1229,15 @@ declare const hexToBytes: (hex: string) => Uint8Array;
1089
1229
  declare const base64ToBytes: (b64: string) => Uint8Array;
1090
1230
  /** Cross-platform `Uint8Array` → base64. */
1091
1231
  declare const bytesToBase64: (bytes: Uint8Array) => string;
1232
+ /**
1233
+ * `Uint8Array` → base58 (Solana addresses and signatures). Leading zero
1234
+ * bytes are significant in base58 and survive the BigInt round-trip only
1235
+ * because they're re-prefixed as `1`s afterwards.
1236
+ */
1237
+ declare const bytesToBase58: (bytes: Uint8Array) => string;
1238
+ /** Decode base58 into raw bytes. Throws on characters outside the alphabet
1239
+ * rather than silently dropping them. */
1240
+ declare const base58ToBytes: (input: string) => Uint8Array;
1092
1241
  //#endregion
1093
- 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 SignerForPlatform, type SignerOf, type SnapshotOptions, type StorageDriver, type StoredPoolEntry, type StoredPoolRecord, type StoredSelectionRecord, type SuiAdapter, type SuiWallet, type SvmAdapter, type SvmWallet, type Wallet, type WalletAdapter, type WalletAvailability, type WalletBase, type WalletCapabilities, type WalletManagerConfig, type WalletPersistence, type WalletSnapshot, type WalletSource, WalletStorage, type WalletStore, type WalletStoreState, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, hexToBytes, isShadowAdapter, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
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 };
1094
1243
  //# sourceMappingURL=index.d.ts.map