@fernolab/wallet-adapter-svelte 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/WalletButton.svelte +152 -0
- package/dist/components/WalletButton.svelte.d.ts +6 -0
- package/dist/components/WalletListButton.svelte +48 -0
- package/dist/components/WalletListButton.svelte.d.ts +8 -0
- package/dist/components/WalletModal.svelte +360 -0
- package/dist/components/WalletModal.svelte.d.ts +7 -0
- package/dist/components/WalletProvider.svelte +25 -0
- package/dist/components/WalletProvider.svelte.d.ts +8 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/wallet/mobile-detection.d.ts +3 -0
- package/dist/wallet/mobile-detection.js +27 -0
- package/dist/wallet/types.d.ts +3 -0
- package/dist/wallet/types.js +1 -0
- package/dist/wallet/wallet.svelte.d.ts +56 -0
- package/dist/wallet/wallet.svelte.js +672 -0
- package/package.json +71 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { getWallets } from '@wallet-standard/app';
|
|
2
|
+
import { PUBLIC_SOLANA_RPC_ENDPOINT } from '../config.js';
|
|
3
|
+
import { browser } from '../environment.js';
|
|
4
|
+
import { MOBILE_DEEPLINK_WALLETS, MOBILE_WALLETS, SolanaSignMessage, StandardConnect, StandardDisconnect, StandardEvents, adaptStandardWallets, adaptWallet, createWalletController, createMobileWalletBrowseUrl, dedupeStandardWallets, detectInjectedWallets, detectMobileRuntime, isSolanaWallet, resolveSolanaChainFromEndpoint } from '@fernolab/wallet-adapter-core';
|
|
5
|
+
export { adaptWallet, dedupeStandardWallets, isSolanaWallet };
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Constants
|
|
8
|
+
// ============================================================================
|
|
9
|
+
const LAST_WALLET_KEY = 'lastConnectedWallet';
|
|
10
|
+
const SolanaMobileWalletAdapterWalletName = 'Mobile Wallet Adapter';
|
|
11
|
+
const MOBILE_WALLET_ADAPTER_DISPLAY_NAME = 'Use Installed Wallet';
|
|
12
|
+
const INJECTED_WALLET_ICON = 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20viewBox%3D%220%200%2048%2048%22%20fill%3D%22none%22%3E%3Crect%20width%3D%2248%22%20height%3D%2248%22%20rx%3D%2210%22%20fill%3D%22%23111827%22%2F%3E%3Cpath%20d%3D%22M14%2018h20M14%2024h20M14%2030h20%22%20stroke%3D%22white%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E';
|
|
13
|
+
export const RUNTIME_DEBUG_MODES = [
|
|
14
|
+
'auto',
|
|
15
|
+
'desktop',
|
|
16
|
+
'ios',
|
|
17
|
+
'android',
|
|
18
|
+
'phantom',
|
|
19
|
+
'solflare',
|
|
20
|
+
'jupiter',
|
|
21
|
+
'backpack'
|
|
22
|
+
];
|
|
23
|
+
const RUNTIME_DEBUG_QUERY_KEYS = ['wallet_mobile', 'wallet_runtime'];
|
|
24
|
+
const MOBILE_WALLET_NAMES = [
|
|
25
|
+
SolanaMobileWalletAdapterWalletName,
|
|
26
|
+
'Phantom',
|
|
27
|
+
'Solflare',
|
|
28
|
+
'Jupiter Mobile',
|
|
29
|
+
'Jupiter',
|
|
30
|
+
'Jupiter Wallet',
|
|
31
|
+
'Backpack'
|
|
32
|
+
];
|
|
33
|
+
const PREFERRED_WALLET_ORDER = [...MOBILE_WALLET_NAMES];
|
|
34
|
+
const DESKTOP_RUNTIME = {
|
|
35
|
+
isMobile: false,
|
|
36
|
+
isWalletBrowser: false,
|
|
37
|
+
platform: 'desktop',
|
|
38
|
+
supportsMobileWalletAdapter: false,
|
|
39
|
+
walletBrowser: null
|
|
40
|
+
};
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Wallet Detection & Adaptation
|
|
43
|
+
// ============================================================================
|
|
44
|
+
function getRuntimeDebugWalletId(mode) {
|
|
45
|
+
switch (mode) {
|
|
46
|
+
case 'phantom':
|
|
47
|
+
case 'solflare':
|
|
48
|
+
case 'jupiter':
|
|
49
|
+
case 'backpack':
|
|
50
|
+
return mode;
|
|
51
|
+
default:
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function normalizeRuntimeDebugMode(value) {
|
|
56
|
+
const normalized = value?.trim().toLowerCase();
|
|
57
|
+
if (normalized === 'mobile') {
|
|
58
|
+
return 'android';
|
|
59
|
+
}
|
|
60
|
+
return RUNTIME_DEBUG_MODES.includes(normalized)
|
|
61
|
+
? normalized
|
|
62
|
+
: 'auto';
|
|
63
|
+
}
|
|
64
|
+
function readRuntimeDebugMode() {
|
|
65
|
+
if (!browser || typeof location === 'undefined') {
|
|
66
|
+
return 'auto';
|
|
67
|
+
}
|
|
68
|
+
const url = new URL(location.href);
|
|
69
|
+
const hashParams = url.hash.includes('=')
|
|
70
|
+
? new URLSearchParams(url.hash.replace(/^#/, ''))
|
|
71
|
+
: new URLSearchParams();
|
|
72
|
+
for (const key of RUNTIME_DEBUG_QUERY_KEYS) {
|
|
73
|
+
const mode = normalizeRuntimeDebugMode(url.searchParams.get(key) ?? hashParams.get(key));
|
|
74
|
+
if (mode !== 'auto') {
|
|
75
|
+
return mode;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return 'auto';
|
|
79
|
+
}
|
|
80
|
+
function writeRuntimeDebugMode(mode) {
|
|
81
|
+
if (!browser || typeof history === 'undefined' || typeof location === 'undefined') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const url = new URL(location.href);
|
|
85
|
+
for (const key of RUNTIME_DEBUG_QUERY_KEYS) {
|
|
86
|
+
url.searchParams.delete(key);
|
|
87
|
+
}
|
|
88
|
+
if (mode !== 'auto') {
|
|
89
|
+
url.searchParams.set(RUNTIME_DEBUG_QUERY_KEYS[0], mode);
|
|
90
|
+
}
|
|
91
|
+
history.replaceState(history.state, '', url);
|
|
92
|
+
}
|
|
93
|
+
function bytesFromString(input, length) {
|
|
94
|
+
const bytes = new Uint8Array(length);
|
|
95
|
+
let hash = 0x811c9dc5;
|
|
96
|
+
for (let index = 0; index < length; index += 1) {
|
|
97
|
+
for (let charIndex = 0; charIndex < input.length; charIndex += 1) {
|
|
98
|
+
hash ^= input.charCodeAt(charIndex) + index;
|
|
99
|
+
hash = Math.imul(hash, 0x01000193);
|
|
100
|
+
}
|
|
101
|
+
hash ^= hash >>> 13;
|
|
102
|
+
hash = Math.imul(hash, 0x85ebca6b);
|
|
103
|
+
bytes[index] = hash & 0xff;
|
|
104
|
+
}
|
|
105
|
+
return bytes;
|
|
106
|
+
}
|
|
107
|
+
function createDebugInjectedWallet(id) {
|
|
108
|
+
const wallet = MOBILE_WALLETS.find((candidate) => candidate.id === id);
|
|
109
|
+
const signature = bytesFromString(`wallet-debug:${id}:signature`, 64);
|
|
110
|
+
const publicKey = '11111111111111111111111111111111';
|
|
111
|
+
const publicKeyBytes = new Uint8Array(32);
|
|
112
|
+
const provider = {
|
|
113
|
+
connect: async () => ({ publicKey: { toBytes: () => publicKeyBytes, toString: () => publicKey } }),
|
|
114
|
+
disconnect: async () => undefined,
|
|
115
|
+
isBackpack: id === 'backpack',
|
|
116
|
+
isJupiter: id === 'jupiter',
|
|
117
|
+
isPhantom: id === 'phantom',
|
|
118
|
+
isSolflare: id === 'solflare',
|
|
119
|
+
signMessage: async () => signature
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
name: wallet?.name ?? id,
|
|
124
|
+
provider
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function normalizeInjectedSignature(result) {
|
|
128
|
+
if (result instanceof Uint8Array) {
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
if (!result.signature) {
|
|
132
|
+
throw new Error('Injected wallet did not return a signature');
|
|
133
|
+
}
|
|
134
|
+
return result.signature instanceof Uint8Array ? result.signature : new Uint8Array(result.signature);
|
|
135
|
+
}
|
|
136
|
+
function getInjectedPublicKeyBytes(publicKey) {
|
|
137
|
+
const bytes = publicKey?.toBytes?.() ?? publicKey?.toBuffer?.();
|
|
138
|
+
return bytes ? new Uint8Array(bytes) : new Uint8Array(32);
|
|
139
|
+
}
|
|
140
|
+
function createInjectedAccount(address, chain, provider, publicKey) {
|
|
141
|
+
return {
|
|
142
|
+
address,
|
|
143
|
+
publicKey: getInjectedPublicKeyBytes(publicKey),
|
|
144
|
+
chains: [chain],
|
|
145
|
+
features: provider.signMessage ? [SolanaSignMessage] : []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function createInjectedStandardWallet(detectedWallet, chain) {
|
|
149
|
+
const { name, provider } = detectedWallet;
|
|
150
|
+
let accounts = [];
|
|
151
|
+
const listeners = new Set();
|
|
152
|
+
function emitChange() {
|
|
153
|
+
const payload = { accounts };
|
|
154
|
+
listeners.forEach((listener) => listener(payload));
|
|
155
|
+
}
|
|
156
|
+
const wallet = {
|
|
157
|
+
version: '1.0.0',
|
|
158
|
+
name,
|
|
159
|
+
icon: INJECTED_WALLET_ICON,
|
|
160
|
+
chains: [chain],
|
|
161
|
+
get accounts() {
|
|
162
|
+
return accounts;
|
|
163
|
+
},
|
|
164
|
+
features: {
|
|
165
|
+
[StandardConnect]: {
|
|
166
|
+
version: '1.0.0',
|
|
167
|
+
connect: async () => {
|
|
168
|
+
if (!provider.connect) {
|
|
169
|
+
throw new Error(`${name} does not expose a connect method`);
|
|
170
|
+
}
|
|
171
|
+
const response = await provider.connect();
|
|
172
|
+
const address = response.publicKey?.toString();
|
|
173
|
+
if (!address) {
|
|
174
|
+
throw new Error(`${name} did not return a public key`);
|
|
175
|
+
}
|
|
176
|
+
accounts = [createInjectedAccount(address, chain, provider, response.publicKey)];
|
|
177
|
+
emitChange();
|
|
178
|
+
return { accounts };
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[StandardDisconnect]: {
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
disconnect: async () => {
|
|
184
|
+
await provider.disconnect?.();
|
|
185
|
+
accounts = [];
|
|
186
|
+
emitChange();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[StandardEvents]: {
|
|
190
|
+
version: '1.0.0',
|
|
191
|
+
on: ((event, listener) => {
|
|
192
|
+
if (event === 'change') {
|
|
193
|
+
listeners.add(listener);
|
|
194
|
+
}
|
|
195
|
+
return () => listeners.delete(listener);
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
...(provider.signMessage
|
|
199
|
+
? {
|
|
200
|
+
[SolanaSignMessage]: {
|
|
201
|
+
version: '1.1.0',
|
|
202
|
+
signMessage: async (...inputs) => await Promise.all(inputs.map(async ({ account, message }) => {
|
|
203
|
+
const currentAccount = accounts[0];
|
|
204
|
+
if (!currentAccount || account.address !== currentAccount.address) {
|
|
205
|
+
throw new Error(`${name} account mismatch`);
|
|
206
|
+
}
|
|
207
|
+
if (!provider.signMessage) {
|
|
208
|
+
throw new Error(`${name} does not support message signing`);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
signature: normalizeInjectedSignature(await provider.signMessage(message, 'utf8')),
|
|
212
|
+
signedMessage: message,
|
|
213
|
+
signatureType: 'ed25519'
|
|
214
|
+
};
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
: {})
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
icon: wallet.icon ?? INJECTED_WALLET_ICON,
|
|
224
|
+
wallet
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createInjectedStandardWallets(chain, detectedWallets) {
|
|
228
|
+
if (!browser) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return detectedWallets.map((wallet) => createInjectedStandardWallet(wallet, chain));
|
|
232
|
+
}
|
|
233
|
+
async function getStandardWallets(chain, detectedInjectedWallets) {
|
|
234
|
+
const registered = getWallets().get();
|
|
235
|
+
const adapted = adaptStandardWallets(registered);
|
|
236
|
+
const injectedWallets = createInjectedStandardWallets(chain, detectedInjectedWallets);
|
|
237
|
+
const unique = dedupeStandardWallets([...adapted, ...injectedWallets], PREFERRED_WALLET_ORDER);
|
|
238
|
+
return unique;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Listen for newly registered wallets
|
|
242
|
+
*/
|
|
243
|
+
function onWalletRegistered(callback) {
|
|
244
|
+
const wallets = getWallets();
|
|
245
|
+
return wallets.on('register', (...newWallets) => {
|
|
246
|
+
for (const wallet of newWallets) {
|
|
247
|
+
const adapted = adaptWallet(wallet);
|
|
248
|
+
if (adapted) {
|
|
249
|
+
callback(adapted);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Utility Functions
|
|
256
|
+
// ============================================================================
|
|
257
|
+
/**
|
|
258
|
+
* Determine Solana chain identifier from RPC endpoint
|
|
259
|
+
*/
|
|
260
|
+
export function resolveChainFromEndpoint(endpoint) {
|
|
261
|
+
return resolveSolanaChainFromEndpoint(endpoint);
|
|
262
|
+
}
|
|
263
|
+
function getRuntimeForDebugMode(mode) {
|
|
264
|
+
switch (mode) {
|
|
265
|
+
case 'desktop':
|
|
266
|
+
return DESKTOP_RUNTIME;
|
|
267
|
+
case 'ios':
|
|
268
|
+
return {
|
|
269
|
+
isMobile: true,
|
|
270
|
+
isWalletBrowser: false,
|
|
271
|
+
platform: 'ios',
|
|
272
|
+
supportsMobileWalletAdapter: false,
|
|
273
|
+
walletBrowser: null
|
|
274
|
+
};
|
|
275
|
+
case 'android':
|
|
276
|
+
return {
|
|
277
|
+
isMobile: true,
|
|
278
|
+
isWalletBrowser: false,
|
|
279
|
+
platform: 'android',
|
|
280
|
+
supportsMobileWalletAdapter: true,
|
|
281
|
+
walletBrowser: null
|
|
282
|
+
};
|
|
283
|
+
case 'phantom':
|
|
284
|
+
case 'solflare':
|
|
285
|
+
case 'jupiter':
|
|
286
|
+
case 'backpack':
|
|
287
|
+
return {
|
|
288
|
+
isMobile: true,
|
|
289
|
+
isWalletBrowser: true,
|
|
290
|
+
platform: 'ios',
|
|
291
|
+
supportsMobileWalletAdapter: false,
|
|
292
|
+
walletBrowser: mode
|
|
293
|
+
};
|
|
294
|
+
default:
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function getCurrentRuntime(mode) {
|
|
299
|
+
if (!browser || typeof navigator === 'undefined') {
|
|
300
|
+
return DESKTOP_RUNTIME;
|
|
301
|
+
}
|
|
302
|
+
const debugRuntime = getRuntimeForDebugMode(mode);
|
|
303
|
+
if (debugRuntime) {
|
|
304
|
+
return debugRuntime;
|
|
305
|
+
}
|
|
306
|
+
return detectMobileRuntime({ maxTouchPoints: navigator.maxTouchPoints });
|
|
307
|
+
}
|
|
308
|
+
function createCurrentMobileWalletLinks() {
|
|
309
|
+
if (!browser || typeof location === 'undefined') {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const targetUrl = location.href;
|
|
313
|
+
const referrerUrl = location.origin;
|
|
314
|
+
const jupiterWallet = MOBILE_WALLETS.find((wallet) => wallet.id === 'jupiter');
|
|
315
|
+
const links = MOBILE_DEEPLINK_WALLETS.map((wallet) => ({
|
|
316
|
+
name: wallet.name,
|
|
317
|
+
url: createMobileWalletBrowseUrl(wallet, targetUrl, referrerUrl)
|
|
318
|
+
}));
|
|
319
|
+
if (jupiterWallet) {
|
|
320
|
+
links.push({ name: jupiterWallet.name, url: jupiterWallet.websiteUrl });
|
|
321
|
+
}
|
|
322
|
+
return links;
|
|
323
|
+
}
|
|
324
|
+
function getDetectedWalletBrowserName(runtime, injectedWallets) {
|
|
325
|
+
const userAgentWallet = MOBILE_WALLETS.find((wallet) => wallet.id === runtime.walletBrowser);
|
|
326
|
+
if (userAgentWallet) {
|
|
327
|
+
return userAgentWallet.name;
|
|
328
|
+
}
|
|
329
|
+
const injectedWallet = injectedWallets[0];
|
|
330
|
+
if (runtime.isMobile && injectedWallet) {
|
|
331
|
+
return injectedWallet.name;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function getRuntimeLabel(runtime, injectedWallets) {
|
|
336
|
+
const walletBrowserName = getDetectedWalletBrowserName(runtime, injectedWallets);
|
|
337
|
+
if (walletBrowserName) {
|
|
338
|
+
return `${walletBrowserName} browser`;
|
|
339
|
+
}
|
|
340
|
+
if (runtime.supportsMobileWalletAdapter) {
|
|
341
|
+
return 'Android Chrome';
|
|
342
|
+
}
|
|
343
|
+
if (runtime.platform === 'ios') {
|
|
344
|
+
return 'iOS browser';
|
|
345
|
+
}
|
|
346
|
+
if (runtime.isMobile) {
|
|
347
|
+
return 'Mobile browser';
|
|
348
|
+
}
|
|
349
|
+
return 'Desktop browser';
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get typed feature from wallet
|
|
353
|
+
*/
|
|
354
|
+
function getWalletFeature(wallet, featureName) {
|
|
355
|
+
const features = wallet.features;
|
|
356
|
+
return features[featureName] ?? null;
|
|
357
|
+
}
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// Wallet Store
|
|
360
|
+
// ============================================================================
|
|
361
|
+
export function createWalletStore() {
|
|
362
|
+
// State
|
|
363
|
+
let rpcEndpoint = $state(PUBLIC_SOLANA_RPC_ENDPOINT);
|
|
364
|
+
let availableWallets = $state([]);
|
|
365
|
+
let standardWallet = $state(null);
|
|
366
|
+
let standardAccount = $state(null);
|
|
367
|
+
let publicKey = $state(null);
|
|
368
|
+
let connected = $state(false);
|
|
369
|
+
let connecting = $state(false);
|
|
370
|
+
let signing = $state(false);
|
|
371
|
+
let isInitialized = $state(false);
|
|
372
|
+
let error = $state(null);
|
|
373
|
+
let useMWA = $state(false);
|
|
374
|
+
let mobileRuntime = $state(DESKTOP_RUNTIME);
|
|
375
|
+
let runtimeDebugMode = $state(readRuntimeDebugMode());
|
|
376
|
+
let injectedWallets = $state([]);
|
|
377
|
+
let mobileWalletLinks = $state([]);
|
|
378
|
+
let walletChangeCallbacks = new Set();
|
|
379
|
+
let walletRegistrationOff = null;
|
|
380
|
+
const controller = createWalletController();
|
|
381
|
+
// Derived state
|
|
382
|
+
const shortAddress = $derived(publicKey ? `${publicKey.slice(0, 4)}...${publicKey.slice(-4)}` : '');
|
|
383
|
+
const connectionStatus = $derived(connecting ? 'connecting' : connected ? 'connected' : 'disconnected');
|
|
384
|
+
const detectedWalletBrowserName = $derived(getDetectedWalletBrowserName(mobileRuntime, injectedWallets));
|
|
385
|
+
const runtimeLabel = $derived(getRuntimeLabel(mobileRuntime, injectedWallets));
|
|
386
|
+
const mobileWalletAdapterWallet = $derived(availableWallets.find((wallet) => wallet.name === SolanaMobileWalletAdapterWalletName) ?? null);
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// Event System
|
|
389
|
+
// ============================================================================
|
|
390
|
+
function notifyWalletChange(wallet) {
|
|
391
|
+
walletChangeCallbacks.forEach((callback) => {
|
|
392
|
+
try {
|
|
393
|
+
callback(wallet);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function onWalletChange(callback) {
|
|
400
|
+
walletChangeCallbacks.add(callback);
|
|
401
|
+
return () => {
|
|
402
|
+
walletChangeCallbacks.delete(callback);
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
controller.subscribe((state) => {
|
|
406
|
+
const previousWallet = standardWallet;
|
|
407
|
+
availableWallets = [...state.availableWallets];
|
|
408
|
+
standardWallet = state.standardWallet;
|
|
409
|
+
standardAccount = state.standardAccount;
|
|
410
|
+
publicKey = state.publicKey;
|
|
411
|
+
connected = state.connected;
|
|
412
|
+
connecting = state.connecting;
|
|
413
|
+
signing = state.signing;
|
|
414
|
+
error = state.error;
|
|
415
|
+
if (previousWallet !== state.standardWallet) {
|
|
416
|
+
if (browser) {
|
|
417
|
+
if (state.standardWallet) {
|
|
418
|
+
localStorage.setItem(LAST_WALLET_KEY, state.standardWallet.name);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
localStorage.removeItem(LAST_WALLET_KEY);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
notifyWalletChange(state.standardWallet);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
// ============================================================================
|
|
428
|
+
// Internal Helper Functions
|
|
429
|
+
// ============================================================================
|
|
430
|
+
function setError(err) {
|
|
431
|
+
error = err;
|
|
432
|
+
}
|
|
433
|
+
function getDetectedInjectedWallets() {
|
|
434
|
+
const debugWalletId = getRuntimeDebugWalletId(runtimeDebugMode);
|
|
435
|
+
if (debugWalletId) {
|
|
436
|
+
return [createDebugInjectedWallet(debugWalletId)];
|
|
437
|
+
}
|
|
438
|
+
return browser ? detectInjectedWallets() : [];
|
|
439
|
+
}
|
|
440
|
+
function refreshRuntimeState() {
|
|
441
|
+
mobileRuntime = getCurrentRuntime(runtimeDebugMode);
|
|
442
|
+
injectedWallets = getDetectedInjectedWallets();
|
|
443
|
+
mobileWalletLinks = createCurrentMobileWalletLinks();
|
|
444
|
+
useMWA = Boolean(mobileWalletAdapterWallet);
|
|
445
|
+
}
|
|
446
|
+
async function refreshAvailableWallets() {
|
|
447
|
+
if (!browser) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
refreshRuntimeState();
|
|
451
|
+
const chain = resolveChainFromEndpoint(rpcEndpoint);
|
|
452
|
+
const detectedInjectedWallets = injectedWallets;
|
|
453
|
+
const wallets = await getStandardWallets(chain, detectedInjectedWallets);
|
|
454
|
+
controller.setAvailableWallets(wallets);
|
|
455
|
+
refreshRuntimeState();
|
|
456
|
+
}
|
|
457
|
+
function getWalletByNames(names) {
|
|
458
|
+
return availableWallets.find((wallet) => names.includes(wallet.name)) ?? null;
|
|
459
|
+
}
|
|
460
|
+
function getWalletBrowserNames() {
|
|
461
|
+
const detectedWallet = injectedWallets[0];
|
|
462
|
+
const walletId = detectedWallet?.id ?? mobileRuntime.walletBrowser;
|
|
463
|
+
switch (walletId) {
|
|
464
|
+
case 'phantom':
|
|
465
|
+
return ['Phantom'];
|
|
466
|
+
case 'solflare':
|
|
467
|
+
return ['Solflare'];
|
|
468
|
+
case 'jupiter':
|
|
469
|
+
return ['Jupiter Mobile', 'Jupiter', 'Jupiter Wallet'];
|
|
470
|
+
case 'backpack':
|
|
471
|
+
return ['Backpack'];
|
|
472
|
+
default:
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// Public API
|
|
478
|
+
// ============================================================================
|
|
479
|
+
/**
|
|
480
|
+
* Initialize the wallet store
|
|
481
|
+
* @param endpoint - Solana RPC endpoint URL
|
|
482
|
+
*/
|
|
483
|
+
async function initialize(endpoint = PUBLIC_SOLANA_RPC_ENDPOINT) {
|
|
484
|
+
try {
|
|
485
|
+
rpcEndpoint = endpoint;
|
|
486
|
+
if (!browser) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await refreshAvailableWallets();
|
|
490
|
+
const wallets = availableWallets;
|
|
491
|
+
// Attempt to restore last connected wallet
|
|
492
|
+
const lastWallet = localStorage.getItem(LAST_WALLET_KEY);
|
|
493
|
+
if (lastWallet) {
|
|
494
|
+
const existing = wallets.find((wallet) => wallet.name === lastWallet);
|
|
495
|
+
if (existing) {
|
|
496
|
+
try {
|
|
497
|
+
await connectStandard(existing);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
localStorage.removeItem(LAST_WALLET_KEY);
|
|
501
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Listen for newly registered wallets
|
|
506
|
+
if (!walletRegistrationOff) {
|
|
507
|
+
walletRegistrationOff = onWalletRegistered((wallet) => {
|
|
508
|
+
controller.setAvailableWallets(dedupeStandardWallets([...availableWallets, wallet], PREFERRED_WALLET_ORDER));
|
|
509
|
+
refreshRuntimeState();
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
isInitialized = true;
|
|
513
|
+
setError(null);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
isInitialized = false;
|
|
517
|
+
setError(err instanceof Error ? err : new Error('Failed to initialize wallet store'));
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function setRuntimeDebugMode(mode) {
|
|
522
|
+
runtimeDebugMode = normalizeRuntimeDebugMode(mode);
|
|
523
|
+
writeRuntimeDebugMode(runtimeDebugMode);
|
|
524
|
+
await refreshAvailableWallets();
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Connect to a wallet using the Wallet Standard
|
|
528
|
+
* @param wallet - The wallet to connect to
|
|
529
|
+
*/
|
|
530
|
+
async function connectStandard(wallet) {
|
|
531
|
+
try {
|
|
532
|
+
await controller.connectStandard(wallet);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
const error = err instanceof Error ? err : new Error('Failed to connect wallet');
|
|
536
|
+
setError(error);
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function connectMobileWalletAdapter() {
|
|
541
|
+
const mobileWallet = getWalletByNames([SolanaMobileWalletAdapterWalletName]);
|
|
542
|
+
if (!mobileWallet) {
|
|
543
|
+
throw new Error('Mobile Wallet Adapter is not available in this browser');
|
|
544
|
+
}
|
|
545
|
+
await connectStandard(mobileWallet);
|
|
546
|
+
}
|
|
547
|
+
async function connectWalletBrowser() {
|
|
548
|
+
const names = getWalletBrowserNames();
|
|
549
|
+
const walletBrowserName = detectedWalletBrowserName ?? 'Wallet browser';
|
|
550
|
+
const wallet = getWalletByNames(names);
|
|
551
|
+
if (!wallet) {
|
|
552
|
+
throw new Error(`${walletBrowserName} is not available in this browser`);
|
|
553
|
+
}
|
|
554
|
+
await connectStandard(wallet);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Disconnect the currently connected wallet
|
|
558
|
+
*/
|
|
559
|
+
async function disconnect() {
|
|
560
|
+
try {
|
|
561
|
+
await controller.disconnect();
|
|
562
|
+
setError(null);
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
const error = err instanceof Error ? err : new Error('Failed to disconnect');
|
|
566
|
+
setError(error);
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Sign a message with the connected wallet
|
|
572
|
+
* @param message - UTF-8 string or raw bytes to sign
|
|
573
|
+
* @returns Wallet Standard signature result
|
|
574
|
+
*/
|
|
575
|
+
async function signMessage(message) {
|
|
576
|
+
try {
|
|
577
|
+
return await controller.signMessage(message);
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
const error = err instanceof Error ? err : new Error('Failed to sign message');
|
|
581
|
+
setError(error);
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// Return Public Interface
|
|
587
|
+
// ============================================================================
|
|
588
|
+
return {
|
|
589
|
+
// State getters
|
|
590
|
+
get rpcEndpoint() {
|
|
591
|
+
return rpcEndpoint;
|
|
592
|
+
},
|
|
593
|
+
get availableWallets() {
|
|
594
|
+
return availableWallets;
|
|
595
|
+
},
|
|
596
|
+
get standardWallet() {
|
|
597
|
+
return standardWallet;
|
|
598
|
+
},
|
|
599
|
+
get standardAccount() {
|
|
600
|
+
return standardAccount;
|
|
601
|
+
},
|
|
602
|
+
get publicKey() {
|
|
603
|
+
return publicKey;
|
|
604
|
+
},
|
|
605
|
+
get connected() {
|
|
606
|
+
return connected;
|
|
607
|
+
},
|
|
608
|
+
get connecting() {
|
|
609
|
+
return connecting;
|
|
610
|
+
},
|
|
611
|
+
get signing() {
|
|
612
|
+
return signing;
|
|
613
|
+
},
|
|
614
|
+
get connectionStatus() {
|
|
615
|
+
return connectionStatus;
|
|
616
|
+
},
|
|
617
|
+
get useMWA() {
|
|
618
|
+
return useMWA;
|
|
619
|
+
},
|
|
620
|
+
get shortAddress() {
|
|
621
|
+
return shortAddress;
|
|
622
|
+
},
|
|
623
|
+
get isInitialized() {
|
|
624
|
+
return isInitialized;
|
|
625
|
+
},
|
|
626
|
+
get error() {
|
|
627
|
+
return error;
|
|
628
|
+
},
|
|
629
|
+
get mobileRuntime() {
|
|
630
|
+
return mobileRuntime;
|
|
631
|
+
},
|
|
632
|
+
get runtimeLabel() {
|
|
633
|
+
return runtimeLabel;
|
|
634
|
+
},
|
|
635
|
+
get runtimeDebugMode() {
|
|
636
|
+
return runtimeDebugMode;
|
|
637
|
+
},
|
|
638
|
+
get runtimeDebugModes() {
|
|
639
|
+
return RUNTIME_DEBUG_MODES;
|
|
640
|
+
},
|
|
641
|
+
get detectedInjectedWallets() {
|
|
642
|
+
return injectedWallets;
|
|
643
|
+
},
|
|
644
|
+
get detectedWalletBrowserName() {
|
|
645
|
+
return detectedWalletBrowserName;
|
|
646
|
+
},
|
|
647
|
+
get mobileWalletLinks() {
|
|
648
|
+
return mobileWalletLinks;
|
|
649
|
+
},
|
|
650
|
+
get mobileWalletAdapterWallet() {
|
|
651
|
+
return mobileWalletAdapterWallet;
|
|
652
|
+
},
|
|
653
|
+
get mobileWalletAdapterName() {
|
|
654
|
+
return SolanaMobileWalletAdapterWalletName;
|
|
655
|
+
},
|
|
656
|
+
get mobileWalletAdapterDisplayName() {
|
|
657
|
+
return MOBILE_WALLET_ADAPTER_DISPLAY_NAME;
|
|
658
|
+
},
|
|
659
|
+
// Methods
|
|
660
|
+
initialize,
|
|
661
|
+
refreshAvailableWallets,
|
|
662
|
+
setRuntimeDebugMode,
|
|
663
|
+
connectStandard,
|
|
664
|
+
connectMobileWalletAdapter,
|
|
665
|
+
connectWalletBrowser,
|
|
666
|
+
disconnect,
|
|
667
|
+
signMessage,
|
|
668
|
+
onWalletChange,
|
|
669
|
+
clearError: () => setError(null)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
export const wallet = createWalletStore();
|