@usebutr/core 2.0.1 → 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 +30 -0
- package/README.md +55 -9
- package/dist/index.d.ts +496 -498
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +870 -920
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -14,81 +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
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
35
46
|
/**
|
|
36
47
|
* EIP-1193 codes: `4001` rejected, `-32002` pending, `4100`/`4900`/`4901`
|
|
37
48
|
* unauthorized or disconnected. Message-substring matching is the last
|
|
38
49
|
* resort for SDKs that ship no codes at all.
|
|
39
50
|
*/
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (code === -32002) return {
|
|
58
|
-
kind: "RequestPending",
|
|
59
|
-
message
|
|
60
|
-
};
|
|
61
|
-
if (code === 4100 || code === 4900 || code === 4901) return {
|
|
62
|
-
kind: "NotConnected",
|
|
63
|
-
message
|
|
64
|
-
};
|
|
65
|
-
if (lower.includes("user rejected") || lower.includes("user denied")) return {
|
|
66
|
-
kind: "UserRejected",
|
|
67
|
-
message
|
|
68
|
-
};
|
|
69
|
-
if (lower.includes("locked")) return {
|
|
70
|
-
kind: "WalletLocked",
|
|
71
|
-
message
|
|
72
|
-
};
|
|
73
|
-
if (lower.includes("chain") && (lower.includes("mismatch") || lower.includes("unsupported"))) return {
|
|
74
|
-
kind: "ChainMismatch",
|
|
75
|
-
message
|
|
76
|
-
};
|
|
77
|
-
return {
|
|
78
|
-
cause: raw,
|
|
79
|
-
kind: "Unknown",
|
|
80
|
-
message
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (typeof raw === "string") return {
|
|
84
|
-
kind: "Unknown",
|
|
85
|
-
message: raw
|
|
86
|
-
};
|
|
87
|
-
return {
|
|
88
|
-
cause: raw,
|
|
89
|
-
kind: "Unknown",
|
|
90
|
-
message: "Connection failed"
|
|
91
|
-
};
|
|
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 });
|
|
92
68
|
};
|
|
93
69
|
|
|
94
70
|
//#endregion
|
|
@@ -105,120 +81,252 @@ const CHAIN_PLATFORMS = [
|
|
|
105
81
|
"bitcoin",
|
|
106
82
|
"polkadot"
|
|
107
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
|
+
};
|
|
108
262
|
|
|
109
263
|
//#endregion
|
|
110
264
|
//#region src/wallet-source.ts
|
|
265
|
+
const isIterable = (value) => Symbol.iterator in value;
|
|
111
266
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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.
|
|
114
270
|
*/
|
|
115
|
-
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
|
+
};
|
|
116
288
|
|
|
117
289
|
//#endregion
|
|
118
290
|
//#region src/store/shadow-adapter.ts
|
|
119
291
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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.
|
|
123
295
|
*/
|
|
124
296
|
var ShadowConnectorError = class extends Error {
|
|
125
|
-
code = "BUTR_RECONNECTING";
|
|
126
297
|
connectorId;
|
|
127
298
|
method;
|
|
128
299
|
constructor(method, connectorId) {
|
|
129
|
-
super(`[butr] ${method} called on
|
|
300
|
+
super(`[butr] ${method} called on "${connectorId}" while it is still reconnecting. Check useIsReconnecting(id) before calling it.`);
|
|
130
301
|
this.name = "ShadowConnectorError";
|
|
131
302
|
this.connectorId = connectorId;
|
|
132
303
|
this.method = method;
|
|
133
304
|
}
|
|
134
305
|
};
|
|
135
|
-
const ALL_FALSE_CAPABILITIES = Object.freeze({
|
|
136
|
-
getBalance: false,
|
|
137
|
-
getTransactionReceipt: false,
|
|
138
|
-
requestAccounts: false,
|
|
139
|
-
sendTransaction: false,
|
|
140
|
-
signIn: false,
|
|
141
|
-
signMessage: false,
|
|
142
|
-
signTransaction: false,
|
|
143
|
-
subscribe: false,
|
|
144
|
-
switchAccount: false,
|
|
145
|
-
switchChain: false
|
|
146
|
-
});
|
|
147
306
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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.
|
|
151
310
|
*/
|
|
152
311
|
const createShadowAdapter = (entry) => {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
getBalance: () => reject("getBalance"),
|
|
163
|
-
getSigner: () => reject("getSigner"),
|
|
164
|
-
getTransactionReceipt: () => reject("getTransactionReceipt"),
|
|
165
|
-
icon,
|
|
166
|
-
id,
|
|
167
|
-
name,
|
|
168
|
-
sendTx: () => reject("sendTx"),
|
|
169
|
-
sendTxToChain: () => reject("sendTxToChain"),
|
|
170
|
-
signMessage: () => reject("signMessage"),
|
|
171
|
-
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
|
|
172
321
|
};
|
|
173
|
-
switch (entry.chainPlatform) {
|
|
174
|
-
case "bitcoin": return {
|
|
175
|
-
...base,
|
|
176
|
-
chainPlatform: "bitcoin"
|
|
177
|
-
};
|
|
178
|
-
case "evm": return {
|
|
179
|
-
...base,
|
|
180
|
-
chainPlatform: "evm"
|
|
181
|
-
};
|
|
182
|
-
case "sui": return {
|
|
183
|
-
...base,
|
|
184
|
-
chainPlatform: "sui"
|
|
185
|
-
};
|
|
186
|
-
case "svm": return {
|
|
187
|
-
...base,
|
|
188
|
-
chainPlatform: "svm"
|
|
189
|
-
};
|
|
190
|
-
case "polkadot": return {
|
|
191
|
-
...base,
|
|
192
|
-
chainPlatform: "polkadot"
|
|
193
|
-
};
|
|
194
|
-
default:
|
|
195
|
-
entry.chainPlatform;
|
|
196
|
-
throw new Error(`[butr] unknown chainPlatform: ${String(entry.chainPlatform)}`);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
/**
|
|
200
|
-
* Structural detection: relies on every live adapter advertising at
|
|
201
|
-
* least one capability, since `getBalance`, `signMessage` and
|
|
202
|
-
* `switchChain` are required on all wallet surfaces.
|
|
203
|
-
*/
|
|
204
|
-
const isShadowAdapter = (adapter) => {
|
|
205
|
-
return Object.values(adapter.capabilities).every((flag) => !flag);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
//#endregion
|
|
209
|
-
//#region src/logger.ts
|
|
210
|
-
const logWarn = (...args) => {
|
|
211
|
-
console.warn(...args);
|
|
212
|
-
};
|
|
213
|
-
const logError = (...args) => {
|
|
214
|
-
console.error(...args);
|
|
215
322
|
};
|
|
216
323
|
|
|
217
324
|
//#endregion
|
|
218
325
|
//#region src/storage/browser-storage-driver.ts
|
|
326
|
+
const webStorageGlobals = globalThis;
|
|
219
327
|
const hasWebStorage = (kind) => {
|
|
220
328
|
try {
|
|
221
|
-
return kind === "localStorage" ?
|
|
329
|
+
return kind === "localStorage" ? webStorageGlobals.localStorage !== void 0 : webStorageGlobals.sessionStorage !== void 0;
|
|
222
330
|
} catch {
|
|
223
331
|
return false;
|
|
224
332
|
}
|
|
@@ -293,14 +401,9 @@ const storedPoolEntrySchema = z.looseObject({
|
|
|
293
401
|
name: z.string().min(1)
|
|
294
402
|
});
|
|
295
403
|
const recordSchema = z.record(z.string(), z.unknown());
|
|
296
|
-
const parseStoredPoolEntry = (key, value) => {
|
|
297
|
-
const parsed = storedPoolEntrySchema.safeParse(value);
|
|
298
|
-
if (!parsed.success || parsed.data.connectorId !== key) return null;
|
|
299
|
-
return parsed.data;
|
|
300
|
-
};
|
|
301
404
|
const DEFAULT_KEY_PREFIX = "butr";
|
|
302
405
|
/**
|
|
303
|
-
* `
|
|
406
|
+
* `createWalletStorage` writes these and `readWalletSnapshot` reads them from
|
|
304
407
|
* a cookie jar; any divergence desyncs the SSR-seeded render from the
|
|
305
408
|
* client that rehydrates it, the failure ADR 0003 exists to prevent.
|
|
306
409
|
*/
|
|
@@ -313,478 +416,308 @@ const storageKeys = (keyPrefix) => {
|
|
|
313
416
|
userDisconnected: `${prefix}-user-disconnected`
|
|
314
417
|
};
|
|
315
418
|
};
|
|
316
|
-
|
|
317
|
-
* Never repairs or throws: one corrupt entry must not take down a whole
|
|
318
|
-
* session, and `readWalletSnapshot` may run on a server with no cookie
|
|
319
|
-
* jar to write the eviction to.
|
|
320
|
-
*/
|
|
321
|
-
const decodePool = (raw, label = "[butr]") => {
|
|
419
|
+
const parseRecord = (raw, label, what) => {
|
|
322
420
|
if (raw === null || raw === void 0 || raw === "") return {};
|
|
323
|
-
let value;
|
|
324
421
|
try {
|
|
325
|
-
|
|
422
|
+
const parsed = recordSchema.safeParse(JSON.parse(raw));
|
|
423
|
+
return parsed.success ? parsed.data : {};
|
|
326
424
|
} catch (error) {
|
|
327
|
-
logWarn(`${label} failed to parse
|
|
425
|
+
logWarn(`${label} failed to parse ${what} from storage:`, error);
|
|
328
426
|
return {};
|
|
329
427
|
}
|
|
330
|
-
|
|
331
|
-
|
|
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]") => {
|
|
332
435
|
const result = {};
|
|
333
|
-
for (const [key,
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
else result[key] = entry;
|
|
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}`);
|
|
338
440
|
}
|
|
339
441
|
return result;
|
|
340
442
|
};
|
|
341
443
|
/** Decode a persisted selection payload. Same contract as `decodePool`. */
|
|
342
444
|
const decodeSelection = (raw, label = "[butr]") => {
|
|
343
|
-
if (raw === null || raw === void 0 || raw === "") return {};
|
|
344
|
-
let value;
|
|
345
|
-
try {
|
|
346
|
-
value = JSON.parse(raw);
|
|
347
|
-
} catch (error) {
|
|
348
|
-
logWarn(`${label} failed to parse selection from storage:`, error);
|
|
349
|
-
return {};
|
|
350
|
-
}
|
|
351
|
-
const parsed = recordSchema.safeParse(value);
|
|
352
|
-
if (!parsed.success) return {};
|
|
353
445
|
const result = {};
|
|
354
|
-
for (const [key,
|
|
446
|
+
for (const [key, value] of Object.entries(parseRecord(raw, label, "selection"))) {
|
|
355
447
|
const platform = chainPlatformSchema.safeParse(key);
|
|
356
|
-
if (platform.success && typeof
|
|
448
|
+
if (platform.success && typeof value === "string" && value.length > 0) result[platform.data] = value;
|
|
357
449
|
}
|
|
358
450
|
return result;
|
|
359
451
|
};
|
|
360
452
|
|
|
361
453
|
//#endregion
|
|
362
454
|
//#region src/storage/wallet-storage.ts
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
*/
|
|
375
|
-
|
|
376
|
-
constructor(config) {
|
|
377
|
-
const keys = storageKeys(config.keyPrefix);
|
|
378
|
-
this.poolKey = keys.pool;
|
|
379
|
-
this.selectionKey = keys.selection;
|
|
380
|
-
this.activeKey = keys.active;
|
|
381
|
-
this.userDisconnectedKey = keys.userDisconnected;
|
|
382
|
-
if (config.persistent && config.session) {
|
|
383
|
-
this.persistent = config.persistent;
|
|
384
|
-
this.session = config.session;
|
|
385
|
-
} else {
|
|
386
|
-
const defaults = createBrowserStorageDriver();
|
|
387
|
-
this.persistent = config.persistent ?? defaults.persistent;
|
|
388
|
-
this.session = config.session ?? defaults.session;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
/** Chain `fn` after the in-flight pool mutation. */
|
|
392
|
-
async serializePoolMutation(fn) {
|
|
393
|
-
const previous = this.poolMutationQueue;
|
|
394
|
-
let resolve = () => {};
|
|
395
|
-
this.poolMutationQueue = new Promise((r) => {
|
|
396
|
-
resolve = r;
|
|
397
|
-
});
|
|
398
|
-
try {
|
|
399
|
-
await previous;
|
|
400
|
-
} catch {}
|
|
401
|
-
try {
|
|
402
|
-
return await fn();
|
|
403
|
-
} finally {
|
|
404
|
-
resolve();
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
/** Read and decode the pool without touching the mutation queue. Safe to
|
|
408
|
-
* call from inside a queued mutation, unlike `getPool`. */
|
|
409
|
-
async readPool() {
|
|
410
|
-
let stored;
|
|
411
|
-
try {
|
|
412
|
-
stored = await this.persistent.getItem(this.poolKey);
|
|
413
|
-
} catch (error) {
|
|
414
|
-
logWarn("[butr] failed to read pool from storage:", error);
|
|
415
|
-
return {
|
|
416
|
-
corrupt: false,
|
|
417
|
-
decoded: {}
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
if (stored === null || stored === "") return {
|
|
421
|
-
corrupt: false,
|
|
422
|
-
decoded: {}
|
|
423
|
-
};
|
|
424
|
-
const decoded = decodePool(stored);
|
|
425
|
-
return {
|
|
426
|
-
corrupt: Object.keys(decoded).length === 0,
|
|
427
|
-
decoded
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
async getPool() {
|
|
431
|
-
const { corrupt, decoded } = await this.readPool();
|
|
432
|
-
if (corrupt) try {
|
|
433
|
-
await this.persistent.removeItem(this.poolKey);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
logWarn("[butr] failed to clear corrupt pool:", error);
|
|
436
|
-
}
|
|
437
|
-
return decoded;
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Additive on purpose: a failed silent reconnect drops the entry from
|
|
441
|
-
* the live pool, and it must survive to be retried next load.
|
|
442
|
-
* Eviction goes through `removePoolEntry` or `clearAll`.
|
|
443
|
-
*/
|
|
444
|
-
async setPool(pool) {
|
|
445
|
-
await this.serializePoolMutation(async () => {
|
|
446
|
-
try {
|
|
447
|
-
const { decoded: existing } = await this.readPool();
|
|
448
|
-
const serializable = { ...existing };
|
|
449
|
-
for (const [connectorId, wallet] of pool) {
|
|
450
|
-
const entry = {
|
|
451
|
-
account: wallet.account,
|
|
452
|
-
accounts: wallet.accounts,
|
|
453
|
-
chainPlatform: wallet.connector.chainPlatform,
|
|
454
|
-
connectorId,
|
|
455
|
-
icon: wallet.connector.icon,
|
|
456
|
-
name: wallet.connector.name
|
|
457
|
-
};
|
|
458
|
-
if (parseStoredPoolEntry(connectorId, entry) === null) throw new Error(`[butr] refusing to persist invalid pool entry for ${connectorId}`);
|
|
459
|
-
serializable[connectorId] = entry;
|
|
460
|
-
}
|
|
461
|
-
await this.persistent.setItem(this.poolKey, JSON.stringify(serializable));
|
|
462
|
-
} catch (error) {
|
|
463
|
-
logWarn("[butr] failed to persist pool:", error);
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
async removePoolEntry(connectorId) {
|
|
468
|
-
await this.serializePoolMutation(async () => {
|
|
469
|
-
try {
|
|
470
|
-
const { decoded: stored } = await this.readPool();
|
|
471
|
-
if (stored[connectorId]) {
|
|
472
|
-
const { [connectorId]: _, ...remaining } = stored;
|
|
473
|
-
await this.persistent.setItem(this.poolKey, JSON.stringify(remaining));
|
|
474
|
-
}
|
|
475
|
-
} catch (error) {
|
|
476
|
-
logWarn(`[butr] failed to remove pool entry ${connectorId}:`, error);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
async clearPool() {
|
|
481
|
-
await this.serializePoolMutation(async () => {
|
|
482
|
-
await this.persistent.removeItem(this.poolKey);
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
async getSelection() {
|
|
486
|
-
try {
|
|
487
|
-
return decodeSelection(await this.persistent.getItem(this.selectionKey));
|
|
488
|
-
} catch (error) {
|
|
489
|
-
logWarn("[butr] failed to read selection from storage:", error);
|
|
490
|
-
return {};
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
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) => {
|
|
494
468
|
try {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
469
|
+
const value = await driver.getItem(key);
|
|
470
|
+
written.set(key, value);
|
|
471
|
+
return value;
|
|
498
472
|
} catch (error) {
|
|
499
|
-
logWarn(
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
async getActiveConnectorId() {
|
|
503
|
-
try {
|
|
504
|
-
const value = await this.persistent.getItem(this.activeKey);
|
|
505
|
-
return value !== null && value.length > 0 ? value : null;
|
|
506
|
-
} catch {
|
|
473
|
+
logWarn(`[butr] failed to read ${key} from storage:`, error);
|
|
507
474
|
return null;
|
|
508
475
|
}
|
|
509
|
-
}
|
|
510
|
-
async
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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;
|
|
534
505
|
}
|
|
535
|
-
}
|
|
536
|
-
async markUserDisconnected(value) {
|
|
537
|
-
try {
|
|
538
|
-
await (value ? this.session.setItem(this.userDisconnectedKey, "true") : this.session.removeItem(this.userDisconnectedKey));
|
|
539
|
-
} catch {}
|
|
540
|
-
}
|
|
506
|
+
};
|
|
541
507
|
};
|
|
542
508
|
|
|
543
509
|
//#endregion
|
|
544
510
|
//#region src/store/connector-lifecycle.ts
|
|
545
511
|
const createConnectorLifecycle = (handlers) => {
|
|
546
|
-
const
|
|
512
|
+
const live = /* @__PURE__ */ new Map();
|
|
547
513
|
const detach = (connectorId) => {
|
|
548
|
-
const
|
|
549
|
-
if (
|
|
514
|
+
const subscription = live.get(connectorId);
|
|
515
|
+
if (subscription === void 0) return;
|
|
516
|
+
live.delete(connectorId);
|
|
550
517
|
try {
|
|
551
|
-
|
|
518
|
+
subscription.unsubscribe();
|
|
552
519
|
} catch (error) {
|
|
553
520
|
logWarn("[butr] unsubscribe threw:", error);
|
|
554
521
|
}
|
|
555
|
-
unsubscribers.delete(connectorId);
|
|
556
522
|
};
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
523
|
+
const attach = (connectorId, connector) => {
|
|
524
|
+
if (connector.subscribe === void 0) return;
|
|
525
|
+
const disconnected = () => {
|
|
560
526
|
detach(connectorId);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
detachAll: () => {
|
|
579
|
-
const ids = [];
|
|
580
|
-
for (const id of unsubscribers.keys()) ids.push(id);
|
|
581
|
-
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);
|
|
582
544
|
}
|
|
583
545
|
};
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
//#endregion
|
|
587
|
-
//#region src/store/hydration.ts
|
|
588
|
-
const VALID_CHAIN_PLATFORMS$1 = new Set(CHAIN_PLATFORMS);
|
|
589
|
-
const isChainPlatform$1 = (value) => VALID_CHAIN_PLATFORMS$1.has(value);
|
|
590
|
-
/** Pick the right `accounts` list: fresh from the connector if it
|
|
591
|
-
* exposes `getAccounts`, otherwise the persisted list, otherwise just
|
|
592
|
-
* the active account. */
|
|
593
|
-
const resolveHydratedAccounts = async (connector, storedAccounts, fallbackAccount) => {
|
|
594
|
-
if (connector.getAccounts) {
|
|
595
|
-
const fresh = await connector.getAccounts().catch(() => null);
|
|
596
|
-
if (fresh && fresh.length > 0) return fresh;
|
|
597
|
-
}
|
|
598
|
-
if (storedAccounts.length > 0) return [...storedAccounts];
|
|
599
|
-
return [fallbackAccount];
|
|
600
|
-
};
|
|
601
|
-
const restoreOneEntry = async (connectorId, entry, connector) => {
|
|
602
|
-
try {
|
|
603
|
-
await connector.connect({ silent: true });
|
|
604
|
-
const accountToUse = await connector.getAccount() ?? entry.account;
|
|
605
|
-
return {
|
|
606
|
-
connectorId,
|
|
607
|
-
entry: {
|
|
608
|
-
account: accountToUse,
|
|
609
|
-
accounts: await resolveHydratedAccounts(connector, entry.accounts, accountToUse),
|
|
610
|
-
connector
|
|
611
|
-
},
|
|
612
|
-
kind: "ok"
|
|
613
|
-
};
|
|
614
|
-
} catch (error) {
|
|
615
|
-
return {
|
|
616
|
-
connectorId,
|
|
617
|
-
error: toErrorCause(error instanceof Error ? error : String(error)),
|
|
618
|
-
kind: "fail"
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
const createHydrationCoordinator = (storage, createConnector) => {
|
|
623
|
-
const pending = /* @__PURE__ */ new Map();
|
|
624
546
|
return {
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
if (!entry) return null;
|
|
628
|
-
const connector = createConnector(connectorId);
|
|
629
|
-
if (!connector) return null;
|
|
630
|
-
const outcome = await restoreOneEntry(connectorId, entry, connector);
|
|
631
|
-
pending.delete(connectorId);
|
|
632
|
-
return outcome;
|
|
633
|
-
},
|
|
634
|
-
hydrate: async () => {
|
|
635
|
-
const [storedPool, storedSelection, storedActive, userDisconnected] = await Promise.all([
|
|
636
|
-
storage.getPool(),
|
|
637
|
-
storage.getSelection(),
|
|
638
|
-
storage.getActiveConnectorId(),
|
|
639
|
-
storage.isUserDisconnected()
|
|
640
|
-
]);
|
|
641
|
-
const pool = /* @__PURE__ */ new Map();
|
|
642
|
-
const dropped = [];
|
|
643
|
-
pending.clear();
|
|
644
|
-
const tasks = [];
|
|
645
|
-
for (const [connectorId, entry] of Object.entries(storedPool)) {
|
|
646
|
-
if (!entry) continue;
|
|
647
|
-
const connector = createConnector(connectorId);
|
|
648
|
-
if (!connector) {
|
|
649
|
-
pending.set(connectorId, entry);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
tasks.push(restoreOneEntry(connectorId, entry, connector));
|
|
653
|
-
}
|
|
654
|
-
const outcomes = await Promise.all(tasks);
|
|
655
|
-
for (const outcome of outcomes) {
|
|
656
|
-
if (outcome.kind === "ok") {
|
|
657
|
-
pool.set(outcome.connectorId, outcome.entry);
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
dropped.push({
|
|
661
|
-
connectorId: outcome.connectorId,
|
|
662
|
-
reason: outcome.error
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
const selection = /* @__PURE__ */ new Map();
|
|
666
|
-
for (const [platform, connectorId] of Object.entries(storedSelection)) if (isChainPlatform$1(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
667
|
-
for (const [connectorId, wallet] of pool) {
|
|
668
|
-
const platform = wallet.connector.chainPlatform;
|
|
669
|
-
if (!selection.has(platform)) selection.set(platform, connectorId);
|
|
670
|
-
}
|
|
671
|
-
let activeConnectorId = null;
|
|
672
|
-
if (storedActive !== null && pool.has(storedActive)) activeConnectorId = storedActive;
|
|
673
|
-
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
674
|
-
return {
|
|
675
|
-
activeConnectorId,
|
|
676
|
-
dropped,
|
|
677
|
-
isUserDisconnected: userDisconnected,
|
|
678
|
-
pendingIds: [...pending.keys()],
|
|
679
|
-
pool,
|
|
680
|
-
selection
|
|
681
|
-
};
|
|
547
|
+
detachAll: () => {
|
|
548
|
+
for (const connectorId of live.keys()) detach(connectorId);
|
|
682
549
|
},
|
|
683
|
-
|
|
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
|
+
}
|
|
684
554
|
};
|
|
685
555
|
};
|
|
686
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
|
+
|
|
687
568
|
//#endregion
|
|
688
569
|
//#region src/store/reducer.ts
|
|
689
570
|
const initialState = {
|
|
690
571
|
activeConnectorId: null,
|
|
572
|
+
adapters: [],
|
|
691
573
|
connectingConnectorId: null,
|
|
692
574
|
connectionError: null,
|
|
693
575
|
connectionStatus: "idle",
|
|
576
|
+
dormant: /* @__PURE__ */ new Map(),
|
|
694
577
|
isHydrated: false,
|
|
695
578
|
isUserDisconnected: false,
|
|
696
579
|
pool: /* @__PURE__ */ new Map(),
|
|
697
580
|
reconnectingIds: /* @__PURE__ */ new Set(),
|
|
698
581
|
selection: /* @__PURE__ */ new Map()
|
|
699
582
|
};
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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;
|
|
703
597
|
};
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
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) => {
|
|
707
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
|
+
};
|
|
708
691
|
case "HYDRATED": {
|
|
709
|
-
const
|
|
710
|
-
for (const [id, entry] of event.pool) pool.set(id, entry);
|
|
711
|
-
for (const id of event.dropped) pool.delete(id);
|
|
712
|
-
const selection = new Map(state.selection);
|
|
713
|
-
for (const [platform, id] of event.selection) selection.set(platform, id);
|
|
714
|
-
let activeConnectorId = null;
|
|
715
|
-
if (event.activeConnectorId !== null && pool.has(event.activeConnectorId)) activeConnectorId = event.activeConnectorId;
|
|
716
|
-
else if (state.activeConnectorId !== null && pool.has(state.activeConnectorId)) activeConnectorId = state.activeConnectorId;
|
|
717
|
-
else if (pool.size > 0) activeConnectorId = pool.keys().next().value ?? null;
|
|
718
|
-
let nextReconnecting = state.reconnectingIds;
|
|
719
|
-
if (state.reconnectingIds.size > 0) {
|
|
720
|
-
const next = new Set(state.reconnectingIds);
|
|
721
|
-
for (const id of event.pool.keys()) next.delete(id);
|
|
722
|
-
for (const id of event.dropped) next.delete(id);
|
|
723
|
-
if (next.size !== state.reconnectingIds.size) nextReconnecting = next;
|
|
724
|
-
}
|
|
725
|
-
const selectionAfterDrop = new Map(selection);
|
|
726
|
-
for (const [platform, id] of selectionAfterDrop) if (!pool.has(id)) {
|
|
727
|
-
selectionAfterDrop.delete(platform);
|
|
728
|
-
const fallback = findConnectorForPlatform(pool, platform);
|
|
729
|
-
if (fallback !== void 0) selectionAfterDrop.set(platform, fallback);
|
|
730
|
-
}
|
|
692
|
+
const preferred = event.activeConnectorId;
|
|
731
693
|
return {
|
|
732
694
|
...state,
|
|
733
|
-
activeConnectorId,
|
|
695
|
+
activeConnectorId: preferred !== null && state.pool.has(preferred) ? preferred : state.activeConnectorId,
|
|
734
696
|
isHydrated: true,
|
|
735
|
-
|
|
736
|
-
pool,
|
|
737
|
-
reconnectingIds: nextReconnecting,
|
|
738
|
-
selection: selectionAfterDrop
|
|
697
|
+
selection: applyStoredSelection(state.selection, event.selection, state.pool)
|
|
739
698
|
};
|
|
740
699
|
}
|
|
741
|
-
case "USER_DISCONNECTED_SET": return {
|
|
742
|
-
...state,
|
|
743
|
-
isUserDisconnected: event.value
|
|
744
|
-
};
|
|
745
700
|
case "CONNECT_STARTED": return {
|
|
746
701
|
...state,
|
|
747
702
|
connectingConnectorId: event.connectorId,
|
|
748
703
|
connectionError: null,
|
|
749
|
-
connectionStatus: "connecting"
|
|
704
|
+
connectionStatus: "connecting",
|
|
705
|
+
isUserDisconnected: false
|
|
750
706
|
};
|
|
751
707
|
case "CONNECT_SUCCEEDED": {
|
|
752
708
|
const { connectorId, entry } = event;
|
|
753
|
-
const
|
|
754
|
-
const nextReconnecting = state.reconnectingIds.has(connectorId) ? new Set([...state.reconnectingIds].filter((id) => id !== connectorId)) : state.reconnectingIds;
|
|
755
|
-
if (existing && existing.account.walletAddress.toLowerCase() === entry.account.walletAddress.toLowerCase()) return {
|
|
756
|
-
...state,
|
|
757
|
-
activeConnectorId: connectorId,
|
|
758
|
-
connectingConnectorId: null,
|
|
759
|
-
connectionError: null,
|
|
760
|
-
connectionStatus: "success",
|
|
761
|
-
pool: existing.connector === entry.connector ? state.pool : new Map([...state.pool, [connectorId, entry]]),
|
|
762
|
-
reconnectingIds: nextReconnecting
|
|
763
|
-
};
|
|
764
|
-
const newPool = new Map([...state.pool, [connectorId, entry]]);
|
|
765
|
-
const newSelection = new Map([...state.selection, [entry.connector.chainPlatform, connectorId]]);
|
|
709
|
+
const isCurrentAttempt = state.connectingConnectorId === connectorId;
|
|
766
710
|
return {
|
|
767
711
|
...state,
|
|
768
712
|
activeConnectorId: connectorId,
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
case "ENTRY_RESTORED": {
|
|
778
|
-
const { connectorId, entry } = event;
|
|
779
|
-
const nextReconnecting = state.reconnectingIds.has(connectorId) ? new Set([...state.reconnectingIds].filter((id) => id !== connectorId)) : state.reconnectingIds;
|
|
780
|
-
const platform = entry.connector.chainPlatform;
|
|
781
|
-
const selection = state.selection.has(platform) ? state.selection : new Map([...state.selection, [platform, connectorId]]);
|
|
782
|
-
return {
|
|
783
|
-
...state,
|
|
784
|
-
activeConnectorId: state.activeConnectorId ?? connectorId,
|
|
785
|
-
pool: new Map([...state.pool, [connectorId, entry]]),
|
|
786
|
-
reconnectingIds: nextReconnecting,
|
|
787
|
-
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)
|
|
788
721
|
};
|
|
789
722
|
}
|
|
790
723
|
case "CONNECT_FAILED":
|
|
@@ -795,398 +728,424 @@ const reducer = (state, event) => {
|
|
|
795
728
|
connectionError: event.error,
|
|
796
729
|
connectionStatus: "error"
|
|
797
730
|
};
|
|
798
|
-
case "
|
|
799
|
-
|
|
800
|
-
if (!wallet) return state;
|
|
801
|
-
const newPool = new Map(state.pool);
|
|
802
|
-
newPool.delete(event.connectorId);
|
|
803
|
-
const newSelection = new Map(state.selection);
|
|
804
|
-
const platform = wallet.connector.chainPlatform;
|
|
805
|
-
if (newSelection.get(platform) === event.connectorId) {
|
|
806
|
-
newSelection.delete(platform);
|
|
807
|
-
const fallback = findConnectorForPlatform(newPool, platform);
|
|
808
|
-
if (fallback !== void 0) newSelection.set(platform, fallback);
|
|
809
|
-
}
|
|
810
|
-
const firstId = newPool.keys().next();
|
|
811
|
-
const activeConnectorId = state.activeConnectorId === event.connectorId ? firstId.value ?? null : state.activeConnectorId;
|
|
812
|
-
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;
|
|
813
733
|
return {
|
|
814
734
|
...state,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
selection: newSelection
|
|
735
|
+
connectingConnectorId: null,
|
|
736
|
+
connectionError: null,
|
|
737
|
+
connectionStatus: "idle"
|
|
819
738
|
};
|
|
820
|
-
|
|
821
|
-
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
|
+
}
|
|
822
752
|
const wallet = state.pool.get(event.connectorId);
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
if (event.active) nextAccount = event.active;
|
|
826
|
-
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;
|
|
827
|
-
const sameAccount = nextAccount.walletAddress === wallet.account.walletAddress && nextAccount.chain.id === wallet.account.chain.id;
|
|
828
|
-
const hasSameAccounts = event.accounts.length === wallet.accounts.length && event.accounts.every((a, i) => a.walletAddress === wallet.accounts[i]?.walletAddress);
|
|
829
|
-
if (sameAccount && hasSameAccounts) return state;
|
|
830
|
-
const updated = {
|
|
831
|
-
...wallet,
|
|
832
|
-
account: nextAccount,
|
|
833
|
-
accounts: [...event.accounts]
|
|
834
|
-
};
|
|
835
|
-
const newPool = new Map([...state.pool, [event.connectorId, updated]]);
|
|
753
|
+
if (wallet === void 0) return state;
|
|
754
|
+
const stored = toStoredEntry(event.connectorId, wallet);
|
|
836
755
|
return {
|
|
837
756
|
...state,
|
|
838
|
-
|
|
757
|
+
dormant: withEntry(state.dormant, event.connectorId, stored),
|
|
758
|
+
pool
|
|
839
759
|
};
|
|
840
760
|
}
|
|
841
|
-
case "
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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;
|
|
845
769
|
return {
|
|
846
770
|
...state,
|
|
847
|
-
|
|
771
|
+
activeConnectorId: event.connectorId
|
|
848
772
|
};
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
...state,
|
|
852
|
-
activeConnectorId: event.connectorId
|
|
853
|
-
};
|
|
854
|
-
case "SELECTION_CHANGED": {
|
|
855
|
-
const newSelection = new Map(state.selection);
|
|
856
|
-
if (event.connectorId === null) newSelection.delete(event.chainPlatform);
|
|
857
|
-
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;
|
|
858
775
|
return {
|
|
859
776
|
...state,
|
|
860
|
-
selection:
|
|
777
|
+
selection: withEntry(state.selection, event.chainPlatform, event.connectorId)
|
|
861
778
|
};
|
|
862
|
-
}
|
|
863
|
-
case "STATUS_RESET": return {
|
|
864
|
-
...state,
|
|
865
|
-
connectingConnectorId: null,
|
|
866
|
-
connectionError: null,
|
|
867
|
-
connectionStatus: "idle"
|
|
868
|
-
};
|
|
869
|
-
case "ERROR_SET": return {
|
|
870
|
-
...state,
|
|
871
|
-
connectionError: event.error,
|
|
872
|
-
connectionStatus: event.error ? "error" : "idle"
|
|
873
|
-
};
|
|
874
|
-
case "RESET": return {
|
|
875
|
-
...initialState,
|
|
876
|
-
isHydrated: state.isHydrated,
|
|
877
|
-
isUserDisconnected: true,
|
|
878
|
-
reconnectingIds: /* @__PURE__ */ new Set()
|
|
879
|
-
};
|
|
880
779
|
default: return state;
|
|
881
780
|
}
|
|
882
781
|
};
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const run = async (fn, onError) => {
|
|
888
|
-
try {
|
|
889
|
-
await fn();
|
|
890
|
-
} catch (error) {
|
|
891
|
-
onError(toErrorCause(error instanceof Error ? error : String(error)));
|
|
892
|
-
}
|
|
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);
|
|
893
786
|
};
|
|
894
|
-
|
|
895
|
-
//#endregion
|
|
896
|
-
//#region src/store/wallet-store.ts
|
|
897
|
-
const VALID_CHAIN_PLATFORMS = new Set(CHAIN_PLATFORMS);
|
|
898
|
-
const isChainPlatform = (value) => VALID_CHAIN_PLATFORMS.has(value);
|
|
899
787
|
/**
|
|
900
788
|
* `isHydrated` is true from the first render on this path, so
|
|
901
|
-
* `reconnectingIds`
|
|
902
|
-
*
|
|
789
|
+
* `reconnectingIds` says whether a connection is verified. Seeded entries
|
|
790
|
+
* are also dormant: persisted, not yet live.
|
|
903
791
|
*/
|
|
904
|
-
const
|
|
792
|
+
const stateFromSnapshot = (snapshot) => {
|
|
905
793
|
const pool = /* @__PURE__ */ new Map();
|
|
906
|
-
const
|
|
907
|
-
for (const [connectorId, entry] of Object.entries(snapshot.pool)) {
|
|
908
|
-
if (!entry) continue;
|
|
909
|
-
const connector = createShadowAdapter(entry);
|
|
794
|
+
const dormant = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const [connectorId, entry] of Object.entries(snapshot.pool)) if (entry !== void 0) {
|
|
910
796
|
pool.set(connectorId, {
|
|
911
797
|
account: entry.account,
|
|
912
|
-
accounts:
|
|
913
|
-
connector
|
|
798
|
+
accounts: entry.accounts,
|
|
799
|
+
connector: createShadowAdapter(entry)
|
|
914
800
|
});
|
|
915
|
-
|
|
801
|
+
dormant.set(connectorId, entry);
|
|
916
802
|
}
|
|
917
|
-
|
|
918
|
-
for (const [platform, connectorId] of Object.entries(snapshot.selection)) if (isChainPlatform(platform) && connectorId !== void 0 && pool.has(connectorId)) selection.set(platform, connectorId);
|
|
919
|
-
const activeConnectorId = snapshot.activeConnectorId !== null && pool.has(snapshot.activeConnectorId) ? snapshot.activeConnectorId : pool.keys().next().value ?? null;
|
|
920
|
-
return {
|
|
803
|
+
return reconcile({
|
|
921
804
|
...initialState,
|
|
922
|
-
activeConnectorId,
|
|
805
|
+
activeConnectorId: snapshot.activeConnectorId,
|
|
806
|
+
dormant,
|
|
923
807
|
isHydrated: true,
|
|
924
808
|
pool,
|
|
925
|
-
reconnectingIds,
|
|
926
|
-
selection
|
|
927
|
-
};
|
|
809
|
+
reconnectingIds: new Set(pool.keys()),
|
|
810
|
+
selection: applyStoredSelection(/* @__PURE__ */ new Map(), snapshot.selection, pool)
|
|
811
|
+
});
|
|
928
812
|
};
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/store/wallet-manager.ts
|
|
929
816
|
const CONNECT_TIMEOUT_MS = 9e4;
|
|
817
|
+
const RESTORE_TIMEOUT_MS = 15e3;
|
|
930
818
|
const DEFAULT_SLOW_CONNECT_THRESHOLD_MS = 5e3;
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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;
|
|
936
891
|
try {
|
|
937
|
-
|
|
938
|
-
} catch (
|
|
939
|
-
|
|
892
|
+
await storage.save(toPersistedState(getState()));
|
|
893
|
+
} catch (error) {
|
|
894
|
+
reportStorageError(error);
|
|
940
895
|
}
|
|
941
|
-
return;
|
|
942
896
|
}
|
|
943
|
-
|
|
897
|
+
flushing = false;
|
|
944
898
|
};
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const persistPool = () => run(() => storage.setPool(get().pool), reportStorageError("failed to persist pool"));
|
|
951
|
-
const persistSelection = () => run(() => storage.setSelection(get().selection), reportStorageError("failed to persist selection"));
|
|
952
|
-
const persistActive = () => run(() => storage.setActiveConnectorId(get().activeConnectorId), reportStorageError("failed to persist active connector"));
|
|
953
|
-
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) => {
|
|
954
904
|
dispatch({
|
|
955
905
|
accounts,
|
|
956
|
-
active,
|
|
906
|
+
active: accounts[0],
|
|
957
907
|
connectorId,
|
|
958
|
-
type: "
|
|
908
|
+
type: "ACCOUNTS_CHANGED"
|
|
959
909
|
});
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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;
|
|
974
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"
|
|
975
988
|
});
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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({
|
|
985
1053
|
connectorId,
|
|
986
|
-
|
|
1054
|
+
entry: wallet,
|
|
1055
|
+
type: "CONNECT_SUCCEEDED"
|
|
987
1056
|
});
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
} catch (cbError) {
|
|
993
|
-
logWarn("[butr] onSlowConnect threw:", cbError);
|
|
994
|
-
}
|
|
995
|
-
}, slowThreshold) : null;
|
|
996
|
-
let connectTimeoutId;
|
|
997
|
-
try {
|
|
998
|
-
const connectPromise = connector.connect();
|
|
999
|
-
connectPromise.catch(() => {});
|
|
1000
|
-
await Promise.race([connectPromise, new Promise((_, reject) => {
|
|
1001
|
-
connectTimeoutId = setTimeout(() => {
|
|
1002
|
-
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
1003
|
-
}, CONNECT_TIMEOUT_MS);
|
|
1004
|
-
})]);
|
|
1005
|
-
const account = await connector.getAccount();
|
|
1006
|
-
if (!account) throw new Error("Failed to get account");
|
|
1007
|
-
const fetchedAccounts = connector.getAccounts ? await connector.getAccounts().catch(() => null) : null;
|
|
1008
|
-
const entry = {
|
|
1009
|
-
account,
|
|
1010
|
-
accounts: fetchedAccounts && fetchedAccounts.length > 0 ? fetchedAccounts : [account],
|
|
1011
|
-
connector
|
|
1012
|
-
};
|
|
1013
|
-
dispatch({
|
|
1014
|
-
connectorId,
|
|
1015
|
-
entry,
|
|
1016
|
-
type: "CONNECT_SUCCEEDED"
|
|
1017
|
-
});
|
|
1018
|
-
lifecycle.attach(connectorId, connector);
|
|
1019
|
-
await Promise.all([
|
|
1020
|
-
persistPool(),
|
|
1021
|
-
persistSelection(),
|
|
1022
|
-
persistActive()
|
|
1023
|
-
]);
|
|
1024
|
-
config.onConnect?.(entry);
|
|
1025
|
-
onSuccess?.(entry);
|
|
1026
|
-
} catch (error) {
|
|
1027
|
-
const normalised = mapConnectionError(error instanceof Error ? error : String(error));
|
|
1028
|
-
dispatch({
|
|
1029
|
-
connectorId,
|
|
1030
|
-
error: normalised,
|
|
1031
|
-
type: "CONNECT_FAILED"
|
|
1032
|
-
});
|
|
1033
|
-
try {
|
|
1034
|
-
await connector.disconnect?.();
|
|
1035
|
-
} catch (disconnectError) {
|
|
1036
|
-
logWarn("[butr] disconnect during error recovery failed:", disconnectError);
|
|
1037
|
-
}
|
|
1038
|
-
try {
|
|
1039
|
-
config.onConnectError?.(normalised, connectorId);
|
|
1040
|
-
} catch (cbError) {
|
|
1041
|
-
logWarn("[butr] onConnectError threw:", cbError);
|
|
1042
|
-
}
|
|
1043
|
-
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
1044
|
-
throw error;
|
|
1045
|
-
} finally {
|
|
1046
|
-
clearTimeout(connectTimeoutId);
|
|
1047
|
-
if (slowTimer) clearTimeout(slowTimer);
|
|
1048
|
-
}
|
|
1049
|
-
},
|
|
1050
|
-
disconnectWallet: (connectorId) => {
|
|
1051
|
-
get().setUserDisconnected(true);
|
|
1052
|
-
const state = get();
|
|
1053
|
-
if (!state.isHydrated) return;
|
|
1054
|
-
const wallet = state.pool.get(connectorId);
|
|
1055
|
-
if (!wallet) return;
|
|
1056
|
-
lifecycle.detach(connectorId);
|
|
1057
|
-
run(() => wallet.connector.disconnect?.() ?? Promise.resolve(), logError);
|
|
1058
|
-
const platform = wallet.connector.chainPlatform;
|
|
1057
|
+
safely("onConnect", config.onConnect, wallet, { reconnected: false });
|
|
1058
|
+
return wallet;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
const failure = toConnectionError(error);
|
|
1059
1061
|
dispatch({
|
|
1060
1062
|
connectorId,
|
|
1061
|
-
|
|
1063
|
+
error: failure,
|
|
1064
|
+
type: "CONNECT_FAILED"
|
|
1062
1065
|
});
|
|
1063
|
-
if (
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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);
|
|
1095
1142
|
}
|
|
1096
|
-
|
|
1097
|
-
},
|
|
1098
|
-
refreshWallet: (connectorId) => {
|
|
1099
|
-
dispatch({
|
|
1100
|
-
connectorId,
|
|
1101
|
-
type: "WALLET_REFRESHED"
|
|
1102
|
-
});
|
|
1103
|
-
},
|
|
1104
|
-
requestAccounts: async (connectorId) => {
|
|
1105
|
-
const entry = get().pool.get(connectorId);
|
|
1106
|
-
if (!entry) return;
|
|
1107
|
-
await entry.connector.requestAccounts?.();
|
|
1108
|
-
const fresh = entry.connector.getAccounts ? await entry.connector.getAccounts().catch(() => null) : null;
|
|
1109
|
-
if (!fresh || fresh.length === 0) return;
|
|
1110
|
-
refreshPoolEntry(connectorId, fresh);
|
|
1111
|
-
},
|
|
1112
|
-
reset: () => {
|
|
1113
|
-
get().setUserDisconnected(true);
|
|
1114
|
-
const state = get();
|
|
1115
|
-
if (!state.isHydrated) return;
|
|
1143
|
+
unsubscribePool();
|
|
1116
1144
|
lifecycle.detachAll();
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
run(() => Promise.resolve(onReset()), logError);
|
|
1122
|
-
}
|
|
1123
|
-
dispatch({ type: "RESET" });
|
|
1124
|
-
},
|
|
1125
|
-
resetConnectionStatus: () => {
|
|
1126
|
-
dispatch({ type: "STATUS_RESET" });
|
|
1127
|
-
},
|
|
1128
|
-
setActiveConnector: (connectorId) => {
|
|
1129
|
-
dispatch({
|
|
1130
|
-
connectorId,
|
|
1131
|
-
type: "ACTIVE_CHANGED"
|
|
1132
|
-
});
|
|
1133
|
-
persistActive();
|
|
1134
|
-
},
|
|
1135
|
-
setConnectionError: (error) => {
|
|
1136
|
-
dispatch({
|
|
1137
|
-
error,
|
|
1138
|
-
type: "ERROR_SET"
|
|
1139
|
-
});
|
|
1140
|
-
},
|
|
1141
|
-
setSelection: (chainPlatform, connectorId) => {
|
|
1142
|
-
dispatch({
|
|
1143
|
-
chainPlatform,
|
|
1144
|
-
connectorId,
|
|
1145
|
-
type: "SELECTION_CHANGED"
|
|
1146
|
-
});
|
|
1147
|
-
persistSelection();
|
|
1148
|
-
},
|
|
1149
|
-
setUserDisconnected: (value) => {
|
|
1150
|
-
dispatch({
|
|
1151
|
-
type: "USER_DISCONNECTED_SET",
|
|
1152
|
-
value
|
|
1153
|
-
});
|
|
1154
|
-
run(() => storage.markUserDisconnected(value), reportStorageError("failed to persist disconnect intent"));
|
|
1155
|
-
},
|
|
1156
|
-
tryRestoreFromPending: async (connectorId) => {
|
|
1157
|
-
const outcome = await hydration.drainPending(connectorId);
|
|
1158
|
-
if (!outcome) return;
|
|
1159
|
-
if (outcome.kind === "fail") {
|
|
1160
|
-
logWarn(`[butr] late restore failed for ${connectorId}:`, outcome.error);
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
dispatch({
|
|
1164
|
-
connectorId,
|
|
1165
|
-
entry: outcome.entry,
|
|
1166
|
-
type: "ENTRY_RESTORED"
|
|
1167
|
-
});
|
|
1168
|
-
lifecycle.attach(connectorId, outcome.entry.connector);
|
|
1169
|
-
await Promise.all([
|
|
1170
|
-
persistPool(),
|
|
1171
|
-
persistSelection(),
|
|
1172
|
-
persistActive()
|
|
1173
|
-
]);
|
|
1174
|
-
config.onConnect?.(outcome.entry);
|
|
1175
|
-
},
|
|
1176
|
-
updateWalletAccount: (connectorId, account) => {
|
|
1177
|
-
const entry = get().pool.get(connectorId);
|
|
1178
|
-
if (!entry) return;
|
|
1179
|
-
const newChain = account.chain;
|
|
1180
|
-
const remapped = entry.accounts.map((a) => a.chain.id === newChain.id ? a : {
|
|
1181
|
-
chain: newChain,
|
|
1182
|
-
id: `${newChain.id}:${normaliseAddress(a.walletAddress)}`,
|
|
1183
|
-
walletAddress: a.walletAddress
|
|
1184
|
-
});
|
|
1185
|
-
const accounts = remapped.some((a) => normaliseAddress(a.walletAddress) === normaliseAddress(account.walletAddress)) ? remapped : [...remapped, account];
|
|
1186
|
-
refreshPoolEntry(connectorId, accounts, account);
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
});
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
subscribe: store.subscribe
|
|
1148
|
+
};
|
|
1190
1149
|
};
|
|
1191
1150
|
|
|
1192
1151
|
//#endregion
|
|
@@ -1303,7 +1262,7 @@ const readWalletSnapshot = (source, options = {}) => {
|
|
|
1303
1262
|
if (rawActive !== void 0 && rawActive.length > 0 && pool[rawActive] !== void 0) activeConnectorId = rawActive;
|
|
1304
1263
|
else {
|
|
1305
1264
|
const firstKey = Object.keys(pool)[0];
|
|
1306
|
-
if (firstKey) activeConnectorId = firstKey;
|
|
1265
|
+
if (firstKey !== void 0 && firstKey !== "") activeConnectorId = firstKey;
|
|
1307
1266
|
}
|
|
1308
1267
|
return {
|
|
1309
1268
|
activeConnectorId,
|
|
@@ -1437,7 +1396,7 @@ const base58ToBytes = (input) => {
|
|
|
1437
1396
|
var SignInUnsupportedError = class extends Error {
|
|
1438
1397
|
connectorId;
|
|
1439
1398
|
constructor(connectorId) {
|
|
1440
|
-
super(`Wallet "${connectorId}"
|
|
1399
|
+
super(`Wallet "${connectorId}" cannot sign messages, so it cannot sign in.`);
|
|
1441
1400
|
this.connectorId = connectorId;
|
|
1442
1401
|
this.name = "SignInUnsupportedError";
|
|
1443
1402
|
}
|
|
@@ -1453,12 +1412,11 @@ const createSignInFlow = (options) => {
|
|
|
1453
1412
|
const sign = async (wallet, account) => {
|
|
1454
1413
|
const { connector } = wallet;
|
|
1455
1414
|
const signingAccount = account ?? wallet.account;
|
|
1456
|
-
if (
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
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
|
+
});
|
|
1462
1420
|
const output = await connector.signIn({ nonce });
|
|
1463
1421
|
return {
|
|
1464
1422
|
account: output.account,
|
|
@@ -1470,12 +1428,17 @@ const createSignInFlow = (options) => {
|
|
|
1470
1428
|
wallet
|
|
1471
1429
|
};
|
|
1472
1430
|
}
|
|
1431
|
+
if (connector.signMessage === void 0) throw new SignInUnsupportedError(connector.id);
|
|
1432
|
+
const nonce = await options.getNonce({
|
|
1433
|
+
account: signingAccount,
|
|
1434
|
+
wallet
|
|
1435
|
+
});
|
|
1473
1436
|
const message = buildMessage({
|
|
1474
1437
|
account: signingAccount,
|
|
1475
1438
|
nonce,
|
|
1476
1439
|
wallet
|
|
1477
1440
|
});
|
|
1478
|
-
const { signature, signedMessage } = await connector.signMessage(new TextEncoder().encode(message), signingAccount);
|
|
1441
|
+
const { signature, signedMessage } = await connector.signMessage(new TextEncoder().encode(message), { account: signingAccount });
|
|
1479
1442
|
return {
|
|
1480
1443
|
account: signingAccount,
|
|
1481
1444
|
message,
|
|
@@ -1494,19 +1457,6 @@ const createSignInFlow = (options) => {
|
|
|
1494
1457
|
} };
|
|
1495
1458
|
};
|
|
1496
1459
|
|
|
1497
|
-
//#endregion
|
|
1498
|
-
//#region src/wallet-equal.ts
|
|
1499
|
-
/**
|
|
1500
|
-
* The adapter is compared by reference, not `connector.id`: hydration
|
|
1501
|
-
* swaps a shadow adapter for the live one under an unchanged id, and an
|
|
1502
|
-
* id check would strand consumers on the throwing placeholder.
|
|
1503
|
-
*/
|
|
1504
|
-
const walletEqual = (a, b) => {
|
|
1505
|
-
if (a === b) return true;
|
|
1506
|
-
if (!a || !b) return false;
|
|
1507
|
-
return a.connector === b.connector && a.account.walletAddress === b.account.walletAddress && a.account.chain.id === b.account.chain.id;
|
|
1508
|
-
};
|
|
1509
|
-
|
|
1510
1460
|
//#endregion
|
|
1511
1461
|
//#region src/sanitize-icon.ts
|
|
1512
1462
|
/**
|
|
@@ -1521,5 +1471,5 @@ const sanitizeIcon = (icon) => {
|
|
|
1521
1471
|
};
|
|
1522
1472
|
|
|
1523
1473
|
//#endregion
|
|
1524
|
-
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 };
|
|
1525
1475
|
//# sourceMappingURL=index.js.map
|