@usebutr/core 1.0.0 → 2.0.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 +226 -504
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +295 -318
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -3,11 +3,9 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
//#region src/types/account.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* must build accounts through this helper (or keep the format
|
|
10
|
-
* byte-identical).
|
|
6
|
+
* The reducer compares accounts by the composite `<chain>:<address>`
|
|
7
|
+
* id, so every adapter must build accounts here or reproduce that
|
|
8
|
+
* format byte for byte.
|
|
11
9
|
*/
|
|
12
10
|
const buildAccount = (address, chain) => ({
|
|
13
11
|
chain,
|
|
@@ -18,30 +16,9 @@ const buildAccount = (address, chain) => ({
|
|
|
18
16
|
//#endregion
|
|
19
17
|
//#region src/types/chains-by-platform.ts
|
|
20
18
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Use this in apps that target one or two chain platforms; importing
|
|
25
|
-
* only those packages keeps unused chain registries out of the bundle.
|
|
26
|
-
* Apps that want every chain reach for `CHAINS_BY_PLATFORM` from
|
|
27
|
-
* `@usebutr/wallets` instead.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // EVM-only app: Solana/Sui/Bitcoin tables never enter the bundle
|
|
31
|
-
* import { EVM_CHAINS_LIST } from "@usebutr/evm";
|
|
32
|
-
* import { buildChainsByPlatform } from "@usebutr/core";
|
|
33
|
-
*
|
|
34
|
-
* const chains = buildChainsByPlatform({ evm: EVM_CHAINS_LIST });
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* // Multi-chain app: pull from each package the app actually uses
|
|
38
|
-
* import { EVM_CHAINS_LIST } from "@usebutr/evm";
|
|
39
|
-
* import { SVM_CHAINS_LIST } from "@usebutr/svm";
|
|
40
|
-
*
|
|
41
|
-
* const chains = buildChainsByPlatform({
|
|
42
|
-
* evm: EVM_CHAINS_LIST,
|
|
43
|
-
* svm: SVM_CHAINS_LIST,
|
|
44
|
-
* });
|
|
19
|
+
* Naming only the platforms an app targets keeps the other packages'
|
|
20
|
+
* chain registries out of its bundle. Apps that want all of them import
|
|
21
|
+
* `CHAINS_BY_PLATFORM` from `@usebutr/wallets`.
|
|
45
22
|
*/
|
|
46
23
|
const buildChainsByPlatform = (partial) => ({
|
|
47
24
|
bitcoin: partial.bitcoin ?? [],
|
|
@@ -54,17 +31,9 @@ const buildChainsByPlatform = (partial) => ({
|
|
|
54
31
|
//#endregion
|
|
55
32
|
//#region src/types/errors.ts
|
|
56
33
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* - butr's own `Error("Connection timeout")` (from the 90s connect timeout)
|
|
61
|
-
* - butr's own `Error("Failed to get account")` (from the connect flow)
|
|
62
|
-
* - EIP-1193 numeric `code` properties (`4001` → UserRejected,
|
|
63
|
-
* `-32002` → RequestPending, `4100`/`4900`/`4901` → NotConnected:
|
|
64
|
-
* unauthorized / disconnected from all-or-one chains)
|
|
65
|
-
* - common message substrings: "user rejected" / "user denied",
|
|
66
|
-
* "locked", "chain", etc.
|
|
67
|
-
* - anything else → `Unknown` with `cause` set to the original value.
|
|
34
|
+
* EIP-1193 codes: `4001` rejected, `-32002` pending, `4100`/`4900`/`4901`
|
|
35
|
+
* unauthorized or disconnected. Message-substring matching is the last
|
|
36
|
+
* resort for SDKs that ship no codes at all.
|
|
68
37
|
*/
|
|
69
38
|
const mapConnectionError = (raw) => {
|
|
70
39
|
if (raw instanceof Error) {
|
|
@@ -123,11 +92,9 @@ const mapConnectionError = (raw) => {
|
|
|
123
92
|
//#endregion
|
|
124
93
|
//#region src/types/platform.ts
|
|
125
94
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* never drift (a missing platform here was why Polkadot connections failed
|
|
130
|
-
* to persist).
|
|
95
|
+
* Single runtime source of truth: `ChainPlatform` and the storage
|
|
96
|
+
* validators' allowlists both derive from it. Omitting a platform here
|
|
97
|
+
* silently stops its connections from persisting.
|
|
131
98
|
*/
|
|
132
99
|
const CHAIN_PLATFORMS = [
|
|
133
100
|
"evm",
|
|
@@ -140,32 +107,17 @@ const CHAIN_PLATFORMS = [
|
|
|
140
107
|
//#endregion
|
|
141
108
|
//#region src/wallet-source.ts
|
|
142
109
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* so an EVM-only app can do
|
|
146
|
-
* `createWalletSource(discoverEvmAdapters)` without importing anything
|
|
147
|
-
* protocol-bearing beyond `@usebutr/evm`.
|
|
110
|
+
* Takes the exact shape of `discoverEvmAdapters` and friends, so a
|
|
111
|
+
* single-platform app keeps `@usebutr/wallets` out of its bundle.
|
|
148
112
|
*/
|
|
149
113
|
const createWalletSource = (subscribe) => ({ subscribe });
|
|
150
114
|
|
|
151
115
|
//#endregion
|
|
152
116
|
//#region src/store/shadow-adapter.ts
|
|
153
117
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* snapshot). Shadow adapters carry the identity and account data of a
|
|
158
|
-
* previously-connected wallet, but the live wallet extension hasn't
|
|
159
|
-
* been verified yet; the silent reconnect happens asynchronously
|
|
160
|
-
* after mount.
|
|
161
|
-
*
|
|
162
|
-
* UI code that gates affordances on `wallet.connector.capabilities.*`
|
|
163
|
-
* never reaches a shadow method (capabilities are all `false`). Code
|
|
164
|
-
* that calls through anyway hits this typed error, which is the
|
|
165
|
-
* correct loud failure: the consumer ignored the capability gate.
|
|
166
|
-
*
|
|
167
|
-
* Consumers wanting to wait out the reconnecting window should branch
|
|
168
|
-
* on whether `connectorId` is in `state.reconnectingIds`.
|
|
118
|
+
* Loud failure for consumers that called a wallet method without gating
|
|
119
|
+
* on `capabilities.*` (all `false` here) or `reconnectingIds`, before
|
|
120
|
+
* silent reconnect swapped in the live adapter.
|
|
169
121
|
*/
|
|
170
122
|
var ShadowConnectorError = class extends Error {
|
|
171
123
|
code = "BUTR_RECONNECTING";
|
|
@@ -191,26 +143,9 @@ const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
|
191
143
|
switchChain: false
|
|
192
144
|
});
|
|
193
145
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* is provided: each stored entry becomes a `ConnectedWallet` whose
|
|
198
|
-
* `connector` is one of these. The store flips `isHydrated` true
|
|
199
|
-
* synchronously and exposes the data through the usual hooks
|
|
200
|
-
* (`useActiveWallet`, `useConnectedWallets`, …), but the connector
|
|
201
|
-
* can't actually talk to a wallet yet, so its capabilities are all
|
|
202
|
-
* `false` and its methods throw `ShadowConnectorError` if called.
|
|
203
|
-
*
|
|
204
|
-
* The hydration coordinator's silent-reconnect pass upgrades each
|
|
205
|
-
* shadow to a live `WalletAdapter` by calling `createConnector(id)`
|
|
206
|
-
* (which only succeeds once the live adapter has been announced via
|
|
207
|
-
* discovery or registered explicitly) and replacing the pool entry.
|
|
208
|
-
* On success the connector id is removed from `reconnectingIds`; on
|
|
209
|
-
* failure the entry is dropped from the pool and storage.
|
|
210
|
-
*
|
|
211
|
-
* The entry's `name` is required (the storage validator rejects
|
|
212
|
-
* entries without it); `icon` is optional only because some live
|
|
213
|
-
* adapters genuinely have no icon to begin with.
|
|
146
|
+
* Lets `createWalletStore` flip `isHydrated` synchronously from
|
|
147
|
+
* `initialState`; the hydration coordinator later swaps each shadow for
|
|
148
|
+
* a live adapter, or drops the entry from pool and storage.
|
|
214
149
|
*/
|
|
215
150
|
const createShadowAdapter = (entry) => {
|
|
216
151
|
const id = entry.connectorId;
|
|
@@ -260,17 +195,9 @@ const createShadowAdapter = (entry) => {
|
|
|
260
195
|
}
|
|
261
196
|
};
|
|
262
197
|
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* for consumers writing wagmi-style "is this connection verified yet"
|
|
267
|
-
* checks without subscribing to `reconnectingIds` directly.
|
|
268
|
-
*
|
|
269
|
-
* Detection is structural: a shadow has all capabilities set to false.
|
|
270
|
-
* Live adapters always advertise at least one capability (every wallet
|
|
271
|
-
* surface includes `getBalance`, `signMessage`, `switchChain` as
|
|
272
|
-
* required methods, and adapter constructors set their flags
|
|
273
|
-
* accordingly).
|
|
198
|
+
* Structural detection: relies on every live adapter advertising at
|
|
199
|
+
* least one capability, since `getBalance`, `signMessage` and
|
|
200
|
+
* `switchChain` are required on all wallet surfaces.
|
|
274
201
|
*/
|
|
275
202
|
const isShadowAdapter = (adapter) => {
|
|
276
203
|
return Object.values(adapter.capabilities).every((flag) => !flag);
|
|
@@ -369,6 +296,64 @@ const parseStoredPoolEntry = (key, value) => {
|
|
|
369
296
|
if (!parsed.success || parsed.data.connectorId !== key) return null;
|
|
370
297
|
return parsed.data;
|
|
371
298
|
};
|
|
299
|
+
const DEFAULT_KEY_PREFIX = "butr";
|
|
300
|
+
/**
|
|
301
|
+
* `WalletStorage` writes these and `readWalletSnapshot` reads them from
|
|
302
|
+
* a cookie jar; any divergence desyncs the SSR-seeded render from the
|
|
303
|
+
* client that rehydrates it, the failure ADR 0003 exists to prevent.
|
|
304
|
+
*/
|
|
305
|
+
const storageKeys = (keyPrefix) => {
|
|
306
|
+
const prefix = keyPrefix === void 0 || keyPrefix === "" ? DEFAULT_KEY_PREFIX : keyPrefix;
|
|
307
|
+
return {
|
|
308
|
+
active: `${prefix}-active`,
|
|
309
|
+
pool: `${prefix}-pool`,
|
|
310
|
+
selection: `${prefix}-selection`,
|
|
311
|
+
userDisconnected: `${prefix}-user-disconnected`
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Never repairs or throws: one corrupt entry must not take down a whole
|
|
316
|
+
* session, and `readWalletSnapshot` may run on a server with no cookie
|
|
317
|
+
* jar to write the eviction to.
|
|
318
|
+
*/
|
|
319
|
+
const decodePool = (raw, label = "[butr]") => {
|
|
320
|
+
if (raw === null || raw === void 0 || raw === "") return {};
|
|
321
|
+
let value;
|
|
322
|
+
try {
|
|
323
|
+
value = JSON.parse(raw);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
logWarn(`${label} failed to parse pool from storage:`, error);
|
|
326
|
+
return {};
|
|
327
|
+
}
|
|
328
|
+
const parsed = recordSchema.safeParse(value);
|
|
329
|
+
if (!parsed.success) return {};
|
|
330
|
+
const result = {};
|
|
331
|
+
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
332
|
+
const entry = parseStoredPoolEntry(key, entryValue);
|
|
333
|
+
if (entry === null) logWarn(`${label} dropping invalid pool entry for ${key}`);
|
|
334
|
+
else result[key] = entry;
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
};
|
|
338
|
+
/** Decode a persisted selection payload. Same contract as `decodePool`. */
|
|
339
|
+
const decodeSelection = (raw, label = "[butr]") => {
|
|
340
|
+
if (raw === null || raw === void 0 || raw === "") return {};
|
|
341
|
+
let value;
|
|
342
|
+
try {
|
|
343
|
+
value = JSON.parse(raw);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
logWarn(`${label} failed to parse selection from storage:`, error);
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
const parsed = recordSchema.safeParse(value);
|
|
349
|
+
if (!parsed.success) return {};
|
|
350
|
+
const result = {};
|
|
351
|
+
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
352
|
+
const platform = chainPlatformSchema.safeParse(key);
|
|
353
|
+
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
};
|
|
372
357
|
|
|
373
358
|
//#endregion
|
|
374
359
|
//#region src/storage/wallet-storage.ts
|
|
@@ -380,19 +365,17 @@ var WalletStorage = class {
|
|
|
380
365
|
persistent;
|
|
381
366
|
session;
|
|
382
367
|
/**
|
|
383
|
-
* Serializes pool-
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* state, each merge their own entries, and whichever finishes last
|
|
387
|
-
* overwrites the other's additions. Reads (`getPool`) don't enter
|
|
388
|
-
* the queue; they observe whatever's currently in the driver.
|
|
368
|
+
* Serializes pool read-modify-writes; without it concurrent `setPool`
|
|
369
|
+
* calls drop each other's entries. Not reentrant, so a queued mutation
|
|
370
|
+
* must read via `readPool`: `getPool` re-enters and deadlocks it.
|
|
389
371
|
*/
|
|
390
372
|
poolMutationQueue = Promise.resolve();
|
|
391
373
|
constructor(config) {
|
|
392
|
-
|
|
393
|
-
this.
|
|
394
|
-
this.
|
|
395
|
-
this.
|
|
374
|
+
const keys = storageKeys(config.keyPrefix);
|
|
375
|
+
this.poolKey = keys.pool;
|
|
376
|
+
this.selectionKey = keys.selection;
|
|
377
|
+
this.activeKey = keys.active;
|
|
378
|
+
this.userDisconnectedKey = keys.userDisconnected;
|
|
396
379
|
if (config.persistent && config.session) {
|
|
397
380
|
this.persistent = config.persistent;
|
|
398
381
|
this.session = config.session;
|
|
@@ -418,43 +401,48 @@ var WalletStorage = class {
|
|
|
418
401
|
resolve();
|
|
419
402
|
}
|
|
420
403
|
}
|
|
421
|
-
|
|
404
|
+
/** Read and decode the pool without touching the mutation queue. Safe to
|
|
405
|
+
* call from inside a queued mutation, unlike `getPool`. */
|
|
406
|
+
async readPool() {
|
|
407
|
+
let stored;
|
|
422
408
|
try {
|
|
423
|
-
|
|
424
|
-
if (stored === null || stored === "") return {};
|
|
425
|
-
const value = JSON.parse(stored);
|
|
426
|
-
const parsed = recordSchema.safeParse(value);
|
|
427
|
-
if (!parsed.success) {
|
|
428
|
-
await this.clearPool();
|
|
429
|
-
return {};
|
|
430
|
-
}
|
|
431
|
-
const result = {};
|
|
432
|
-
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
433
|
-
const entry = parseStoredPoolEntry(key, entryValue);
|
|
434
|
-
if (entry === null) logWarn(`[butr] dropping invalid pool entry for ${key}`);
|
|
435
|
-
else result[key] = entry;
|
|
436
|
-
}
|
|
437
|
-
return result;
|
|
409
|
+
stored = await this.persistent.getItem(this.poolKey);
|
|
438
410
|
} catch (error) {
|
|
439
|
-
logWarn("[butr] failed to
|
|
440
|
-
|
|
441
|
-
|
|
411
|
+
logWarn("[butr] failed to read pool from storage:", error);
|
|
412
|
+
return {
|
|
413
|
+
corrupt: false,
|
|
414
|
+
decoded: {}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (stored === null || stored === "") return {
|
|
418
|
+
corrupt: false,
|
|
419
|
+
decoded: {}
|
|
420
|
+
};
|
|
421
|
+
const decoded = decodePool(stored);
|
|
422
|
+
return {
|
|
423
|
+
corrupt: Object.keys(decoded).length === 0,
|
|
424
|
+
decoded
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async getPool() {
|
|
428
|
+
const { corrupt, decoded } = await this.readPool();
|
|
429
|
+
if (corrupt) try {
|
|
430
|
+
await this.persistent.removeItem(this.poolKey);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logWarn("[butr] failed to clear corrupt pool:", error);
|
|
442
433
|
}
|
|
434
|
+
return decoded;
|
|
443
435
|
}
|
|
444
436
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* now", not "the complete list of remembered connections"; a
|
|
449
|
-
* silent reconnect that fails on reload leaves the entry out of
|
|
450
|
-
* the pool but the saved entry stays so the next load can retry.
|
|
451
|
-
* Use `removePoolEntry` for explicit eviction (the user clicked
|
|
452
|
-
* Disconnect) and `clearAll` for a full wipe (reset).
|
|
437
|
+
* Additive on purpose: a failed silent reconnect drops the entry from
|
|
438
|
+
* the live pool, and it must survive to be retried next load.
|
|
439
|
+
* Eviction goes through `removePoolEntry` or `clearAll`.
|
|
453
440
|
*/
|
|
454
441
|
async setPool(pool) {
|
|
455
442
|
await this.serializePoolMutation(async () => {
|
|
456
443
|
try {
|
|
457
|
-
const
|
|
444
|
+
const { decoded: existing } = await this.readPool();
|
|
445
|
+
const serializable = { ...existing };
|
|
458
446
|
for (const [connectorId, wallet] of pool) {
|
|
459
447
|
const entry = {
|
|
460
448
|
account: wallet.account,
|
|
@@ -476,7 +464,7 @@ var WalletStorage = class {
|
|
|
476
464
|
async removePoolEntry(connectorId) {
|
|
477
465
|
await this.serializePoolMutation(async () => {
|
|
478
466
|
try {
|
|
479
|
-
const stored = await this.
|
|
467
|
+
const { decoded: stored } = await this.readPool();
|
|
480
468
|
if (stored[connectorId]) {
|
|
481
469
|
const { [connectorId]: _, ...remaining } = stored;
|
|
482
470
|
await this.persistent.setItem(this.poolKey, JSON.stringify(remaining));
|
|
@@ -493,19 +481,9 @@ var WalletStorage = class {
|
|
|
493
481
|
}
|
|
494
482
|
async getSelection() {
|
|
495
483
|
try {
|
|
496
|
-
|
|
497
|
-
if (stored === null || stored === "") return {};
|
|
498
|
-
const value = JSON.parse(stored);
|
|
499
|
-
const parsed = recordSchema.safeParse(value);
|
|
500
|
-
if (!parsed.success) return {};
|
|
501
|
-
const result = {};
|
|
502
|
-
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
503
|
-
const platform = chainPlatformSchema.safeParse(key);
|
|
504
|
-
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
505
|
-
}
|
|
506
|
-
return result;
|
|
484
|
+
return decodeSelection(await this.persistent.getItem(this.selectionKey));
|
|
507
485
|
} catch (error) {
|
|
508
|
-
logWarn("[butr] failed to
|
|
486
|
+
logWarn("[butr] failed to read selection from storage:", error);
|
|
509
487
|
return {};
|
|
510
488
|
}
|
|
511
489
|
}
|
|
@@ -541,12 +519,9 @@ var WalletStorage = class {
|
|
|
541
519
|
]);
|
|
542
520
|
}
|
|
543
521
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* but clears when the session ends (unlike the persistent driver).
|
|
548
|
-
* Prevents auto-connect from firing immediately after a manual disconnect,
|
|
549
|
-
* while still allowing auto-connect on fresh sessions.
|
|
522
|
+
* Kept in the session driver so it survives remounts (unlike a ref)
|
|
523
|
+
* yet clears at session end (unlike the persistent driver): a manual
|
|
524
|
+
* disconnect must suppress auto-connect now, not forever.
|
|
550
525
|
*/
|
|
551
526
|
async isUserDisconnected() {
|
|
552
527
|
try {
|
|
@@ -589,8 +564,6 @@ const createConnectorLifecycle = (handlers) => {
|
|
|
589
564
|
case "disconnected":
|
|
590
565
|
detach(connectorId);
|
|
591
566
|
handlers.onDisconnected(connectorId, connector.chainPlatform);
|
|
592
|
-
break;
|
|
593
|
-
default:
|
|
594
567
|
}
|
|
595
568
|
});
|
|
596
569
|
unsubscribers.set(connectorId, unsub);
|
|
@@ -732,6 +705,7 @@ const reducer = (state, event) => {
|
|
|
732
705
|
case "HYDRATED": {
|
|
733
706
|
const pool = new Map(state.pool);
|
|
734
707
|
for (const [id, entry] of event.pool) pool.set(id, entry);
|
|
708
|
+
for (const id of event.dropped) pool.delete(id);
|
|
735
709
|
const selection = new Map(state.selection);
|
|
736
710
|
for (const [platform, id] of event.selection) selection.set(platform, id);
|
|
737
711
|
let activeConnectorId = null;
|
|
@@ -739,11 +713,18 @@ const reducer = (state, event) => {
|
|
|
739
713
|
else if (state.activeConnectorId !== null && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
740
714
|
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
741
715
|
let nextReconnecting = state.reconnectingIds;
|
|
742
|
-
if (state.reconnectingIds.size > 0
|
|
716
|
+
if (state.reconnectingIds.size > 0) {
|
|
743
717
|
const next = new Set(state.reconnectingIds);
|
|
744
718
|
for (const id of event.pool.keys()) next.delete(id);
|
|
719
|
+
for (const id of event.dropped) next.delete(id);
|
|
745
720
|
if (next.size !== state.reconnectingIds.size) nextReconnecting = next;
|
|
746
721
|
}
|
|
722
|
+
const selectionAfterDrop = new Map(selection);
|
|
723
|
+
for (const [platform, id] of selectionAfterDrop) if (!pool.has(id)) {
|
|
724
|
+
selectionAfterDrop.delete(platform);
|
|
725
|
+
const fallback = findConnectorForPlatform(pool, platform);
|
|
726
|
+
if (fallback !== void 0) selectionAfterDrop.set(platform, fallback);
|
|
727
|
+
}
|
|
747
728
|
return {
|
|
748
729
|
...state,
|
|
749
730
|
activeConnectorId,
|
|
@@ -751,7 +732,7 @@ const reducer = (state, event) => {
|
|
|
751
732
|
isUserDisconnected: event.isUserDisconnected,
|
|
752
733
|
pool,
|
|
753
734
|
reconnectingIds: nextReconnecting,
|
|
754
|
-
selection
|
|
735
|
+
selection: selectionAfterDrop
|
|
755
736
|
};
|
|
756
737
|
}
|
|
757
738
|
case "USER_DISCONNECTED_SET": return {
|
|
@@ -772,6 +753,7 @@ const reducer = (state, event) => {
|
|
|
772
753
|
...state,
|
|
773
754
|
activeConnectorId: connectorId,
|
|
774
755
|
connectingConnectorId: null,
|
|
756
|
+
connectionError: null,
|
|
775
757
|
connectionStatus: "success",
|
|
776
758
|
pool: existing.connector === entry.connector ? state.pool : new Map([...state.pool, [connectorId, entry]]),
|
|
777
759
|
reconnectingIds: nextReconnecting
|
|
@@ -782,18 +764,34 @@ const reducer = (state, event) => {
|
|
|
782
764
|
...state,
|
|
783
765
|
activeConnectorId: connectorId,
|
|
784
766
|
connectingConnectorId: null,
|
|
767
|
+
connectionError: null,
|
|
785
768
|
connectionStatus: "success",
|
|
786
769
|
pool: newPool,
|
|
787
770
|
reconnectingIds: nextReconnecting,
|
|
788
771
|
selection: newSelection
|
|
789
772
|
};
|
|
790
773
|
}
|
|
791
|
-
case "
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
774
|
+
case "ENTRY_RESTORED": {
|
|
775
|
+
const { connectorId, entry } = event;
|
|
776
|
+
const nextReconnecting = state.reconnectingIds.has(connectorId) ? new Set([...state.reconnectingIds].filter((id) => id !== connectorId)) : state.reconnectingIds;
|
|
777
|
+
const platform = entry.connector.chainPlatform;
|
|
778
|
+
const selection = state.selection.has(platform) ? state.selection : new Map([...state.selection, [platform, connectorId]]);
|
|
779
|
+
return {
|
|
780
|
+
...state,
|
|
781
|
+
activeConnectorId: state.activeConnectorId ?? connectorId,
|
|
782
|
+
pool: new Map([...state.pool, [connectorId, entry]]),
|
|
783
|
+
reconnectingIds: nextReconnecting,
|
|
784
|
+
selection
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
case "CONNECT_FAILED":
|
|
788
|
+
if (state.connectingConnectorId !== event.connectorId) return state;
|
|
789
|
+
return {
|
|
790
|
+
...state,
|
|
791
|
+
connectingConnectorId: null,
|
|
792
|
+
connectionError: event.error,
|
|
793
|
+
connectionStatus: "error"
|
|
794
|
+
};
|
|
797
795
|
case "DISCONNECTED": {
|
|
798
796
|
const wallet = state.pool.get(event.connectorId);
|
|
799
797
|
if (!wallet) return state;
|
|
@@ -896,13 +894,9 @@ const run = async (fn, onError) => {
|
|
|
896
894
|
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
897
895
|
const isChainPlatform = (value) => VALID_CHAIN_PLATFORMS.has(value);
|
|
898
896
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* consumers can branch on "is this connection verified" without
|
|
903
|
-
* waiting for the async silent reconnect. `isHydrated` flips true
|
|
904
|
-
* synchronously: the consumer's first render sees the persisted
|
|
905
|
-
* state in the live store, not undefined.
|
|
897
|
+
* `isHydrated` is true from the first render on this path, so
|
|
898
|
+
* `reconnectingIds` (not `isHydrated`) is the "is this connection
|
|
899
|
+
* verified" signal until silent reconnect lands.
|
|
906
900
|
*/
|
|
907
901
|
const seedStateFromSnapshot = (snapshot) => {
|
|
908
902
|
const pool = /* @__PURE__ */ new Map();
|
|
@@ -933,8 +927,7 @@ const CONNECT_TIMEOUT_MS = 9e4;
|
|
|
933
927
|
const DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5e3;
|
|
934
928
|
const normaliseAddress = (addr) => addr.toLowerCase();
|
|
935
929
|
const createWalletStore = (config) => {
|
|
936
|
-
const
|
|
937
|
-
const storage = config.storage ?? new WalletStorage({ keyPrefix: storageKeyPrefix });
|
|
930
|
+
const storage = config.storage ?? new WalletStorage({ keyPrefix: config.storageKeyPrefix });
|
|
938
931
|
const reportStorageError = (context) => (error) => {
|
|
939
932
|
if (config.onStorageError) {
|
|
940
933
|
try {
|
|
@@ -968,6 +961,8 @@ const createWalletStore = (config) => {
|
|
|
968
961
|
refreshPoolEntry(connectorId, [...accounts], active);
|
|
969
962
|
},
|
|
970
963
|
onDisconnected: (connectorId, chainPlatform) => {
|
|
964
|
+
const wallet = get().pool.get(connectorId);
|
|
965
|
+
if (wallet) run(() => wallet.connector.disconnect?.() ?? Promise.resolve(), logError);
|
|
971
966
|
dispatch({
|
|
972
967
|
connectorId,
|
|
973
968
|
type: "DISCONNECTED"
|
|
@@ -1028,6 +1023,7 @@ const createWalletStore = (config) => {
|
|
|
1028
1023
|
} catch (error) {
|
|
1029
1024
|
const normalised = mapConnectionError(error);
|
|
1030
1025
|
dispatch({
|
|
1026
|
+
connectorId,
|
|
1031
1027
|
error: normalised,
|
|
1032
1028
|
type: "CONNECT_FAILED"
|
|
1033
1029
|
});
|
|
@@ -1074,6 +1070,7 @@ const createWalletStore = (config) => {
|
|
|
1074
1070
|
const result = await hydration.hydrate();
|
|
1075
1071
|
dispatch({
|
|
1076
1072
|
activeConnectorId: result.activeConnectorId,
|
|
1073
|
+
dropped: result.dropped.map((d) => d.connectorId),
|
|
1077
1074
|
isUserDisconnected: result.isUserDisconnected,
|
|
1078
1075
|
pool: result.pool,
|
|
1079
1076
|
selection: result.selection,
|
|
@@ -1163,7 +1160,7 @@ const createWalletStore = (config) => {
|
|
|
1163
1160
|
dispatch({
|
|
1164
1161
|
connectorId,
|
|
1165
1162
|
entry: outcome.entry,
|
|
1166
|
-
type: "
|
|
1163
|
+
type: "ENTRY_RESTORED"
|
|
1167
1164
|
});
|
|
1168
1165
|
lifecycle.attach(connectorId, outcome.entry.connector);
|
|
1169
1166
|
await Promise.all([
|
|
@@ -1191,7 +1188,7 @@ const createWalletStore = (config) => {
|
|
|
1191
1188
|
|
|
1192
1189
|
//#endregion
|
|
1193
1190
|
//#region src/storage/cookie-storage-driver.ts
|
|
1194
|
-
const DEFAULT_MAX_AGE_SECONDS =
|
|
1191
|
+
const DEFAULT_MAX_AGE_SECONDS = 2592e3;
|
|
1195
1192
|
const encode = (value) => encodeURIComponent(value);
|
|
1196
1193
|
const decode = (value) => decodeURIComponent(value);
|
|
1197
1194
|
const readCookies = () => {
|
|
@@ -1230,28 +1227,9 @@ const toCookieMap$1 = (input) => {
|
|
|
1230
1227
|
return new Map(Object.entries(input));
|
|
1231
1228
|
};
|
|
1232
1229
|
/**
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
* **When to use this:** SSR apps that need to know who's connected
|
|
1237
|
-
* during the server render (so they can stream the connected-wallet
|
|
1238
|
-
* UI without a client-side hydration flicker). Pass `initialCookies`
|
|
1239
|
-
* from a server-side cookie source (e.g. `cookies()` in Next.js'
|
|
1240
|
-
* `next/headers`, or a parsed `req.headers.cookie`) and the same
|
|
1241
|
-
* driver will serve those values during the server render and switch
|
|
1242
|
-
* to `document.cookie` once it mounts in the browser.
|
|
1243
|
-
*
|
|
1244
|
-
* **Trade-offs vs `localStorage`:** cookies travel with every
|
|
1245
|
-
* request, so they cost bytes on the wire. Keep the storage key
|
|
1246
|
-
* prefix short, and prefer this driver for the `persistent` slot
|
|
1247
|
-
* only: the `session` slot can stay in `sessionStorage` (which
|
|
1248
|
-
* cookies can't natively model anyway).
|
|
1249
|
-
*
|
|
1250
|
-
* **Server-side writes are no-ops.** Emitting `Set-Cookie` requires
|
|
1251
|
-
* access to the framework's response object, which a storage driver
|
|
1252
|
-
* shouldn't reach into. The store doesn't mutate persisted state
|
|
1253
|
-
* during the SSR pass anyway; writes only fire after client mount,
|
|
1254
|
-
* once `document.cookie` is reachable.
|
|
1230
|
+
* Server-side writes are no-ops: `Set-Cookie` needs the framework's
|
|
1231
|
+
* response object. Cookies ride every request, so use this driver for
|
|
1232
|
+
* the `persistent` slot only and leave `session` on `sessionStorage`.
|
|
1255
1233
|
*/
|
|
1256
1234
|
const createCookieStorageDriver = (options = {}) => {
|
|
1257
1235
|
const seeded = toCookieMap$1(options.initialCookies);
|
|
@@ -1304,74 +1282,18 @@ const toCookieMap = (input) => {
|
|
|
1304
1282
|
}
|
|
1305
1283
|
return out;
|
|
1306
1284
|
};
|
|
1307
|
-
const
|
|
1308
|
-
if (raw === void 0 || raw === "") return {};
|
|
1309
|
-
try {
|
|
1310
|
-
const value = JSON.parse(raw);
|
|
1311
|
-
const parsed = recordSchema.safeParse(value);
|
|
1312
|
-
if (!parsed.success) return {};
|
|
1313
|
-
const result = {};
|
|
1314
|
-
for (const [key, entryValue] of Object.entries(parsed.data)) {
|
|
1315
|
-
const entry = parseStoredPoolEntry(key, entryValue);
|
|
1316
|
-
if (entry === null) logWarn(`[butr] readWalletSnapshot: dropping invalid pool entry for ${key}`);
|
|
1317
|
-
else result[key] = entry;
|
|
1318
|
-
}
|
|
1319
|
-
return result;
|
|
1320
|
-
} catch (error) {
|
|
1321
|
-
logWarn("[butr] readWalletSnapshot: failed to parse pool cookie:", error);
|
|
1322
|
-
return {};
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1325
|
-
const parseSelection = (raw) => {
|
|
1326
|
-
if (raw === void 0 || raw === "") return {};
|
|
1327
|
-
try {
|
|
1328
|
-
const value = JSON.parse(raw);
|
|
1329
|
-
const parsed = recordSchema.safeParse(value);
|
|
1330
|
-
if (!parsed.success) return {};
|
|
1331
|
-
const result = {};
|
|
1332
|
-
for (const [key, selectionValue] of Object.entries(parsed.data)) {
|
|
1333
|
-
const platform = chainPlatformSchema.safeParse(key);
|
|
1334
|
-
if (platform.success && typeof selectionValue === "string" && selectionValue.length > 0) result[platform.data] = selectionValue;
|
|
1335
|
-
}
|
|
1336
|
-
return result;
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
logWarn("[butr] readWalletSnapshot: failed to parse selection cookie:", error);
|
|
1339
|
-
return {};
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1285
|
+
const SNAPSHOT_LABEL = "[butr] readWalletSnapshot:";
|
|
1342
1286
|
/**
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* (Server Component, route handler, edge middleware, even client
|
|
1347
|
-
* code). Pair with `createCookieStorageDriver({ initialCookies })`
|
|
1348
|
-
* and `<WalletManagerProvider initialSnapshot={…} />` to render a
|
|
1349
|
-
* connected shell server-side without a hydration flash.
|
|
1350
|
-
*
|
|
1351
|
-
* **Stale-snapshot semantics.** The snapshot reflects whatever the
|
|
1352
|
-
* browser most recently persisted. If the user has since uninstalled
|
|
1353
|
-
* the wallet, switched accounts, or disconnected in another tab, the
|
|
1354
|
-
* client-side hydration will reconcile reality and the live store
|
|
1355
|
-
* will diverge from the snapshot. Treat the snapshot as an
|
|
1356
|
-
* *optimistic* shell; accurate enough to avoid a paint flicker,
|
|
1357
|
-
* authoritative only after `useIsHydrated()` is true.
|
|
1358
|
-
*
|
|
1359
|
-
* **Inputs.** Accepts the three shapes Next.js / Express / Hono /
|
|
1360
|
-
* generic-Node cookie code naturally produces:
|
|
1361
|
-
* - A plain object: `{ "butr-pool": "{...}", … }`
|
|
1362
|
-
* - An array of `{ name, value }` (Next.js' `cookies().getAll()`)
|
|
1363
|
-
* - An iterable of `[name, value]` tuples
|
|
1364
|
-
*
|
|
1365
|
-
* Malformed entries are dropped with a `logWarn` (same policy as
|
|
1366
|
-
* `WalletStorage.getPool`); a cross-tab corruption shouldn't crash
|
|
1367
|
-
* the server render.
|
|
1287
|
+
* Optimistic: only what the browser last persisted, so an uninstall or
|
|
1288
|
+
* other-tab disconnect makes it stale. Authoritative once the entry
|
|
1289
|
+
* leaves `reconnectingIds`; `isHydrated` is true from render one.
|
|
1368
1290
|
*/
|
|
1369
1291
|
const readWalletSnapshot = (source, options = {}) => {
|
|
1370
|
-
const
|
|
1292
|
+
const keys = storageKeys(options.keyPrefix);
|
|
1371
1293
|
const cookies = toCookieMap(source);
|
|
1372
|
-
const pool =
|
|
1373
|
-
const selection =
|
|
1374
|
-
const rawActive = cookies.get(
|
|
1294
|
+
const pool = decodePool(cookies.get(keys.pool), SNAPSHOT_LABEL);
|
|
1295
|
+
const selection = decodeSelection(cookies.get(keys.selection), SNAPSHOT_LABEL);
|
|
1296
|
+
const rawActive = cookies.get(keys.active);
|
|
1375
1297
|
let activeConnectorId = null;
|
|
1376
1298
|
if (rawActive !== void 0 && rawActive.length > 0 && pool[rawActive] !== void 0) activeConnectorId = rawActive;
|
|
1377
1299
|
else {
|
|
@@ -1386,70 +1308,34 @@ const readWalletSnapshot = (source, options = {}) => {
|
|
|
1386
1308
|
};
|
|
1387
1309
|
|
|
1388
1310
|
//#endregion
|
|
1389
|
-
//#region src/
|
|
1311
|
+
//#region src/group-by-platform.ts
|
|
1390
1312
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
* (active-wallet, selected-wallet, useWalletEntry) to suppress spurious
|
|
1395
|
-
* re-renders when the underlying Map churns but the resolved entry
|
|
1396
|
-
* hasn't changed.
|
|
1397
|
-
*
|
|
1398
|
-
* The adapter is compared by reference, not by `connector.id`: hydration
|
|
1399
|
-
* replaces a shadow adapter with the live one under an unchanged id and
|
|
1400
|
-
* address, so an id comparison reports "equal" and leaves consumers
|
|
1401
|
-
* holding a placeholder whose every method throws ShadowConnectorError.
|
|
1402
|
-
*
|
|
1403
|
-
* Hoisted to its own module so the equivalence rule lives in one place;
|
|
1404
|
-
* if we ever extend the snapshot (e.g. to consider `accounts.length`),
|
|
1405
|
-
* every selector hook picks up the new rule for free.
|
|
1313
|
+
* A multi-chain wallet announces one adapter per platform, so brands
|
|
1314
|
+
* repeat in the flat list. Keys follow `CHAIN_PLATFORMS` order and empty
|
|
1315
|
+
* platforms are omitted, so `[...groups]` needs no emptiness filter.
|
|
1406
1316
|
*/
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
* surrounding whitespace (a newline left over from a pretty-printed
|
|
1422
|
-
* manifest). Strict consumers reject it: Next.js's `<Image>` throws
|
|
1423
|
-
* because `src` must not start with a control character.
|
|
1424
|
-
*
|
|
1425
|
-
* Trims surrounding whitespace and treats an all-whitespace (or empty)
|
|
1426
|
-
* icon as absent, so consumers get either a usable string or
|
|
1427
|
-
* `undefined`: never a blank or malformed one. `undefined` passes
|
|
1428
|
-
* through untouched.
|
|
1429
|
-
*/
|
|
1430
|
-
const sanitizeIcon = (icon) => {
|
|
1431
|
-
if (icon === void 0) return;
|
|
1432
|
-
const trimmed = icon.trim();
|
|
1433
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
1317
|
+
const groupByPlatform = (items, getPlatform) => {
|
|
1318
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1319
|
+
for (const item of items) {
|
|
1320
|
+
const platform = getPlatform(item);
|
|
1321
|
+
const bucket = buckets.get(platform);
|
|
1322
|
+
if (bucket === void 0) buckets.set(platform, [item]);
|
|
1323
|
+
else bucket.push(item);
|
|
1324
|
+
}
|
|
1325
|
+
const ordered = /* @__PURE__ */ new Map();
|
|
1326
|
+
for (const platform of CHAIN_PLATFORMS) {
|
|
1327
|
+
const bucket = buckets.get(platform);
|
|
1328
|
+
if (bucket !== void 0) ordered.set(platform, bucket);
|
|
1329
|
+
}
|
|
1330
|
+
return ordered;
|
|
1434
1331
|
};
|
|
1435
1332
|
|
|
1436
1333
|
//#endregion
|
|
1437
1334
|
//#region src/encoding/bytes.ts
|
|
1438
1335
|
/**
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1442
|
-
* chain. They used to be hand-reimplemented in ~10 connector files; a
|
|
1443
|
-
* single tested module removes the drift surface (a `padStart` omission
|
|
1444
|
-
* or base64 variant mismatch corrupts signatures/addresses for one chain
|
|
1445
|
-
* only, and the divergence is invisible because the copies look "the
|
|
1446
|
-
* same").
|
|
1447
|
-
*
|
|
1448
|
-
* Hex prefixing diverges across chains, so the module exposes **explicit
|
|
1449
|
-
* variants** rather than one function:
|
|
1450
|
-
* - {@link bytesToHex} returns bare hex (Bitcoin, Ledger).
|
|
1451
|
-
* - {@link bytesToHexPrefixed} returns `0x`-prefixed hex (EVM, Polkadot).
|
|
1452
|
-
* - {@link hexToBytes} tolerantly strips an optional `0x` (all callers).
|
|
1336
|
+
* Hex prefixing diverges by chain (bare for Bitcoin and Ledger, `0x` for
|
|
1337
|
+
* EVM and Polkadot), so the variants stay explicit: these sit on the
|
|
1338
|
+
* signing path, where a silent mismatch corrupts one chain's signatures.
|
|
1453
1339
|
*/
|
|
1454
1340
|
/** Bitcoin/Solana alphabet; omits the visually ambiguous `0`, `O`, `I`, `l`. */
|
|
1455
1341
|
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
@@ -1539,5 +1425,96 @@ const base58ToBytes = (input) => {
|
|
|
1539
1425
|
};
|
|
1540
1426
|
|
|
1541
1427
|
//#endregion
|
|
1542
|
-
|
|
1428
|
+
//#region src/sign-in/sign-in-flow.ts
|
|
1429
|
+
/** Thrown before any wallet interaction when the wallet can't sign at
|
|
1430
|
+
* all. Distinct from a rejection: nothing was asked of the user, so UI
|
|
1431
|
+
* should say "this wallet can't sign in" rather than "you declined". */
|
|
1432
|
+
var SignInUnsupportedError = class extends Error {
|
|
1433
|
+
connectorId;
|
|
1434
|
+
constructor(connectorId) {
|
|
1435
|
+
super(`Wallet "${connectorId}" reports capabilities.signMessage === false, so it cannot sign in.`);
|
|
1436
|
+
this.connectorId = connectorId;
|
|
1437
|
+
this.name = "SignInUnsupportedError";
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
const defaultBuildMessage = ({ account, nonce }) => `${account.walletAddress} signs in.\nNonce: ${nonce}`;
|
|
1441
|
+
/**
|
|
1442
|
+
* Solana wallets advertising `solana:signIn` take the SIWS path, so
|
|
1443
|
+
* `result.signedMessage` holds the wallet-composed statement and
|
|
1444
|
+
* `result.message` is absent. `preferSignMessage` opts out.
|
|
1445
|
+
*/
|
|
1446
|
+
const createSignInFlow = (options) => {
|
|
1447
|
+
const buildMessage = options.buildMessage ?? defaultBuildMessage;
|
|
1448
|
+
const sign = async (wallet, account) => {
|
|
1449
|
+
const { connector } = wallet;
|
|
1450
|
+
const signingAccount = account ?? wallet.account;
|
|
1451
|
+
if (!connector.capabilities.signMessage) throw new SignInUnsupportedError(connector.id);
|
|
1452
|
+
const nonce = await options.getNonce({
|
|
1453
|
+
account: signingAccount,
|
|
1454
|
+
wallet
|
|
1455
|
+
});
|
|
1456
|
+
if (options.preferSignMessage !== true && connector.chainPlatform === "svm" && connector.capabilities.signIn && connector.signIn !== void 0) {
|
|
1457
|
+
const output = await connector.signIn({ nonce });
|
|
1458
|
+
return {
|
|
1459
|
+
account: output.account,
|
|
1460
|
+
nonce,
|
|
1461
|
+
signature: output.signature,
|
|
1462
|
+
signatureBase64: bytesToBase64(output.signature),
|
|
1463
|
+
signedMessage: output.signedMessage,
|
|
1464
|
+
signedMessageBase64: bytesToBase64(output.signedMessage),
|
|
1465
|
+
wallet
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
const message = buildMessage({
|
|
1469
|
+
account: signingAccount,
|
|
1470
|
+
nonce,
|
|
1471
|
+
wallet
|
|
1472
|
+
});
|
|
1473
|
+
const { signature, signedMessage } = await connector.signMessage(new TextEncoder().encode(message), signingAccount);
|
|
1474
|
+
return {
|
|
1475
|
+
account: signingAccount,
|
|
1476
|
+
message,
|
|
1477
|
+
nonce,
|
|
1478
|
+
signature,
|
|
1479
|
+
signatureBase64: bytesToBase64(signature),
|
|
1480
|
+
signedMessage,
|
|
1481
|
+
signedMessageBase64: bytesToBase64(signedMessage),
|
|
1482
|
+
wallet
|
|
1483
|
+
};
|
|
1484
|
+
};
|
|
1485
|
+
return { signIn: async (wallet, account) => {
|
|
1486
|
+
const result = await sign(wallet, account);
|
|
1487
|
+
await options.verify(result);
|
|
1488
|
+
return result;
|
|
1489
|
+
} };
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region src/wallet-equal.ts
|
|
1494
|
+
/**
|
|
1495
|
+
* The adapter is compared by reference, not `connector.id`: hydration
|
|
1496
|
+
* swaps a shadow adapter for the live one under an unchanged id, and an
|
|
1497
|
+
* id check would strand consumers on the throwing placeholder.
|
|
1498
|
+
*/
|
|
1499
|
+
const walletEqual = (a, b) => {
|
|
1500
|
+
if (a === b) return true;
|
|
1501
|
+
if (!a || !b) return false;
|
|
1502
|
+
return a.connector === b.connector && a.account.walletAddress === b.account.walletAddress && a.account.chain.id === b.account.chain.id;
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
//#endregion
|
|
1506
|
+
//#region src/sanitize-icon.ts
|
|
1507
|
+
/**
|
|
1508
|
+
* Some wallets announce data-URI icons wrapped in whitespace, which
|
|
1509
|
+
* makes Next.js `<Image>` throw on a leading control character.
|
|
1510
|
+
* Discovery already applies this; only hand-built metadata needs it.
|
|
1511
|
+
*/
|
|
1512
|
+
const sanitizeIcon = (icon) => {
|
|
1513
|
+
if (icon === void 0) return;
|
|
1514
|
+
const trimmed = icon.trim();
|
|
1515
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
//#endregion
|
|
1519
|
+
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, ShadowConnectorError, SignInUnsupportedError, WalletStorage, base58ToBytes, base64ToBytes, buildAccount, buildChainsByPlatform, bytesToBase58, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createSignInFlow, createWalletSource, createWalletStore, groupByPlatform, hexToBytes, isShadowAdapter, logError, logWarn, mapConnectionError, readWalletSnapshot, sanitizeIcon, walletEqual };
|
|
1543
1520
|
//# sourceMappingURL=index.js.map
|