@usebutr/core 0.4.0 → 0.4.2
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 +167 -131
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -125
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { createStore } from "zustand/vanilla";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
//#region src/types/chains-by-platform.ts
|
|
4
5
|
/**
|
|
5
6
|
* Build a fully-populated `ChainsByPlatform` from a partial. Platforms
|
|
6
7
|
* the consumer doesn't specify default to an empty list.
|
|
7
8
|
*
|
|
8
|
-
* Use this in apps that target one or two chain platforms
|
|
9
|
+
* Use this in apps that target one or two chain platforms; importing
|
|
9
10
|
* only those packages keeps unused chain registries out of the bundle.
|
|
10
11
|
* Apps that want every chain reach for `CHAINS_BY_PLATFORM` from
|
|
11
12
|
* `@usebutr/wallets` instead.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
|
-
* // EVM-only app
|
|
15
|
+
* // EVM-only app: Solana/Sui/Bitcoin tables never enter the bundle
|
|
15
16
|
* import { EVM_CHAINS_LIST } from "@usebutr/evm";
|
|
16
17
|
* import { buildChainsByPlatform } from "@usebutr/core";
|
|
17
18
|
*
|
|
18
19
|
* const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });
|
|
19
20
|
*
|
|
20
21
|
* @example
|
|
21
|
-
* // Multi-chain app
|
|
22
|
+
* // Multi-chain app: pull from each package the app actually uses
|
|
22
23
|
* import { EVM_CHAINS_LIST } from "@usebutr/evm";
|
|
23
24
|
* import { SVM_CHAINS_LIST } from "@usebutr/svm";
|
|
24
25
|
*
|
|
@@ -44,7 +45,7 @@ const buildChainsByPlatform = (partial) => ({
|
|
|
44
45
|
* - butr's own `Error("Connection timeout")` (from the 90s connect timeout)
|
|
45
46
|
* - butr's own `Error("Failed to get account")` (from the connect flow)
|
|
46
47
|
* - EIP-1193 numeric `code` properties (`4001` → UserRejected,
|
|
47
|
-
* `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected
|
|
48
|
+
* `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected:
|
|
48
49
|
* unauthorized / disconnected from all-or-one chains)
|
|
49
50
|
* - common message substrings: "user rejected" / "user denied",
|
|
50
51
|
* "locked", "chain", etc.
|
|
@@ -62,7 +63,7 @@ const mapConnectionError = (raw) => {
|
|
|
62
63
|
kind: "NotConnected",
|
|
63
64
|
message
|
|
64
65
|
};
|
|
65
|
-
const code = raw.code;
|
|
66
|
+
const code = "code" in raw ? raw.code : void 0;
|
|
66
67
|
if (code === 4001) return {
|
|
67
68
|
kind: "UserRejected",
|
|
68
69
|
message
|
|
@@ -120,6 +121,18 @@ const CHAIN_PLATFORMS = [
|
|
|
120
121
|
"bitcoin",
|
|
121
122
|
"polkadot"
|
|
122
123
|
];
|
|
124
|
+
/**
|
|
125
|
+
* Build butr's `Account` shape from a wallet address and a resolved
|
|
126
|
+
* `ChainBase`. The composite id (`<chain>:<address>`) is what the
|
|
127
|
+
* reducer uses to compare accounts across refreshes, so every adapter
|
|
128
|
+
* must build accounts through this helper (or keep the format
|
|
129
|
+
* byte-identical).
|
|
130
|
+
*/
|
|
131
|
+
const buildAccount = (address, chain) => ({
|
|
132
|
+
chain,
|
|
133
|
+
id: `${chain.id}:${address}`,
|
|
134
|
+
walletAddress: address
|
|
135
|
+
});
|
|
123
136
|
|
|
124
137
|
//#endregion
|
|
125
138
|
//#region src/wallet-source.ts
|
|
@@ -166,7 +179,7 @@ const createMemoryStorageDriver = () => {
|
|
|
166
179
|
};
|
|
167
180
|
const createWebStorageDriver = (kind) => {
|
|
168
181
|
if (!hasWebStorage(kind)) return createMemoryStorageDriver();
|
|
169
|
-
const storage = globalThis
|
|
182
|
+
const storage = kind === "localStorage" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
170
183
|
return {
|
|
171
184
|
getItem(key) {
|
|
172
185
|
try {
|
|
@@ -198,41 +211,36 @@ const createBrowserStorageDriver = () => ({
|
|
|
198
211
|
});
|
|
199
212
|
|
|
200
213
|
//#endregion
|
|
201
|
-
//#region src/storage/
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
|
|
227
|
-
if (typeof entry.connectorId !== "string" || entry.connectorId !== key) return false;
|
|
228
|
-
if (typeof entry.chainPlatform !== "string") return false;
|
|
229
|
-
if (!VALID_CHAIN_PLATFORMS$1.has(entry.chainPlatform)) return false;
|
|
230
|
-
if (typeof entry.name !== "string" || entry.name.length === 0) return false;
|
|
231
|
-
if (entry.icon !== void 0 && typeof entry.icon !== "string") return false;
|
|
232
|
-
if (!isValidAccount$1(entry.account)) return false;
|
|
233
|
-
if (!Array.isArray(entry.accounts) || !entry.accounts.every(isValidAccount$1)) return false;
|
|
234
|
-
return true;
|
|
214
|
+
//#region src/storage/validation.ts
|
|
215
|
+
const chainPlatformSchema = z.enum(CHAIN_PLATFORMS);
|
|
216
|
+
const chainSchema = z.looseObject({
|
|
217
|
+
id: z.string(),
|
|
218
|
+
name: z.string(),
|
|
219
|
+
namespace: z.string(),
|
|
220
|
+
reference: z.string()
|
|
221
|
+
});
|
|
222
|
+
const accountSchema = z.looseObject({
|
|
223
|
+
chain: chainSchema,
|
|
224
|
+
id: z.string(),
|
|
225
|
+
walletAddress: z.string()
|
|
226
|
+
});
|
|
227
|
+
const storedPoolEntrySchema = z.looseObject({
|
|
228
|
+
account: accountSchema,
|
|
229
|
+
accounts: z.array(accountSchema),
|
|
230
|
+
chainPlatform: chainPlatformSchema,
|
|
231
|
+
connectorId: z.string(),
|
|
232
|
+
icon: z.string().optional(),
|
|
233
|
+
name: z.string().min(1)
|
|
234
|
+
});
|
|
235
|
+
const recordSchema = z.record(z.string(), z.unknown());
|
|
236
|
+
const parseStoredPoolEntry = (key, value) => {
|
|
237
|
+
const parsed = storedPoolEntrySchema.safeParse(value);
|
|
238
|
+
if (!parsed.success || parsed.data.connectorId !== key) return null;
|
|
239
|
+
return parsed.data;
|
|
235
240
|
};
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/storage/wallet-storage.ts
|
|
236
244
|
var WalletStorage = class {
|
|
237
245
|
poolKey;
|
|
238
246
|
selectionKey;
|
|
@@ -246,7 +254,7 @@ var WalletStorage = class {
|
|
|
246
254
|
* this, two simultaneous `setPool` calls both read the pre-write
|
|
247
255
|
* state, each merge their own entries, and whichever finishes last
|
|
248
256
|
* overwrites the other's additions. Reads (`getPool`) don't enter
|
|
249
|
-
* the queue
|
|
257
|
+
* the queue; they observe whatever's currently in the driver.
|
|
250
258
|
*/
|
|
251
259
|
poolMutationQueue = Promise.resolve();
|
|
252
260
|
constructor(config) {
|
|
@@ -282,15 +290,19 @@ var WalletStorage = class {
|
|
|
282
290
|
async getPool() {
|
|
283
291
|
try {
|
|
284
292
|
const stored = await this.persistent.getItem(this.poolKey);
|
|
285
|
-
if (
|
|
286
|
-
const
|
|
287
|
-
|
|
293
|
+
if (stored === null || stored === "") return {};
|
|
294
|
+
const value = JSON.parse(stored);
|
|
295
|
+
const parsed = recordSchema.safeParse(value);
|
|
296
|
+
if (!parsed.success) {
|
|
288
297
|
await this.clearPool();
|
|
289
298
|
return {};
|
|
290
299
|
}
|
|
291
300
|
const result = {};
|
|
292
|
-
for (const [key,
|
|
293
|
-
|
|
301
|
+
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
302
|
+
const entry = parseStoredPoolEntry(key, entryValue);
|
|
303
|
+
if (entry === null) logWarn(`[butr] dropping invalid pool entry for ${key}`);
|
|
304
|
+
else result[key] = entry;
|
|
305
|
+
}
|
|
294
306
|
return result;
|
|
295
307
|
} catch (error) {
|
|
296
308
|
logWarn("[butr] failed to parse pool from storage:", error);
|
|
@@ -302,14 +314,14 @@ var WalletStorage = class {
|
|
|
302
314
|
* Upsert the in-memory pool into storage. Additive: entries in
|
|
303
315
|
* `pool` are written; entries already in storage that aren't in
|
|
304
316
|
* `pool` are kept. The in-memory pool reflects "what's live right
|
|
305
|
-
* now", not "the complete list of remembered connections"
|
|
317
|
+
* now", not "the complete list of remembered connections"; a
|
|
306
318
|
* silent reconnect that fails on reload leaves the entry out of
|
|
307
319
|
* the pool but the saved entry stays so the next load can retry.
|
|
308
320
|
* Use `removePoolEntry` for explicit eviction (the user clicked
|
|
309
321
|
* Disconnect) and `clearAll` for a full wipe (reset).
|
|
310
322
|
*/
|
|
311
|
-
setPool(pool) {
|
|
312
|
-
|
|
323
|
+
async setPool(pool) {
|
|
324
|
+
await this.serializePoolMutation(async () => {
|
|
313
325
|
try {
|
|
314
326
|
const serializable = { ...await this.getPool() };
|
|
315
327
|
for (const [connectorId, wallet] of pool) {
|
|
@@ -321,7 +333,7 @@ var WalletStorage = class {
|
|
|
321
333
|
icon: wallet.connector.icon,
|
|
322
334
|
name: wallet.connector.name
|
|
323
335
|
};
|
|
324
|
-
if (
|
|
336
|
+
if (parseStoredPoolEntry(connectorId, entry) === null) throw new Error(`[butr] refusing to persist invalid pool entry for ${connectorId}`);
|
|
325
337
|
serializable[connectorId] = entry;
|
|
326
338
|
}
|
|
327
339
|
await this.persistent.setItem(this.poolKey, JSON.stringify(serializable));
|
|
@@ -330,8 +342,8 @@ var WalletStorage = class {
|
|
|
330
342
|
}
|
|
331
343
|
});
|
|
332
344
|
}
|
|
333
|
-
removePoolEntry(connectorId) {
|
|
334
|
-
|
|
345
|
+
async removePoolEntry(connectorId) {
|
|
346
|
+
await this.serializePoolMutation(async () => {
|
|
335
347
|
try {
|
|
336
348
|
const stored = await this.getPool();
|
|
337
349
|
if (stored[connectorId]) {
|
|
@@ -343,19 +355,23 @@ var WalletStorage = class {
|
|
|
343
355
|
}
|
|
344
356
|
});
|
|
345
357
|
}
|
|
346
|
-
clearPool() {
|
|
347
|
-
|
|
358
|
+
async clearPool() {
|
|
359
|
+
await this.serializePoolMutation(async () => {
|
|
348
360
|
await this.persistent.removeItem(this.poolKey);
|
|
349
361
|
});
|
|
350
362
|
}
|
|
351
363
|
async getSelection() {
|
|
352
364
|
try {
|
|
353
365
|
const stored = await this.persistent.getItem(this.selectionKey);
|
|
354
|
-
if (
|
|
355
|
-
const
|
|
356
|
-
|
|
366
|
+
if (stored === null || stored === "") return {};
|
|
367
|
+
const value = JSON.parse(stored);
|
|
368
|
+
const parsed = recordSchema.safeParse(value);
|
|
369
|
+
if (!parsed.success) return {};
|
|
357
370
|
const result = {};
|
|
358
|
-
for (const [key,
|
|
371
|
+
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
372
|
+
const platform = chainPlatformSchema.safeParse(key);
|
|
373
|
+
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
374
|
+
}
|
|
359
375
|
return result;
|
|
360
376
|
} catch (error) {
|
|
361
377
|
logWarn("[butr] failed to parse selection from storage:", error);
|
|
@@ -374,7 +390,7 @@ var WalletStorage = class {
|
|
|
374
390
|
async getActiveConnectorId() {
|
|
375
391
|
try {
|
|
376
392
|
const value = await this.persistent.getItem(this.activeKey);
|
|
377
|
-
return value && value.length > 0 ? value : null;
|
|
393
|
+
return value !== null && value.length > 0 ? value : null;
|
|
378
394
|
} catch {
|
|
379
395
|
return null;
|
|
380
396
|
}
|
|
@@ -462,6 +478,8 @@ const createConnectorLifecycle = (handlers) => {
|
|
|
462
478
|
|
|
463
479
|
//#endregion
|
|
464
480
|
//#region src/store/hydration.ts
|
|
481
|
+
const VALID_CHAIN_PLATFORMS$1 = new Set(CHAIN_PLATFORMS);
|
|
482
|
+
const isChainPlatform$1 = (value) => VALID_CHAIN_PLATFORMS$1.has(value);
|
|
465
483
|
/** Pick the right `accounts` list: fresh from the connector if it
|
|
466
484
|
* exposes `getAccounts`, otherwise the persisted list, otherwise just
|
|
467
485
|
* the active account. */
|
|
@@ -476,7 +494,7 @@ const resolveHydratedAccounts = async (connector, storedAccounts, fallbackAccoun
|
|
|
476
494
|
const restoreOneEntry = async (connectorId, entry, connector) => {
|
|
477
495
|
try {
|
|
478
496
|
await connector.connect({ silent: true });
|
|
479
|
-
const accountToUse = await connector.getAccount()
|
|
497
|
+
const accountToUse = await connector.getAccount() ?? entry.account;
|
|
480
498
|
return {
|
|
481
499
|
connectorId,
|
|
482
500
|
entry: {
|
|
@@ -538,13 +556,13 @@ const createHydrationCoordinator = (storage, createConnector) => {
|
|
|
538
556
|
});
|
|
539
557
|
}
|
|
540
558
|
const selection = /* @__PURE__ */ new Map();
|
|
541
|
-
for (const [platform, connectorId] of Object.entries(storedSelection)) if (connectorId && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
559
|
+
for (const [platform, connectorId] of Object.entries(storedSelection)) if (isChainPlatform$1(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
542
560
|
for (const [connectorId, wallet] of pool) {
|
|
543
561
|
const platform = wallet.connector.chainPlatform;
|
|
544
562
|
if (!selection.has(platform)) selection.set(platform, connectorId);
|
|
545
563
|
}
|
|
546
564
|
let activeConnectorId = null;
|
|
547
|
-
if (storedActive && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
565
|
+
if (storedActive !== null && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
548
566
|
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
549
567
|
return {
|
|
550
568
|
activeConnectorId,
|
|
@@ -586,8 +604,8 @@ const reducer = (state, event) => {
|
|
|
586
604
|
const selection = new Map(state.selection);
|
|
587
605
|
for (const [platform, id] of event.selection) selection.set(platform, id);
|
|
588
606
|
let activeConnectorId = null;
|
|
589
|
-
if (event.activeConnectorId && pool.has(event.activeConnectorId)) activeConnectorId = event.activeConnectorId;
|
|
590
|
-
else if (state.activeConnectorId && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
607
|
+
if (event.activeConnectorId !== null && pool.has(event.activeConnectorId)) activeConnectorId = event.activeConnectorId;
|
|
608
|
+
else if (state.activeConnectorId !== null && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
591
609
|
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
592
610
|
let nextReconnecting = state.reconnectingIds;
|
|
593
611
|
if (state.reconnectingIds.size > 0 && event.pool.size > 0) {
|
|
@@ -655,7 +673,7 @@ const reducer = (state, event) => {
|
|
|
655
673
|
if (newSelection.get(platform) === event.connectorId) {
|
|
656
674
|
newSelection.delete(platform);
|
|
657
675
|
const fallback = findConnectorForPlatform(newPool, platform);
|
|
658
|
-
if (fallback) newSelection.set(platform, fallback);
|
|
676
|
+
if (fallback !== void 0) newSelection.set(platform, fallback);
|
|
659
677
|
}
|
|
660
678
|
const firstId = newPool.keys().next();
|
|
661
679
|
const activeConnectorId = state.activeConnectorId === event.connectorId ? firstId.value ?? null : state.activeConnectorId;
|
|
@@ -734,12 +752,12 @@ const reducer = (state, event) => {
|
|
|
734
752
|
//#endregion
|
|
735
753
|
//#region src/store/shadow-adapter.ts
|
|
736
754
|
/**
|
|
737
|
-
* Error thrown when a method is called on a shadow adapter
|
|
755
|
+
* Error thrown when a method is called on a shadow adapter; the
|
|
738
756
|
* placeholder `WalletAdapter` that the store seeds into the pool when
|
|
739
757
|
* an `initialState` is provided (e.g. from a server-rendered cookie
|
|
740
758
|
* snapshot). Shadow adapters carry the identity and account data of a
|
|
741
759
|
* previously-connected wallet, but the live wallet extension hasn't
|
|
742
|
-
* been verified yet
|
|
760
|
+
* been verified yet; the silent reconnect happens asynchronously
|
|
743
761
|
* after mount.
|
|
744
762
|
*
|
|
745
763
|
* UI code that gates affordances on `wallet.connector.capabilities.*`
|
|
@@ -780,7 +798,7 @@ const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
|
780
798
|
* is provided: each stored entry becomes a `ConnectedWallet` whose
|
|
781
799
|
* `connector` is one of these. The store flips `isHydrated` true
|
|
782
800
|
* synchronously and exposes the data through the usual hooks
|
|
783
|
-
* (`useActiveWallet`, `useConnectedWallets`, …)
|
|
801
|
+
* (`useActiveWallet`, `useConnectedWallets`, …), but the connector
|
|
784
802
|
* can't actually talk to a wallet yet, so its capabilities are all
|
|
785
803
|
* `false` and its methods throw `ShadowConnectorError` if called.
|
|
786
804
|
*
|
|
@@ -856,13 +874,15 @@ const run = async (fn, onError) => {
|
|
|
856
874
|
|
|
857
875
|
//#endregion
|
|
858
876
|
//#region src/store/wallet-store.ts
|
|
877
|
+
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
878
|
+
const isChainPlatform = (value) => VALID_CHAIN_PLATFORMS.has(value);
|
|
859
879
|
/**
|
|
860
880
|
* Build a synchronously-populated `State` from `config.initialState`.
|
|
861
881
|
* Each pool entry becomes a `ConnectedWallet` whose `connector` is a
|
|
862
882
|
* shadow adapter; every connector id enters `reconnectingIds` so
|
|
863
883
|
* consumers can branch on "is this connection verified" without
|
|
864
884
|
* waiting for the async silent reconnect. `isHydrated` flips true
|
|
865
|
-
* synchronously
|
|
885
|
+
* synchronously: the consumer's first render sees the persisted
|
|
866
886
|
* state in the live store, not undefined.
|
|
867
887
|
*/
|
|
868
888
|
const seedStateFromSnapshot = (snapshot) => {
|
|
@@ -879,8 +899,8 @@ const seedStateFromSnapshot = (snapshot) => {
|
|
|
879
899
|
reconnectingIds.add(connectorId);
|
|
880
900
|
}
|
|
881
901
|
const selection = /* @__PURE__ */ new Map();
|
|
882
|
-
for (const [platform, connectorId] of Object.entries(snapshot.selection)) if (connectorId && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
883
|
-
const activeConnectorId = snapshot.activeConnectorId && pool.has(snapshot.activeConnectorId) ? snapshot.activeConnectorId : pool.keys().next().value ?? null;
|
|
902
|
+
for (const [platform, connectorId] of Object.entries(snapshot.selection)) if (isChainPlatform(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
903
|
+
const activeConnectorId = snapshot.activeConnectorId !== null && pool.has(snapshot.activeConnectorId) ? snapshot.activeConnectorId : pool.keys().next().value ?? null;
|
|
884
904
|
return {
|
|
885
905
|
...initialState,
|
|
886
906
|
activeConnectorId,
|
|
@@ -894,8 +914,8 @@ const CONNECT_TIMEOUT_MS = 9e4;
|
|
|
894
914
|
const DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5e3;
|
|
895
915
|
const normaliseAddress = (addr) => addr.toLowerCase();
|
|
896
916
|
const createWalletStore = (config) => {
|
|
897
|
-
const storageKeyPrefix = config.storageKeyPrefix || "butr";
|
|
898
|
-
const storage = config.storage
|
|
917
|
+
const storageKeyPrefix = config.storageKeyPrefix === void 0 || config.storageKeyPrefix === "" ? "butr" : config.storageKeyPrefix;
|
|
918
|
+
const storage = config.storage ?? new WalletStorage({ keyPrefix: storageKeyPrefix });
|
|
899
919
|
const reportStorageError = (context) => (error) => {
|
|
900
920
|
if (config.onStorageError) {
|
|
901
921
|
try {
|
|
@@ -956,7 +976,7 @@ const createWalletStore = (config) => {
|
|
|
956
976
|
logWarn("[butr] onSlowConnect threw:", cbError);
|
|
957
977
|
}
|
|
958
978
|
}, slowThreshold) : null;
|
|
959
|
-
let connectTimeoutId
|
|
979
|
+
let connectTimeoutId;
|
|
960
980
|
try {
|
|
961
981
|
const connectPromise = connector.connect();
|
|
962
982
|
connectPromise.catch(() => {});
|
|
@@ -1002,10 +1022,10 @@ const createWalletStore = (config) => {
|
|
|
1002
1022
|
} catch (cbError) {
|
|
1003
1023
|
logWarn("[butr] onConnectError threw:", cbError);
|
|
1004
1024
|
}
|
|
1005
|
-
onError?.(error);
|
|
1025
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1006
1026
|
throw error;
|
|
1007
1027
|
} finally {
|
|
1008
|
-
|
|
1028
|
+
clearTimeout(connectTimeoutId);
|
|
1009
1029
|
if (slowTimer) clearTimeout(slowTimer);
|
|
1010
1030
|
}
|
|
1011
1031
|
},
|
|
@@ -1042,7 +1062,9 @@ const createWalletStore = (config) => {
|
|
|
1042
1062
|
});
|
|
1043
1063
|
for (const [connectorId, wallet] of result.pool) lifecycle.attach(connectorId, wallet.connector);
|
|
1044
1064
|
await Promise.all([persistSelection(), persistActive()]);
|
|
1045
|
-
for (const id of hydration.pendingIds()) run(() => get().tryRestoreFromPending(id), (e) =>
|
|
1065
|
+
for (const id of hydration.pendingIds()) run(() => get().tryRestoreFromPending(id), (e) => {
|
|
1066
|
+
logWarn(`[butr] late restore rejected for ${id}:`, e);
|
|
1067
|
+
});
|
|
1046
1068
|
if (config.onHydrated) try {
|
|
1047
1069
|
config.onHydrated({
|
|
1048
1070
|
dropped: result.dropped,
|
|
@@ -1141,7 +1163,8 @@ const createWalletStore = (config) => {
|
|
|
1141
1163
|
id: `${newChain.id}:${normaliseAddress(a.walletAddress)}`,
|
|
1142
1164
|
walletAddress: a.walletAddress
|
|
1143
1165
|
});
|
|
1144
|
-
|
|
1166
|
+
const accounts = remapped.some((a) => normaliseAddress(a.walletAddress) === normaliseAddress(account.walletAddress)) ? remapped : [...remapped, account];
|
|
1167
|
+
refreshPoolEntry(connectorId, accounts, account);
|
|
1145
1168
|
}
|
|
1146
1169
|
};
|
|
1147
1170
|
});
|
|
@@ -1172,7 +1195,7 @@ const readCookies = () => {
|
|
|
1172
1195
|
};
|
|
1173
1196
|
const buildCookieString = (name, value, options, expired = false) => {
|
|
1174
1197
|
const parts = [`${name}=${encode(value)}`, `Path=${options.path ?? "/"}`];
|
|
1175
|
-
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
1198
|
+
if (options.domain !== void 0 && options.domain !== "") parts.push(`Domain=${options.domain}`);
|
|
1176
1199
|
if (expired) parts.push("Max-Age=0");
|
|
1177
1200
|
else {
|
|
1178
1201
|
const maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
|
|
@@ -1184,11 +1207,11 @@ const buildCookieString = (name, value, options, expired = false) => {
|
|
|
1184
1207
|
};
|
|
1185
1208
|
const toCookieMap$1 = (input) => {
|
|
1186
1209
|
if (!input) return null;
|
|
1187
|
-
if (
|
|
1210
|
+
if (Symbol.iterator in input) return new Map(input);
|
|
1188
1211
|
return new Map(Object.entries(input));
|
|
1189
1212
|
};
|
|
1190
1213
|
/**
|
|
1191
|
-
* Cookie-backed storage driver. Reads/writes `document.cookie
|
|
1214
|
+
* Cookie-backed storage driver. Reads/writes `document.cookie`;
|
|
1192
1215
|
* server-readable, survives reloads, scoped per `domain`/`path`.
|
|
1193
1216
|
*
|
|
1194
1217
|
* **When to use this:** SSR apps that need to know who's connected
|
|
@@ -1202,13 +1225,13 @@ const toCookieMap$1 = (input) => {
|
|
|
1202
1225
|
* **Trade-offs vs `localStorage`:** cookies travel with every
|
|
1203
1226
|
* request, so they cost bytes on the wire. Keep the storage key
|
|
1204
1227
|
* prefix short, and prefer this driver for the `persistent` slot
|
|
1205
|
-
* only
|
|
1228
|
+
* only: the `session` slot can stay in `sessionStorage` (which
|
|
1206
1229
|
* cookies can't natively model anyway).
|
|
1207
1230
|
*
|
|
1208
1231
|
* **Server-side writes are no-ops.** Emitting `Set-Cookie` requires
|
|
1209
1232
|
* access to the framework's response object, which a storage driver
|
|
1210
1233
|
* shouldn't reach into. The store doesn't mutate persisted state
|
|
1211
|
-
* during the SSR pass anyway
|
|
1234
|
+
* during the SSR pass anyway; writes only fire after client mount,
|
|
1212
1235
|
* once `document.cookie` is reachable.
|
|
1213
1236
|
*/
|
|
1214
1237
|
const createCookieStorageDriver = (options = {}) => {
|
|
@@ -1244,14 +1267,13 @@ const createCookieStorageDriver = (options = {}) => {
|
|
|
1244
1267
|
|
|
1245
1268
|
//#endregion
|
|
1246
1269
|
//#region src/storage/snapshot.ts
|
|
1247
|
-
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
1248
1270
|
const EMPTY_SNAPSHOT = Object.freeze({
|
|
1249
1271
|
activeConnectorId: null,
|
|
1250
1272
|
pool: Object.freeze({}),
|
|
1251
1273
|
selection: Object.freeze({})
|
|
1252
1274
|
});
|
|
1253
1275
|
const toCookieMap = (input) => {
|
|
1254
|
-
if (
|
|
1276
|
+
if (!(Symbol.iterator in input)) return new Map(Object.entries(input));
|
|
1255
1277
|
const out = /* @__PURE__ */ new Map();
|
|
1256
1278
|
for (const entry of input) {
|
|
1257
1279
|
if (Array.isArray(entry)) {
|
|
@@ -1259,37 +1281,22 @@ const toCookieMap = (input) => {
|
|
|
1259
1281
|
if (typeof name === "string" && typeof value === "string") out.set(name, value);
|
|
1260
1282
|
continue;
|
|
1261
1283
|
}
|
|
1262
|
-
if (
|
|
1284
|
+
if (typeof entry.name === "string" && typeof entry.value === "string") out.set(entry.name, entry.value);
|
|
1263
1285
|
}
|
|
1264
1286
|
return out;
|
|
1265
1287
|
};
|
|
1266
|
-
const isValidAccount = (value) => {
|
|
1267
|
-
if (!value || typeof value !== "object") return false;
|
|
1268
|
-
const account = value;
|
|
1269
|
-
if (typeof account.walletAddress !== "string" || typeof account.id !== "string") return false;
|
|
1270
|
-
if (!account.chain || typeof account.chain !== "object") return false;
|
|
1271
|
-
return true;
|
|
1272
|
-
};
|
|
1273
|
-
const isValidPoolEntry = (key, value) => {
|
|
1274
|
-
if (!value || typeof value !== "object") return false;
|
|
1275
|
-
const entry = value;
|
|
1276
|
-
if (typeof entry.connectorId !== "string" || entry.connectorId !== key) return false;
|
|
1277
|
-
if (typeof entry.chainPlatform !== "string") return false;
|
|
1278
|
-
if (!VALID_CHAIN_PLATFORMS.has(entry.chainPlatform)) return false;
|
|
1279
|
-
if (typeof entry.name !== "string" || entry.name.length === 0) return false;
|
|
1280
|
-
if (entry.icon !== void 0 && typeof entry.icon !== "string") return false;
|
|
1281
|
-
if (!isValidAccount(entry.account)) return false;
|
|
1282
|
-
if (!Array.isArray(entry.accounts) || !entry.accounts.every(isValidAccount)) return false;
|
|
1283
|
-
return true;
|
|
1284
|
-
};
|
|
1285
1288
|
const parsePool = (raw) => {
|
|
1286
|
-
if (
|
|
1289
|
+
if (raw === void 0 || raw === "") return {};
|
|
1287
1290
|
try {
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1291
|
+
const value = JSON.parse(raw);
|
|
1292
|
+
const parsed = recordSchema.safeParse(value);
|
|
1293
|
+
if (!parsed.success) return {};
|
|
1290
1294
|
const result = {};
|
|
1291
|
-
for (const [key,
|
|
1292
|
-
|
|
1295
|
+
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
1296
|
+
const entry = parseStoredPoolEntry(key, entryValue);
|
|
1297
|
+
if (entry === null) logWarn(`[butr] readWalletSnapshot: dropping invalid pool entry for ${key}`);
|
|
1298
|
+
else result[key] = entry;
|
|
1299
|
+
}
|
|
1293
1300
|
return result;
|
|
1294
1301
|
} catch (error) {
|
|
1295
1302
|
logWarn("[butr] readWalletSnapshot: failed to parse pool cookie:", error);
|
|
@@ -1297,12 +1304,16 @@ const parsePool = (raw) => {
|
|
|
1297
1304
|
}
|
|
1298
1305
|
};
|
|
1299
1306
|
const parseSelection = (raw) => {
|
|
1300
|
-
if (
|
|
1307
|
+
if (raw === void 0 || raw === "") return {};
|
|
1301
1308
|
try {
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1309
|
+
const value = JSON.parse(raw);
|
|
1310
|
+
const parsed = recordSchema.safeParse(value);
|
|
1311
|
+
if (!parsed.success) return {};
|
|
1304
1312
|
const result = {};
|
|
1305
|
-
for (const [key,
|
|
1313
|
+
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
1314
|
+
const platform = chainPlatformSchema.safeParse(key);
|
|
1315
|
+
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
1316
|
+
}
|
|
1306
1317
|
return result;
|
|
1307
1318
|
} catch (error) {
|
|
1308
1319
|
logWarn("[butr] readWalletSnapshot: failed to parse selection cookie:", error);
|
|
@@ -1312,7 +1323,7 @@ const parseSelection = (raw) => {
|
|
|
1312
1323
|
/**
|
|
1313
1324
|
* Parse a cookie source into a server-safe `WalletSnapshot`.
|
|
1314
1325
|
*
|
|
1315
|
-
* Pure, sync, no `document`, no React
|
|
1326
|
+
* Pure, sync, no `document`, no React; runnable in any environment
|
|
1316
1327
|
* (Server Component, route handler, edge middleware, even client
|
|
1317
1328
|
* code). Pair with `createCookieStorageDriver({ initialCookies })`
|
|
1318
1329
|
* and `<WalletManagerProvider initialSnapshot={…} />` to render a
|
|
@@ -1323,7 +1334,7 @@ const parseSelection = (raw) => {
|
|
|
1323
1334
|
* the wallet, switched accounts, or disconnected in another tab, the
|
|
1324
1335
|
* client-side hydration will reconcile reality and the live store
|
|
1325
1336
|
* will diverge from the snapshot. Treat the snapshot as an
|
|
1326
|
-
* *optimistic* shell
|
|
1337
|
+
* *optimistic* shell; accurate enough to avoid a paint flicker,
|
|
1327
1338
|
* authoritative only after `useIsHydrated()` is true.
|
|
1328
1339
|
*
|
|
1329
1340
|
* **Inputs.** Accepts the three shapes Next.js / Express / Hono /
|
|
@@ -1333,17 +1344,17 @@ const parseSelection = (raw) => {
|
|
|
1333
1344
|
* - An iterable of `[name, value]` tuples
|
|
1334
1345
|
*
|
|
1335
1346
|
* Malformed entries are dropped with a `logWarn` (same policy as
|
|
1336
|
-
* `WalletStorage.getPool`)
|
|
1347
|
+
* `WalletStorage.getPool`); a cross-tab corruption shouldn't crash
|
|
1337
1348
|
* the server render.
|
|
1338
1349
|
*/
|
|
1339
1350
|
const readWalletSnapshot = (source, options = {}) => {
|
|
1340
|
-
const keyPrefix = options.keyPrefix || "butr";
|
|
1351
|
+
const keyPrefix = options.keyPrefix === void 0 || options.keyPrefix === "" ? "butr" : options.keyPrefix;
|
|
1341
1352
|
const cookies = toCookieMap(source);
|
|
1342
1353
|
const pool = parsePool(cookies.get(`${keyPrefix}-pool`));
|
|
1343
1354
|
const selection = parseSelection(cookies.get(`${keyPrefix}-selection`));
|
|
1344
1355
|
const rawActive = cookies.get(`${keyPrefix}-active`);
|
|
1345
1356
|
let activeConnectorId = null;
|
|
1346
|
-
if (rawActive && rawActive.length > 0 && pool[rawActive]) activeConnectorId = rawActive;
|
|
1357
|
+
if (rawActive !== void 0 && rawActive.length > 0 && pool[rawActive] !== void 0) activeConnectorId = rawActive;
|
|
1347
1358
|
else {
|
|
1348
1359
|
const firstKey = Object.keys(pool)[0];
|
|
1349
1360
|
if (firstKey) activeConnectorId = firstKey;
|
|
@@ -1364,8 +1375,8 @@ const readWalletSnapshot = (source, options = {}) => {
|
|
|
1364
1375
|
* selected-wallet, useWalletEntry) to suppress spurious re-renders
|
|
1365
1376
|
* when the underlying Map churns but the resolved entry hasn't changed.
|
|
1366
1377
|
*
|
|
1367
|
-
* Hoisted to its own module so the equivalence rule lives in one place
|
|
1368
|
-
*
|
|
1378
|
+
* Hoisted to its own module so the equivalence rule lives in one place;
|
|
1379
|
+
* if we ever extend the snapshot (e.g. to consider `accounts.length`),
|
|
1369
1380
|
* every selector hook picks up the new rule for free.
|
|
1370
1381
|
*/
|
|
1371
1382
|
const walletEqual = (a, b) => {
|
|
@@ -1379,7 +1390,7 @@ const walletEqual = (a, b) => {
|
|
|
1379
1390
|
/**
|
|
1380
1391
|
* Normalize a wallet-announced icon string.
|
|
1381
1392
|
*
|
|
1382
|
-
* Wallets announce their icon through external metadata
|
|
1393
|
+
* Wallets announce their icon through external metadata; EIP-6963
|
|
1383
1394
|
* `providerInfo.icon`, Wallet Standard `wallet.icon`. That value is
|
|
1384
1395
|
* not under butr's control, and some wallets ship data-URI icons with
|
|
1385
1396
|
* surrounding whitespace (a newline left over from a pretty-printed
|
|
@@ -1388,7 +1399,7 @@ const walletEqual = (a, b) => {
|
|
|
1388
1399
|
*
|
|
1389
1400
|
* Trims surrounding whitespace and treats an all-whitespace (or empty)
|
|
1390
1401
|
* icon as absent, so consumers get either a usable string or
|
|
1391
|
-
* `undefined
|
|
1402
|
+
* `undefined`: never a blank or malformed one. `undefined` passes
|
|
1392
1403
|
* through untouched.
|
|
1393
1404
|
*/
|
|
1394
1405
|
const sanitizeIcon = (icon) => {
|
|
@@ -1415,7 +1426,7 @@ const sanitizeIcon = (icon) => {
|
|
|
1415
1426
|
* - {@link bytesToHexPrefixed} returns `0x`-prefixed hex (EVM, Polkadot).
|
|
1416
1427
|
* - {@link hexToBytes} tolerantly strips an optional `0x` (all callers).
|
|
1417
1428
|
*/
|
|
1418
|
-
/** Bare lowercase hex
|
|
1429
|
+
/** Bare lowercase hex, no `0x` prefix (Bitcoin, Ledger). */
|
|
1419
1430
|
const bytesToHex = (bytes) => {
|
|
1420
1431
|
let hex = "";
|
|
1421
1432
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
@@ -1457,5 +1468,5 @@ const bytesToBase64 = (bytes) => {
|
|
|
1457
1468
|
};
|
|
1458
1469
|
|
|
1459
1470
|
//#endregion
|
|
1460
|
-
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, WalletStorage, base64ToBytes, buildChainsByPlatform, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, hexToBytes, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
|
|
1471
|
+
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, WalletStorage, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, hexToBytes, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
|
|
1461
1472
|
//# sourceMappingURL=index.js.map
|