@usebutr/core 0.4.1 → 0.5.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 +186 -105
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +270 -238
- 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
|
|
@@ -132,6 +145,134 @@ const CHAIN_PLATFORMS = [
|
|
|
132
145
|
*/
|
|
133
146
|
const createWalletSource = (subscribe) => ({ subscribe });
|
|
134
147
|
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/store/shadow-adapter.ts
|
|
150
|
+
/**
|
|
151
|
+
* Error thrown when a method is called on a shadow adapter; the
|
|
152
|
+
* placeholder `WalletAdapter` that the store seeds into the pool when
|
|
153
|
+
* an `initialState` is provided (e.g. from a server-rendered cookie
|
|
154
|
+
* snapshot). Shadow adapters carry the identity and account data of a
|
|
155
|
+
* previously-connected wallet, but the live wallet extension hasn't
|
|
156
|
+
* been verified yet; the silent reconnect happens asynchronously
|
|
157
|
+
* after mount.
|
|
158
|
+
*
|
|
159
|
+
* UI code that gates affordances on `wallet.connector.capabilities.*`
|
|
160
|
+
* never reaches a shadow method (capabilities are all `false`). Code
|
|
161
|
+
* that calls through anyway hits this typed error, which is the
|
|
162
|
+
* correct loud failure: the consumer ignored the capability gate.
|
|
163
|
+
*
|
|
164
|
+
* Consumers wanting to wait out the reconnecting window should branch
|
|
165
|
+
* on whether `connectorId` is in `state.reconnectingIds`.
|
|
166
|
+
*/
|
|
167
|
+
var ShadowConnectorError = class extends Error {
|
|
168
|
+
code = "BUTR_RECONNECTING";
|
|
169
|
+
connectorId;
|
|
170
|
+
method;
|
|
171
|
+
constructor(method, connectorId) {
|
|
172
|
+
super(`[butr] ${method} called on shadow connector "${connectorId}". Wait for silent reconnect to complete (check reconnectingIds.has(id) before calling).`);
|
|
173
|
+
this.name = "ShadowConnectorError";
|
|
174
|
+
this.connectorId = connectorId;
|
|
175
|
+
this.method = method;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
179
|
+
getBalance: false,
|
|
180
|
+
getTransactionReceipt: false,
|
|
181
|
+
requestAccounts: false,
|
|
182
|
+
sendTransaction: false,
|
|
183
|
+
signIn: false,
|
|
184
|
+
signMessage: false,
|
|
185
|
+
signTransaction: false,
|
|
186
|
+
subscribe: false,
|
|
187
|
+
switchAccount: false,
|
|
188
|
+
switchChain: false
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* Builds a placeholder `WalletAdapter` from a persisted pool entry.
|
|
192
|
+
*
|
|
193
|
+
* Used by `createWalletStore` when `WalletManagerConfig.initialState`
|
|
194
|
+
* is provided: each stored entry becomes a `ConnectedWallet` whose
|
|
195
|
+
* `connector` is one of these. The store flips `isHydrated` true
|
|
196
|
+
* synchronously and exposes the data through the usual hooks
|
|
197
|
+
* (`useActiveWallet`, `useConnectedWallets`, …), but the connector
|
|
198
|
+
* can't actually talk to a wallet yet, so its capabilities are all
|
|
199
|
+
* `false` and its methods throw `ShadowConnectorError` if called.
|
|
200
|
+
*
|
|
201
|
+
* The hydration coordinator's silent-reconnect pass upgrades each
|
|
202
|
+
* shadow to a live `WalletAdapter` by calling `createConnector(id)`
|
|
203
|
+
* (which only succeeds once the live adapter has been announced via
|
|
204
|
+
* discovery or registered explicitly) and replacing the pool entry.
|
|
205
|
+
* On success the connector id is removed from `reconnectingIds`; on
|
|
206
|
+
* failure the entry is dropped from the pool and storage.
|
|
207
|
+
*
|
|
208
|
+
* The entry's `name` is required (the storage validator rejects
|
|
209
|
+
* entries without it); `icon` is optional only because some live
|
|
210
|
+
* adapters genuinely have no icon to begin with.
|
|
211
|
+
*/
|
|
212
|
+
const createShadowAdapter = (entry) => {
|
|
213
|
+
const id = entry.connectorId;
|
|
214
|
+
const name = entry.name;
|
|
215
|
+
const icon = entry.icon;
|
|
216
|
+
const reject = (method) => Promise.reject(new ShadowConnectorError(method, id));
|
|
217
|
+
const base = {
|
|
218
|
+
capabilities: ALL_FALSE_CAPABILITIES,
|
|
219
|
+
connect: () => reject("connect"),
|
|
220
|
+
disconnect: () => reject("disconnect"),
|
|
221
|
+
getAccount: () => reject("getAccount"),
|
|
222
|
+
getBalance: () => reject("getBalance"),
|
|
223
|
+
getSigner: () => reject("getSigner"),
|
|
224
|
+
getTransactionReceipt: () => reject("getTransactionReceipt"),
|
|
225
|
+
icon,
|
|
226
|
+
id,
|
|
227
|
+
name,
|
|
228
|
+
sendTx: () => reject("sendTx"),
|
|
229
|
+
sendTxToChain: () => reject("sendTxToChain"),
|
|
230
|
+
signMessage: () => reject("signMessage"),
|
|
231
|
+
switchChain: () => reject("switchChain")
|
|
232
|
+
};
|
|
233
|
+
switch (entry.chainPlatform) {
|
|
234
|
+
case "bitcoin": return {
|
|
235
|
+
...base,
|
|
236
|
+
chainPlatform: "bitcoin"
|
|
237
|
+
};
|
|
238
|
+
case "evm": return {
|
|
239
|
+
...base,
|
|
240
|
+
chainPlatform: "evm"
|
|
241
|
+
};
|
|
242
|
+
case "sui": return {
|
|
243
|
+
...base,
|
|
244
|
+
chainPlatform: "sui"
|
|
245
|
+
};
|
|
246
|
+
case "svm": return {
|
|
247
|
+
...base,
|
|
248
|
+
chainPlatform: "svm"
|
|
249
|
+
};
|
|
250
|
+
case "polkadot": return {
|
|
251
|
+
...base,
|
|
252
|
+
chainPlatform: "polkadot"
|
|
253
|
+
};
|
|
254
|
+
default:
|
|
255
|
+
entry.chainPlatform;
|
|
256
|
+
throw new Error(`[butr] unknown chainPlatform: ${entry.chainPlatform}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Type guard. Returns true when an adapter is a placeholder created
|
|
261
|
+
* by `createShadowAdapter`. Useful for the hydration coordinator
|
|
262
|
+
* (which needs to know which pool entries still need upgrading) and
|
|
263
|
+
* for consumers writing wagmi-style "is this connection verified yet"
|
|
264
|
+
* checks without subscribing to `reconnectingIds` directly.
|
|
265
|
+
*
|
|
266
|
+
* Detection is structural: a shadow has all capabilities set to false.
|
|
267
|
+
* Live adapters always advertise at least one capability (every wallet
|
|
268
|
+
* surface includes `getBalance`, `signMessage`, `switchChain` as
|
|
269
|
+
* required methods, and adapter constructors set their flags
|
|
270
|
+
* accordingly).
|
|
271
|
+
*/
|
|
272
|
+
const isShadowAdapter = (adapter) => {
|
|
273
|
+
return Object.values(adapter.capabilities).every((flag) => !flag);
|
|
274
|
+
};
|
|
275
|
+
|
|
135
276
|
//#endregion
|
|
136
277
|
//#region src/logger.ts
|
|
137
278
|
const logWarn = (...args) => {
|
|
@@ -166,7 +307,7 @@ const createMemoryStorageDriver = () => {
|
|
|
166
307
|
};
|
|
167
308
|
const createWebStorageDriver = (kind) => {
|
|
168
309
|
if (!hasWebStorage(kind)) return createMemoryStorageDriver();
|
|
169
|
-
const storage = globalThis
|
|
310
|
+
const storage = kind === "localStorage" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
170
311
|
return {
|
|
171
312
|
getItem(key) {
|
|
172
313
|
try {
|
|
@@ -198,41 +339,36 @@ const createBrowserStorageDriver = () => ({
|
|
|
198
339
|
});
|
|
199
340
|
|
|
200
341
|
//#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;
|
|
342
|
+
//#region src/storage/validation.ts
|
|
343
|
+
const chainPlatformSchema = z.enum(CHAIN_PLATFORMS);
|
|
344
|
+
const chainSchema = z.looseObject({
|
|
345
|
+
id: z.string(),
|
|
346
|
+
name: z.string(),
|
|
347
|
+
namespace: z.string(),
|
|
348
|
+
reference: z.string()
|
|
349
|
+
});
|
|
350
|
+
const accountSchema = z.looseObject({
|
|
351
|
+
chain: chainSchema,
|
|
352
|
+
id: z.string(),
|
|
353
|
+
walletAddress: z.string()
|
|
354
|
+
});
|
|
355
|
+
const storedPoolEntrySchema = z.looseObject({
|
|
356
|
+
account: accountSchema,
|
|
357
|
+
accounts: z.array(accountSchema),
|
|
358
|
+
chainPlatform: chainPlatformSchema,
|
|
359
|
+
connectorId: z.string(),
|
|
360
|
+
icon: z.string().optional(),
|
|
361
|
+
name: z.string().min(1)
|
|
362
|
+
});
|
|
363
|
+
const recordSchema = z.record(z.string(), z.unknown());
|
|
364
|
+
const parseStoredPoolEntry = (key, value) => {
|
|
365
|
+
const parsed = storedPoolEntrySchema.safeParse(value);
|
|
366
|
+
if (!parsed.success || parsed.data.connectorId !== key) return null;
|
|
367
|
+
return parsed.data;
|
|
235
368
|
};
|
|
369
|
+
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/storage/wallet-storage.ts
|
|
236
372
|
var WalletStorage = class {
|
|
237
373
|
poolKey;
|
|
238
374
|
selectionKey;
|
|
@@ -246,7 +382,7 @@ var WalletStorage = class {
|
|
|
246
382
|
* this, two simultaneous `setPool` calls both read the pre-write
|
|
247
383
|
* state, each merge their own entries, and whichever finishes last
|
|
248
384
|
* overwrites the other's additions. Reads (`getPool`) don't enter
|
|
249
|
-
* the queue
|
|
385
|
+
* the queue; they observe whatever's currently in the driver.
|
|
250
386
|
*/
|
|
251
387
|
poolMutationQueue = Promise.resolve();
|
|
252
388
|
constructor(config) {
|
|
@@ -282,15 +418,19 @@ var WalletStorage = class {
|
|
|
282
418
|
async getPool() {
|
|
283
419
|
try {
|
|
284
420
|
const stored = await this.persistent.getItem(this.poolKey);
|
|
285
|
-
if (
|
|
286
|
-
const
|
|
287
|
-
|
|
421
|
+
if (stored === null || stored === "") return {};
|
|
422
|
+
const value = JSON.parse(stored);
|
|
423
|
+
const parsed = recordSchema.safeParse(value);
|
|
424
|
+
if (!parsed.success) {
|
|
288
425
|
await this.clearPool();
|
|
289
426
|
return {};
|
|
290
427
|
}
|
|
291
428
|
const result = {};
|
|
292
|
-
for (const [key,
|
|
293
|
-
|
|
429
|
+
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
430
|
+
const entry = parseStoredPoolEntry(key, entryValue);
|
|
431
|
+
if (entry === null) logWarn(`[butr] dropping invalid pool entry for ${key}`);
|
|
432
|
+
else result[key] = entry;
|
|
433
|
+
}
|
|
294
434
|
return result;
|
|
295
435
|
} catch (error) {
|
|
296
436
|
logWarn("[butr] failed to parse pool from storage:", error);
|
|
@@ -302,14 +442,14 @@ var WalletStorage = class {
|
|
|
302
442
|
* Upsert the in-memory pool into storage. Additive: entries in
|
|
303
443
|
* `pool` are written; entries already in storage that aren't in
|
|
304
444
|
* `pool` are kept. The in-memory pool reflects "what's live right
|
|
305
|
-
* now", not "the complete list of remembered connections"
|
|
445
|
+
* now", not "the complete list of remembered connections"; a
|
|
306
446
|
* silent reconnect that fails on reload leaves the entry out of
|
|
307
447
|
* the pool but the saved entry stays so the next load can retry.
|
|
308
448
|
* Use `removePoolEntry` for explicit eviction (the user clicked
|
|
309
449
|
* Disconnect) and `clearAll` for a full wipe (reset).
|
|
310
450
|
*/
|
|
311
|
-
setPool(pool) {
|
|
312
|
-
|
|
451
|
+
async setPool(pool) {
|
|
452
|
+
await this.serializePoolMutation(async () => {
|
|
313
453
|
try {
|
|
314
454
|
const serializable = { ...await this.getPool() };
|
|
315
455
|
for (const [connectorId, wallet] of pool) {
|
|
@@ -321,7 +461,7 @@ var WalletStorage = class {
|
|
|
321
461
|
icon: wallet.connector.icon,
|
|
322
462
|
name: wallet.connector.name
|
|
323
463
|
};
|
|
324
|
-
if (
|
|
464
|
+
if (parseStoredPoolEntry(connectorId, entry) === null) throw new Error(`[butr] refusing to persist invalid pool entry for ${connectorId}`);
|
|
325
465
|
serializable[connectorId] = entry;
|
|
326
466
|
}
|
|
327
467
|
await this.persistent.setItem(this.poolKey, JSON.stringify(serializable));
|
|
@@ -330,8 +470,8 @@ var WalletStorage = class {
|
|
|
330
470
|
}
|
|
331
471
|
});
|
|
332
472
|
}
|
|
333
|
-
removePoolEntry(connectorId) {
|
|
334
|
-
|
|
473
|
+
async removePoolEntry(connectorId) {
|
|
474
|
+
await this.serializePoolMutation(async () => {
|
|
335
475
|
try {
|
|
336
476
|
const stored = await this.getPool();
|
|
337
477
|
if (stored[connectorId]) {
|
|
@@ -343,19 +483,23 @@ var WalletStorage = class {
|
|
|
343
483
|
}
|
|
344
484
|
});
|
|
345
485
|
}
|
|
346
|
-
clearPool() {
|
|
347
|
-
|
|
486
|
+
async clearPool() {
|
|
487
|
+
await this.serializePoolMutation(async () => {
|
|
348
488
|
await this.persistent.removeItem(this.poolKey);
|
|
349
489
|
});
|
|
350
490
|
}
|
|
351
491
|
async getSelection() {
|
|
352
492
|
try {
|
|
353
493
|
const stored = await this.persistent.getItem(this.selectionKey);
|
|
354
|
-
if (
|
|
355
|
-
const
|
|
356
|
-
|
|
494
|
+
if (stored === null || stored === "") return {};
|
|
495
|
+
const value = JSON.parse(stored);
|
|
496
|
+
const parsed = recordSchema.safeParse(value);
|
|
497
|
+
if (!parsed.success) return {};
|
|
357
498
|
const result = {};
|
|
358
|
-
for (const [key,
|
|
499
|
+
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
500
|
+
const platform = chainPlatformSchema.safeParse(key);
|
|
501
|
+
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
502
|
+
}
|
|
359
503
|
return result;
|
|
360
504
|
} catch (error) {
|
|
361
505
|
logWarn("[butr] failed to parse selection from storage:", error);
|
|
@@ -374,7 +518,7 @@ var WalletStorage = class {
|
|
|
374
518
|
async getActiveConnectorId() {
|
|
375
519
|
try {
|
|
376
520
|
const value = await this.persistent.getItem(this.activeKey);
|
|
377
|
-
return value && value.length > 0 ? value : null;
|
|
521
|
+
return value !== null && value.length > 0 ? value : null;
|
|
378
522
|
} catch {
|
|
379
523
|
return null;
|
|
380
524
|
}
|
|
@@ -462,6 +606,8 @@ const createConnectorLifecycle = (handlers) => {
|
|
|
462
606
|
|
|
463
607
|
//#endregion
|
|
464
608
|
//#region src/store/hydration.ts
|
|
609
|
+
const VALID_CHAIN_PLATFORMS$1 = new Set(CHAIN_PLATFORMS);
|
|
610
|
+
const isChainPlatform$1 = (value) => VALID_CHAIN_PLATFORMS$1.has(value);
|
|
465
611
|
/** Pick the right `accounts` list: fresh from the connector if it
|
|
466
612
|
* exposes `getAccounts`, otherwise the persisted list, otherwise just
|
|
467
613
|
* the active account. */
|
|
@@ -476,7 +622,7 @@ const resolveHydratedAccounts = async (connector, storedAccounts, fallbackAccoun
|
|
|
476
622
|
const restoreOneEntry = async (connectorId, entry, connector) => {
|
|
477
623
|
try {
|
|
478
624
|
await connector.connect({ silent: true });
|
|
479
|
-
const accountToUse = await connector.getAccount()
|
|
625
|
+
const accountToUse = await connector.getAccount() ?? entry.account;
|
|
480
626
|
return {
|
|
481
627
|
connectorId,
|
|
482
628
|
entry: {
|
|
@@ -538,13 +684,13 @@ const createHydrationCoordinator = (storage, createConnector) => {
|
|
|
538
684
|
});
|
|
539
685
|
}
|
|
540
686
|
const selection = /* @__PURE__ */ new Map();
|
|
541
|
-
for (const [platform, connectorId] of Object.entries(storedSelection)) if (connectorId && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
687
|
+
for (const [platform, connectorId] of Object.entries(storedSelection)) if (isChainPlatform$1(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
542
688
|
for (const [connectorId, wallet] of pool) {
|
|
543
689
|
const platform = wallet.connector.chainPlatform;
|
|
544
690
|
if (!selection.has(platform)) selection.set(platform, connectorId);
|
|
545
691
|
}
|
|
546
692
|
let activeConnectorId = null;
|
|
547
|
-
if (storedActive && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
693
|
+
if (storedActive !== null && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
548
694
|
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
549
695
|
return {
|
|
550
696
|
activeConnectorId,
|
|
@@ -586,8 +732,8 @@ const reducer = (state, event) => {
|
|
|
586
732
|
const selection = new Map(state.selection);
|
|
587
733
|
for (const [platform, id] of event.selection) selection.set(platform, id);
|
|
588
734
|
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;
|
|
735
|
+
if (event.activeConnectorId !== null && pool.has(event.activeConnectorId)) activeConnectorId = event.activeConnectorId;
|
|
736
|
+
else if (state.activeConnectorId !== null && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
591
737
|
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
592
738
|
let nextReconnecting = state.reconnectingIds;
|
|
593
739
|
if (state.reconnectingIds.size > 0 && event.pool.size > 0) {
|
|
@@ -655,7 +801,7 @@ const reducer = (state, event) => {
|
|
|
655
801
|
if (newSelection.get(platform) === event.connectorId) {
|
|
656
802
|
newSelection.delete(platform);
|
|
657
803
|
const fallback = findConnectorForPlatform(newPool, platform);
|
|
658
|
-
if (fallback) newSelection.set(platform, fallback);
|
|
804
|
+
if (fallback !== void 0) newSelection.set(platform, fallback);
|
|
659
805
|
}
|
|
660
806
|
const firstId = newPool.keys().next();
|
|
661
807
|
const activeConnectorId = state.activeConnectorId === event.connectorId ? firstId.value ?? null : state.activeConnectorId;
|
|
@@ -731,118 +877,6 @@ const reducer = (state, event) => {
|
|
|
731
877
|
}
|
|
732
878
|
};
|
|
733
879
|
|
|
734
|
-
//#endregion
|
|
735
|
-
//#region src/store/shadow-adapter.ts
|
|
736
|
-
/**
|
|
737
|
-
* Error thrown when a method is called on a shadow adapter — the
|
|
738
|
-
* placeholder `WalletAdapter` that the store seeds into the pool when
|
|
739
|
-
* an `initialState` is provided (e.g. from a server-rendered cookie
|
|
740
|
-
* snapshot). Shadow adapters carry the identity and account data of a
|
|
741
|
-
* previously-connected wallet, but the live wallet extension hasn't
|
|
742
|
-
* been verified yet — the silent reconnect happens asynchronously
|
|
743
|
-
* after mount.
|
|
744
|
-
*
|
|
745
|
-
* UI code that gates affordances on `wallet.connector.capabilities.*`
|
|
746
|
-
* never reaches a shadow method (capabilities are all `false`). Code
|
|
747
|
-
* that calls through anyway hits this typed error, which is the
|
|
748
|
-
* correct loud failure: the consumer ignored the capability gate.
|
|
749
|
-
*
|
|
750
|
-
* Consumers wanting to wait out the reconnecting window should branch
|
|
751
|
-
* on whether `connectorId` is in `state.reconnectingIds`.
|
|
752
|
-
*/
|
|
753
|
-
var ShadowConnectorError = class extends Error {
|
|
754
|
-
code = "BUTR_RECONNECTING";
|
|
755
|
-
connectorId;
|
|
756
|
-
method;
|
|
757
|
-
constructor(method, connectorId) {
|
|
758
|
-
super(`[butr] ${method} called on shadow connector "${connectorId}" — wait for silent reconnect to complete (check reconnectingIds.has(id) before calling).`);
|
|
759
|
-
this.name = "ShadowConnectorError";
|
|
760
|
-
this.connectorId = connectorId;
|
|
761
|
-
this.method = method;
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
765
|
-
getBalance: false,
|
|
766
|
-
getTransactionReceipt: false,
|
|
767
|
-
requestAccounts: false,
|
|
768
|
-
sendTransaction: false,
|
|
769
|
-
signIn: false,
|
|
770
|
-
signMessage: false,
|
|
771
|
-
signTransaction: false,
|
|
772
|
-
subscribe: false,
|
|
773
|
-
switchAccount: false,
|
|
774
|
-
switchChain: false
|
|
775
|
-
});
|
|
776
|
-
/**
|
|
777
|
-
* Builds a placeholder `WalletAdapter` from a persisted pool entry.
|
|
778
|
-
*
|
|
779
|
-
* Used by `createWalletStore` when `WalletManagerConfig.initialState`
|
|
780
|
-
* is provided: each stored entry becomes a `ConnectedWallet` whose
|
|
781
|
-
* `connector` is one of these. The store flips `isHydrated` true
|
|
782
|
-
* synchronously and exposes the data through the usual hooks
|
|
783
|
-
* (`useActiveWallet`, `useConnectedWallets`, …) — but the connector
|
|
784
|
-
* can't actually talk to a wallet yet, so its capabilities are all
|
|
785
|
-
* `false` and its methods throw `ShadowConnectorError` if called.
|
|
786
|
-
*
|
|
787
|
-
* The hydration coordinator's silent-reconnect pass upgrades each
|
|
788
|
-
* shadow to a live `WalletAdapter` by calling `createConnector(id)`
|
|
789
|
-
* (which only succeeds once the live adapter has been announced via
|
|
790
|
-
* discovery or registered explicitly) and replacing the pool entry.
|
|
791
|
-
* On success the connector id is removed from `reconnectingIds`; on
|
|
792
|
-
* failure the entry is dropped from the pool and storage.
|
|
793
|
-
*
|
|
794
|
-
* The entry's `name` is required (the storage validator rejects
|
|
795
|
-
* entries without it); `icon` is optional only because some live
|
|
796
|
-
* adapters genuinely have no icon to begin with.
|
|
797
|
-
*/
|
|
798
|
-
const createShadowAdapter = (entry) => {
|
|
799
|
-
const id = entry.connectorId;
|
|
800
|
-
const name = entry.name;
|
|
801
|
-
const icon = entry.icon;
|
|
802
|
-
const reject = (method) => Promise.reject(new ShadowConnectorError(method, id));
|
|
803
|
-
const base = {
|
|
804
|
-
capabilities: ALL_FALSE_CAPABILITIES,
|
|
805
|
-
connect: () => reject("connect"),
|
|
806
|
-
disconnect: () => reject("disconnect"),
|
|
807
|
-
getAccount: () => reject("getAccount"),
|
|
808
|
-
getBalance: () => reject("getBalance"),
|
|
809
|
-
getSigner: () => reject("getSigner"),
|
|
810
|
-
getTransactionReceipt: () => reject("getTransactionReceipt"),
|
|
811
|
-
icon,
|
|
812
|
-
id,
|
|
813
|
-
name,
|
|
814
|
-
sendTx: () => reject("sendTx"),
|
|
815
|
-
sendTxToChain: () => reject("sendTxToChain"),
|
|
816
|
-
signMessage: () => reject("signMessage"),
|
|
817
|
-
switchChain: () => reject("switchChain")
|
|
818
|
-
};
|
|
819
|
-
switch (entry.chainPlatform) {
|
|
820
|
-
case "bitcoin": return {
|
|
821
|
-
...base,
|
|
822
|
-
chainPlatform: "bitcoin"
|
|
823
|
-
};
|
|
824
|
-
case "evm": return {
|
|
825
|
-
...base,
|
|
826
|
-
chainPlatform: "evm"
|
|
827
|
-
};
|
|
828
|
-
case "sui": return {
|
|
829
|
-
...base,
|
|
830
|
-
chainPlatform: "sui"
|
|
831
|
-
};
|
|
832
|
-
case "svm": return {
|
|
833
|
-
...base,
|
|
834
|
-
chainPlatform: "svm"
|
|
835
|
-
};
|
|
836
|
-
case "polkadot": return {
|
|
837
|
-
...base,
|
|
838
|
-
chainPlatform: "polkadot"
|
|
839
|
-
};
|
|
840
|
-
default:
|
|
841
|
-
entry.chainPlatform;
|
|
842
|
-
throw new Error(`[butr] unknown chainPlatform: ${entry.chainPlatform}`);
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
|
|
846
880
|
//#endregion
|
|
847
881
|
//#region src/store/wallet-store-helpers.ts
|
|
848
882
|
/** Run an async operation fire-and-forget, calling onError if it throws. */
|
|
@@ -856,13 +890,15 @@ const run = async (fn, onError) => {
|
|
|
856
890
|
|
|
857
891
|
//#endregion
|
|
858
892
|
//#region src/store/wallet-store.ts
|
|
893
|
+
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
894
|
+
const isChainPlatform = (value) => VALID_CHAIN_PLATFORMS.has(value);
|
|
859
895
|
/**
|
|
860
896
|
* Build a synchronously-populated `State` from `config.initialState`.
|
|
861
897
|
* Each pool entry becomes a `ConnectedWallet` whose `connector` is a
|
|
862
898
|
* shadow adapter; every connector id enters `reconnectingIds` so
|
|
863
899
|
* consumers can branch on "is this connection verified" without
|
|
864
900
|
* waiting for the async silent reconnect. `isHydrated` flips true
|
|
865
|
-
* synchronously
|
|
901
|
+
* synchronously: the consumer's first render sees the persisted
|
|
866
902
|
* state in the live store, not undefined.
|
|
867
903
|
*/
|
|
868
904
|
const seedStateFromSnapshot = (snapshot) => {
|
|
@@ -879,8 +915,8 @@ const seedStateFromSnapshot = (snapshot) => {
|
|
|
879
915
|
reconnectingIds.add(connectorId);
|
|
880
916
|
}
|
|
881
917
|
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;
|
|
918
|
+
for (const [platform, connectorId] of Object.entries(snapshot.selection)) if (isChainPlatform(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
919
|
+
const activeConnectorId = snapshot.activeConnectorId !== null && pool.has(snapshot.activeConnectorId) ? snapshot.activeConnectorId : pool.keys().next().value ?? null;
|
|
884
920
|
return {
|
|
885
921
|
...initialState,
|
|
886
922
|
activeConnectorId,
|
|
@@ -894,8 +930,8 @@ const CONNECT_TIMEOUT_MS = 9e4;
|
|
|
894
930
|
const DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5e3;
|
|
895
931
|
const normaliseAddress = (addr) => addr.toLowerCase();
|
|
896
932
|
const createWalletStore = (config) => {
|
|
897
|
-
const storageKeyPrefix = config.storageKeyPrefix || "butr";
|
|
898
|
-
const storage = config.storage
|
|
933
|
+
const storageKeyPrefix = config.storageKeyPrefix === void 0 || config.storageKeyPrefix === "" ? "butr" : config.storageKeyPrefix;
|
|
934
|
+
const storage = config.storage ?? new WalletStorage({ keyPrefix: storageKeyPrefix });
|
|
899
935
|
const reportStorageError = (context) => (error) => {
|
|
900
936
|
if (config.onStorageError) {
|
|
901
937
|
try {
|
|
@@ -956,7 +992,7 @@ const createWalletStore = (config) => {
|
|
|
956
992
|
logWarn("[butr] onSlowConnect threw:", cbError);
|
|
957
993
|
}
|
|
958
994
|
}, slowThreshold) : null;
|
|
959
|
-
let connectTimeoutId
|
|
995
|
+
let connectTimeoutId;
|
|
960
996
|
try {
|
|
961
997
|
const connectPromise = connector.connect();
|
|
962
998
|
connectPromise.catch(() => {});
|
|
@@ -1002,10 +1038,10 @@ const createWalletStore = (config) => {
|
|
|
1002
1038
|
} catch (cbError) {
|
|
1003
1039
|
logWarn("[butr] onConnectError threw:", cbError);
|
|
1004
1040
|
}
|
|
1005
|
-
onError?.(error);
|
|
1041
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1006
1042
|
throw error;
|
|
1007
1043
|
} finally {
|
|
1008
|
-
|
|
1044
|
+
clearTimeout(connectTimeoutId);
|
|
1009
1045
|
if (slowTimer) clearTimeout(slowTimer);
|
|
1010
1046
|
}
|
|
1011
1047
|
},
|
|
@@ -1042,7 +1078,9 @@ const createWalletStore = (config) => {
|
|
|
1042
1078
|
});
|
|
1043
1079
|
for (const [connectorId, wallet] of result.pool) lifecycle.attach(connectorId, wallet.connector);
|
|
1044
1080
|
await Promise.all([persistSelection(), persistActive()]);
|
|
1045
|
-
for (const id of hydration.pendingIds()) run(() => get().tryRestoreFromPending(id), (e) =>
|
|
1081
|
+
for (const id of hydration.pendingIds()) run(() => get().tryRestoreFromPending(id), (e) => {
|
|
1082
|
+
logWarn(`[butr] late restore rejected for ${id}:`, e);
|
|
1083
|
+
});
|
|
1046
1084
|
if (config.onHydrated) try {
|
|
1047
1085
|
config.onHydrated({
|
|
1048
1086
|
dropped: result.dropped,
|
|
@@ -1173,7 +1211,7 @@ const readCookies = () => {
|
|
|
1173
1211
|
};
|
|
1174
1212
|
const buildCookieString = (name, value, options, expired = false) => {
|
|
1175
1213
|
const parts = [`${name}=${encode(value)}`, `Path=${options.path ?? "/"}`];
|
|
1176
|
-
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
1214
|
+
if (options.domain !== void 0 && options.domain !== "") parts.push(`Domain=${options.domain}`);
|
|
1177
1215
|
if (expired) parts.push("Max-Age=0");
|
|
1178
1216
|
else {
|
|
1179
1217
|
const maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
|
|
@@ -1185,11 +1223,11 @@ const buildCookieString = (name, value, options, expired = false) => {
|
|
|
1185
1223
|
};
|
|
1186
1224
|
const toCookieMap$1 = (input) => {
|
|
1187
1225
|
if (!input) return null;
|
|
1188
|
-
if (
|
|
1226
|
+
if (Symbol.iterator in input) return new Map(input);
|
|
1189
1227
|
return new Map(Object.entries(input));
|
|
1190
1228
|
};
|
|
1191
1229
|
/**
|
|
1192
|
-
* Cookie-backed storage driver. Reads/writes `document.cookie
|
|
1230
|
+
* Cookie-backed storage driver. Reads/writes `document.cookie`;
|
|
1193
1231
|
* server-readable, survives reloads, scoped per `domain`/`path`.
|
|
1194
1232
|
*
|
|
1195
1233
|
* **When to use this:** SSR apps that need to know who's connected
|
|
@@ -1203,13 +1241,13 @@ const toCookieMap$1 = (input) => {
|
|
|
1203
1241
|
* **Trade-offs vs `localStorage`:** cookies travel with every
|
|
1204
1242
|
* request, so they cost bytes on the wire. Keep the storage key
|
|
1205
1243
|
* prefix short, and prefer this driver for the `persistent` slot
|
|
1206
|
-
* only
|
|
1244
|
+
* only: the `session` slot can stay in `sessionStorage` (which
|
|
1207
1245
|
* cookies can't natively model anyway).
|
|
1208
1246
|
*
|
|
1209
1247
|
* **Server-side writes are no-ops.** Emitting `Set-Cookie` requires
|
|
1210
1248
|
* access to the framework's response object, which a storage driver
|
|
1211
1249
|
* shouldn't reach into. The store doesn't mutate persisted state
|
|
1212
|
-
* during the SSR pass anyway
|
|
1250
|
+
* during the SSR pass anyway; writes only fire after client mount,
|
|
1213
1251
|
* once `document.cookie` is reachable.
|
|
1214
1252
|
*/
|
|
1215
1253
|
const createCookieStorageDriver = (options = {}) => {
|
|
@@ -1245,14 +1283,13 @@ const createCookieStorageDriver = (options = {}) => {
|
|
|
1245
1283
|
|
|
1246
1284
|
//#endregion
|
|
1247
1285
|
//#region src/storage/snapshot.ts
|
|
1248
|
-
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
1249
1286
|
const EMPTY_SNAPSHOT = Object.freeze({
|
|
1250
1287
|
activeConnectorId: null,
|
|
1251
1288
|
pool: Object.freeze({}),
|
|
1252
1289
|
selection: Object.freeze({})
|
|
1253
1290
|
});
|
|
1254
1291
|
const toCookieMap = (input) => {
|
|
1255
|
-
if (
|
|
1292
|
+
if (!(Symbol.iterator in input)) return new Map(Object.entries(input));
|
|
1256
1293
|
const out = /* @__PURE__ */ new Map();
|
|
1257
1294
|
for (const entry of input) {
|
|
1258
1295
|
if (Array.isArray(entry)) {
|
|
@@ -1260,37 +1297,22 @@ const toCookieMap = (input) => {
|
|
|
1260
1297
|
if (typeof name === "string" && typeof value === "string") out.set(name, value);
|
|
1261
1298
|
continue;
|
|
1262
1299
|
}
|
|
1263
|
-
if (
|
|
1300
|
+
if (typeof entry.name === "string" && typeof entry.value === "string") out.set(entry.name, entry.value);
|
|
1264
1301
|
}
|
|
1265
1302
|
return out;
|
|
1266
1303
|
};
|
|
1267
|
-
const isValidAccount = (value) => {
|
|
1268
|
-
if (!value || typeof value !== "object") return false;
|
|
1269
|
-
const account = value;
|
|
1270
|
-
if (typeof account.walletAddress !== "string" || typeof account.id !== "string") return false;
|
|
1271
|
-
if (!account.chain || typeof account.chain !== "object") return false;
|
|
1272
|
-
return true;
|
|
1273
|
-
};
|
|
1274
|
-
const isValidPoolEntry = (key, value) => {
|
|
1275
|
-
if (!value || typeof value !== "object") return false;
|
|
1276
|
-
const entry = value;
|
|
1277
|
-
if (typeof entry.connectorId !== "string" || entry.connectorId !== key) return false;
|
|
1278
|
-
if (typeof entry.chainPlatform !== "string") return false;
|
|
1279
|
-
if (!VALID_CHAIN_PLATFORMS.has(entry.chainPlatform)) return false;
|
|
1280
|
-
if (typeof entry.name !== "string" || entry.name.length === 0) return false;
|
|
1281
|
-
if (entry.icon !== void 0 && typeof entry.icon !== "string") return false;
|
|
1282
|
-
if (!isValidAccount(entry.account)) return false;
|
|
1283
|
-
if (!Array.isArray(entry.accounts) || !entry.accounts.every(isValidAccount)) return false;
|
|
1284
|
-
return true;
|
|
1285
|
-
};
|
|
1286
1304
|
const parsePool = (raw) => {
|
|
1287
|
-
if (
|
|
1305
|
+
if (raw === void 0 || raw === "") return {};
|
|
1288
1306
|
try {
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1307
|
+
const value = JSON.parse(raw);
|
|
1308
|
+
const parsed = recordSchema.safeParse(value);
|
|
1309
|
+
if (!parsed.success) return {};
|
|
1291
1310
|
const result = {};
|
|
1292
|
-
for (const [key,
|
|
1293
|
-
|
|
1311
|
+
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
1312
|
+
const entry = parseStoredPoolEntry(key, entryValue);
|
|
1313
|
+
if (entry === null) logWarn(`[butr] readWalletSnapshot: dropping invalid pool entry for ${key}`);
|
|
1314
|
+
else result[key] = entry;
|
|
1315
|
+
}
|
|
1294
1316
|
return result;
|
|
1295
1317
|
} catch (error) {
|
|
1296
1318
|
logWarn("[butr] readWalletSnapshot: failed to parse pool cookie:", error);
|
|
@@ -1298,12 +1320,16 @@ const parsePool = (raw) => {
|
|
|
1298
1320
|
}
|
|
1299
1321
|
};
|
|
1300
1322
|
const parseSelection = (raw) => {
|
|
1301
|
-
if (
|
|
1323
|
+
if (raw === void 0 || raw === "") return {};
|
|
1302
1324
|
try {
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1325
|
+
const value = JSON.parse(raw);
|
|
1326
|
+
const parsed = recordSchema.safeParse(value);
|
|
1327
|
+
if (!parsed.success) return {};
|
|
1305
1328
|
const result = {};
|
|
1306
|
-
for (const [key,
|
|
1329
|
+
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
1330
|
+
const platform = chainPlatformSchema.safeParse(key);
|
|
1331
|
+
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
1332
|
+
}
|
|
1307
1333
|
return result;
|
|
1308
1334
|
} catch (error) {
|
|
1309
1335
|
logWarn("[butr] readWalletSnapshot: failed to parse selection cookie:", error);
|
|
@@ -1313,7 +1339,7 @@ const parseSelection = (raw) => {
|
|
|
1313
1339
|
/**
|
|
1314
1340
|
* Parse a cookie source into a server-safe `WalletSnapshot`.
|
|
1315
1341
|
*
|
|
1316
|
-
* Pure, sync, no `document`, no React
|
|
1342
|
+
* Pure, sync, no `document`, no React; runnable in any environment
|
|
1317
1343
|
* (Server Component, route handler, edge middleware, even client
|
|
1318
1344
|
* code). Pair with `createCookieStorageDriver({ initialCookies })`
|
|
1319
1345
|
* and `<WalletManagerProvider initialSnapshot={…} />` to render a
|
|
@@ -1324,7 +1350,7 @@ const parseSelection = (raw) => {
|
|
|
1324
1350
|
* the wallet, switched accounts, or disconnected in another tab, the
|
|
1325
1351
|
* client-side hydration will reconcile reality and the live store
|
|
1326
1352
|
* will diverge from the snapshot. Treat the snapshot as an
|
|
1327
|
-
* *optimistic* shell
|
|
1353
|
+
* *optimistic* shell; accurate enough to avoid a paint flicker,
|
|
1328
1354
|
* authoritative only after `useIsHydrated()` is true.
|
|
1329
1355
|
*
|
|
1330
1356
|
* **Inputs.** Accepts the three shapes Next.js / Express / Hono /
|
|
@@ -1334,17 +1360,17 @@ const parseSelection = (raw) => {
|
|
|
1334
1360
|
* - An iterable of `[name, value]` tuples
|
|
1335
1361
|
*
|
|
1336
1362
|
* Malformed entries are dropped with a `logWarn` (same policy as
|
|
1337
|
-
* `WalletStorage.getPool`)
|
|
1363
|
+
* `WalletStorage.getPool`); a cross-tab corruption shouldn't crash
|
|
1338
1364
|
* the server render.
|
|
1339
1365
|
*/
|
|
1340
1366
|
const readWalletSnapshot = (source, options = {}) => {
|
|
1341
|
-
const keyPrefix = options.keyPrefix || "butr";
|
|
1367
|
+
const keyPrefix = options.keyPrefix === void 0 || options.keyPrefix === "" ? "butr" : options.keyPrefix;
|
|
1342
1368
|
const cookies = toCookieMap(source);
|
|
1343
1369
|
const pool = parsePool(cookies.get(`${keyPrefix}-pool`));
|
|
1344
1370
|
const selection = parseSelection(cookies.get(`${keyPrefix}-selection`));
|
|
1345
1371
|
const rawActive = cookies.get(`${keyPrefix}-active`);
|
|
1346
1372
|
let activeConnectorId = null;
|
|
1347
|
-
if (rawActive && rawActive.length > 0 && pool[rawActive]) activeConnectorId = rawActive;
|
|
1373
|
+
if (rawActive !== void 0 && rawActive.length > 0 && pool[rawActive] !== void 0) activeConnectorId = rawActive;
|
|
1348
1374
|
else {
|
|
1349
1375
|
const firstKey = Object.keys(pool)[0];
|
|
1350
1376
|
if (firstKey) activeConnectorId = firstKey;
|
|
@@ -1360,19 +1386,25 @@ const readWalletSnapshot = (source, options = {}) => {
|
|
|
1360
1386
|
//#region src/wallet-equal.ts
|
|
1361
1387
|
/**
|
|
1362
1388
|
* Two wallet snapshots are equivalent for selector purposes iff they
|
|
1363
|
-
* share
|
|
1364
|
-
* id. Used by `useStoreWithEqualityFn` consumers
|
|
1365
|
-
* selected-wallet, useWalletEntry) to suppress spurious
|
|
1366
|
-
* when the underlying Map churns but the resolved entry
|
|
1389
|
+
* share the same adapter instance, active account address, and active
|
|
1390
|
+
* account chain id. Used by `useStoreWithEqualityFn` consumers
|
|
1391
|
+
* (active-wallet, selected-wallet, useWalletEntry) to suppress spurious
|
|
1392
|
+
* re-renders when the underlying Map churns but the resolved entry
|
|
1393
|
+
* hasn't changed.
|
|
1394
|
+
*
|
|
1395
|
+
* The adapter is compared by reference, not by `connector.id`: hydration
|
|
1396
|
+
* replaces a shadow adapter with the live one under an unchanged id and
|
|
1397
|
+
* address, so an id comparison reports "equal" and leaves consumers
|
|
1398
|
+
* holding a placeholder whose every method throws ShadowConnectorError.
|
|
1367
1399
|
*
|
|
1368
|
-
* Hoisted to its own module so the equivalence rule lives in one place
|
|
1369
|
-
*
|
|
1400
|
+
* Hoisted to its own module so the equivalence rule lives in one place;
|
|
1401
|
+
* if we ever extend the snapshot (e.g. to consider `accounts.length`),
|
|
1370
1402
|
* every selector hook picks up the new rule for free.
|
|
1371
1403
|
*/
|
|
1372
1404
|
const walletEqual = (a, b) => {
|
|
1373
1405
|
if (a === b) return true;
|
|
1374
1406
|
if (!a || !b) return false;
|
|
1375
|
-
return a.connector
|
|
1407
|
+
return a.connector === b.connector && a.account.walletAddress === b.account.walletAddress && a.account.chain.id === b.account.chain.id;
|
|
1376
1408
|
};
|
|
1377
1409
|
|
|
1378
1410
|
//#endregion
|
|
@@ -1380,7 +1412,7 @@ const walletEqual = (a, b) => {
|
|
|
1380
1412
|
/**
|
|
1381
1413
|
* Normalize a wallet-announced icon string.
|
|
1382
1414
|
*
|
|
1383
|
-
* Wallets announce their icon through external metadata
|
|
1415
|
+
* Wallets announce their icon through external metadata; EIP-6963
|
|
1384
1416
|
* `providerInfo.icon`, Wallet Standard `wallet.icon`. That value is
|
|
1385
1417
|
* not under butr's control, and some wallets ship data-URI icons with
|
|
1386
1418
|
* surrounding whitespace (a newline left over from a pretty-printed
|
|
@@ -1389,7 +1421,7 @@ const walletEqual = (a, b) => {
|
|
|
1389
1421
|
*
|
|
1390
1422
|
* Trims surrounding whitespace and treats an all-whitespace (or empty)
|
|
1391
1423
|
* icon as absent, so consumers get either a usable string or
|
|
1392
|
-
* `undefined
|
|
1424
|
+
* `undefined`: never a blank or malformed one. `undefined` passes
|
|
1393
1425
|
* through untouched.
|
|
1394
1426
|
*/
|
|
1395
1427
|
const sanitizeIcon = (icon) => {
|
|
@@ -1416,7 +1448,7 @@ const sanitizeIcon = (icon) => {
|
|
|
1416
1448
|
* - {@link bytesToHexPrefixed} returns `0x`-prefixed hex (EVM, Polkadot).
|
|
1417
1449
|
* - {@link hexToBytes} tolerantly strips an optional `0x` (all callers).
|
|
1418
1450
|
*/
|
|
1419
|
-
/** Bare lowercase hex
|
|
1451
|
+
/** Bare lowercase hex, no `0x` prefix (Bitcoin, Ledger). */
|
|
1420
1452
|
const bytesToHex = (bytes) => {
|
|
1421
1453
|
let hex = "";
|
|
1422
1454
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
@@ -1458,5 +1490,5 @@ const bytesToBase64 = (bytes) => {
|
|
|
1458
1490
|
};
|
|
1459
1491
|
|
|
1460
1492
|
//#endregion
|
|
1461
|
-
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, WalletStorage, base64ToBytes, buildChainsByPlatform, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, hexToBytes, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
|
|
1493
|
+
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, ShadowConnectorError, WalletStorage, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createWalletSource, createWalletStore, hexToBytes, isShadowAdapter, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
|
|
1462
1494
|
//# sourceMappingURL=index.js.map
|