web3-tools-mcp 1.3.0 → 1.3.2

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,677 @@
1
+ // State Management
2
+ const state = {
3
+ account: null,
4
+ provider: null,
5
+ ws: null,
6
+ currentRequest: null,
7
+ chainId: null,
8
+ chainName: null,
9
+ balance: null
10
+ };
11
+
12
+ // Chain configurations (wallet providers will use their default RPCs)
13
+ const CHAIN_CONFIGS = {
14
+ 'mainnet': { chainId: '0x1', name: 'Ethereum', explorer: 'https://etherscan.io' },
15
+ 'arbitrum': { chainId: '0xa4b1', name: 'Arbitrum', explorer: 'https://arbiscan.io' },
16
+ 'avalanche': { chainId: '0xa86a', name: 'Avalanche', explorer: 'https://snowtrace.io' },
17
+ 'base': { chainId: '0x2105', name: 'Base', explorer: 'https://basescan.org' },
18
+ 'bnb': { chainId: '0x38', name: 'BNB Chain', explorer: 'https://bscscan.com' },
19
+ 'gnosis': { chainId: '0x64', name: 'Gnosis', explorer: 'https://gnosisscan.io' },
20
+ 'sonic': { chainId: '0x92', name: 'Sonic', explorer: 'https://sonicscan.org' },
21
+ 'optimism': { chainId: '0xa', name: 'Optimism', explorer: 'https://optimistic.etherscan.io' },
22
+ 'polygon': { chainId: '0x89', name: 'Polygon', explorer: 'https://polygonscan.com' },
23
+ 'zksync': { chainId: '0x144', name: 'zkSync Era', explorer: 'https://explorer.zksync.io' },
24
+ 'linea': { chainId: '0xe708', name: 'Linea', explorer: 'https://lineascan.build' },
25
+ 'unichain': { chainId: '0x82', name: 'Unichain', explorer: 'https://unichain.org' }
26
+ };
27
+
28
+ // Transaction History Manager
29
+ class TransactionHistory {
30
+ constructor() {
31
+ this.storageKey = 'web3_tx_history';
32
+ this.maxItems = 20;
33
+ }
34
+
35
+ getAll() {
36
+ try {
37
+ const data = localStorage.getItem(this.storageKey);
38
+ return data ? JSON.parse(data) : [];
39
+ } catch (e) {
40
+ console.error('Failed to load transaction history:', e);
41
+ return [];
42
+ }
43
+ }
44
+
45
+ add(tx) {
46
+ const history = this.getAll();
47
+ history.unshift({
48
+ ...tx,
49
+ timestamp: Date.now()
50
+ });
51
+
52
+ // Keep only recent transactions
53
+ if (history.length > this.maxItems) {
54
+ history.length = this.maxItems;
55
+ }
56
+
57
+ localStorage.setItem(this.storageKey, JSON.stringify(history));
58
+ this.render();
59
+ }
60
+
61
+ render() {
62
+ const history = this.getAll();
63
+ const container = document.getElementById('txHistoryList');
64
+ const historySection = document.getElementById('txHistory');
65
+
66
+ if (history.length === 0) {
67
+ historySection.classList.add('hidden');
68
+ return;
69
+ }
70
+
71
+ historySection.classList.remove('hidden');
72
+ container.innerHTML = history.map(tx => this.renderItem(tx)).join('');
73
+ }
74
+
75
+ renderItem(tx) {
76
+ const date = new Date(tx.timestamp).toLocaleString();
77
+ const statusClass = tx.status || 'pending';
78
+ const explorerUrl = this.getExplorerUrl(tx.chain, tx.hash);
79
+
80
+ return `
81
+ <div class="tx-history-item ${statusClass}">
82
+ <div class="tx-history-header">
83
+ <span class="tx-history-function">${tx.function || 'Transaction'}</span>
84
+ <span class="tx-history-status ${statusClass}">${statusClass.toUpperCase()}</span>
85
+ </div>
86
+ <div class="tx-history-details">
87
+ <div>${tx.chain} • ${date}</div>
88
+ ${tx.contract ? `<div>Contract: ${this.formatAddress(tx.contract)}</div>` : ''}
89
+ </div>
90
+ ${tx.hash ? `<a href="${explorerUrl}" target="_blank" class="tx-history-link">View on Explorer →</a>` : ''}
91
+ </div>
92
+ `;
93
+ }
94
+
95
+ formatAddress(addr) {
96
+ return `${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}`;
97
+ }
98
+
99
+ getExplorerUrl(chainName, txHash) {
100
+ const config = CHAIN_CONFIGS[chainName];
101
+ return config ? `${config.explorer}/tx/${txHash}` : `https://etherscan.io/tx/${txHash}`;
102
+ }
103
+ }
104
+
105
+ const txHistory = new TransactionHistory();
106
+
107
+ // Error Message Parser
108
+ function parseError(error) {
109
+ const message = error.message || String(error);
110
+
111
+ // User rejected
112
+ if (message.includes('User rejected') || message.includes('User denied')) {
113
+ return {
114
+ title: 'Transaction Rejected',
115
+ message: 'You rejected the transaction in your wallet.',
116
+ type: 'warning'
117
+ };
118
+ }
119
+
120
+ // Insufficient funds
121
+ if (message.includes('insufficient funds') || message.includes('insufficient balance')) {
122
+ return {
123
+ title: 'Insufficient Balance',
124
+ message: 'Your wallet does not have enough funds to complete this transaction. Please add funds and try again.',
125
+ type: 'error'
126
+ };
127
+ }
128
+
129
+ // Gas estimation failed
130
+ if (message.includes('gas') && message.includes('estimation')) {
131
+ return {
132
+ title: 'Gas Estimation Failed',
133
+ message: 'Unable to estimate gas for this transaction. The transaction may fail or the contract may have restrictions.',
134
+ type: 'error'
135
+ };
136
+ }
137
+
138
+ // Network error
139
+ if (message.includes('network') || message.includes('connection')) {
140
+ return {
141
+ title: 'Network Error',
142
+ message: 'Failed to connect to the network. Please check your internet connection and try again.',
143
+ type: 'error'
144
+ };
145
+ }
146
+
147
+ // Chain mismatch
148
+ if (message.includes('chain')) {
149
+ return {
150
+ title: 'Wrong Network',
151
+ message: 'Please switch to the correct network in your wallet.',
152
+ type: 'warning'
153
+ };
154
+ }
155
+
156
+ // Default
157
+ return {
158
+ title: 'Transaction Failed',
159
+ message: message.length > 100 ? message.substring(0, 100) + '...' : message,
160
+ type: 'error'
161
+ };
162
+ }
163
+
164
+ // UI Functions
165
+ function showStatus(title, message, type = 'info') {
166
+ const statusEl = document.getElementById('statusMessage');
167
+ statusEl.className = `status ${type}`;
168
+ statusEl.innerHTML = `<strong>${title}:</strong> ${message}`;
169
+ statusEl.classList.remove('hidden');
170
+
171
+ if (type === 'success' || type === 'info') {
172
+ setTimeout(() => statusEl.classList.add('hidden'), 5000);
173
+ }
174
+ }
175
+
176
+ function updateChainBadge(connected = false, chainName = 'Not Connected') {
177
+ const badge = document.getElementById('chainBadge');
178
+ const indicator = badge.querySelector('.chain-indicator');
179
+ const nameSpan = document.getElementById('chainName');
180
+
181
+ if (connected) {
182
+ badge.classList.add('connected');
183
+ indicator.classList.add('active');
184
+ nameSpan.textContent = chainName;
185
+ } else {
186
+ badge.classList.remove('connected');
187
+ indicator.classList.remove('active');
188
+ nameSpan.textContent = chainName;
189
+ }
190
+ }
191
+
192
+ function toggleChainDropdown(event) {
193
+ event.stopPropagation();
194
+
195
+ if (!state.account) {
196
+ showStatus('Connect Wallet', 'Please connect your wallet first.', 'warning');
197
+ return;
198
+ }
199
+
200
+ const badge = document.getElementById('chainBadge');
201
+ const dropdown = document.getElementById('chainDropdown');
202
+
203
+ badge.classList.toggle('open');
204
+ dropdown.classList.toggle('show');
205
+
206
+ // Populate dropdown if empty
207
+ if (dropdown.children.length === 0) {
208
+ populateChainDropdown();
209
+ }
210
+ }
211
+
212
+ function populateChainDropdown() {
213
+ const dropdown = document.getElementById('chainDropdown');
214
+ dropdown.innerHTML = '';
215
+
216
+ Object.entries(CHAIN_CONFIGS).forEach(([key, config]) => {
217
+ const option = document.createElement('div');
218
+ option.className = 'chain-option';
219
+ option.textContent = config.name;
220
+ option.onclick = () => selectChain(key);
221
+
222
+ // Mark current chain as active
223
+ if (state.chainId === config.chainId) {
224
+ option.classList.add('active');
225
+ }
226
+
227
+ dropdown.appendChild(option);
228
+ });
229
+ }
230
+
231
+ async function selectChain(chainName) {
232
+ const dropdown = document.getElementById('chainDropdown');
233
+ const badge = document.getElementById('chainBadge');
234
+
235
+ dropdown.classList.remove('show');
236
+ badge.classList.remove('open');
237
+
238
+ if (chainName === Object.keys(CHAIN_CONFIGS).find(k => CHAIN_CONFIGS[k].chainId === state.chainId)) {
239
+ return; // Already on this chain
240
+ }
241
+
242
+ await switchToChain(chainName);
243
+ }
244
+
245
+ // Close dropdown when clicking outside
246
+ document.addEventListener('click', () => {
247
+ const dropdown = document.getElementById('chainDropdown');
248
+ const badge = document.getElementById('chainBadge');
249
+ if (dropdown && dropdown.classList.contains('show')) {
250
+ dropdown.classList.remove('show');
251
+ badge.classList.remove('open');
252
+ }
253
+ });
254
+
255
+ function log(message, type = 'info') {
256
+ const logEl = document.getElementById('log');
257
+ const entry = document.createElement('div');
258
+ entry.className = `log-entry ${type}`;
259
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
260
+ logEl.appendChild(entry);
261
+ logEl.scrollTop = logEl.scrollHeight;
262
+ }
263
+
264
+ // Wallet Functions
265
+
266
+ function toggleDarkMode() {
267
+ document.body.classList.toggle('dark-mode');
268
+ const isDark = document.body.classList.contains('dark-mode');
269
+ localStorage.setItem('darkMode', isDark ? 'true' : 'false');
270
+
271
+ // Update button icon
272
+ const button = document.querySelector('.dark-mode-toggle');
273
+ button.textContent = isDark ? '☀️' : '🌙';
274
+ }
275
+
276
+ // Load dark mode preference on startup
277
+ // Priority: localStorage > system preference
278
+ const savedDarkMode = localStorage.getItem('darkMode');
279
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
280
+ const shouldUseDarkMode = savedDarkMode !== null ? savedDarkMode === 'true' : prefersDark;
281
+
282
+ if (shouldUseDarkMode) {
283
+ document.body.classList.add('dark-mode');
284
+ // Save the initial preference if not already saved
285
+ if (savedDarkMode === null) {
286
+ localStorage.setItem('darkMode', 'true');
287
+ }
288
+ }
289
+
290
+ // Update button icon to match current state
291
+ const button = document.querySelector('.dark-mode-toggle');
292
+ if (button) {
293
+ button.textContent = shouldUseDarkMode ? '☀️' : '🌙';
294
+ }
295
+
296
+ async function connectWallet() {
297
+ try {
298
+ // Prioritize Rabby over MetaMask
299
+ let selectedProvider = null;
300
+ let walletName = 'Web3 Wallet';
301
+
302
+ if (window.rabby) {
303
+ selectedProvider = window.rabby;
304
+ walletName = 'Rabby';
305
+ } else if (window.ethereum) {
306
+ if (window.ethereum.isRabby) {
307
+ selectedProvider = window.ethereum;
308
+ walletName = 'Rabby';
309
+ } else if (window.ethereum.isMetaMask) {
310
+ selectedProvider = window.ethereum;
311
+ walletName = 'MetaMask';
312
+ } else {
313
+ selectedProvider = window.ethereum;
314
+ }
315
+ } else {
316
+ showStatus('No Wallet Found', 'Please install Rabby or MetaMask wallet.', 'error');
317
+ return;
318
+ }
319
+
320
+ log(`Requesting ${walletName} connection...`, 'info');
321
+ const accounts = await selectedProvider.request({
322
+ method: 'eth_requestAccounts'
323
+ });
324
+
325
+ state.account = accounts[0];
326
+ state.provider = selectedProvider;
327
+
328
+ if (!window.ethereum) {
329
+ window.ethereum = selectedProvider;
330
+ }
331
+
332
+ log(`Connected to ${walletName}: ${state.account}`, 'success');
333
+
334
+ // Save connection state
335
+ localStorage.setItem('walletConnected', 'true');
336
+ localStorage.setItem('walletAddress', state.account);
337
+
338
+ // Update UI - hide connect button
339
+ document.getElementById('connectBtn').classList.add('hidden');
340
+
341
+ await updateWalletInfo();
342
+ connectWebSocket();
343
+ txHistory.render();
344
+
345
+ // Listen for account changes
346
+ state.provider.on('accountsChanged', (accounts) => {
347
+ if (accounts.length === 0) {
348
+ log('Wallet disconnected', 'error');
349
+ localStorage.removeItem('walletConnected');
350
+ localStorage.removeItem('walletAddress');
351
+ location.reload();
352
+ } else {
353
+ state.account = accounts[0];
354
+ localStorage.setItem('walletAddress', state.account);
355
+ updateWalletInfo();
356
+ }
357
+ });
358
+
359
+ // Listen for chain changes
360
+ state.provider.on('chainChanged', () => {
361
+ log('Chain changed, reloading...', 'info');
362
+ location.reload();
363
+ });
364
+
365
+ showStatus('Connected', `Wallet connected successfully`, 'success');
366
+
367
+ } catch (error) {
368
+ const err = parseError(error);
369
+ log(`Connection failed: ${err.message}`, 'error');
370
+ showStatus(err.title, err.message, err.type);
371
+ }
372
+ }
373
+
374
+ async function updateWalletInfo() {
375
+ try {
376
+ state.chainId = await window.ethereum.request({ method: 'eth_chainId' });
377
+ const balance = await window.ethereum.request({
378
+ method: 'eth_getBalance',
379
+ params: [state.account, 'latest']
380
+ });
381
+
382
+ state.balance = parseInt(balance, 16) / 1e18;
383
+
384
+ // Find chain name
385
+ state.chainName = Object.entries(CHAIN_CONFIGS).find(
386
+ ([_, config]) => config.chainId === state.chainId
387
+ )?.[1]?.name || `Chain ${parseInt(state.chainId, 16)}`;
388
+
389
+ // Update UI
390
+ document.getElementById('address').textContent = `${state.account.substring(0, 6)}...${state.account.substring(state.account.length - 4)}`;
391
+ document.getElementById('networkName').textContent = `${state.chainName} (${parseInt(state.chainId, 16)})`;
392
+ document.getElementById('balance').textContent = `${state.balance.toFixed(4)} ETH`;
393
+ document.getElementById('walletInfo').classList.remove('hidden');
394
+
395
+ updateChainBadge(true, state.chainName);
396
+ } catch (error) {
397
+ log(`Failed to update wallet info: ${error.message}`, 'error');
398
+ }
399
+ }
400
+
401
+ // Chain Switching
402
+ async function switchToChain(chainName) {
403
+ try {
404
+ const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
405
+ const targetChain = CHAIN_CONFIGS[chainName];
406
+
407
+ if (!targetChain) {
408
+ log(`Unknown chain: ${chainName}`, 'error');
409
+ return;
410
+ }
411
+
412
+ if (currentChainId === targetChain.chainId) {
413
+ log(`Already on ${targetChain.name}`, 'info');
414
+ return;
415
+ }
416
+
417
+ log(`Switching to ${targetChain.name}...`, 'info');
418
+
419
+ try {
420
+ await window.ethereum.request({
421
+ method: 'wallet_switchEthereumChain',
422
+ params: [{ chainId: targetChain.chainId }],
423
+ });
424
+ log(`Switched to ${targetChain.name}`, 'success');
425
+ } catch (switchError) {
426
+ if (switchError.code === 4902) {
427
+ log(`Adding ${targetChain.name} to wallet...`, 'info');
428
+ await window.ethereum.request({
429
+ method: 'wallet_addEthereumChain',
430
+ params: [{
431
+ chainId: targetChain.chainId,
432
+ chainName: targetChain.name,
433
+ // Let wallet use its default RPC
434
+ }],
435
+ });
436
+ log(`Added ${targetChain.name}`, 'success');
437
+ } else {
438
+ throw switchError;
439
+ }
440
+ }
441
+
442
+ await updateWalletInfo();
443
+ } catch (error) {
444
+ const err = parseError(error);
445
+ log(`Failed to switch chain: ${err.message}`, 'error');
446
+ showStatus(err.title, err.message, err.type);
447
+ throw error;
448
+ }
449
+ }
450
+
451
+ // Transaction Preview
452
+ function renderTransactionPreview(request) {
453
+ const details = document.getElementById('txDetails');
454
+ const data = request.data;
455
+
456
+ let html = '';
457
+
458
+ if (request.type === 'send_transaction') {
459
+ html += `<div class="tx-param">
460
+ <span class="tx-param-name">To:</span>
461
+ <span class="tx-param-value">${data.to}</span>
462
+ </div>`;
463
+
464
+ if (data.value && data.value !== '0x0') {
465
+ const ethValue = parseInt(data.value, 16) / 1e18;
466
+ html += `<div class="tx-param">
467
+ <span class="tx-param-name">Value:</span>
468
+ <span class="tx-param-value">${ethValue.toFixed(6)} ETH</span>
469
+ </div>`;
470
+ }
471
+
472
+ if (data.data && data.data !== '0x') {
473
+ html += `<div class="tx-param">
474
+ <span class="tx-param-name">Data:</span>
475
+ <span class="tx-param-value">${data.data.substring(0, 66)}${data.data.length > 66 ? '...' : ''}</span>
476
+ </div>`;
477
+ }
478
+ } else if (request.type === 'sign_message') {
479
+ html += `<div class="tx-param">
480
+ <span class="tx-param-name">Message:</span>
481
+ <span class="tx-param-value">${data.message}</span>
482
+ </div>`;
483
+ }
484
+
485
+ details.innerHTML = html;
486
+ }
487
+
488
+ // WebSocket Connection
489
+ function connectWebSocket() {
490
+ // Close existing connection if any
491
+ if (state.ws && state.ws.readyState === WebSocket.OPEN) {
492
+ state.ws.close();
493
+ }
494
+
495
+ state.ws = new WebSocket('ws://localhost:3456');
496
+
497
+ state.ws.onopen = () => {
498
+ log('WebSocket connected', 'success');
499
+ showStatus('Ready', 'Ready to sign transactions', 'success');
500
+ };
501
+
502
+ state.ws.onmessage = async (event) => {
503
+ try {
504
+ state.currentRequest = JSON.parse(event.data);
505
+ log(`Received ${state.currentRequest.type} request on ${state.currentRequest.chain}`, 'info');
506
+
507
+ // Switch chain if needed
508
+ if (state.currentRequest.chain && state.currentRequest.chain !== 'any') {
509
+ await switchToChain(state.currentRequest.chain);
510
+ }
511
+
512
+ // Show transaction preview
513
+ renderTransactionPreview(state.currentRequest);
514
+ document.getElementById('txPreview').classList.remove('hidden');
515
+ showStatus('Pending', 'Transaction waiting for approval', 'warning');
516
+
517
+ } catch (error) {
518
+ const err = parseError(error);
519
+ log(`Error handling message: ${err.message}`, 'error');
520
+ showStatus(err.title, err.message, err.type);
521
+ }
522
+ };
523
+
524
+ state.ws.onerror = (error) => {
525
+ log('WebSocket error', 'error');
526
+ console.error(error);
527
+ };
528
+
529
+ state.ws.onclose = () => {
530
+ log('WebSocket disconnected', 'error');
531
+ showStatus('Disconnected', 'Connection to server lost. Reconnecting...', 'error');
532
+ setTimeout(connectWebSocket, 3000);
533
+ };
534
+ }
535
+
536
+ // Transaction Actions
537
+ async function approveTx() {
538
+ if (!state.currentRequest) return;
539
+
540
+ // Prevent double submission - clear current request immediately
541
+ const request = state.currentRequest;
542
+ state.currentRequest = null;
543
+
544
+ try {
545
+ // Ensure we have account
546
+ if (!state.account) {
547
+ const accounts = await window.ethereum.request({ method: 'eth_accounts' });
548
+ state.account = accounts[0];
549
+ }
550
+
551
+ if (!state.account) {
552
+ throw new Error('No account available. Please connect your wallet first.');
553
+ }
554
+
555
+ log('Sending transaction...', 'info');
556
+ let result;
557
+
558
+ if (request.type === 'send_transaction') {
559
+ const txData = {
560
+ ...request.data,
561
+ from: state.account
562
+ };
563
+
564
+ result = await window.ethereum.request({
565
+ method: 'eth_sendTransaction',
566
+ params: [txData]
567
+ });
568
+ } else if (request.type === 'sign_message') {
569
+ result = await window.ethereum.request({
570
+ method: 'personal_sign',
571
+ params: [request.data.message, state.account]
572
+ });
573
+ } else if (request.type === 'sign_typed_data') {
574
+ result = await window.ethereum.request({
575
+ method: 'eth_signTypedData_v4',
576
+ params: [state.account, JSON.stringify(request.data)]
577
+ });
578
+ }
579
+
580
+ log(`Transaction successful: ${result}`, 'success');
581
+ showStatus('Success', 'Transaction submitted successfully', 'success');
582
+
583
+ // Add to history
584
+ txHistory.add({
585
+ hash: result,
586
+ chain: request.chain,
587
+ function: request.data.data ? 'Contract Call' : 'Transfer',
588
+ contract: request.data.to,
589
+ status: 'success'
590
+ });
591
+
592
+ state.ws.send(JSON.stringify({
593
+ id: request.id,
594
+ success: true,
595
+ result
596
+ }));
597
+
598
+ document.getElementById('txPreview').classList.add('hidden');
599
+
600
+ } catch (error) {
601
+ const err = parseError(error);
602
+ log(`Transaction failed: ${err.message}`, 'error');
603
+ showStatus(err.title, err.message, err.type);
604
+
605
+ // Add to history as failed (unless it was a user rejection)
606
+ if (!error.message.includes('User rejected') && !error.message.includes('User denied')) {
607
+ txHistory.add({
608
+ chain: request.chain,
609
+ function: request.data.data ? 'Contract Call' : 'Transfer',
610
+ contract: request.data.to,
611
+ status: 'failed'
612
+ });
613
+ }
614
+
615
+ state.ws.send(JSON.stringify({
616
+ id: request.id,
617
+ success: false,
618
+ error: error.message
619
+ }));
620
+
621
+ document.getElementById('txPreview').classList.add('hidden');
622
+ state.currentRequest = null;
623
+ }
624
+ }
625
+
626
+ function rejectTx() {
627
+ if (!state.currentRequest) return;
628
+
629
+ log('Transaction rejected by user', 'info');
630
+ showStatus('Rejected', 'Transaction rejected', 'warning');
631
+
632
+ // Don't add rejected transactions to history
633
+
634
+ state.ws.send(JSON.stringify({
635
+ id: state.currentRequest.id,
636
+ success: false,
637
+ error: 'User rejected transaction'
638
+ }));
639
+
640
+ document.getElementById('txPreview').classList.add('hidden');
641
+ state.currentRequest = null;
642
+ }
643
+
644
+ // Auto-connect on load
645
+ window.addEventListener('load', async () => {
646
+ // Determine which provider to use
647
+ let availableProvider = null;
648
+ if (window.rabby) {
649
+ availableProvider = window.rabby;
650
+ } else if (window.ethereum) {
651
+ availableProvider = window.ethereum;
652
+ }
653
+
654
+ if (availableProvider) {
655
+ try {
656
+ const wasConnected = localStorage.getItem('walletConnected') === 'true';
657
+
658
+ if (wasConnected) {
659
+ log('Reconnecting to saved wallet...', 'info');
660
+ }
661
+
662
+ const accounts = await availableProvider.request({
663
+ method: 'eth_accounts'
664
+ });
665
+
666
+ if (accounts.length > 0 || wasConnected) {
667
+ connectWallet();
668
+ }
669
+ } catch (error) {
670
+ log('Auto-connect failed', 'error');
671
+ console.error(error);
672
+ }
673
+ }
674
+
675
+ // Load transaction history
676
+ txHistory.render();
677
+ });