@usebutr/core 2.0.0 → 3.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/CHANGELOG.md +243 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.ts +502 -482
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +874 -919
- package/dist/index.js.map +1 -1
- package/package.json +22 -6
package/dist/index.js
CHANGED
|
@@ -14,79 +14,57 @@ const buildAccount = (address, chain) => ({
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
//#endregion
|
|
17
|
-
//#region src/types/
|
|
17
|
+
//#region src/types/chain.ts
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* chain
|
|
21
|
-
*
|
|
19
|
+
* Wallets report bare CAIP-2 ids, so the display name comes from a chain
|
|
20
|
+
* registry. A chain outside `known` keeps its id as its name rather than
|
|
21
|
+
* borrowing the wallet's.
|
|
22
22
|
*/
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
const resolveChain = (id, known = []) => {
|
|
24
|
+
const match = known.find((chain) => chain.id === id);
|
|
25
|
+
if (match !== void 0) return match;
|
|
26
|
+
const [namespace = "", reference = ""] = id.split(":");
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
name: id,
|
|
30
|
+
namespace,
|
|
31
|
+
reference
|
|
32
|
+
};
|
|
33
|
+
};
|
|
30
34
|
|
|
31
35
|
//#endregion
|
|
32
36
|
//#region src/types/errors.ts
|
|
37
|
+
var ConnectionError = class extends Error {
|
|
38
|
+
kind;
|
|
39
|
+
constructor(kind, message, options) {
|
|
40
|
+
super(message, options);
|
|
41
|
+
this.name = "ConnectionError";
|
|
42
|
+
this.kind = kind;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const readCode = (error) => "code" in error && (typeof error.code === "number" || typeof error.code === "string") ? error.code : void 0;
|
|
33
46
|
/**
|
|
34
47
|
* EIP-1193 codes: `4001` rejected, `-32002` pending, `4100`/`4900`/`4901`
|
|
35
48
|
* unauthorized or disconnected. Message-substring matching is the last
|
|
36
49
|
* resort for SDKs that ship no codes at all.
|
|
37
50
|
*/
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (code === -32002) return {
|
|
56
|
-
kind: "RequestPending",
|
|
57
|
-
message
|
|
58
|
-
};
|
|
59
|
-
if (code === 4100 || code === 4900 || code === 4901) return {
|
|
60
|
-
kind: "NotConnected",
|
|
61
|
-
message
|
|
62
|
-
};
|
|
63
|
-
if (lower.includes("user rejected") || lower.includes("user denied")) return {
|
|
64
|
-
kind: "UserRejected",
|
|
65
|
-
message
|
|
66
|
-
};
|
|
67
|
-
if (lower.includes("locked")) return {
|
|
68
|
-
kind: "WalletLocked",
|
|
69
|
-
message
|
|
70
|
-
};
|
|
71
|
-
if (lower.includes("chain") && (lower.includes("mismatch") || lower.includes("unsupported"))) return {
|
|
72
|
-
kind: "ChainMismatch",
|
|
73
|
-
message
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
cause: raw,
|
|
77
|
-
kind: "Unknown",
|
|
78
|
-
message
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (typeof raw === "string") return {
|
|
82
|
-
kind: "Unknown",
|
|
83
|
-
message: raw
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
cause: raw,
|
|
87
|
-
kind: "Unknown",
|
|
88
|
-
message: "Connection failed"
|
|
89
|
-
};
|
|
51
|
+
const classify = (error) => {
|
|
52
|
+
const code = readCode(error);
|
|
53
|
+
if (code === 4001) return "UserRejected";
|
|
54
|
+
if (code === -32002) return "RequestPending";
|
|
55
|
+
if (code === 4100 || code === 4900 || code === 4901) return "NotConnected";
|
|
56
|
+
const lower = error.message.toLowerCase();
|
|
57
|
+
if (lower.includes("user rejected") || lower.includes("user denied")) return "UserRejected";
|
|
58
|
+
if (lower.includes("not connected")) return "NotConnected";
|
|
59
|
+
if (lower.includes("locked")) return "WalletLocked";
|
|
60
|
+
if (lower.includes("chain") && (lower.includes("mismatch") || lower.includes("unsupported"))) return "ChainMismatch";
|
|
61
|
+
return "Unknown";
|
|
62
|
+
};
|
|
63
|
+
/** The one boundary where a thrown value of unknown shape becomes typed. */
|
|
64
|
+
const toConnectionError = (cause) => {
|
|
65
|
+
if (cause instanceof ConnectionError) return cause;
|
|
66
|
+
if (cause instanceof Error) return new ConnectionError(classify(cause), cause.message, { cause });
|
|
67
|
+
return new ConnectionError("Unknown", typeof cause === "string" && cause !== "" ? cause : "Connection failed", { cause });
|
|
90
68
|
};
|
|
91
69
|
|
|
92
70
|
//#endregion
|
|
@@ -103,120 +81,252 @@ const CHAIN_PLATFORMS = [
|
|
|
103
81
|
"bitcoin",
|
|
104
82
|
"polkadot"
|
|
105
83
|
];
|
|
84
|
+
const PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
85
|
+
const isChainPlatform = (value) => PLATFORMS.has(value);
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/types/wallet.ts
|
|
89
|
+
/** Narrows a pool entry to one platform, e.g. before calling its `sendTx`. */
|
|
90
|
+
const isPlatformWallet = (wallet, platform) => wallet.connector.chainPlatform === platform;
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/chains.ts
|
|
94
|
+
/** Common EVM chains. `switchChain` accepts any `ChainBase`, so a chain
|
|
95
|
+
* missing here still works; it is just named by its CAIP-2 id. */
|
|
96
|
+
const EVM_CHAINS = {
|
|
97
|
+
arbitrum: {
|
|
98
|
+
id: "eip155:42161",
|
|
99
|
+
name: "Arbitrum One",
|
|
100
|
+
namespace: "eip155",
|
|
101
|
+
reference: "42161"
|
|
102
|
+
},
|
|
103
|
+
base: {
|
|
104
|
+
id: "eip155:8453",
|
|
105
|
+
name: "Base",
|
|
106
|
+
namespace: "eip155",
|
|
107
|
+
reference: "8453"
|
|
108
|
+
},
|
|
109
|
+
bsc: {
|
|
110
|
+
id: "eip155:56",
|
|
111
|
+
name: "BNB Smart Chain",
|
|
112
|
+
namespace: "eip155",
|
|
113
|
+
reference: "56"
|
|
114
|
+
},
|
|
115
|
+
ethereum: {
|
|
116
|
+
id: "eip155:1",
|
|
117
|
+
name: "Ethereum",
|
|
118
|
+
namespace: "eip155",
|
|
119
|
+
reference: "1"
|
|
120
|
+
},
|
|
121
|
+
optimism: {
|
|
122
|
+
id: "eip155:10",
|
|
123
|
+
name: "Optimism",
|
|
124
|
+
namespace: "eip155",
|
|
125
|
+
reference: "10"
|
|
126
|
+
},
|
|
127
|
+
polygon: {
|
|
128
|
+
id: "eip155:137",
|
|
129
|
+
name: "Polygon",
|
|
130
|
+
namespace: "eip155",
|
|
131
|
+
reference: "137"
|
|
132
|
+
},
|
|
133
|
+
sepolia: {
|
|
134
|
+
id: "eip155:11155111",
|
|
135
|
+
name: "Sepolia",
|
|
136
|
+
namespace: "eip155",
|
|
137
|
+
reference: "11155111"
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const SVM_CHAINS = {
|
|
141
|
+
devnet: {
|
|
142
|
+
id: "solana:devnet",
|
|
143
|
+
name: "Solana Devnet",
|
|
144
|
+
namespace: "solana",
|
|
145
|
+
reference: "devnet"
|
|
146
|
+
},
|
|
147
|
+
mainnet: {
|
|
148
|
+
id: "solana:mainnet",
|
|
149
|
+
name: "Solana Mainnet",
|
|
150
|
+
namespace: "solana",
|
|
151
|
+
reference: "mainnet"
|
|
152
|
+
},
|
|
153
|
+
testnet: {
|
|
154
|
+
id: "solana:testnet",
|
|
155
|
+
name: "Solana Testnet",
|
|
156
|
+
namespace: "solana",
|
|
157
|
+
reference: "testnet"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const SUI_CHAINS = {
|
|
161
|
+
devnet: {
|
|
162
|
+
id: "sui:devnet",
|
|
163
|
+
name: "Sui Devnet",
|
|
164
|
+
namespace: "sui",
|
|
165
|
+
reference: "devnet"
|
|
166
|
+
},
|
|
167
|
+
localnet: {
|
|
168
|
+
id: "sui:localnet",
|
|
169
|
+
name: "Sui Localnet",
|
|
170
|
+
namespace: "sui",
|
|
171
|
+
reference: "localnet"
|
|
172
|
+
},
|
|
173
|
+
mainnet: {
|
|
174
|
+
id: "sui:mainnet",
|
|
175
|
+
name: "Sui Mainnet",
|
|
176
|
+
namespace: "sui",
|
|
177
|
+
reference: "mainnet"
|
|
178
|
+
},
|
|
179
|
+
testnet: {
|
|
180
|
+
id: "sui:testnet",
|
|
181
|
+
name: "Sui Testnet",
|
|
182
|
+
namespace: "sui",
|
|
183
|
+
reference: "testnet"
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
/** CAIP-2 Bitcoin references are the first 16 bytes of each network's
|
|
187
|
+
* genesis block hash, which is what wallets advertise in `wallet.chains`. */
|
|
188
|
+
const BITCOIN_CHAINS = {
|
|
189
|
+
mainnet: {
|
|
190
|
+
id: "bip122:000000000019d6689c085ae165831e93",
|
|
191
|
+
name: "Bitcoin",
|
|
192
|
+
namespace: "bip122",
|
|
193
|
+
reference: "000000000019d6689c085ae165831e93"
|
|
194
|
+
},
|
|
195
|
+
signet: {
|
|
196
|
+
id: "bip122:00000008819873e925422c1ff0f99f7c",
|
|
197
|
+
name: "Bitcoin Signet",
|
|
198
|
+
namespace: "bip122",
|
|
199
|
+
reference: "00000008819873e925422c1ff0f99f7c"
|
|
200
|
+
},
|
|
201
|
+
testnet: {
|
|
202
|
+
id: "bip122:000000000933ea01ad0ee984209779ba",
|
|
203
|
+
name: "Bitcoin Testnet",
|
|
204
|
+
namespace: "bip122",
|
|
205
|
+
reference: "000000000933ea01ad0ee984209779ba"
|
|
206
|
+
},
|
|
207
|
+
testnet4: {
|
|
208
|
+
id: "bip122:00000000da84f2bafbbc53dee25a72ae",
|
|
209
|
+
name: "Bitcoin Testnet4",
|
|
210
|
+
namespace: "bip122",
|
|
211
|
+
reference: "00000000da84f2bafbbc53dee25a72ae"
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const POLKADOT_CHAINS = {
|
|
215
|
+
kusama: {
|
|
216
|
+
id: "polkadot:b0a8d493285c2df73290dfb7e61f870f",
|
|
217
|
+
name: "Kusama",
|
|
218
|
+
namespace: "polkadot",
|
|
219
|
+
reference: "b0a8d493285c2df73290dfb7e61f870f"
|
|
220
|
+
},
|
|
221
|
+
paseo: {
|
|
222
|
+
id: "polkadot:77afd6190f1554ad45fd0d31aee62aac",
|
|
223
|
+
name: "Paseo",
|
|
224
|
+
namespace: "polkadot",
|
|
225
|
+
reference: "77afd6190f1554ad45fd0d31aee62aac"
|
|
226
|
+
},
|
|
227
|
+
polkadot: {
|
|
228
|
+
id: "polkadot:91b171bb158e2d3848fa23a9f1c25182",
|
|
229
|
+
name: "Polkadot",
|
|
230
|
+
namespace: "polkadot",
|
|
231
|
+
reference: "91b171bb158e2d3848fa23a9f1c25182"
|
|
232
|
+
},
|
|
233
|
+
westend: {
|
|
234
|
+
id: "polkadot:e143f23803ac50e8f6f8e62695d1ce9e",
|
|
235
|
+
name: "Westend",
|
|
236
|
+
namespace: "polkadot",
|
|
237
|
+
reference: "e143f23803ac50e8f6f8e62695d1ce9e"
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const EVM_CHAINS_LIST = Object.values(EVM_CHAINS);
|
|
241
|
+
const SVM_CHAINS_LIST = Object.values(SVM_CHAINS);
|
|
242
|
+
const SUI_CHAINS_LIST = Object.values(SUI_CHAINS);
|
|
243
|
+
const BITCOIN_CHAINS_LIST = Object.values(BITCOIN_CHAINS);
|
|
244
|
+
const POLKADOT_CHAINS_LIST = Object.values(POLKADOT_CHAINS);
|
|
245
|
+
/** Every registry as a list, keyed by platform: the shape chain pickers want. */
|
|
246
|
+
const CHAINS_BY_PLATFORM = {
|
|
247
|
+
bitcoin: BITCOIN_CHAINS_LIST,
|
|
248
|
+
evm: EVM_CHAINS_LIST,
|
|
249
|
+
polkadot: POLKADOT_CHAINS_LIST,
|
|
250
|
+
sui: SUI_CHAINS_LIST,
|
|
251
|
+
svm: SVM_CHAINS_LIST
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/logger.ts
|
|
256
|
+
const logWarn = (...args) => {
|
|
257
|
+
console.warn(...args);
|
|
258
|
+
};
|
|
259
|
+
const logError = (...args) => {
|
|
260
|
+
console.error(...args);
|
|
261
|
+
};
|
|
106
262
|
|
|
107
263
|
//#endregion
|
|
108
264
|
//#region src/wallet-source.ts
|
|
265
|
+
const isIterable = (value) => Symbol.iterator in value;
|
|
109
266
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
267
|
+
* For adapters that are built rather than discovered: WalletConnect, Ledger,
|
|
268
|
+
* hand-rolled ones. Takes one adapter or several, or a promise of either; a
|
|
269
|
+
* rejected promise is logged and contributes nothing.
|
|
112
270
|
*/
|
|
113
|
-
const
|
|
271
|
+
const fromAdapters = (adapters) => (onAdapter) => {
|
|
272
|
+
let active = true;
|
|
273
|
+
(async () => {
|
|
274
|
+
try {
|
|
275
|
+
const resolved = await adapters;
|
|
276
|
+
for (const adapter of isIterable(resolved) ? resolved : [resolved]) {
|
|
277
|
+
if (!active) return;
|
|
278
|
+
onAdapter(adapter);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logWarn("[butr] fromAdapters: adapters failed to load:", error);
|
|
282
|
+
}
|
|
283
|
+
})();
|
|
284
|
+
return () => {
|
|
285
|
+
active = false;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
114
288
|
|
|
115
289
|
//#endregion
|
|
116
290
|
//#region src/store/shadow-adapter.ts
|
|
117
291
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
292
|
+
* Thrown by the three required methods of a shadow adapter. Every optional
|
|
293
|
+
* method is simply absent, so only code that skipped the `reconnectingIds`
|
|
294
|
+
* check and called `connect`, `getAccounts` or `getSigner` sees this.
|
|
121
295
|
*/
|
|
122
296
|
var ShadowConnectorError = class extends Error {
|
|
123
|
-
code = "BUTR_RECONNECTING";
|
|
124
297
|
connectorId;
|
|
125
298
|
method;
|
|
126
299
|
constructor(method, connectorId) {
|
|
127
|
-
super(`[butr] ${method} called on
|
|
300
|
+
super(`[butr] ${method} called on "${connectorId}" while it is still reconnecting. Check useIsReconnecting(id) before calling it.`);
|
|
128
301
|
this.name = "ShadowConnectorError";
|
|
129
302
|
this.connectorId = connectorId;
|
|
130
303
|
this.method = method;
|
|
131
304
|
}
|
|
132
305
|
};
|
|
133
|
-
const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
134
|
-
getBalance: false,
|
|
135
|
-
getTransactionReceipt: false,
|
|
136
|
-
requestAccounts: false,
|
|
137
|
-
sendTransaction: false,
|
|
138
|
-
signIn: false,
|
|
139
|
-
signMessage: false,
|
|
140
|
-
signTransaction: false,
|
|
141
|
-
subscribe: false,
|
|
142
|
-
switchAccount: false,
|
|
143
|
-
switchChain: false
|
|
144
|
-
});
|
|
145
306
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
307
|
+
* Stands in for a connection seeded from `initialState` so its identity
|
|
308
|
+
* renders before discovery runs. It exposes no optional methods; the manager
|
|
309
|
+
* swaps it for the live adapter, or evicts it, once silent reconnect settles.
|
|
149
310
|
*/
|
|
150
311
|
const createShadowAdapter = (entry) => {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
getBalance: () => reject("getBalance"),
|
|
161
|
-
getSigner: () => reject("getSigner"),
|
|
162
|
-
getTransactionReceipt: () => reject("getTransactionReceipt"),
|
|
163
|
-
icon,
|
|
164
|
-
id,
|
|
165
|
-
name,
|
|
166
|
-
sendTx: () => reject("sendTx"),
|
|
167
|
-
sendTxToChain: () => reject("sendTxToChain"),
|
|
168
|
-
signMessage: () => reject("signMessage"),
|
|
169
|
-
switchChain: () => reject("switchChain")
|
|
312
|
+
const reject = (method) => () => Promise.reject(new ShadowConnectorError(method, entry.connectorId));
|
|
313
|
+
return {
|
|
314
|
+
chainPlatform: entry.chainPlatform,
|
|
315
|
+
connect: reject("connect"),
|
|
316
|
+
getAccounts: reject("getAccounts"),
|
|
317
|
+
getSigner: reject("getSigner"),
|
|
318
|
+
icon: entry.icon,
|
|
319
|
+
id: entry.connectorId,
|
|
320
|
+
name: entry.name
|
|
170
321
|
};
|
|
171
|
-
switch (entry.chainPlatform) {
|
|
172
|
-
case "bitcoin": return {
|
|
173
|
-
...base,
|
|
174
|
-
chainPlatform: "bitcoin"
|
|
175
|
-
};
|
|
176
|
-
case "evm": return {
|
|
177
|
-
...base,
|
|
178
|
-
chainPlatform: "evm"
|
|
179
|
-
};
|
|
180
|
-
case "sui": return {
|
|
181
|
-
...base,
|
|
182
|
-
chainPlatform: "sui"
|
|
183
|
-
};
|
|
184
|
-
case "svm": return {
|
|
185
|
-
...base,
|
|
186
|
-
chainPlatform: "svm"
|
|
187
|
-
};
|
|
188
|
-
case "polkadot": return {
|
|
189
|
-
...base,
|
|
190
|
-
chainPlatform: "polkadot"
|
|
191
|
-
};
|
|
192
|
-
default:
|
|
193
|
-
entry.chainPlatform;
|
|
194
|
-
throw new Error(`[butr] unknown chainPlatform: ${entry.chainPlatform}`);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
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.
|
|
201
|
-
*/
|
|
202
|
-
const isShadowAdapter = (adapter) => {
|
|
203
|
-
return Object.values(adapter.capabilities).every((flag) => !flag);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region src/logger.ts
|
|
208
|
-
const logWarn = (...args) => {
|
|
209
|
-
console.warn(...args);
|
|
210
|
-
};
|
|
211
|
-
const logError = (...args) => {
|
|
212
|
-
console.error(...args);
|
|
213
322
|
};
|
|
214
323
|
|
|
215
324
|
//#endregion
|
|
216
325
|
//#region src/storage/browser-storage-driver.ts
|
|
326
|
+
const webStorageGlobals = globalThis;
|
|
217
327
|
const hasWebStorage = (kind) => {
|
|
218
328
|
try {
|
|
219
|
-
return
|
|
329
|
+
return kind === "localStorage" ? webStorageGlobals.localStorage !== void 0 : webStorageGlobals.sessionStorage !== void 0;
|
|
220
330
|
} catch {
|
|
221
331
|
return false;
|
|
222
332
|
}
|
|
@@ -291,14 +401,9 @@ const storedPoolEntrySchema = z.looseObject({
|
|
|
291
401
|
name: z.string().min(1)
|
|
292
402
|
});
|
|
293
403
|
const recordSchema = z.record(z.string(), z.unknown());
|
|
294
|
-
const parseStoredPoolEntry = (key, value) => {
|
|
295
|
-
const parsed = storedPoolEntrySchema.safeParse(value);
|
|
296
|
-
if (!parsed.success || parsed.data.connectorId !== key) return null;
|
|
297
|
-
return parsed.data;
|
|
298
|
-
};
|
|
299
404
|
const DEFAULT_KEY_PREFIX = "butr";
|
|
300
405
|
/**
|
|
301
|
-
* `
|
|
406
|
+
* `createWalletStorage` writes these and `readWalletSnapshot` reads them from
|
|
302
407
|
* a cookie jar; any divergence desyncs the SSR-seeded render from the
|
|
303
408
|
* client that rehydrates it, the failure ADR 0003 exists to prevent.
|
|
304
409
|
*/
|
|
@@ -311,477 +416,308 @@ const storageKeys = (keyPrefix) => {
|
|
|
311
416
|
userDisconnected: `${prefix}-user-disconnected`
|
|
312
417
|
};
|
|
313
418
|
};
|
|
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]") => {
|
|
419
|
+
const parseRecord = (raw, label, what) => {
|
|
320
420
|
if (raw === null || raw === void 0 || raw === "") return {};
|
|
321
|
-
let value;
|
|
322
421
|
try {
|
|
323
|
-
|
|
422
|
+
const parsed = recordSchema.safeParse(JSON.parse(raw));
|
|
423
|
+
return parsed.success ? parsed.data : {};
|
|
324
424
|
} catch (error) {
|
|
325
|
-
logWarn(`${label} failed to parse
|
|
425
|
+
logWarn(`${label} failed to parse ${what} from storage:`, error);
|
|
326
426
|
return {};
|
|
327
427
|
}
|
|
328
|
-
|
|
329
|
-
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Never repairs or throws: one corrupt entry must not take down a whole
|
|
431
|
+
* session, and `readWalletSnapshot` may run on a server with no cookie
|
|
432
|
+
* jar to write the eviction to. The next save overwrites the bad value.
|
|
433
|
+
*/
|
|
434
|
+
const decodePool = (raw, label = "[butr]") => {
|
|
330
435
|
const result = {};
|
|
331
|
-
for (const [key,
|
|
332
|
-
const entry =
|
|
333
|
-
if (entry ===
|
|
334
|
-
else
|
|
436
|
+
for (const [key, value] of Object.entries(parseRecord(raw, label, "pool"))) {
|
|
437
|
+
const entry = storedPoolEntrySchema.safeParse(value);
|
|
438
|
+
if (entry.success && entry.data.connectorId === key) result[key] = entry.data;
|
|
439
|
+
else logWarn(`${label} dropping invalid pool entry for ${key}`);
|
|
335
440
|
}
|
|
336
441
|
return result;
|
|
337
442
|
};
|
|
338
443
|
/** Decode a persisted selection payload. Same contract as `decodePool`. */
|
|
339
444
|
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
445
|
const result = {};
|
|
351
|
-
for (const [key,
|
|
446
|
+
for (const [key, value] of Object.entries(parseRecord(raw, label, "selection"))) {
|
|
352
447
|
const platform = chainPlatformSchema.safeParse(key);
|
|
353
|
-
if (platform.success && typeof
|
|
448
|
+
if (platform.success && typeof value === "string" && value.length > 0) result[platform.data] = value;
|
|
354
449
|
}
|
|
355
450
|
return result;
|
|
356
451
|
};
|
|
357
452
|
|
|
358
453
|
//#endregion
|
|
359
454
|
//#region src/storage/wallet-storage.ts
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
*/
|
|
372
|
-
|
|
373
|
-
constructor(config) {
|
|
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;
|
|
379
|
-
if (config.persistent && config.session) {
|
|
380
|
-
this.persistent = config.persistent;
|
|
381
|
-
this.session = config.session;
|
|
382
|
-
} else {
|
|
383
|
-
const defaults = createBrowserStorageDriver();
|
|
384
|
-
this.persistent = config.persistent ?? defaults.persistent;
|
|
385
|
-
this.session = config.session ?? defaults.session;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/** Chain `fn` after the in-flight pool mutation. */
|
|
389
|
-
async serializePoolMutation(fn) {
|
|
390
|
-
const previous = this.poolMutationQueue;
|
|
391
|
-
let resolve = () => {};
|
|
392
|
-
this.poolMutationQueue = new Promise((r) => {
|
|
393
|
-
resolve = r;
|
|
394
|
-
});
|
|
395
|
-
try {
|
|
396
|
-
await previous;
|
|
397
|
-
} catch {}
|
|
398
|
-
try {
|
|
399
|
-
return await fn();
|
|
400
|
-
} finally {
|
|
401
|
-
resolve();
|
|
402
|
-
}
|
|
403
|
-
}
|
|
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;
|
|
408
|
-
try {
|
|
409
|
-
stored = await this.persistent.getItem(this.poolKey);
|
|
410
|
-
} catch (error) {
|
|
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);
|
|
433
|
-
}
|
|
434
|
-
return decoded;
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
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`.
|
|
440
|
-
*/
|
|
441
|
-
async setPool(pool) {
|
|
442
|
-
await this.serializePoolMutation(async () => {
|
|
443
|
-
try {
|
|
444
|
-
const { decoded: existing } = await this.readPool();
|
|
445
|
-
const serializable = { ...existing };
|
|
446
|
-
for (const [connectorId, wallet] of pool) {
|
|
447
|
-
const entry = {
|
|
448
|
-
account: wallet.account,
|
|
449
|
-
accounts: wallet.accounts,
|
|
450
|
-
chainPlatform: wallet.connector.chainPlatform,
|
|
451
|
-
connectorId,
|
|
452
|
-
icon: wallet.connector.icon,
|
|
453
|
-
name: wallet.connector.name
|
|
454
|
-
};
|
|
455
|
-
if (parseStoredPoolEntry(connectorId, entry) === null) throw new Error(`[butr] refusing to persist invalid pool entry for ${connectorId}`);
|
|
456
|
-
serializable[connectorId] = entry;
|
|
457
|
-
}
|
|
458
|
-
await this.persistent.setItem(this.poolKey, JSON.stringify(serializable));
|
|
459
|
-
} catch (error) {
|
|
460
|
-
logWarn("[butr] failed to persist pool:", error);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
async removePoolEntry(connectorId) {
|
|
465
|
-
await this.serializePoolMutation(async () => {
|
|
466
|
-
try {
|
|
467
|
-
const { decoded: stored } = await this.readPool();
|
|
468
|
-
if (stored[connectorId]) {
|
|
469
|
-
const { [connectorId]: _, ...remaining } = stored;
|
|
470
|
-
await this.persistent.setItem(this.poolKey, JSON.stringify(remaining));
|
|
471
|
-
}
|
|
472
|
-
} catch (error) {
|
|
473
|
-
logWarn(`[butr] failed to remove pool entry ${connectorId}:`, error);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
async clearPool() {
|
|
478
|
-
await this.serializePoolMutation(async () => {
|
|
479
|
-
await this.persistent.removeItem(this.poolKey);
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
async getSelection() {
|
|
483
|
-
try {
|
|
484
|
-
return decodeSelection(await this.persistent.getItem(this.selectionKey));
|
|
485
|
-
} catch (error) {
|
|
486
|
-
logWarn("[butr] failed to read selection from storage:", error);
|
|
487
|
-
return {};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
async setSelection(selection) {
|
|
455
|
+
const isEmpty = (record) => Object.keys(record).length === 0;
|
|
456
|
+
/**
|
|
457
|
+
* Pool, selection and active id go to the persistent driver, the disconnect
|
|
458
|
+
* intent to the session driver. Unchanged keys are not rewritten, so a
|
|
459
|
+
* cookie-backed driver does not re-send every cookie on each save.
|
|
460
|
+
*/
|
|
461
|
+
const createWalletStorage = (options = {}) => {
|
|
462
|
+
const keys = storageKeys(options.keyPrefix);
|
|
463
|
+
const browser = createBrowserStorageDriver();
|
|
464
|
+
const persistent = options.persistent ?? browser.persistent;
|
|
465
|
+
const session = options.session ?? browser.session;
|
|
466
|
+
const written = /* @__PURE__ */ new Map();
|
|
467
|
+
const read = async (driver, key) => {
|
|
491
468
|
try {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
469
|
+
const value = await driver.getItem(key);
|
|
470
|
+
written.set(key, value);
|
|
471
|
+
return value;
|
|
495
472
|
} catch (error) {
|
|
496
|
-
logWarn(
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
async getActiveConnectorId() {
|
|
500
|
-
try {
|
|
501
|
-
const value = await this.persistent.getItem(this.activeKey);
|
|
502
|
-
return value !== null && value.length > 0 ? value : null;
|
|
503
|
-
} catch {
|
|
473
|
+
logWarn(`[butr] failed to read ${key} from storage:`, error);
|
|
504
474
|
return null;
|
|
505
475
|
}
|
|
506
|
-
}
|
|
507
|
-
async
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
476
|
+
};
|
|
477
|
+
const write = async (driver, key, value) => {
|
|
478
|
+
if (written.get(key) === value) return;
|
|
479
|
+
await (value === null ? driver.removeItem(key) : driver.setItem(key, value));
|
|
480
|
+
written.set(key, value);
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
load: async () => {
|
|
484
|
+
const [pool, selection, active, userDisconnected] = await Promise.all([
|
|
485
|
+
read(persistent, keys.pool),
|
|
486
|
+
read(persistent, keys.selection),
|
|
487
|
+
read(persistent, keys.active),
|
|
488
|
+
read(session, keys.userDisconnected)
|
|
489
|
+
]);
|
|
490
|
+
return {
|
|
491
|
+
activeConnectorId: active === null || active === "" ? null : active,
|
|
492
|
+
isUserDisconnected: userDisconnected === "true",
|
|
493
|
+
pool: decodePool(pool),
|
|
494
|
+
selection: decodeSelection(selection)
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
save: async (state) => {
|
|
498
|
+
const failure = (await Promise.allSettled([
|
|
499
|
+
write(persistent, keys.pool, isEmpty(state.pool) ? null : JSON.stringify(state.pool)),
|
|
500
|
+
write(persistent, keys.selection, isEmpty(state.selection) ? null : JSON.stringify(state.selection)),
|
|
501
|
+
write(persistent, keys.active, state.activeConnectorId),
|
|
502
|
+
write(session, keys.userDisconnected, state.isUserDisconnected ? "true" : null)
|
|
503
|
+
])).find((result) => result.status === "rejected");
|
|
504
|
+
if (failure !== void 0) throw failure.reason;
|
|
531
505
|
}
|
|
532
|
-
}
|
|
533
|
-
async markUserDisconnected(value) {
|
|
534
|
-
try {
|
|
535
|
-
await (value ? this.session.setItem(this.userDisconnectedKey, "true") : this.session.removeItem(this.userDisconnectedKey));
|
|
536
|
-
} catch {}
|
|
537
|
-
}
|
|
506
|
+
};
|
|
538
507
|
};
|
|
539
508
|
|
|
540
509
|
//#endregion
|
|
541
510
|
//#region src/store/connector-lifecycle.ts
|
|
542
511
|
const createConnectorLifecycle = (handlers) => {
|
|
543
|
-
const
|
|
512
|
+
const live = /* @__PURE__ */ new Map();
|
|
544
513
|
const detach = (connectorId) => {
|
|
545
|
-
const
|
|
546
|
-
if (
|
|
514
|
+
const subscription = live.get(connectorId);
|
|
515
|
+
if (subscription === void 0) return;
|
|
516
|
+
live.delete(connectorId);
|
|
547
517
|
try {
|
|
548
|
-
|
|
518
|
+
subscription.unsubscribe();
|
|
549
519
|
} catch (error) {
|
|
550
520
|
logWarn("[butr] unsubscribe threw:", error);
|
|
551
521
|
}
|
|
552
|
-
unsubscribers.delete(connectorId);
|
|
553
522
|
};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
523
|
+
const attach = (connectorId, connector) => {
|
|
524
|
+
if (connector.subscribe === void 0) return;
|
|
525
|
+
const disconnected = () => {
|
|
557
526
|
detach(connectorId);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
detachAll: () => {
|
|
576
|
-
const ids = [];
|
|
577
|
-
for (const id of unsubscribers.keys()) ids.push(id);
|
|
578
|
-
for (const id of ids) detach(id);
|
|
527
|
+
handlers.onDisconnected(connectorId);
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
const unsubscribe = connector.subscribe((event) => {
|
|
531
|
+
if (live.get(connectorId)?.connector !== connector) return;
|
|
532
|
+
if (event.type === "disconnected" || event.accounts.length === 0) {
|
|
533
|
+
disconnected();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
handlers.onAccountsChanged(connectorId, event.accounts);
|
|
537
|
+
});
|
|
538
|
+
live.set(connectorId, {
|
|
539
|
+
connector,
|
|
540
|
+
unsubscribe
|
|
541
|
+
});
|
|
542
|
+
} catch (error) {
|
|
543
|
+
logWarn(`[butr] subscribe failed for ${connectorId}:`, error);
|
|
579
544
|
}
|
|
580
545
|
};
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
//#endregion
|
|
584
|
-
//#region src/store/hydration.ts
|
|
585
|
-
const VALID_CHAIN_PLATFORMS$1 = new Set(CHAIN_PLATFORMS);
|
|
586
|
-
const isChainPlatform$1 = (value) => VALID_CHAIN_PLATFORMS$1.has(value);
|
|
587
|
-
/** Pick the right `accounts` list: fresh from the connector if it
|
|
588
|
-
* exposes `getAccounts`, otherwise the persisted list, otherwise just
|
|
589
|
-
* the active account. */
|
|
590
|
-
const resolveHydratedAccounts = async (connector, storedAccounts, fallbackAccount) => {
|
|
591
|
-
if (connector.getAccounts) {
|
|
592
|
-
const fresh = await connector.getAccounts().catch(() => null);
|
|
593
|
-
if (fresh && fresh.length > 0) return fresh;
|
|
594
|
-
}
|
|
595
|
-
if (storedAccounts.length > 0) return [...storedAccounts];
|
|
596
|
-
return [fallbackAccount];
|
|
597
|
-
};
|
|
598
|
-
const restoreOneEntry = async (connectorId, entry, connector) => {
|
|
599
|
-
try {
|
|
600
|
-
await connector.connect({ silent: true });
|
|
601
|
-
const accountToUse = await connector.getAccount() ?? entry.account;
|
|
602
|
-
return {
|
|
603
|
-
connectorId,
|
|
604
|
-
entry: {
|
|
605
|
-
account: accountToUse,
|
|
606
|
-
accounts: await resolveHydratedAccounts(connector, entry.accounts, accountToUse),
|
|
607
|
-
connector
|
|
608
|
-
},
|
|
609
|
-
kind: "ok"
|
|
610
|
-
};
|
|
611
|
-
} catch (error) {
|
|
612
|
-
return {
|
|
613
|
-
connectorId,
|
|
614
|
-
error,
|
|
615
|
-
kind: "fail"
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
const createHydrationCoordinator = (storage, createConnector) => {
|
|
620
|
-
const pending = /* @__PURE__ */ new Map();
|
|
621
546
|
return {
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
if (!entry) return null;
|
|
625
|
-
const connector = createConnector(connectorId);
|
|
626
|
-
if (!connector) return null;
|
|
627
|
-
const outcome = await restoreOneEntry(connectorId, entry, connector);
|
|
628
|
-
pending.delete(connectorId);
|
|
629
|
-
return outcome;
|
|
630
|
-
},
|
|
631
|
-
hydrate: async () => {
|
|
632
|
-
const [storedPool, storedSelection, storedActive, userDisconnected] = await Promise.all([
|
|
633
|
-
storage.getPool(),
|
|
634
|
-
storage.getSelection(),
|
|
635
|
-
storage.getActiveConnectorId(),
|
|
636
|
-
storage.isUserDisconnected()
|
|
637
|
-
]);
|
|
638
|
-
const pool = /* @__PURE__ */ new Map();
|
|
639
|
-
const dropped = [];
|
|
640
|
-
pending.clear();
|
|
641
|
-
const tasks = [];
|
|
642
|
-
for (const [connectorId, entry] of Object.entries(storedPool)) {
|
|
643
|
-
if (!entry) continue;
|
|
644
|
-
const connector = createConnector(connectorId);
|
|
645
|
-
if (!connector) {
|
|
646
|
-
pending.set(connectorId, entry);
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
tasks.push(restoreOneEntry(connectorId, entry, connector));
|
|
650
|
-
}
|
|
651
|
-
const outcomes = await Promise.all(tasks);
|
|
652
|
-
for (const outcome of outcomes) {
|
|
653
|
-
if (outcome.kind === "ok") {
|
|
654
|
-
pool.set(outcome.connectorId, outcome.entry);
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
dropped.push({
|
|
658
|
-
connectorId: outcome.connectorId,
|
|
659
|
-
reason: outcome.error
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
const selection = /* @__PURE__ */ new Map();
|
|
663
|
-
for (const [platform, connectorId] of Object.entries(storedSelection)) if (isChainPlatform$1(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
664
|
-
for (const [connectorId, wallet] of pool) {
|
|
665
|
-
const platform = wallet.connector.chainPlatform;
|
|
666
|
-
if (!selection.has(platform)) selection.set(platform, connectorId);
|
|
667
|
-
}
|
|
668
|
-
let activeConnectorId = null;
|
|
669
|
-
if (storedActive !== null && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
670
|
-
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
671
|
-
return {
|
|
672
|
-
activeConnectorId,
|
|
673
|
-
dropped,
|
|
674
|
-
isUserDisconnected: userDisconnected,
|
|
675
|
-
pendingIds: [...pending.keys()],
|
|
676
|
-
pool,
|
|
677
|
-
selection
|
|
678
|
-
};
|
|
547
|
+
detachAll: () => {
|
|
548
|
+
for (const connectorId of live.keys()) detach(connectorId);
|
|
679
549
|
},
|
|
680
|
-
|
|
550
|
+
sync: (pool) => {
|
|
551
|
+
for (const [connectorId, subscription] of live) if (pool.get(connectorId)?.connector !== subscription.connector) detach(connectorId);
|
|
552
|
+
for (const [connectorId, wallet] of pool) if (!live.has(connectorId)) attach(connectorId, wallet.connector);
|
|
553
|
+
}
|
|
681
554
|
};
|
|
682
555
|
};
|
|
683
556
|
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/wallet-equal.ts
|
|
559
|
+
/** Account ids are canonical (`buildAccount`), so identity is the id. */
|
|
560
|
+
const accountsEqual = (a, b) => a === b || a.length === b.length && a.every((account, i) => account.id === b[i]?.id);
|
|
561
|
+
/**
|
|
562
|
+
* The adapter is compared by reference, not `connector.id`: hydration
|
|
563
|
+
* swaps a shadow adapter for the live one under an unchanged id, and an
|
|
564
|
+
* id check would strand consumers on the placeholder.
|
|
565
|
+
*/
|
|
566
|
+
const walletEqual = (a, b) => a === b || a !== void 0 && b !== void 0 && a.connector === b.connector && a.account.id === b.account.id && accountsEqual(a.accounts, b.accounts);
|
|
567
|
+
|
|
684
568
|
//#endregion
|
|
685
569
|
//#region src/store/reducer.ts
|
|
686
570
|
const initialState = {
|
|
687
571
|
activeConnectorId: null,
|
|
572
|
+
adapters: [],
|
|
688
573
|
connectingConnectorId: null,
|
|
689
574
|
connectionError: null,
|
|
690
575
|
connectionStatus: "idle",
|
|
576
|
+
dormant: /* @__PURE__ */ new Map(),
|
|
691
577
|
isHydrated: false,
|
|
692
578
|
isUserDisconnected: false,
|
|
693
579
|
pool: /* @__PURE__ */ new Map(),
|
|
694
580
|
reconnectingIds: /* @__PURE__ */ new Set(),
|
|
695
581
|
selection: /* @__PURE__ */ new Map()
|
|
696
582
|
};
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
583
|
+
const toStoredEntry = (connectorId, wallet) => ({
|
|
584
|
+
account: wallet.account,
|
|
585
|
+
accounts: wallet.accounts,
|
|
586
|
+
chainPlatform: wallet.connector.chainPlatform,
|
|
587
|
+
connectorId,
|
|
588
|
+
icon: wallet.connector.icon,
|
|
589
|
+
name: wallet.connector.name
|
|
590
|
+
});
|
|
591
|
+
const withEntry = (map, key, value) => new Map(map).set(key, value);
|
|
592
|
+
const withoutKey = (map, key) => {
|
|
593
|
+
if (!map.has(key)) return map;
|
|
594
|
+
const next = new Map(map);
|
|
595
|
+
next.delete(key);
|
|
596
|
+
return next;
|
|
700
597
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
598
|
+
const withoutId = (set, id) => {
|
|
599
|
+
if (!set.has(id)) return set;
|
|
600
|
+
const next = new Set(set);
|
|
601
|
+
next.delete(id);
|
|
602
|
+
return next;
|
|
603
|
+
};
|
|
604
|
+
const keepEntries = (map, keep) => {
|
|
605
|
+
const kept = [...map].filter(([key, value]) => keep(key, value));
|
|
606
|
+
return kept.length === map.size ? map : new Map(kept);
|
|
607
|
+
};
|
|
608
|
+
const keepIds = (set, keep) => {
|
|
609
|
+
const kept = [...set].filter(keep);
|
|
610
|
+
return kept.length === set.size ? set : new Set(kept);
|
|
611
|
+
};
|
|
612
|
+
const sameEntries = (a, b) => a.size === b.size && [...a].every(([key, value]) => b.get(key) === value);
|
|
613
|
+
const reconcileSelection = (selection, pool) => {
|
|
614
|
+
const next = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const [platform, id] of selection) if (pool.get(id)?.connector.chainPlatform === platform) next.set(platform, id);
|
|
616
|
+
for (const [id, wallet] of pool) if (!next.has(wallet.connector.chainPlatform)) next.set(wallet.connector.chainPlatform, id);
|
|
617
|
+
return sameEntries(next, selection) ? selection : next;
|
|
618
|
+
};
|
|
619
|
+
/**
|
|
620
|
+
* The invariants every event relies on, enforced once after every event so
|
|
621
|
+
* no case re-derives a fallback. Returns `state` itself when it already
|
|
622
|
+
* holds, which keeps zustand from notifying.
|
|
623
|
+
*/
|
|
624
|
+
const reconcile = (state) => {
|
|
625
|
+
const { pool } = state;
|
|
626
|
+
const reconnectingIds = keepIds(state.reconnectingIds, (id) => pool.has(id));
|
|
627
|
+
const dormant = keepEntries(state.dormant, (id) => !pool.has(id) || reconnectingIds.has(id));
|
|
628
|
+
const selection = reconcileSelection(state.selection, pool);
|
|
629
|
+
const activeConnectorId = state.activeConnectorId !== null && pool.has(state.activeConnectorId) ? state.activeConnectorId : pool.keys().next().value ?? null;
|
|
630
|
+
if (reconnectingIds === state.reconnectingIds && dormant === state.dormant && selection === state.selection && activeConnectorId === state.activeConnectorId) return state;
|
|
631
|
+
return {
|
|
632
|
+
...state,
|
|
633
|
+
activeConnectorId,
|
|
634
|
+
dormant,
|
|
635
|
+
reconnectingIds,
|
|
636
|
+
selection
|
|
637
|
+
};
|
|
638
|
+
};
|
|
639
|
+
const applyStoredSelection = (selection, stored, pool) => {
|
|
640
|
+
let next = selection;
|
|
641
|
+
for (const [platform, id] of Object.entries(stored)) if (isChainPlatform(platform) && pool.get(id)?.connector.chainPlatform === platform) next = withEntry(next, platform, id);
|
|
642
|
+
return next;
|
|
643
|
+
};
|
|
644
|
+
const changeAccounts = (state, event) => {
|
|
645
|
+
const wallet = state.pool.get(event.connectorId);
|
|
646
|
+
const first = event.accounts.at(0);
|
|
647
|
+
if (wallet === void 0 || first === void 0) return state;
|
|
648
|
+
const account = event.active ?? event.accounts.find((a) => a.id === wallet.account.id) ?? first;
|
|
649
|
+
if (account.id === wallet.account.id && accountsEqual(event.accounts, wallet.accounts)) return state;
|
|
650
|
+
const updated = {
|
|
651
|
+
...wallet,
|
|
652
|
+
account,
|
|
653
|
+
accounts: event.accounts
|
|
654
|
+
};
|
|
655
|
+
return {
|
|
656
|
+
...state,
|
|
657
|
+
pool: withEntry(state.pool, event.connectorId, updated)
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
const apply = (state, event) => {
|
|
704
661
|
switch (event.type) {
|
|
662
|
+
case "ADAPTER_REGISTERED":
|
|
663
|
+
if (state.adapters.some((adapter) => adapter.id === event.adapter.id)) return state;
|
|
664
|
+
return {
|
|
665
|
+
...state,
|
|
666
|
+
adapters: [...state.adapters, event.adapter]
|
|
667
|
+
};
|
|
668
|
+
case "STORAGE_LOADED": {
|
|
669
|
+
const dormant = new Map(state.dormant);
|
|
670
|
+
for (const [id, entry] of Object.entries(event.entries)) if (entry !== void 0) dormant.set(id, entry);
|
|
671
|
+
return {
|
|
672
|
+
...state,
|
|
673
|
+
dormant,
|
|
674
|
+
isUserDisconnected: event.isUserDisconnected
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
case "ENTRY_RESTORED":
|
|
678
|
+
if (!state.dormant.has(event.connectorId)) return state;
|
|
679
|
+
return {
|
|
680
|
+
...state,
|
|
681
|
+
pool: withEntry(state.pool, event.connectorId, event.entry),
|
|
682
|
+
reconnectingIds: withoutId(state.reconnectingIds, event.connectorId)
|
|
683
|
+
};
|
|
684
|
+
case "RESTORE_FAILED":
|
|
685
|
+
if (!state.reconnectingIds.has(event.connectorId)) return state;
|
|
686
|
+
return {
|
|
687
|
+
...state,
|
|
688
|
+
pool: withoutKey(state.pool, event.connectorId),
|
|
689
|
+
reconnectingIds: withoutId(state.reconnectingIds, event.connectorId)
|
|
690
|
+
};
|
|
705
691
|
case "HYDRATED": {
|
|
706
|
-
const
|
|
707
|
-
for (const [id, entry] of event.pool) pool.set(id, entry);
|
|
708
|
-
for (const id of event.dropped) pool.delete(id);
|
|
709
|
-
const selection = new Map(state.selection);
|
|
710
|
-
for (const [platform, id] of event.selection) selection.set(platform, id);
|
|
711
|
-
let activeConnectorId = null;
|
|
712
|
-
if (event.activeConnectorId !== null && pool.has(event.activeConnectorId)) activeConnectorId = event.activeConnectorId;
|
|
713
|
-
else if (state.activeConnectorId !== null && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
714
|
-
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
715
|
-
let nextReconnecting = state.reconnectingIds;
|
|
716
|
-
if (state.reconnectingIds.size > 0) {
|
|
717
|
-
const next = new Set(state.reconnectingIds);
|
|
718
|
-
for (const id of event.pool.keys()) next.delete(id);
|
|
719
|
-
for (const id of event.dropped) next.delete(id);
|
|
720
|
-
if (next.size !== state.reconnectingIds.size) nextReconnecting = next;
|
|
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
|
-
}
|
|
692
|
+
const preferred = event.activeConnectorId;
|
|
728
693
|
return {
|
|
729
694
|
...state,
|
|
730
|
-
activeConnectorId,
|
|
695
|
+
activeConnectorId: preferred !== null && state.pool.has(preferred) ? preferred : state.activeConnectorId,
|
|
731
696
|
isHydrated: true,
|
|
732
|
-
|
|
733
|
-
pool,
|
|
734
|
-
reconnectingIds: nextReconnecting,
|
|
735
|
-
selection: selectionAfterDrop
|
|
697
|
+
selection: applyStoredSelection(state.selection, event.selection, state.pool)
|
|
736
698
|
};
|
|
737
699
|
}
|
|
738
|
-
case "USER_DISCONNECTED_SET": return {
|
|
739
|
-
...state,
|
|
740
|
-
isUserDisconnected: event.value
|
|
741
|
-
};
|
|
742
700
|
case "CONNECT_STARTED": return {
|
|
743
701
|
...state,
|
|
744
702
|
connectingConnectorId: event.connectorId,
|
|
745
703
|
connectionError: null,
|
|
746
|
-
connectionStatus: "connecting"
|
|
704
|
+
connectionStatus: "connecting",
|
|
705
|
+
isUserDisconnected: false
|
|
747
706
|
};
|
|
748
707
|
case "CONNECT_SUCCEEDED": {
|
|
749
708
|
const { connectorId, entry } = event;
|
|
750
|
-
const
|
|
751
|
-
const nextReconnecting = state.reconnectingIds.has(connectorId) ? new Set([...state.reconnectingIds].filter((id) => id !== connectorId)) : state.reconnectingIds;
|
|
752
|
-
if (existing && existing.account.walletAddress.toLowerCase() === entry.account.walletAddress.toLowerCase()) return {
|
|
753
|
-
...state,
|
|
754
|
-
activeConnectorId: connectorId,
|
|
755
|
-
connectingConnectorId: null,
|
|
756
|
-
connectionError: null,
|
|
757
|
-
connectionStatus: "success",
|
|
758
|
-
pool: existing.connector === entry.connector ? state.pool : new Map([...state.pool, [connectorId, entry]]),
|
|
759
|
-
reconnectingIds: nextReconnecting
|
|
760
|
-
};
|
|
761
|
-
const newPool = new Map([...state.pool, [connectorId, entry]]);
|
|
762
|
-
const newSelection = new Map([...state.selection, [entry.connector.chainPlatform, connectorId]]);
|
|
709
|
+
const isCurrentAttempt = state.connectingConnectorId === connectorId;
|
|
763
710
|
return {
|
|
764
711
|
...state,
|
|
765
712
|
activeConnectorId: connectorId,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
713
|
+
...isCurrentAttempt && {
|
|
714
|
+
connectingConnectorId: null,
|
|
715
|
+
connectionError: null,
|
|
716
|
+
connectionStatus: "success"
|
|
717
|
+
},
|
|
718
|
+
pool: withEntry(state.pool, connectorId, entry),
|
|
719
|
+
reconnectingIds: withoutId(state.reconnectingIds, connectorId),
|
|
720
|
+
selection: state.pool.has(connectorId) ? state.selection : withEntry(state.selection, entry.connector.chainPlatform, connectorId)
|
|
785
721
|
};
|
|
786
722
|
}
|
|
787
723
|
case "CONNECT_FAILED":
|
|
@@ -792,398 +728,424 @@ const reducer = (state, event) => {
|
|
|
792
728
|
connectionError: event.error,
|
|
793
729
|
connectionStatus: "error"
|
|
794
730
|
};
|
|
795
|
-
case "
|
|
796
|
-
|
|
797
|
-
if (!wallet) return state;
|
|
798
|
-
const newPool = new Map(state.pool);
|
|
799
|
-
newPool.delete(event.connectorId);
|
|
800
|
-
const newSelection = new Map(state.selection);
|
|
801
|
-
const platform = wallet.connector.chainPlatform;
|
|
802
|
-
if (newSelection.get(platform) === event.connectorId) {
|
|
803
|
-
newSelection.delete(platform);
|
|
804
|
-
const fallback = findConnectorForPlatform(newPool, platform);
|
|
805
|
-
if (fallback !== void 0) newSelection.set(platform, fallback);
|
|
806
|
-
}
|
|
807
|
-
const firstId = newPool.keys().next();
|
|
808
|
-
const activeConnectorId = state.activeConnectorId === event.connectorId ? firstId.value ?? null : state.activeConnectorId;
|
|
809
|
-
const nextReconnecting = state.reconnectingIds.has(event.connectorId) ? new Set([...state.reconnectingIds].filter((id) => id !== event.connectorId)) : state.reconnectingIds;
|
|
731
|
+
case "STATUS_RESET":
|
|
732
|
+
if (state.connectionStatus === "idle") return state;
|
|
810
733
|
return {
|
|
811
734
|
...state,
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
selection: newSelection
|
|
735
|
+
connectingConnectorId: null,
|
|
736
|
+
connectionError: null,
|
|
737
|
+
connectionStatus: "idle"
|
|
816
738
|
};
|
|
817
|
-
|
|
818
|
-
case "
|
|
739
|
+
case "ACCOUNTS_CHANGED": return changeAccounts(state, event);
|
|
740
|
+
case "DISCONNECTED": {
|
|
741
|
+
const pool = withoutKey(state.pool, event.connectorId);
|
|
742
|
+
if (event.byUser) {
|
|
743
|
+
const dormant = pool.size === 0 ? keepEntries(state.dormant, () => false) : withoutKey(state.dormant, event.connectorId);
|
|
744
|
+
if (pool === state.pool && dormant === state.dormant && state.isUserDisconnected) return state;
|
|
745
|
+
return {
|
|
746
|
+
...state,
|
|
747
|
+
dormant,
|
|
748
|
+
isUserDisconnected: true,
|
|
749
|
+
pool
|
|
750
|
+
};
|
|
751
|
+
}
|
|
819
752
|
const wallet = state.pool.get(event.connectorId);
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
if (event.active) nextAccount = event.active;
|
|
823
|
-
else nextAccount = event.accounts.some((a) => a.walletAddress === wallet.account.walletAddress && a.chain.id === wallet.account.chain.id) ? wallet.account : event.accounts[0] ?? wallet.account;
|
|
824
|
-
const sameAccount = nextAccount.walletAddress === wallet.account.walletAddress && nextAccount.chain.id === wallet.account.chain.id;
|
|
825
|
-
const sameAccountsShape = event.accounts.length === wallet.accounts.length && event.accounts.every((a, i) => a.walletAddress === wallet.accounts[i]?.walletAddress);
|
|
826
|
-
if (sameAccount && sameAccountsShape) return state;
|
|
827
|
-
const updated = {
|
|
828
|
-
...wallet,
|
|
829
|
-
account: nextAccount,
|
|
830
|
-
accounts: [...event.accounts]
|
|
831
|
-
};
|
|
832
|
-
const newPool = new Map([...state.pool, [event.connectorId, updated]]);
|
|
753
|
+
if (wallet === void 0) return state;
|
|
754
|
+
const stored = toStoredEntry(event.connectorId, wallet);
|
|
833
755
|
return {
|
|
834
756
|
...state,
|
|
835
|
-
|
|
757
|
+
dormant: withEntry(state.dormant, event.connectorId, stored),
|
|
758
|
+
pool
|
|
836
759
|
};
|
|
837
760
|
}
|
|
838
|
-
case "
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
761
|
+
case "RESET": return {
|
|
762
|
+
...initialState,
|
|
763
|
+
adapters: state.adapters,
|
|
764
|
+
isHydrated: state.isHydrated,
|
|
765
|
+
isUserDisconnected: true
|
|
766
|
+
};
|
|
767
|
+
case "ACTIVE_CHANGED":
|
|
768
|
+
if (state.activeConnectorId === event.connectorId || !state.pool.has(event.connectorId)) return state;
|
|
842
769
|
return {
|
|
843
770
|
...state,
|
|
844
|
-
|
|
771
|
+
activeConnectorId: event.connectorId
|
|
845
772
|
};
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
...state,
|
|
849
|
-
activeConnectorId: event.connectorId
|
|
850
|
-
};
|
|
851
|
-
case "SELECTION_CHANGED": {
|
|
852
|
-
const newSelection = new Map(state.selection);
|
|
853
|
-
if (event.connectorId === null) newSelection.delete(event.chainPlatform);
|
|
854
|
-
else newSelection.set(event.chainPlatform, event.connectorId);
|
|
773
|
+
case "SELECTION_CHANGED":
|
|
774
|
+
if (state.selection.get(event.chainPlatform) === event.connectorId || state.pool.get(event.connectorId)?.connector.chainPlatform !== event.chainPlatform) return state;
|
|
855
775
|
return {
|
|
856
776
|
...state,
|
|
857
|
-
selection:
|
|
777
|
+
selection: withEntry(state.selection, event.chainPlatform, event.connectorId)
|
|
858
778
|
};
|
|
859
|
-
}
|
|
860
|
-
case "STATUS_RESET": return {
|
|
861
|
-
...state,
|
|
862
|
-
connectingConnectorId: null,
|
|
863
|
-
connectionError: null,
|
|
864
|
-
connectionStatus: "idle"
|
|
865
|
-
};
|
|
866
|
-
case "ERROR_SET": return {
|
|
867
|
-
...state,
|
|
868
|
-
connectionError: event.error,
|
|
869
|
-
connectionStatus: event.error ? "error" : "idle"
|
|
870
|
-
};
|
|
871
|
-
case "RESET": return {
|
|
872
|
-
...initialState,
|
|
873
|
-
isHydrated: state.isHydrated,
|
|
874
|
-
isUserDisconnected: true,
|
|
875
|
-
reconnectingIds: /* @__PURE__ */ new Set()
|
|
876
|
-
};
|
|
877
779
|
default: return state;
|
|
878
780
|
}
|
|
879
781
|
};
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const run = async (fn, onError) => {
|
|
885
|
-
try {
|
|
886
|
-
await fn();
|
|
887
|
-
} catch (error) {
|
|
888
|
-
onError(error);
|
|
889
|
-
}
|
|
782
|
+
/** Pure: `(state, event) => state`. Side effects live in the manager. */
|
|
783
|
+
const reducer = (state, event) => {
|
|
784
|
+
const next = apply(state, event);
|
|
785
|
+
return next === state ? state : reconcile(next);
|
|
890
786
|
};
|
|
891
|
-
|
|
892
|
-
//#endregion
|
|
893
|
-
//#region src/store/wallet-store.ts
|
|
894
|
-
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
895
|
-
const isChainPlatform = (value) => VALID_CHAIN_PLATFORMS.has(value);
|
|
896
787
|
/**
|
|
897
788
|
* `isHydrated` is true from the first render on this path, so
|
|
898
|
-
* `reconnectingIds`
|
|
899
|
-
*
|
|
789
|
+
* `reconnectingIds` says whether a connection is verified. Seeded entries
|
|
790
|
+
* are also dormant: persisted, not yet live.
|
|
900
791
|
*/
|
|
901
|
-
const
|
|
792
|
+
const stateFromSnapshot = (snapshot) => {
|
|
902
793
|
const pool = /* @__PURE__ */ new Map();
|
|
903
|
-
const
|
|
904
|
-
for (const [connectorId, entry] of Object.entries(snapshot.pool)) {
|
|
905
|
-
if (!entry) continue;
|
|
906
|
-
const connector = createShadowAdapter(entry);
|
|
794
|
+
const dormant = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const [connectorId, entry] of Object.entries(snapshot.pool)) if (entry !== void 0) {
|
|
907
796
|
pool.set(connectorId, {
|
|
908
797
|
account: entry.account,
|
|
909
|
-
accounts:
|
|
910
|
-
connector
|
|
798
|
+
accounts: entry.accounts,
|
|
799
|
+
connector: createShadowAdapter(entry)
|
|
911
800
|
});
|
|
912
|
-
|
|
801
|
+
dormant.set(connectorId, entry);
|
|
913
802
|
}
|
|
914
|
-
|
|
915
|
-
for (const [platform, connectorId] of Object.entries(snapshot.selection)) if (isChainPlatform(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
916
|
-
const activeConnectorId = snapshot.activeConnectorId !== null && pool.has(snapshot.activeConnectorId) ? snapshot.activeConnectorId : pool.keys().next().value ?? null;
|
|
917
|
-
return {
|
|
803
|
+
return reconcile({
|
|
918
804
|
...initialState,
|
|
919
|
-
activeConnectorId,
|
|
805
|
+
activeConnectorId: snapshot.activeConnectorId,
|
|
806
|
+
dormant,
|
|
920
807
|
isHydrated: true,
|
|
921
808
|
pool,
|
|
922
|
-
reconnectingIds,
|
|
923
|
-
selection
|
|
924
|
-
};
|
|
809
|
+
reconnectingIds: new Set(pool.keys()),
|
|
810
|
+
selection: applyStoredSelection(/* @__PURE__ */ new Map(), snapshot.selection, pool)
|
|
811
|
+
});
|
|
925
812
|
};
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/store/wallet-manager.ts
|
|
926
816
|
const CONNECT_TIMEOUT_MS = 9e4;
|
|
817
|
+
const RESTORE_TIMEOUT_MS = 15e3;
|
|
927
818
|
const DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5e3;
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
819
|
+
const EMPTY_PERSISTED = {
|
|
820
|
+
activeConnectorId: null,
|
|
821
|
+
isUserDisconnected: false,
|
|
822
|
+
pool: {},
|
|
823
|
+
selection: {}
|
|
824
|
+
};
|
|
825
|
+
const toPersistedState = (state) => {
|
|
826
|
+
const pool = Object.fromEntries(state.dormant);
|
|
827
|
+
for (const [connectorId, wallet] of state.pool) pool[connectorId] = toStoredEntry(connectorId, wallet);
|
|
828
|
+
return {
|
|
829
|
+
activeConnectorId: state.activeConnectorId,
|
|
830
|
+
isUserDisconnected: state.isUserDisconnected,
|
|
831
|
+
pool,
|
|
832
|
+
selection: Object.fromEntries(state.selection)
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
const persistedSliceChanged = (state, prev) => state.pool !== prev.pool || state.dormant !== prev.dormant || state.selection !== prev.selection || state.activeConnectorId !== prev.activeConnectorId || state.isUserDisconnected !== prev.isUserDisconnected;
|
|
836
|
+
const toError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
|
|
837
|
+
/** A consumer callback that throws must not break the manager. */
|
|
838
|
+
const safely = (name, callback, ...args) => {
|
|
839
|
+
try {
|
|
840
|
+
callback?.(...args);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
logWarn(`[butr] ${name} threw:`, error);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
const withTimeout = async (promise, ms) => {
|
|
846
|
+
promise.catch(() => {});
|
|
847
|
+
let timer;
|
|
848
|
+
try {
|
|
849
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
850
|
+
timer = setTimeout(() => {
|
|
851
|
+
reject(new ConnectionError("Timeout", `Connection timed out after ${ms / 1e3}s`));
|
|
852
|
+
}, ms);
|
|
853
|
+
})]);
|
|
854
|
+
} finally {
|
|
855
|
+
clearTimeout(timer);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const teardown = async (connector) => {
|
|
859
|
+
try {
|
|
860
|
+
await connector.disconnect?.();
|
|
861
|
+
} catch (error) {
|
|
862
|
+
logWarn(`[butr] disconnect of ${connector.id} failed:`, error);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Framework-free: React binds it through `WalletManagerProvider`, anything
|
|
867
|
+
* else calls `start()` itself. Creating one has no side effects, so it is
|
|
868
|
+
* safe during a server render.
|
|
869
|
+
*/
|
|
870
|
+
const createWalletManager = (config = {}, options = {}) => {
|
|
871
|
+
const storage = config.storage ?? createWalletStorage({ keyPrefix: config.storageKeyPrefix });
|
|
872
|
+
const store = createStore()(() => options.initialState ? stateFromSnapshot(options.initialState) : initialState);
|
|
873
|
+
const { getState } = store;
|
|
874
|
+
const dispatch = (event) => {
|
|
875
|
+
store.setState((state) => reducer(state, event), true);
|
|
876
|
+
};
|
|
877
|
+
const findAdapter = (connectorId) => getState().adapters.find((adapter) => adapter.id === connectorId);
|
|
878
|
+
const reportStorageError = (cause) => {
|
|
879
|
+
if (config.onStorageError) safely("onStorageError", config.onStorageError, toError(cause));
|
|
880
|
+
else logWarn("[butr] wallet persistence failed:", cause);
|
|
881
|
+
};
|
|
882
|
+
let persistEnabled = false;
|
|
883
|
+
let flushing = false;
|
|
884
|
+
let dirty = false;
|
|
885
|
+
const persist = async () => {
|
|
886
|
+
dirty = true;
|
|
887
|
+
if (flushing) return;
|
|
888
|
+
flushing = true;
|
|
889
|
+
while (dirty) {
|
|
890
|
+
dirty = false;
|
|
933
891
|
try {
|
|
934
|
-
|
|
935
|
-
} catch (
|
|
936
|
-
|
|
892
|
+
await storage.save(toPersistedState(getState()));
|
|
893
|
+
} catch (error) {
|
|
894
|
+
reportStorageError(error);
|
|
937
895
|
}
|
|
938
|
-
return;
|
|
939
896
|
}
|
|
940
|
-
|
|
897
|
+
flushing = false;
|
|
941
898
|
};
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const persistPool = () => run(() => storage.setPool(get().pool), reportStorageError("failed to persist pool"));
|
|
948
|
-
const persistSelection = () => run(() => storage.setSelection(get().selection), reportStorageError("failed to persist selection"));
|
|
949
|
-
const persistActive = () => run(() => storage.setActiveConnectorId(get().activeConnectorId), reportStorageError("failed to persist active connector"));
|
|
950
|
-
const refreshPoolEntry = (connectorId, accounts, active) => {
|
|
899
|
+
store.subscribe((state, prev) => {
|
|
900
|
+
if (persistEnabled && persistedSliceChanged(state, prev)) persist();
|
|
901
|
+
});
|
|
902
|
+
const lifecycle = createConnectorLifecycle({
|
|
903
|
+
onAccountsChanged: (connectorId, accounts) => {
|
|
951
904
|
dispatch({
|
|
952
905
|
accounts,
|
|
953
|
-
active,
|
|
906
|
+
active: accounts[0],
|
|
954
907
|
connectorId,
|
|
955
|
-
type: "
|
|
908
|
+
type: "ACCOUNTS_CHANGED"
|
|
956
909
|
});
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
910
|
+
},
|
|
911
|
+
onDisconnected: (connectorId) => {
|
|
912
|
+
const wallet = getState().pool.get(connectorId);
|
|
913
|
+
if (wallet === void 0) return;
|
|
914
|
+
teardown(wallet.connector);
|
|
915
|
+
dispatch({
|
|
916
|
+
byUser: false,
|
|
917
|
+
connectorId,
|
|
918
|
+
type: "DISCONNECTED"
|
|
919
|
+
});
|
|
920
|
+
safely("onDisconnect", config.onDisconnect, wallet, { byUser: false });
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
924
|
+
let loaded = false;
|
|
925
|
+
let hydration = null;
|
|
926
|
+
let userChose = false;
|
|
927
|
+
const choose = (event) => {
|
|
928
|
+
const before = getState();
|
|
929
|
+
dispatch(event);
|
|
930
|
+
if (getState() !== before) userChose = true;
|
|
931
|
+
};
|
|
932
|
+
const restore = async (connectorId) => {
|
|
933
|
+
const adapter = findAdapter(connectorId);
|
|
934
|
+
if (adapter === void 0 || attempted.has(connectorId) || !getState().dormant.has(connectorId)) return null;
|
|
935
|
+
attempted.add(connectorId);
|
|
936
|
+
try {
|
|
937
|
+
await withTimeout(adapter.connect({ silent: true }), RESTORE_TIMEOUT_MS);
|
|
938
|
+
const accounts = await adapter.getAccounts();
|
|
939
|
+
const account = accounts.at(0);
|
|
940
|
+
if (account === void 0) throw new ConnectionError("NotConnected", `${adapter.name} exposed no accounts`);
|
|
941
|
+
const wallet = {
|
|
942
|
+
account,
|
|
943
|
+
accounts,
|
|
944
|
+
connector: adapter
|
|
945
|
+
};
|
|
946
|
+
const before = getState();
|
|
947
|
+
dispatch({
|
|
948
|
+
connectorId,
|
|
949
|
+
entry: wallet,
|
|
950
|
+
type: "ENTRY_RESTORED"
|
|
951
|
+
});
|
|
952
|
+
if (getState() === before) {
|
|
953
|
+
if (!before.pool.has(connectorId)) await teardown(adapter);
|
|
954
|
+
return null;
|
|
971
955
|
}
|
|
956
|
+
safely("onConnect", config.onConnect, wallet, { reconnected: true });
|
|
957
|
+
return {
|
|
958
|
+
connectorId,
|
|
959
|
+
ok: true
|
|
960
|
+
};
|
|
961
|
+
} catch (error) {
|
|
962
|
+
dispatch({
|
|
963
|
+
connectorId,
|
|
964
|
+
type: "RESTORE_FAILED"
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
connectorId,
|
|
968
|
+
error: toConnectionError(error),
|
|
969
|
+
ok: false
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
const restoreLate = async (connectorId) => {
|
|
974
|
+
const result = await restore(connectorId);
|
|
975
|
+
if (result?.ok === false) logWarn(`[butr] silent reconnect failed for ${connectorId}:`, result.error);
|
|
976
|
+
};
|
|
977
|
+
const hydrate = async () => {
|
|
978
|
+
let persisted = EMPTY_PERSISTED;
|
|
979
|
+
try {
|
|
980
|
+
persisted = await storage.load();
|
|
981
|
+
} catch (error) {
|
|
982
|
+
reportStorageError(error);
|
|
983
|
+
}
|
|
984
|
+
dispatch({
|
|
985
|
+
entries: persisted.pool,
|
|
986
|
+
isUserDisconnected: persisted.isUserDisconnected,
|
|
987
|
+
type: "STORAGE_LOADED"
|
|
972
988
|
});
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
989
|
+
loaded = true;
|
|
990
|
+
const ids = [...getState().dormant.keys()];
|
|
991
|
+
const restores = ids.map(restore);
|
|
992
|
+
const pendingIds = ids.filter((id) => !attempted.has(id));
|
|
993
|
+
const results = (await Promise.all(restores)).filter((result) => result !== null);
|
|
994
|
+
dispatch({
|
|
995
|
+
activeConnectorId: userChose ? null : persisted.activeConnectorId,
|
|
996
|
+
selection: userChose ? {} : persisted.selection,
|
|
997
|
+
type: "HYDRATED"
|
|
998
|
+
});
|
|
999
|
+
persistEnabled = true;
|
|
1000
|
+
persist();
|
|
1001
|
+
const outcome = {
|
|
1002
|
+
dropped: results.flatMap((result) => result.ok ? [] : [{
|
|
1003
|
+
connectorId: result.connectorId,
|
|
1004
|
+
reason: result.error
|
|
1005
|
+
}]),
|
|
1006
|
+
pendingIds,
|
|
1007
|
+
restoredIds: results.flatMap((result) => result.ok ? [result.connectorId] : [])
|
|
1008
|
+
};
|
|
1009
|
+
if (config.onHydrated) safely("onHydrated", config.onHydrated, outcome);
|
|
1010
|
+
else for (const { connectorId, reason } of outcome.dropped) logWarn(`[butr] silent reconnect failed for ${connectorId}:`, reason);
|
|
1011
|
+
};
|
|
1012
|
+
const register = (adapter) => {
|
|
1013
|
+
const before = getState();
|
|
1014
|
+
dispatch({
|
|
1015
|
+
adapter,
|
|
1016
|
+
type: "ADAPTER_REGISTERED"
|
|
1017
|
+
});
|
|
1018
|
+
if (loaded && getState() !== before) restoreLate(adapter.id);
|
|
1019
|
+
};
|
|
1020
|
+
const subscribeSource = (source) => {
|
|
1021
|
+
try {
|
|
1022
|
+
return source(register);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
logError("[butr] a wallet source threw on subscribe:", error);
|
|
1025
|
+
return () => {};
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
return {
|
|
1029
|
+
clearConnectionError: () => {
|
|
1030
|
+
dispatch({ type: "STATUS_RESET" });
|
|
1031
|
+
},
|
|
1032
|
+
connect: async (connectorId) => {
|
|
1033
|
+
dispatch({
|
|
1034
|
+
connectorId,
|
|
1035
|
+
type: "CONNECT_STARTED"
|
|
1036
|
+
});
|
|
1037
|
+
const slowTimer = config.onSlowConnect ? setTimeout(() => {
|
|
1038
|
+
safely("onSlowConnect", config.onSlowConnect, connectorId);
|
|
1039
|
+
}, config.slowConnectThresholdMs ?? DEFAULT_SLOW_CONNECT_THRESHOLD_MS) : void 0;
|
|
1040
|
+
const connector = findAdapter(connectorId);
|
|
1041
|
+
try {
|
|
1042
|
+
if (connector === void 0) throw new ConnectionError("WalletNotFound", `No wallet adapter is registered as "${connectorId}"`);
|
|
1043
|
+
await withTimeout(connector.connect(), CONNECT_TIMEOUT_MS);
|
|
1044
|
+
const accounts = await connector.getAccounts();
|
|
1045
|
+
const account = accounts.at(0);
|
|
1046
|
+
if (account === void 0) throw new ConnectionError("NotConnected", `${connector.name} exposed no accounts`);
|
|
1047
|
+
const wallet = {
|
|
1048
|
+
account,
|
|
1049
|
+
accounts,
|
|
1050
|
+
connector
|
|
1051
|
+
};
|
|
1052
|
+
choose({
|
|
982
1053
|
connectorId,
|
|
983
|
-
|
|
1054
|
+
entry: wallet,
|
|
1055
|
+
type: "CONNECT_SUCCEEDED"
|
|
984
1056
|
});
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
} catch (cbError) {
|
|
990
|
-
logWarn("[butr] onSlowConnect threw:", cbError);
|
|
991
|
-
}
|
|
992
|
-
}, slowThreshold) : null;
|
|
993
|
-
let connectTimeoutId;
|
|
994
|
-
try {
|
|
995
|
-
const connectPromise = connector.connect();
|
|
996
|
-
connectPromise.catch(() => {});
|
|
997
|
-
await Promise.race([connectPromise, new Promise((_, reject) => {
|
|
998
|
-
connectTimeoutId = setTimeout(() => {
|
|
999
|
-
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
1000
|
-
}, CONNECT_TIMEOUT_MS);
|
|
1001
|
-
})]);
|
|
1002
|
-
const account = await connector.getAccount();
|
|
1003
|
-
if (!account) throw new Error("Failed to get account");
|
|
1004
|
-
const fetchedAccounts = connector.getAccounts ? await connector.getAccounts().catch(() => null) : null;
|
|
1005
|
-
const entry = {
|
|
1006
|
-
account,
|
|
1007
|
-
accounts: fetchedAccounts && fetchedAccounts.length > 0 ? fetchedAccounts : [account],
|
|
1008
|
-
connector
|
|
1009
|
-
};
|
|
1010
|
-
dispatch({
|
|
1011
|
-
connectorId,
|
|
1012
|
-
entry,
|
|
1013
|
-
type: "CONNECT_SUCCEEDED"
|
|
1014
|
-
});
|
|
1015
|
-
lifecycle.attach(connectorId, connector);
|
|
1016
|
-
await Promise.all([
|
|
1017
|
-
persistPool(),
|
|
1018
|
-
persistSelection(),
|
|
1019
|
-
persistActive()
|
|
1020
|
-
]);
|
|
1021
|
-
config.onConnect?.(entry);
|
|
1022
|
-
onSuccess?.(entry);
|
|
1023
|
-
} catch (error) {
|
|
1024
|
-
const normalised = mapConnectionError(error);
|
|
1025
|
-
dispatch({
|
|
1026
|
-
connectorId,
|
|
1027
|
-
error: normalised,
|
|
1028
|
-
type: "CONNECT_FAILED"
|
|
1029
|
-
});
|
|
1030
|
-
try {
|
|
1031
|
-
await connector.disconnect?.();
|
|
1032
|
-
} catch (disconnectError) {
|
|
1033
|
-
logWarn("[butr] disconnect during error recovery failed:", disconnectError);
|
|
1034
|
-
}
|
|
1035
|
-
try {
|
|
1036
|
-
config.onConnectError?.(normalised, connectorId);
|
|
1037
|
-
} catch (cbError) {
|
|
1038
|
-
logWarn("[butr] onConnectError threw:", cbError);
|
|
1039
|
-
}
|
|
1040
|
-
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1041
|
-
throw error;
|
|
1042
|
-
} finally {
|
|
1043
|
-
clearTimeout(connectTimeoutId);
|
|
1044
|
-
if (slowTimer) clearTimeout(slowTimer);
|
|
1045
|
-
}
|
|
1046
|
-
},
|
|
1047
|
-
disconnectWallet: (connectorId) => {
|
|
1048
|
-
get().setUserDisconnected(true);
|
|
1049
|
-
const state = get();
|
|
1050
|
-
if (!state.isHydrated) return;
|
|
1051
|
-
const wallet = state.pool.get(connectorId);
|
|
1052
|
-
if (!wallet) return;
|
|
1053
|
-
lifecycle.detach(connectorId);
|
|
1054
|
-
run(() => wallet.connector.disconnect?.() ?? Promise.resolve(), logError);
|
|
1055
|
-
const platform = wallet.connector.chainPlatform;
|
|
1057
|
+
safely("onConnect", config.onConnect, wallet, { reconnected: false });
|
|
1058
|
+
return wallet;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
const failure = toConnectionError(error);
|
|
1056
1061
|
dispatch({
|
|
1057
1062
|
connectorId,
|
|
1058
|
-
|
|
1063
|
+
error: failure,
|
|
1064
|
+
type: "CONNECT_FAILED"
|
|
1059
1065
|
});
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1066
|
+
if (connector !== void 0 && getState().pool.get(connectorId)?.connector !== connector) await teardown(connector);
|
|
1067
|
+
safely("onConnectError", config.onConnectError, failure, connectorId);
|
|
1068
|
+
throw failure;
|
|
1069
|
+
} finally {
|
|
1070
|
+
clearTimeout(slowTimer);
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
disconnect: (connectorId) => {
|
|
1074
|
+
const wallet = getState().pool.get(connectorId);
|
|
1075
|
+
dispatch({
|
|
1076
|
+
byUser: true,
|
|
1077
|
+
connectorId,
|
|
1078
|
+
type: "DISCONNECTED"
|
|
1079
|
+
});
|
|
1080
|
+
if (wallet !== void 0) {
|
|
1081
|
+
teardown(wallet.connector);
|
|
1082
|
+
safely("onDisconnect", config.onDisconnect, wallet, { byUser: true });
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
disconnectAll: () => {
|
|
1086
|
+
const wallets = [...getState().pool.values()];
|
|
1087
|
+
dispatch({ type: "RESET" });
|
|
1088
|
+
for (const wallet of wallets) {
|
|
1089
|
+
teardown(wallet.connector);
|
|
1090
|
+
safely("onDisconnect", config.onDisconnect, wallet, { byUser: true });
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
getInitialState: store.getInitialState,
|
|
1094
|
+
getState,
|
|
1095
|
+
requestAccounts: async (connectorId) => {
|
|
1096
|
+
const connector = getState().pool.get(connectorId)?.connector;
|
|
1097
|
+
if (connector?.requestAccounts === void 0) return;
|
|
1098
|
+
await connector.requestAccounts();
|
|
1099
|
+
const accounts = await connector.getAccounts();
|
|
1100
|
+
dispatch({
|
|
1101
|
+
accounts,
|
|
1102
|
+
connectorId,
|
|
1103
|
+
type: "ACCOUNTS_CHANGED"
|
|
1104
|
+
});
|
|
1105
|
+
},
|
|
1106
|
+
setAccount: (connectorId, account) => {
|
|
1107
|
+
const wallet = getState().pool.get(connectorId);
|
|
1108
|
+
const active = wallet?.accounts.find((candidate) => candidate.id === account.id);
|
|
1109
|
+
if (wallet === void 0 || active === void 0) return;
|
|
1110
|
+
dispatch({
|
|
1111
|
+
accounts: wallet.accounts,
|
|
1112
|
+
active,
|
|
1113
|
+
connectorId,
|
|
1114
|
+
type: "ACCOUNTS_CHANGED"
|
|
1115
|
+
});
|
|
1116
|
+
},
|
|
1117
|
+
setActive: (connectorId) => {
|
|
1118
|
+
choose({
|
|
1119
|
+
connectorId,
|
|
1120
|
+
type: "ACTIVE_CHANGED"
|
|
1121
|
+
});
|
|
1122
|
+
},
|
|
1123
|
+
setSelection: (chainPlatform, connectorId) => {
|
|
1124
|
+
choose({
|
|
1125
|
+
chainPlatform,
|
|
1126
|
+
connectorId,
|
|
1127
|
+
type: "SELECTION_CHANGED"
|
|
1128
|
+
});
|
|
1129
|
+
},
|
|
1130
|
+
start: () => {
|
|
1131
|
+
const unsubscribes = (config.sources ?? []).map(subscribeSource);
|
|
1132
|
+
lifecycle.sync(getState().pool);
|
|
1133
|
+
const unsubscribePool = store.subscribe((state, prev) => {
|
|
1134
|
+
if (state.pool !== prev.pool) lifecycle.sync(state.pool);
|
|
1135
|
+
});
|
|
1136
|
+
hydration ??= hydrate();
|
|
1137
|
+
return () => {
|
|
1138
|
+
for (const unsubscribe of unsubscribes) try {
|
|
1139
|
+
unsubscribe();
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
logWarn("[butr] a wallet source threw on unsubscribe:", error);
|
|
1092
1142
|
}
|
|
1093
|
-
|
|
1094
|
-
},
|
|
1095
|
-
refreshWallet: (connectorId) => {
|
|
1096
|
-
dispatch({
|
|
1097
|
-
connectorId,
|
|
1098
|
-
type: "WALLET_REFRESHED"
|
|
1099
|
-
});
|
|
1100
|
-
},
|
|
1101
|
-
requestAccounts: async (connectorId) => {
|
|
1102
|
-
const entry = get().pool.get(connectorId);
|
|
1103
|
-
if (!entry) return;
|
|
1104
|
-
await entry.connector.requestAccounts?.();
|
|
1105
|
-
const fresh = entry.connector.getAccounts ? await entry.connector.getAccounts().catch(() => null) : null;
|
|
1106
|
-
if (!fresh || fresh.length === 0) return;
|
|
1107
|
-
refreshPoolEntry(connectorId, fresh);
|
|
1108
|
-
},
|
|
1109
|
-
reset: () => {
|
|
1110
|
-
get().setUserDisconnected(true);
|
|
1111
|
-
const state = get();
|
|
1112
|
-
if (!state.isHydrated) return;
|
|
1143
|
+
unsubscribePool();
|
|
1113
1144
|
lifecycle.detachAll();
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
run(() => Promise.resolve(onReset()), logError);
|
|
1119
|
-
}
|
|
1120
|
-
dispatch({ type: "RESET" });
|
|
1121
|
-
},
|
|
1122
|
-
resetConnectionStatus: () => {
|
|
1123
|
-
dispatch({ type: "STATUS_RESET" });
|
|
1124
|
-
},
|
|
1125
|
-
setActiveConnector: (connectorId) => {
|
|
1126
|
-
dispatch({
|
|
1127
|
-
connectorId,
|
|
1128
|
-
type: "ACTIVE_CHANGED"
|
|
1129
|
-
});
|
|
1130
|
-
persistActive();
|
|
1131
|
-
},
|
|
1132
|
-
setConnectionError: (error) => {
|
|
1133
|
-
dispatch({
|
|
1134
|
-
error,
|
|
1135
|
-
type: "ERROR_SET"
|
|
1136
|
-
});
|
|
1137
|
-
},
|
|
1138
|
-
setSelection: (chainPlatform, connectorId) => {
|
|
1139
|
-
dispatch({
|
|
1140
|
-
chainPlatform,
|
|
1141
|
-
connectorId,
|
|
1142
|
-
type: "SELECTION_CHANGED"
|
|
1143
|
-
});
|
|
1144
|
-
persistSelection();
|
|
1145
|
-
},
|
|
1146
|
-
setUserDisconnected: (value) => {
|
|
1147
|
-
dispatch({
|
|
1148
|
-
type: "USER_DISCONNECTED_SET",
|
|
1149
|
-
value
|
|
1150
|
-
});
|
|
1151
|
-
run(() => storage.markUserDisconnected(value), reportStorageError("failed to persist disconnect intent"));
|
|
1152
|
-
},
|
|
1153
|
-
tryRestoreFromPending: async (connectorId) => {
|
|
1154
|
-
const outcome = await hydration.drainPending(connectorId);
|
|
1155
|
-
if (!outcome) return;
|
|
1156
|
-
if (outcome.kind === "fail") {
|
|
1157
|
-
logWarn(`[butr] late restore failed for ${connectorId}:`, outcome.error);
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
dispatch({
|
|
1161
|
-
connectorId,
|
|
1162
|
-
entry: outcome.entry,
|
|
1163
|
-
type: "ENTRY_RESTORED"
|
|
1164
|
-
});
|
|
1165
|
-
lifecycle.attach(connectorId, outcome.entry.connector);
|
|
1166
|
-
await Promise.all([
|
|
1167
|
-
persistPool(),
|
|
1168
|
-
persistSelection(),
|
|
1169
|
-
persistActive()
|
|
1170
|
-
]);
|
|
1171
|
-
config.onConnect?.(outcome.entry);
|
|
1172
|
-
},
|
|
1173
|
-
updateWalletAccount: (connectorId, account) => {
|
|
1174
|
-
const entry = get().pool.get(connectorId);
|
|
1175
|
-
if (!entry) return;
|
|
1176
|
-
const newChain = account.chain;
|
|
1177
|
-
const remapped = entry.accounts.map((a) => a.chain.id === newChain.id ? a : {
|
|
1178
|
-
chain: newChain,
|
|
1179
|
-
id: `${newChain.id}:${normaliseAddress(a.walletAddress)}`,
|
|
1180
|
-
walletAddress: a.walletAddress
|
|
1181
|
-
});
|
|
1182
|
-
const accounts = remapped.some((a) => normaliseAddress(a.walletAddress) === normaliseAddress(account.walletAddress)) ? remapped : [...remapped, account];
|
|
1183
|
-
refreshPoolEntry(connectorId, accounts, account);
|
|
1184
|
-
}
|
|
1185
|
-
};
|
|
1186
|
-
});
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
subscribe: store.subscribe
|
|
1148
|
+
};
|
|
1187
1149
|
};
|
|
1188
1150
|
|
|
1189
1151
|
//#endregion
|
|
@@ -1264,10 +1226,12 @@ const createCookieStorageDriver = (options = {}) => {
|
|
|
1264
1226
|
|
|
1265
1227
|
//#endregion
|
|
1266
1228
|
//#region src/storage/snapshot.ts
|
|
1229
|
+
const EMPTY_POOL = Object.freeze({});
|
|
1230
|
+
const EMPTY_SELECTION = Object.freeze({});
|
|
1267
1231
|
const EMPTY_SNAPSHOT = Object.freeze({
|
|
1268
1232
|
activeConnectorId: null,
|
|
1269
|
-
pool:
|
|
1270
|
-
selection:
|
|
1233
|
+
pool: EMPTY_POOL,
|
|
1234
|
+
selection: EMPTY_SELECTION
|
|
1271
1235
|
});
|
|
1272
1236
|
const toCookieMap = (input) => {
|
|
1273
1237
|
if (!(Symbol.iterator in input)) return new Map(Object.entries(input));
|
|
@@ -1298,7 +1262,7 @@ const readWalletSnapshot = (source, options = {}) => {
|
|
|
1298
1262
|
if (rawActive !== void 0 && rawActive.length > 0 && pool[rawActive] !== void 0) activeConnectorId = rawActive;
|
|
1299
1263
|
else {
|
|
1300
1264
|
const firstKey = Object.keys(pool)[0];
|
|
1301
|
-
if (firstKey) activeConnectorId = firstKey;
|
|
1265
|
+
if (firstKey !== void 0 && firstKey !== "") activeConnectorId = firstKey;
|
|
1302
1266
|
}
|
|
1303
1267
|
return {
|
|
1304
1268
|
activeConnectorId,
|
|
@@ -1432,7 +1396,7 @@ const base58ToBytes = (input) => {
|
|
|
1432
1396
|
var SignInUnsupportedError = class extends Error {
|
|
1433
1397
|
connectorId;
|
|
1434
1398
|
constructor(connectorId) {
|
|
1435
|
-
super(`Wallet "${connectorId}"
|
|
1399
|
+
super(`Wallet "${connectorId}" cannot sign messages, so it cannot sign in.`);
|
|
1436
1400
|
this.connectorId = connectorId;
|
|
1437
1401
|
this.name = "SignInUnsupportedError";
|
|
1438
1402
|
}
|
|
@@ -1448,12 +1412,11 @@ const createSignInFlow = (options) => {
|
|
|
1448
1412
|
const sign = async (wallet, account) => {
|
|
1449
1413
|
const { connector } = wallet;
|
|
1450
1414
|
const signingAccount = account ?? wallet.account;
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
if (options.preferSignMessage !== true && connector.chainPlatform === "svm" && connector.capabilities.signIn && connector.signIn !== void 0) {
|
|
1415
|
+
if (options.preferSignMessage !== true && connector.chainPlatform === "svm" && connector.signIn !== void 0) {
|
|
1416
|
+
const nonce = await options.getNonce({
|
|
1417
|
+
account: signingAccount,
|
|
1418
|
+
wallet
|
|
1419
|
+
});
|
|
1457
1420
|
const output = await connector.signIn({ nonce });
|
|
1458
1421
|
return {
|
|
1459
1422
|
account: output.account,
|
|
@@ -1465,12 +1428,17 @@ const createSignInFlow = (options) => {
|
|
|
1465
1428
|
wallet
|
|
1466
1429
|
};
|
|
1467
1430
|
}
|
|
1431
|
+
if (connector.signMessage === void 0) throw new SignInUnsupportedError(connector.id);
|
|
1432
|
+
const nonce = await options.getNonce({
|
|
1433
|
+
account: signingAccount,
|
|
1434
|
+
wallet
|
|
1435
|
+
});
|
|
1468
1436
|
const message = buildMessage({
|
|
1469
1437
|
account: signingAccount,
|
|
1470
1438
|
nonce,
|
|
1471
1439
|
wallet
|
|
1472
1440
|
});
|
|
1473
|
-
const { signature, signedMessage } = await connector.signMessage(new TextEncoder().encode(message), signingAccount);
|
|
1441
|
+
const { signature, signedMessage } = await connector.signMessage(new TextEncoder().encode(message), { account: signingAccount });
|
|
1474
1442
|
return {
|
|
1475
1443
|
account: signingAccount,
|
|
1476
1444
|
message,
|
|
@@ -1489,19 +1457,6 @@ const createSignInFlow = (options) => {
|
|
|
1489
1457
|
} };
|
|
1490
1458
|
};
|
|
1491
1459
|
|
|
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
1460
|
//#endregion
|
|
1506
1461
|
//#region src/sanitize-icon.ts
|
|
1507
1462
|
/**
|
|
@@ -1516,5 +1471,5 @@ const sanitizeIcon = (icon) => {
|
|
|
1516
1471
|
};
|
|
1517
1472
|
|
|
1518
1473
|
//#endregion
|
|
1519
|
-
export { CHAIN_PLATFORMS, EMPTY_SNAPSHOT, ShadowConnectorError, SignInUnsupportedError,
|
|
1474
|
+
export { BITCOIN_CHAINS, BITCOIN_CHAINS_LIST, CHAINS_BY_PLATFORM, CHAIN_PLATFORMS, ConnectionError, EMPTY_SNAPSHOT, EVM_CHAINS, EVM_CHAINS_LIST, POLKADOT_CHAINS, POLKADOT_CHAINS_LIST, SUI_CHAINS, SUI_CHAINS_LIST, SVM_CHAINS, SVM_CHAINS_LIST, ShadowConnectorError, SignInUnsupportedError, accountsEqual, base58ToBytes, base64ToBytes, buildAccount, bytesToBase58, bytesToBase64, bytesToHex, bytesToHexPrefixed, createBrowserStorageDriver, createCookieStorageDriver, createMemoryStorageDriver, createSignInFlow, createWalletManager, createWalletStorage, fromAdapters, groupByPlatform, hexToBytes, isChainPlatform, isPlatformWallet, logError, logWarn, readWalletSnapshot, resolveChain, sanitizeIcon, toConnectionError, walletEqual };
|
|
1520
1475
|
//# sourceMappingURL=index.js.map
|