web3-tools-mcp 1.3.1 → 1.3.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.
- package/README.md +40 -3
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/signatures.d.ts +18 -0
- package/dist/tools/signatures.d.ts.map +1 -1
- package/dist/tools/signatures.js +26 -1
- package/dist/tools/signatures.js.map +1 -1
- package/dist/tools/transactions.d.ts +111 -0
- package/dist/tools/transactions.d.ts.map +1 -0
- package/dist/tools/transactions.js +198 -0
- package/dist/tools/transactions.js.map +1 -0
- package/dist/wallet-server.d.ts +35 -0
- package/dist/wallet-server.d.ts.map +1 -0
- package/dist/wallet-server.js +232 -0
- package/dist/wallet-server.js.map +1 -0
- package/package.json +8 -1
- package/public/wallet-app.js +677 -0
- package/public/wallet.html +723 -0
- package/src/index.ts +8 -0
- package/src/tools/index.ts +3 -1
- package/src/tools/signatures.ts +34 -1
- package/src/tools/transactions.ts +246 -0
- package/src/wallet-server.ts +283 -0
|
@@ -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
|
+
});
|