@monygroupcorp/micro-web3 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.
@@ -0,0 +1,541 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ /**
4
+ * WalletService - Handles wallet connection, detection, and interactions
5
+ */
6
+ class WalletService {
7
+ constructor(eventBus) {
8
+ if (!eventBus) {
9
+ throw new Error('WalletService requires an eventBus instance.');
10
+ }
11
+ this.eventBus = eventBus;
12
+ this.provider = null;
13
+ this.signer = null;
14
+ this.connectedAddress = null;
15
+ this.selectedWallet = null;
16
+ this.connected = false;
17
+ this.ethers = ethers;
18
+ this.isInitialized = false;
19
+
20
+ // Supported wallet providers - using a more careful detection approach
21
+ this.providerMap = {
22
+ rabby: () => {
23
+ if (window.ethereum && window.ethereum.isRabby) {
24
+ return window.ethereum;
25
+ }
26
+ return null;
27
+ },
28
+ rainbow: () => {
29
+ if (window.ethereum && window.ethereum.isRainbow) {
30
+ return window.ethereum;
31
+ }
32
+ return window.rainbow || null;
33
+ },
34
+ phantom: () => {
35
+ if (window.phantom && window.phantom.ethereum) {
36
+ return window.phantom.ethereum;
37
+ }
38
+ return null;
39
+ },
40
+ metamask: () => {
41
+ // Simplified approach - just use window.ethereum directly
42
+ console.log('Checking for MetaMask with direct window.ethereum access');
43
+ return window.ethereum || null;
44
+ }
45
+ };
46
+
47
+ // Wallet icons mapping
48
+ this.walletIcons = {
49
+ rabby: '/public/wallets/rabby.webp',
50
+ rainbow: '/public/wallets/rainbow.webp',
51
+ phantom: '/public/wallets/phantom.webp',
52
+ metamask: '/public/wallets/MetaMask.webp',
53
+ };
54
+
55
+ // We'll set up event listeners after a wallet is selected, not in constructor
56
+ }
57
+
58
+ /**
59
+ * Initialize the wallet service - just check for wallet presence
60
+ */
61
+ async initialize() {
62
+ try {
63
+ console.log('Initializing WalletService...');
64
+
65
+ // Check if window.ethereum exists
66
+ if (typeof window.ethereum !== 'undefined') {
67
+ // Log that we found a wallet provider
68
+ console.log('Found Ethereum provider');
69
+
70
+ // Let other components know a wallet was detected
71
+ this.eventBus.emit('wallet:detected');
72
+
73
+ // Check if the provider is in MetaMask compatibility mode
74
+ this.isMetaMask = window.ethereum.isMetaMask;
75
+
76
+ // Set up event listeners for wallet changes
77
+ this.setupEventListeners();
78
+ } else {
79
+ console.log('No Ethereum provider found');
80
+ this.eventBus.emit('wallet:notdetected');
81
+ }
82
+
83
+ // Mark as initialized
84
+ this.isInitialized = true;
85
+
86
+ return true;
87
+ } catch (error) {
88
+ console.error('Error initializing WalletService:', error);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Format error to provide better context
95
+ * @private
96
+ */
97
+ formatError(error, context = '') {
98
+ // If error is a string, convert to Error object
99
+ if (typeof error === 'string') {
100
+ return new Error(`${context}: ${error}`);
101
+ }
102
+
103
+ // If error has a message, add context
104
+ if (error && error.message) {
105
+ // Common wallet errors, make more user-friendly
106
+ const message = this.getUserFriendlyErrorMessage(error, context);
107
+ const formattedError = new Error(message);
108
+
109
+ // Copy properties
110
+ formattedError.code = error.code;
111
+ formattedError.originalError = error;
112
+
113
+ return formattedError;
114
+ }
115
+
116
+ // Default case
117
+ return new Error(`${context}: Unknown error`);
118
+ }
119
+
120
+ /**
121
+ * Convert technical errors to user-friendly messages
122
+ * @private
123
+ */
124
+ getUserFriendlyErrorMessage(error, context) {
125
+ const message = error.message || 'Unknown error';
126
+
127
+ // Handle common wallet error codes
128
+ if (error.code === 4001) {
129
+ return 'Connection request rejected by user';
130
+ }
131
+
132
+ if (error.code === 4902) {
133
+ return 'Network needs to be added to your wallet';
134
+ }
135
+
136
+ if (message.includes('redefine property') || message.includes('which has only a getter')) {
137
+ return 'Wallet conflict detected. Please disable all wallet extensions except the one you want to use, then refresh.';
138
+ }
139
+
140
+ if (message.includes('user rejected')) {
141
+ return 'Connection rejected by user';
142
+ }
143
+
144
+ if (message.includes('already pending')) {
145
+ return 'A wallet request is already pending, please check your wallet';
146
+ }
147
+
148
+ // If no specific handling, add context
149
+ return context ? `${context}: ${message}` : message;
150
+ }
151
+
152
+ /**
153
+ * Get all available wallet providers
154
+ * @returns {Object} Map of wallet providers
155
+ */
156
+ getAvailableWallets() {
157
+ const available = {};
158
+
159
+ console.log('Checking for available wallets...');
160
+
161
+ // First check if window.ethereum exists at all
162
+ if (window.ethereum) {
163
+ console.log('window.ethereum is available');
164
+ } else {
165
+ console.log('window.ethereum is not available');
166
+ }
167
+
168
+ for (const [name, getProvider] of Object.entries(this.providerMap)) {
169
+ try {
170
+ console.log(`Checking for ${name} wallet...`);
171
+ const provider = getProvider();
172
+ if (provider) {
173
+ console.log(`Found ${name} wallet`);
174
+ available[name] = {
175
+ name,
176
+ icon: this.walletIcons[name],
177
+ provider: provider
178
+ };
179
+ } else {
180
+ console.log(`${name} wallet not detected`);
181
+ }
182
+ } catch (error) {
183
+ console.warn(`Error detecting ${name} wallet:`, error);
184
+ }
185
+ }
186
+
187
+ // If no specific wallets detected but ethereum provider exists,
188
+ // add metamask as a fallback option
189
+ if (Object.keys(available).length === 0 && window.ethereum) {
190
+ console.log('No specific wallets detected, but window.ethereum exists. Adding MetaMask as fallback.');
191
+ available['metamask'] = {
192
+ name: 'metamask',
193
+ icon: this.walletIcons['metamask'],
194
+ provider: window.ethereum
195
+ };
196
+ } else if (!available['metamask'] && window.ethereum) {
197
+ // Always add MetaMask if ethereum is available and not already added
198
+ console.log('window.ethereum exists. Adding MetaMask option regardless of detection.');
199
+ available['metamask'] = {
200
+ name: 'metamask',
201
+ icon: this.walletIcons['metamask'],
202
+ provider: window.ethereum
203
+ };
204
+ }
205
+
206
+ console.log('Available wallets:', Object.keys(available));
207
+ return available;
208
+ }
209
+
210
+ /**
211
+ * Select a specific wallet provider
212
+ * @param {string} walletType - The type of wallet to select
213
+ */
214
+ async selectWallet(walletType) {
215
+ try {
216
+ // Clear any previous wallet selection
217
+ this.cleanup();
218
+
219
+ if (!this.providerMap[walletType]) {
220
+ throw new Error(`Unsupported wallet: ${walletType}`);
221
+ }
222
+
223
+ const provider = this.providerMap[walletType]();
224
+
225
+ if (!provider) {
226
+ throw new Error(`${walletType} not detected. Please install it first.`);
227
+ }
228
+
229
+ this.selectedWallet = walletType;
230
+ this.provider = provider;
231
+
232
+ // Set up event listeners now that we have a provider
233
+ this.setupEventListeners();
234
+
235
+ this.eventBus.emit('wallet:selected', {
236
+ type: walletType,
237
+ provider: provider
238
+ });
239
+
240
+ return walletType;
241
+ } catch (error) {
242
+ const formattedError = this.formatError(error, 'Wallet selection failed');
243
+ this.eventBus.emit('wallet:error', formattedError);
244
+ throw formattedError;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Set up event listeners for the selected wallet
250
+ * @private
251
+ */
252
+ setupEventListeners() {
253
+ if (this.provider) {
254
+ // Remove any existing listeners to avoid duplicates
255
+ try {
256
+ this.provider.removeListener('chainChanged', this.handleNetworkChange);
257
+ this.provider.removeListener('accountsChanged', this.handleAccountChange);
258
+ } catch (error) {
259
+ // Ignore errors when removing non-existent listeners
260
+ }
261
+
262
+ // Add new listeners
263
+ this.provider.on('chainChanged', () => this.handleNetworkChange());
264
+ this.provider.on('accountsChanged', (accounts) => this.handleAccountChange(accounts));
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Clean up resources when switching wallets
270
+ * @private
271
+ */
272
+ cleanup() {
273
+ if (this.provider) {
274
+ try {
275
+ this.provider.removeListener('chainChanged', this.handleNetworkChange);
276
+ this.provider.removeListener('accountsChanged', this.handleAccountChange);
277
+ } catch (error) {
278
+ // Ignore errors when removing non-existent listeners
279
+ }
280
+ }
281
+
282
+ this.provider = null;
283
+ this.signer = null;
284
+ this.connectedAddress = null;
285
+ this.connected = false;
286
+ this.ethersProvider = null;
287
+ }
288
+
289
+ /**
290
+ * Connect to the selected wallet
291
+ * @returns {string} The connected address
292
+ */
293
+ async connect() {
294
+ try {
295
+ if (!this.selectedWallet || !this.provider) {
296
+ throw new Error('Please select a wallet first');
297
+ }
298
+
299
+ this.eventBus.emit('wallet:connecting');
300
+ console.log(`Attempting to connect to ${this.selectedWallet}...`);
301
+
302
+ let accounts;
303
+
304
+ try {
305
+ // Give the wallet time to initialize if needed
306
+ await new Promise(resolve => setTimeout(resolve, 500));
307
+
308
+ // Handle wallet-specific connection methods
309
+ if (this.selectedWallet === 'phantom' && window.phantom && window.phantom.ethereum) {
310
+ console.log('Using phantom-specific connection method');
311
+ accounts = await window.phantom.ethereum.request({
312
+ method: 'eth_requestAccounts'
313
+ });
314
+ } else {
315
+ console.log(`Using standard connection method for ${this.selectedWallet}`);
316
+ // Try to check if the wallet has any accounts before requesting new ones
317
+ try {
318
+ const existingAccounts = await this.provider.request({
319
+ method: 'eth_accounts'
320
+ });
321
+
322
+ console.log('Existing accounts:', existingAccounts);
323
+
324
+ // If no accounts, explicitly check before requesting
325
+ if (!existingAccounts || existingAccounts.length === 0) {
326
+ console.log('No existing accounts found, requesting user approval...');
327
+ }
328
+ } catch (accountCheckError) {
329
+ console.warn('Error checking existing accounts:', accountCheckError);
330
+ }
331
+
332
+ // Request accounts with explicit params
333
+ accounts = await this.provider.request({
334
+ method: 'eth_requestAccounts',
335
+ params: []
336
+ });
337
+ }
338
+ } catch (error) {
339
+ console.error('Error requesting accounts:', error);
340
+
341
+ if (error && error.code === 4001) {
342
+ throw this.formatError(error, 'Wallet connection rejected');
343
+ } else if (error && error.message && error.message.includes('wallet must has at least one account')) {
344
+ throw new Error('Your wallet has no accounts. Please create at least one account in your wallet and try again.');
345
+ } else {
346
+ throw error;
347
+ }
348
+ }
349
+
350
+ console.log('Accounts received:', accounts);
351
+
352
+ if (!accounts || !accounts[0]) {
353
+ throw new Error('No accounts found. Please unlock your wallet and make sure you have at least one account set up.');
354
+ }
355
+
356
+ this.connectedAddress = accounts[0];
357
+ this.connected = true;
358
+
359
+ console.log(`Successfully connected to account: ${this.connectedAddress}`);
360
+
361
+ // Store the selected wallet in localStorage for future auto-reconnect
362
+ if (this.selectedWallet) {
363
+ localStorage.setItem('ms2fun_lastWallet', this.selectedWallet);
364
+ }
365
+
366
+ // Create ethers provider
367
+ this.ethersProvider = new ethers.providers.Web3Provider(this.provider, 'any');
368
+ this.signer = this.ethersProvider.getSigner();
369
+
370
+ this.eventBus.emit('wallet:connected', {
371
+ address: this.connectedAddress,
372
+ walletType: this.selectedWallet,
373
+ provider: this.provider,
374
+ ethersProvider: this.ethersProvider,
375
+ signer: this.signer
376
+ });
377
+
378
+ return this.connectedAddress;
379
+ } catch (error) {
380
+ console.error('Wallet connection error:', error);
381
+
382
+ // Better handle the specific "must have at least one account" error
383
+ if (error.message && (
384
+ error.message.includes('at least one account') ||
385
+ error.message.includes('wallet must has') ||
386
+ error.message.includes('no accounts')
387
+ )) {
388
+ const friendlyMessage = 'Your wallet has no accounts. Please create at least one account in your wallet and try again.';
389
+ const formattedError = new Error(friendlyMessage);
390
+ formattedError.code = error.code;
391
+ formattedError.originalError = error;
392
+
393
+ this.eventBus.emit('wallet:error', formattedError);
394
+ throw formattedError;
395
+ }
396
+
397
+ const formattedError = this.formatError(error, 'Wallet connection failed');
398
+ this.eventBus.emit('wallet:error', formattedError);
399
+ throw formattedError;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Switch to a specific network
405
+ * @param {number} networkId - The network ID to switch to
406
+ */
407
+ async switchNetwork(networkId) {
408
+ try {
409
+ if (!this.provider) {
410
+ throw new Error('No wallet connected');
411
+ }
412
+
413
+ const hexChainId = `0x${Number(networkId).toString(16)}`;
414
+
415
+ this.eventBus.emit('network:switching', {
416
+ to: networkId,
417
+ automatic: false
418
+ });
419
+
420
+ try {
421
+ await this.provider.request({
422
+ method: 'wallet_switchEthereumChain',
423
+ params: [{ chainId: hexChainId }],
424
+ });
425
+
426
+ // Refresh provider after switch
427
+ this.ethersProvider = new ethers.providers.Web3Provider(this.provider, 'any');
428
+ this.signer = this.ethersProvider.getSigner();
429
+
430
+ this.eventBus.emit('network:switched', {
431
+ to: networkId,
432
+ success: true
433
+ });
434
+
435
+ return true;
436
+ } catch (switchError) {
437
+ // Network needs to be added
438
+ if (switchError.code === 4902) {
439
+ // You would need network metadata here to add properly
440
+ throw new Error('Network needs to be added first');
441
+ } else {
442
+ throw switchError;
443
+ }
444
+ }
445
+ } catch (error) {
446
+ const formattedError = this.formatError(error, 'Network switch failed');
447
+
448
+ // Emit error event for consistency with BlockchainService
449
+ this.eventBus.emit('network:switch:error', {
450
+ from: null,
451
+ to: networkId,
452
+ error: formattedError.message,
453
+ originalError: error
454
+ });
455
+
456
+ // Also emit switched event with success: false for backward compatibility
457
+ this.eventBus.emit('network:switched', {
458
+ to: networkId,
459
+ success: false,
460
+ error: formattedError.message
461
+ });
462
+
463
+ throw formattedError;
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Get the currently connected address
469
+ * @returns {string|null} The connected address or null
470
+ */
471
+ getAddress() {
472
+ return this.connectedAddress;
473
+ }
474
+
475
+ /**
476
+ * Check if a wallet is connected
477
+ * @returns {boolean} Whether a wallet is connected
478
+ */
479
+ isConnected() {
480
+ return this.connected && !!this.connectedAddress;
481
+ }
482
+
483
+ /**
484
+ * Get the ethers provider and signer
485
+ * @returns {Object} The provider and signer
486
+ */
487
+ getProviderAndSigner() {
488
+ return {
489
+ provider: this.ethersProvider,
490
+ signer: this.signer
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Handle network changes
496
+ * @private
497
+ */
498
+ handleNetworkChange() {
499
+ // Refresh provider
500
+ if (this.provider) {
501
+ this.ethersProvider = new ethers.providers.Web3Provider(this.provider, 'any');
502
+ this.signer = this.ethersProvider.getSigner();
503
+
504
+ // Don't emit network:changed here - BlockchainService handles it
505
+ // This prevents duplicate messages when both services detect the change
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Handle account changes
511
+ * @private
512
+ */
513
+ handleAccountChange(accounts) {
514
+ if (!accounts || !Array.isArray(accounts)) {
515
+ return;
516
+ }
517
+
518
+ if (accounts.length === 0) {
519
+ // Disconnected
520
+ this.connected = false;
521
+ this.connectedAddress = null;
522
+ this.eventBus.emit('wallet:disconnected');
523
+ } else if (accounts[0] !== this.connectedAddress) {
524
+ // Changed account
525
+ this.connectedAddress = accounts[0];
526
+ this.eventBus.emit('wallet:changed', { address: accounts[0] });
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Disconnect from the wallet (if supported)
532
+ */
533
+ async disconnect() {
534
+ // Most wallets don't support programmatic disconnect
535
+ // But we can reset our state
536
+ this.cleanup();
537
+ this.eventBus.emit('wallet:disconnected');
538
+ }
539
+ }
540
+
541
+ export default WalletService;