@monygroupcorp/micro-web3 0.1.0 → 0.1.3

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,680 @@
1
+ import { Component, eventBus } from '@monygroupcorp/microact';
2
+ import WalletModal from '../Wallet/WalletModal.js';
3
+ import { ethers } from 'ethers';
4
+
5
+ /**
6
+ * FloatingWalletButton component
7
+ * Persistent floating wallet button (bottom-right corner) with power user dropdown menu
8
+ * Replaces WalletSplash for non-blocking wallet connection
9
+ */
10
+ export class FloatingWalletButton extends Component {
11
+ constructor(props) {
12
+ super();
13
+ this.walletService = props.walletService;
14
+ this.walletModal = null;
15
+ this.state = {
16
+ walletConnected: false,
17
+ address: null,
18
+ balance: '0.00',
19
+ loading: true,
20
+ menuOpen: false,
21
+ // Conditional menu items
22
+ hasExecTokens: false,
23
+ isVaultBenefactor: false
24
+ };
25
+ }
26
+
27
+ onMount() {
28
+ console.log('[FloatingWalletButton] onMount called');
29
+ this.initialize();
30
+ this.setupDOMEventListeners();
31
+ }
32
+
33
+ async initialize() {
34
+ await this.checkWalletConnection();
35
+ this.setupEventListeners();
36
+ }
37
+
38
+ async checkWalletConnection() {
39
+ console.log('[FloatingWalletButton] checkWalletConnection called');
40
+ try {
41
+ // Initialize wallet service if needed
42
+ if (!this.walletService.isInitialized) {
43
+ await this.walletService.initialize();
44
+ }
45
+
46
+ // Check if wallet service thinks it's connected
47
+ let isConnected = this.walletService.isConnected();
48
+ let address = this.walletService.getAddress();
49
+
50
+ // If not connected, try to auto-reconnect to last used wallet
51
+ if (!isConnected && typeof window.ethereum !== 'undefined') {
52
+ try {
53
+ const lastWallet = localStorage.getItem('ms2fun_lastWallet');
54
+
55
+ if (lastWallet) {
56
+ // Check if that wallet has accounts (without prompting)
57
+ const accounts = await window.ethereum.request({ method: 'eth_accounts' });
58
+ const hasAccounts = accounts && accounts.length > 0;
59
+
60
+ // Only try to reconnect if the last wallet has accounts
61
+ if (hasAccounts) {
62
+ try {
63
+ await this.walletService.selectWallet(lastWallet);
64
+ await this.walletService.connect();
65
+ isConnected = this.walletService.isConnected();
66
+ address = this.walletService.getAddress();
67
+
68
+ if (isConnected) {
69
+ console.log('[FloatingWalletButton] Auto-reconnected to', lastWallet);
70
+ }
71
+ } catch (connectError) {
72
+ console.log('[FloatingWalletButton] Auto-reconnect not possible');
73
+ }
74
+ }
75
+ }
76
+ } catch (error) {
77
+ console.log('[FloatingWalletButton] Could not check existing accounts:', error);
78
+ }
79
+ }
80
+
81
+ if (isConnected && address) {
82
+ await this.loadWalletInfo(address);
83
+ } else {
84
+ this.setState({ loading: false, walletConnected: false });
85
+ }
86
+ } catch (error) {
87
+ console.error('[FloatingWalletButton] Error checking wallet connection:', error);
88
+ this.setState({ loading: false, walletConnected: false });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Load wallet info (balance, EXEC tokens, vault benefactor status)
94
+ */
95
+ async loadWalletInfo(address) {
96
+ try {
97
+ // Get ETH balance
98
+ let provider = this.walletService.ethersProvider;
99
+ if (!provider && typeof window.ethereum !== 'undefined') {
100
+ provider = new ethers.providers.Web3Provider(window.ethereum);
101
+ }
102
+
103
+ let balance = '0.00';
104
+ if (provider && address) {
105
+ const balanceWei = await provider.getBalance(address);
106
+ balance = parseFloat(ethers.utils.formatEther(balanceWei)).toFixed(4);
107
+ }
108
+
109
+ // TODO: Check EXEC token holdings for governance menu visibility
110
+ // const hasExecTokens = await this.checkExecTokens(address);
111
+ const hasExecTokens = false; // Placeholder for now
112
+
113
+ // TODO: Check vault benefactor status
114
+ // const isVaultBenefactor = await this.checkVaultBenefactor(address);
115
+ const isVaultBenefactor = false; // Placeholder for now
116
+
117
+ this.setState({
118
+ walletConnected: true,
119
+ address,
120
+ balance,
121
+ loading: false,
122
+ hasExecTokens,
123
+ isVaultBenefactor
124
+ });
125
+ } catch (error) {
126
+ console.error('[FloatingWalletButton] Failed to load wallet info:', error);
127
+ this.setState({
128
+ walletConnected: true,
129
+ address,
130
+ loading: false
131
+ });
132
+ }
133
+ }
134
+
135
+ /**
136
+ * TODO: Check if user holds EXEC tokens
137
+ */
138
+ async checkExecTokens(address) {
139
+ // Will implement when ERC404 adapter is wired up
140
+ // const execBalance = await ERC404Adapter.balanceOf(address, EXEC_TOKEN_ADDRESS);
141
+ // return execBalance > 0;
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * TODO: Check if user is a vault benefactor
147
+ */
148
+ async checkVaultBenefactor(address) {
149
+ // Will implement when vault adapters are wired up
150
+ // Iterate vaults, check getBenefactorShares(address) > 0
151
+ return false;
152
+ }
153
+
154
+ setupEventListeners() {
155
+ // Listen for wallet connection
156
+ const unsubscribeConnected = eventBus.on('wallet:connected', async (data) => {
157
+ await this.loadWalletInfo(data.address);
158
+ });
159
+
160
+ // Listen for wallet disconnection
161
+ const unsubscribeDisconnected = eventBus.on('wallet:disconnected', () => {
162
+ this.setState({
163
+ walletConnected: false,
164
+ address: null,
165
+ balance: '0.00',
166
+ menuOpen: false,
167
+ hasExecTokens: false,
168
+ isVaultBenefactor: false
169
+ });
170
+ });
171
+
172
+ // Listen for wallet/account change
173
+ const unsubscribeChanged = eventBus.on('wallet:changed', async (data) => {
174
+ await this.loadWalletInfo(data.address);
175
+ });
176
+
177
+ // Register cleanup
178
+ this.registerCleanup(() => {
179
+ unsubscribeConnected();
180
+ unsubscribeDisconnected();
181
+ unsubscribeChanged();
182
+ });
183
+ }
184
+
185
+ setupClickOutsideHandler() {
186
+ const handleClickOutside = (e) => {
187
+ if (!this.element) return;
188
+
189
+ // Check if click is outside the floating wallet button
190
+ if (!this.element.contains(e.target) && this.state.menuOpen) {
191
+ this.setState({ menuOpen: false });
192
+ }
193
+ };
194
+
195
+ document.addEventListener('click', handleClickOutside);
196
+
197
+ this.registerCleanup(() => {
198
+ document.removeEventListener('click', handleClickOutside);
199
+ });
200
+ }
201
+
202
+ async handleButtonClick(e) {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+
206
+ if (this.state.walletConnected) {
207
+ // Toggle dropdown menu
208
+ this.setState({ menuOpen: !this.state.menuOpen });
209
+ } else {
210
+ // Show wallet connection modal
211
+ await this.showWalletModal();
212
+ }
213
+ }
214
+
215
+ async showWalletModal() {
216
+ // Get provider map and icons from this.walletService
217
+ const providerMap = {
218
+ rabby: () => window.ethereum?.isRabby ? window.ethereum : null,
219
+ rainbow: () => window.ethereum?.isRainbow ? window.ethereum : null,
220
+ phantom: () => window.phantom?.ethereum || null,
221
+ metamask: () => window.ethereum || null
222
+ };
223
+
224
+ const walletIcons = {
225
+ rabby: '/public/wallets/rabby.webp',
226
+ rainbow: '/public/wallets/rainbow.webp',
227
+ phantom: '/public/wallets/phantom.webp',
228
+ metamask: '/public/wallets/MetaMask.webp'
229
+ };
230
+
231
+ // Create WalletModal if not exists
232
+ if (!this.walletModal) {
233
+ this.walletModal = new WalletModal({
234
+ providerMap,
235
+ walletIcons,
236
+ onWalletSelected: async (walletType) => {
237
+ await this.handleWalletSelection(walletType);
238
+ },
239
+ });
240
+
241
+ if (!this.modalRoot) {
242
+ this.modalRoot = document.createElement('div');
243
+ document.body.appendChild(this.modalRoot);
244
+ }
245
+ this.walletModal.mount(this.modalRoot);
246
+ }
247
+
248
+ this.walletModal.show();
249
+ }
250
+
251
+ async handleWalletSelection(walletType) {
252
+ try {
253
+ // Select the wallet
254
+ await this.walletService.selectWallet(walletType);
255
+
256
+ // Store the selected wallet in localStorage for future auto-reconnect
257
+ localStorage.setItem('ms2fun_lastWallet', walletType);
258
+
259
+ // Connect to the wallet
260
+ await this.walletService.connect();
261
+
262
+ // Wallet is now connected (event listener will update state)
263
+
264
+ } catch (error) {
265
+ console.error('[FloatingWalletButton] Error connecting wallet:', error);
266
+ // TODO: Show error message
267
+ }
268
+ }
269
+
270
+ handleMenuItemClick(route) {
271
+ // Close menu
272
+ this.setState({ menuOpen: false });
273
+
274
+ // Navigate to route
275
+ if (window.router) {
276
+ window.router.navigate(route);
277
+ } else {
278
+ window.location.href = route;
279
+ }
280
+ }
281
+
282
+ async handleDisconnect(e) {
283
+ e.preventDefault();
284
+ e.stopPropagation();
285
+
286
+ try {
287
+ await this.walletService.disconnect();
288
+ this.setState({ menuOpen: false });
289
+ } catch (error) {
290
+ console.error('[FloatingWalletButton] Failed to disconnect:', error);
291
+ }
292
+ }
293
+
294
+ static get styles() {
295
+ return `
296
+ /* FloatingWalletButton - Bottom-right floating wallet connection button with power user dropdown */
297
+
298
+ .floating-wallet-button {
299
+ position: fixed;
300
+ bottom: 2rem;
301
+ right: 2rem;
302
+ z-index: 9999; /* Above all content */
303
+ }
304
+
305
+ .floating-wallet-button .wallet-btn {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 0.5rem;
309
+ padding: 0.75rem 1.25rem;
310
+ background: linear-gradient(135deg, rgba(139, 69, 19, 0.9), rgba(101, 67, 33, 0.9));
311
+ border: 2px solid rgba(218, 165, 32, 0.6);
312
+ border-radius: 50px;
313
+ color: #fff;
314
+ font-family: 'Cinzel', serif;
315
+ font-size: 0.9rem;
316
+ font-weight: 600;
317
+ cursor: pointer;
318
+ transition: all 0.3s ease;
319
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
320
+ backdrop-filter: blur(10px);
321
+ }
322
+
323
+ .floating-wallet-button .wallet-btn:hover {
324
+ transform: translateY(-2px);
325
+ box-shadow: 0 6px 20px rgba(218, 165, 32, 0.4);
326
+ border-color: rgba(218, 165, 32, 0.8);
327
+ background: linear-gradient(135deg, rgba(160, 82, 45, 0.95), rgba(120, 80, 50, 0.95));
328
+ }
329
+
330
+ .floating-wallet-button .wallet-btn:active {
331
+ transform: translateY(0);
332
+ }
333
+
334
+ /* Wallet icon */
335
+ .floating-wallet-button .wallet-icon {
336
+ font-size: 1.2rem;
337
+ line-height: 1;
338
+ }
339
+
340
+ /* Wallet text/address */
341
+ .floating-wallet-button .wallet-text,
342
+ .floating-wallet-button .wallet-address {
343
+ font-size: 0.875rem;
344
+ white-space: nowrap;
345
+ }
346
+
347
+ /* Loading state */
348
+ .floating-wallet-button.loading {
349
+ pointer-events: none;
350
+ }
351
+
352
+ .floating-wallet-button .wallet-spinner {
353
+ width: 40px;
354
+ height: 40px;
355
+ border: 3px solid rgba(218, 165, 32, 0.3);
356
+ border-top-color: rgba(218, 165, 32, 0.9);
357
+ border-radius: 50%;
358
+ animation: wallet-spin 0.8s linear infinite;
359
+ }
360
+
361
+ @keyframes wallet-spin {
362
+ to { transform: rotate(360deg); }
363
+ }
364
+
365
+ /* Dropdown menu */
366
+ .wallet-dropdown-menu {
367
+ position: absolute;
368
+ bottom: calc(100% + 0.5rem);
369
+ right: 0;
370
+ min-width: 240px;
371
+ background: linear-gradient(135deg, rgba(139, 69, 19, 0.98), rgba(101, 67, 33, 0.98));
372
+ border: 2px solid rgba(218, 165, 32, 0.6);
373
+ border-radius: 12px;
374
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
375
+ backdrop-filter: blur(15px);
376
+ overflow: hidden;
377
+ animation: dropdown-slide-up 0.2s ease-out;
378
+ }
379
+
380
+ @keyframes dropdown-slide-up {
381
+ from {
382
+ opacity: 0;
383
+ transform: translateY(10px);
384
+ }
385
+ to {
386
+ opacity: 1;
387
+ transform: translateY(0);
388
+ }
389
+ }
390
+
391
+ /* Dropdown header */
392
+ .wallet-dropdown-menu .dropdown-header {
393
+ padding: 1rem;
394
+ background: rgba(0, 0, 0, 0.3);
395
+ border-bottom: 1px solid rgba(218, 165, 32, 0.3);
396
+ }
397
+
398
+ .wallet-dropdown-menu .dropdown-address {
399
+ font-family: 'Cinzel', serif;
400
+ font-size: 0.875rem;
401
+ font-weight: 600;
402
+ color: rgba(218, 165, 32, 0.95);
403
+ margin-bottom: 0.25rem;
404
+ }
405
+
406
+ .wallet-dropdown-menu .dropdown-balance {
407
+ font-family: 'Lato', sans-serif;
408
+ font-size: 0.75rem;
409
+ color: rgba(255, 255, 255, 0.7);
410
+ }
411
+
412
+ /* Dropdown divider */
413
+ .wallet-dropdown-menu .dropdown-divider {
414
+ height: 1px;
415
+ background: linear-gradient(90deg,
416
+ transparent,
417
+ rgba(218, 165, 32, 0.3) 50%,
418
+ transparent
419
+ );
420
+ margin: 0;
421
+ }
422
+
423
+ /* Dropdown items */
424
+ .wallet-dropdown-menu .dropdown-items {
425
+ padding: 0.5rem;
426
+ }
427
+
428
+ .wallet-dropdown-menu .dropdown-item {
429
+ display: flex;
430
+ align-items: center;
431
+ gap: 0.75rem;
432
+ width: 100%;
433
+ padding: 0.75rem 1rem;
434
+ background: transparent;
435
+ border: none;
436
+ border-radius: 8px;
437
+ color: rgba(255, 255, 255, 0.9);
438
+ font-family: 'Lato', sans-serif;
439
+ font-size: 0.875rem;
440
+ text-align: left;
441
+ cursor: pointer;
442
+ transition: all 0.2s ease;
443
+ }
444
+
445
+ .wallet-dropdown-menu .dropdown-item:hover {
446
+ background: rgba(218, 165, 32, 0.15);
447
+ color: #fff;
448
+ }
449
+
450
+ .wallet-dropdown-menu .dropdown-item:active {
451
+ background: rgba(218, 165, 32, 0.25);
452
+ }
453
+
454
+ .wallet-dropdown-menu .dropdown-item .item-icon {
455
+ font-size: 1.125rem;
456
+ line-height: 1;
457
+ opacity: 0.9;
458
+ }
459
+
460
+ .wallet-dropdown-menu .dropdown-item .item-label {
461
+ flex: 1;
462
+ }
463
+
464
+ /* Disconnect button special styling */
465
+ .wallet-dropdown-menu .dropdown-item.disconnect {
466
+ color: rgba(255, 99, 71, 0.9);
467
+ }
468
+
469
+ .wallet-dropdown-menu .dropdown-item.disconnect:hover {
470
+ background: rgba(255, 99, 71, 0.15);
471
+ color: rgba(255, 99, 71, 1);
472
+ }
473
+
474
+ /* Responsive adjustments */
475
+ @media (max-width: 768px) {
476
+ .floating-wallet-button {
477
+ bottom: 1rem;
478
+ right: 1rem;
479
+ }
480
+
481
+ .floating-wallet-button .wallet-btn {
482
+ padding: 0.625rem 1rem;
483
+ font-size: 0.8rem;
484
+ }
485
+
486
+ .wallet-dropdown-menu {
487
+ min-width: 200px;
488
+ right: 0;
489
+ }
490
+
491
+ .wallet-dropdown-menu .dropdown-item {
492
+ padding: 0.625rem 0.875rem;
493
+ font-size: 0.8rem;
494
+ }
495
+ }
496
+
497
+ /* Small screens - even more compact */
498
+ @media (max-width: 480px) {
499
+ .floating-wallet-button {
500
+ bottom: 0.75rem;
501
+ right: 0.75rem;
502
+ }
503
+
504
+ .floating-wallet-button .wallet-btn {
505
+ padding: 0.5rem 0.875rem;
506
+ font-size: 0.75rem;
507
+ }
508
+
509
+ .floating-wallet-button .wallet-icon {
510
+ font-size: 1rem;
511
+ }
512
+
513
+ .wallet-dropdown-menu {
514
+ min-width: 180px;
515
+ }
516
+ }
517
+
518
+ /* Ensure menu appears above button when open */
519
+ .floating-wallet-button.menu-open .wallet-btn {
520
+ border-color: rgba(218, 165, 32, 0.9);
521
+ background: linear-gradient(135deg, rgba(160, 82, 45, 0.95), rgba(120, 80, 50, 0.95));
522
+ }
523
+ `;
524
+ }
525
+
526
+ render() {
527
+ console.log('[FloatingWalletButton] render called, loading:', this.state.loading, 'walletConnected:', this.state.walletConnected);
528
+ if (this.state.loading) {
529
+ return `
530
+ <div class="floating-wallet-button loading">
531
+ <div class="wallet-spinner"></div>
532
+ </div>
533
+ `;
534
+ }
535
+
536
+ const { walletConnected, address, balance, menuOpen, hasExecTokens, isVaultBenefactor } = this.state;
537
+
538
+ if (!walletConnected) {
539
+ // Not connected - show "Connect" button
540
+ return `
541
+ <div class="floating-wallet-button disconnected" data-ref="wallet-button">
542
+ <button class="wallet-btn" data-ref="connect-btn">
543
+ <span class="wallet-icon">🦊</span>
544
+ <span class="wallet-text">Connect</span>
545
+ </button>
546
+ </div>
547
+ `;
548
+ }
549
+
550
+ // Connected - show abbreviated address
551
+ const truncatedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
552
+
553
+ return `
554
+ <div class="floating-wallet-button connected ${menuOpen ? 'menu-open' : ''}" data-ref="wallet-button">
555
+ <button class="wallet-btn" data-ref="wallet-btn" title="${this.escapeHtml(address)}\nBalance: ${balance} ETH">
556
+ <span class="wallet-icon">🦊</span>
557
+ <span class="wallet-address">${this.escapeHtml(truncatedAddress)}</span>
558
+ </button>
559
+
560
+ ${menuOpen ? this.renderDropdownMenu(address, balance, hasExecTokens, isVaultBenefactor) : ''}
561
+ </div>
562
+ `;
563
+ }
564
+
565
+ renderDropdownMenu(address, balance, hasExecTokens, isVaultBenefactor) {
566
+ const truncatedAddress = `${address.slice(0, 6)}...${address.slice(-4)}`;
567
+
568
+ return `
569
+ <div class="wallet-dropdown-menu" data-ref="dropdown-menu">
570
+ <div class="dropdown-header">
571
+ <div class="dropdown-address">${this.escapeHtml(truncatedAddress)}</div>
572
+ <div class="dropdown-balance">${balance} ETH</div>
573
+ </div>
574
+
575
+ <div class="dropdown-divider"></div>
576
+
577
+ <div class="dropdown-items">
578
+ <button class="dropdown-item" data-route="/portfolio" data-ref="menu-item">
579
+ <span class="item-icon">📊</span>
580
+ <span class="item-label">Portfolio</span>
581
+ </button>
582
+
583
+ ${hasExecTokens ? `
584
+ <button class="dropdown-item" data-route="/governance" data-ref="menu-item">
585
+ <span class="item-icon">🗳️</span>
586
+ <span class="item-label">Governance</span>
587
+ </button>
588
+ ` : ''}
589
+
590
+ <button class="dropdown-item" data-route="/staking" data-ref="menu-item">
591
+ <span class="item-icon">🎯</span>
592
+ <span class="item-label">Staking</span>
593
+ </button>
594
+
595
+ ${isVaultBenefactor ? `
596
+ <button class="dropdown-item" data-route="/portfolio?filter=vaults" data-ref="menu-item">
597
+ <span class="item-icon">💰</span>
598
+ <span class="item-label">Vault Positions</span>
599
+ </button>
600
+ ` : ''}
601
+ </div>
602
+
603
+ <div class="dropdown-divider"></div>
604
+
605
+ <div class="dropdown-items">
606
+ <button class="dropdown-item" data-action="settings" data-ref="menu-item">
607
+ <span class="item-icon">⚙️</span>
608
+ <span class="item-label">Settings</span>
609
+ </button>
610
+
611
+ <button class="dropdown-item disconnect" data-ref="disconnect-btn">
612
+ <span class="item-icon">🔌</span>
613
+ <span class="item-label">Disconnect</span>
614
+ </button>
615
+ </div>
616
+ </div>
617
+ `;
618
+ }
619
+
620
+ setupDOMEventListeners() {
621
+ console.log('[FloatingWalletButton] setupDOMEventListeners called, element:', this.element);
622
+ if (!this.element) return;
623
+
624
+ // Main button click handler - use querySelector directly since getRef only takes one param
625
+ const mainBtn = this.element.querySelector('.wallet-btn');
626
+ console.log('[FloatingWalletButton] mainBtn found:', mainBtn);
627
+
628
+ if (mainBtn) {
629
+ mainBtn.addEventListener('click', (e) => this.handleButtonClick(e));
630
+ }
631
+
632
+ // Menu item click handlers
633
+ const menuItems = Array.from(this.element.querySelectorAll('.dropdown-item[data-route], .dropdown-item[data-action]'));
634
+ menuItems.forEach(item => {
635
+ item.addEventListener('click', (e) => {
636
+ e.preventDefault();
637
+ e.stopPropagation();
638
+
639
+ const route = item.getAttribute('data-route');
640
+ const action = item.getAttribute('data-action');
641
+
642
+ if (route) {
643
+ this.handleMenuItemClick(route);
644
+ } else if (action === 'settings') {
645
+ this.setState({ menuOpen: false });
646
+ }
647
+ });
648
+ });
649
+
650
+ // Disconnect button handler
651
+ const disconnectBtn = this.element.querySelector('.dropdown-item.disconnect');
652
+ if (disconnectBtn) {
653
+ disconnectBtn.addEventListener('click', (e) => this.handleDisconnect(e));
654
+ }
655
+ }
656
+
657
+ onStateUpdate(oldState, newState) {
658
+ // Re-setup DOM listeners when state changes
659
+ if (oldState.menuOpen !== newState.menuOpen) {
660
+ this.setTimeout(() => {
661
+ this.setupDOMEventListeners();
662
+ }, 0);
663
+ }
664
+ }
665
+
666
+ onUnmount() {
667
+ // Clean up wallet modal
668
+ if (this.walletModal) {
669
+ this.walletModal.hide();
670
+ this.walletModal = null;
671
+ }
672
+ }
673
+
674
+ escapeHtml(text) {
675
+ if (!text) return '';
676
+ const div = document.createElement('div');
677
+ div.textContent = text;
678
+ return div.innerHTML;
679
+ }
680
+ }