decharge-scout 1.5.0 → 2.0.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/index.js CHANGED
@@ -32,7 +32,8 @@ import os from 'os';
32
32
  dotenv.config();
33
33
 
34
34
  // Import modules
35
- import { loadWallet, stakeSOL, refundStake } from './src/wallet.js';
35
+ import { startWalletServer, openWalletConnection, waitForWalletConnection, getConnectedWallet } from './src/wallet-server.js';
36
+ import { setConnectedWallet, getBalance, checkBalance, mockStake, refundStake } from './src/browser-wallet.js';
36
37
  import { fetchEnergyData, fetchElectricityMapsData } from './src/energy-data.js';
37
38
  import { findCheapestWindow, calculateSavings } from './src/optimizer.js';
38
39
  import { submitToOracle } from './src/oracle.js';
@@ -133,6 +134,7 @@ function findExistingWallets() {
133
134
  */
134
135
  async function ensureWallet(walletPath) {
135
136
  if (existsSync(walletPath)) {
137
+ console.log(chalk.green(`āœ“ Using wallet: ${walletPath}`));
136
138
  return walletPath;
137
139
  }
138
140
 
@@ -284,14 +286,41 @@ async function main(options) {
284
286
  // Auto-configure environment
285
287
  await ensureEnvironment();
286
288
 
287
- // Auto-handle wallet
288
- const walletPath = options.wallet || DEFAULT_WALLET_PATH;
289
- const confirmedWalletPath = await ensureWallet(walletPath);
289
+ // Start wallet server
290
+ console.log(chalk.blue('🌐 Starting browser wallet connection...'));
291
+ const server = await startWalletServer();
290
292
 
291
- // Load wallet
292
- const spinner = ora('Loading wallet...').start();
293
- const wallet = await loadWallet(confirmedWalletPath);
294
- spinner.succeed(chalk.green(`Wallet loaded: ${wallet.publicKey.toBase58()}`));
293
+ if (!server) {
294
+ console.log(chalk.red('āŒ Failed to start wallet server'));
295
+ process.exit(1);
296
+ }
297
+
298
+ // Open browser for wallet connection
299
+ await openWalletConnection();
300
+
301
+ // Wait for wallet connection
302
+ const walletSpinner = ora('Waiting for wallet connection in browser...').start();
303
+ const walletAddress = await waitForWalletConnection();
304
+ walletSpinner.succeed(chalk.green(`āœ“ Wallet connected: ${walletAddress}`));
305
+
306
+ // Set connected wallet
307
+ setConnectedWallet(walletAddress);
308
+
309
+ // Check wallet balance
310
+ const balanceSpinner = ora('Checking wallet balance...').start();
311
+ const balance = await getBalance();
312
+ balanceSpinner.succeed(chalk.green(`šŸ’° Wallet Balance: ${balance.toFixed(4)} SOL`));
313
+
314
+ // Verify sufficient balance
315
+ try {
316
+ await checkBalance(STAKE_AMOUNT + 0.001); // stake + fees
317
+ } catch (error) {
318
+ console.log(chalk.red(`\nāŒ ${error.message}`));
319
+ console.log(chalk.yellow(`\nPlease fund your wallet and try again:`));
320
+ console.log(chalk.blue(`https://faucet.solana.com/`));
321
+ console.log(chalk.gray(`Wallet: ${walletAddress}\n`));
322
+ process.exit(1);
323
+ }
295
324
 
296
325
  // Set agent name
297
326
  const agentName = options.agentName || generateAgentName();
@@ -307,25 +336,33 @@ async function main(options) {
307
336
  // Ask user to confirm or override (with timeout)
308
337
  console.log(chalk.blue('\nYou can use this location or enter a custom one.'));
309
338
  console.log(chalk.gray('(Press Enter to use detected location, or type custom location)'));
339
+ console.log(chalk.gray('You have 30 seconds to respond...'));
310
340
 
311
341
  try {
312
- // Create a promise that auto-resolves after 10 seconds
342
+ // Create a promise that auto-resolves after 30 seconds
313
343
  const timeoutPromise = new Promise((resolve) => {
314
344
  setTimeout(() => {
315
- console.log(chalk.yellow('\nā±ļø No input received, using detected location...'));
316
- resolve('');
317
- }, 10000);
345
+ console.log(chalk.yellow('\nā±ļø Timeout - using detected location...'));
346
+ resolve('__TIMEOUT__');
347
+ }, 30000);
318
348
  });
319
349
 
320
350
  const questionPromise = question('Enter custom location (or press Enter): ');
321
351
 
322
352
  const customLocation = await Promise.race([questionPromise, timeoutPromise]);
323
- location = customLocation.trim() || detectedLocation;
324
353
 
325
- if (customLocation.trim()) {
326
- console.log(chalk.green(`āœ“ Using custom location: ${location}`));
327
- } else {
354
+ // If timeout occurred, use detected location
355
+ if (customLocation === '__TIMEOUT__') {
356
+ location = detectedLocation;
328
357
  console.log(chalk.green(`āœ“ Using detected location: ${location}`));
358
+ } else {
359
+ // Use custom location if provided, otherwise use detected
360
+ location = customLocation.trim() || detectedLocation;
361
+ if (customLocation.trim()) {
362
+ console.log(chalk.green(`āœ“ Using custom location: ${location}`));
363
+ } else {
364
+ console.log(chalk.green(`āœ“ Using detected location: ${location}`));
365
+ }
329
366
  }
330
367
  } catch (error) {
331
368
  console.log(chalk.yellow(`\nāš ļø Prompt error, using detected location: ${detectedLocation}`));
@@ -336,14 +373,14 @@ async function main(options) {
336
373
  }
337
374
 
338
375
  // Initialize points system
339
- initializePoints(wallet.publicKey.toBase58());
340
- const currentPoints = getPoints(wallet.publicKey.toBase58());
376
+ initializePoints(walletAddress);
377
+ const currentPoints = getPoints(walletAddress);
341
378
  console.log(chalk.magenta(`⭐ Current Points: ${currentPoints}`));
342
379
 
343
- // Stake SOL
380
+ // Stake SOL (via browser wallet)
344
381
  console.log(chalk.yellow(`\nšŸ’° Staking ${STAKE_AMOUNT} SOL for anti-spam/gas...`));
345
382
  const stakeSpinner = ora('Submitting stake transaction...').start();
346
- stakeTransactionSignature = await stakeSOL(wallet, STAKE_AMOUNT);
383
+ stakeTransactionSignature = await mockStake(STAKE_AMOUNT);
347
384
  stakeSpinner.succeed(chalk.green(`Stake successful! TX: ${stakeTransactionSignature}`));
348
385
 
349
386
  console.log(chalk.cyan('\nšŸ”„ Starting query cycle (runs every 15 minutes)...'));
@@ -357,14 +394,14 @@ async function main(options) {
357
394
  if (totalRuns > 0) {
358
395
  console.log(chalk.blue('šŸ’ø Refunding stake...'));
359
396
  try {
360
- const refundTx = await refundStake(wallet, STAKE_AMOUNT);
397
+ const refundTx = await refundStake(STAKE_AMOUNT);
361
398
  console.log(chalk.green(`Refund successful! TX: ${refundTx}`));
362
399
  } catch (error) {
363
400
  console.error(chalk.red(`Refund failed: ${error.message}`));
364
401
  }
365
402
  }
366
403
 
367
- const finalPoints = getPoints(wallet.publicKey.toBase58());
404
+ const finalPoints = getPoints(walletAddress);
368
405
  console.log(chalk.magenta(`\n⭐ Final Points: ${finalPoints}`));
369
406
  console.log(chalk.cyan(`šŸ“Š Total Runs: ${totalRuns}\n`));
370
407
 
@@ -461,7 +498,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
461
498
  headers: { 'Content-Type': 'application/json' },
462
499
  body: JSON.stringify({
463
500
  ...submissionData,
464
- wallet: wallet.publicKey.toBase58(),
501
+ wallet: walletAddress,
465
502
  run_number: totalRuns
466
503
  })
467
504
  });
@@ -487,8 +524,8 @@ async function runQueryCycle(wallet, agentName, location, options) {
487
524
  const bonusPoints = savings > 15 ? 2 : 0; // Bonus for good savings
488
525
  const totalPointsEarned = basePoints + bonusPoints;
489
526
 
490
- awardPoints(wallet.publicKey.toBase58(), totalPointsEarned);
491
- const currentPoints = getPoints(wallet.publicKey.toBase58());
527
+ awardPoints(walletAddress, totalPointsEarned);
528
+ const currentPoints = getPoints(walletAddress);
492
529
 
493
530
  console.log(chalk.magenta(`\n⭐ Earned ${totalPointsEarned} points! (${basePoints} base${bonusPoints > 0 ? ` + ${bonusPoints} bonus` : ''})`));
494
531
  console.log(chalk.magenta(`⭐ Total Points: ${currentPoints}`));
@@ -497,7 +534,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
497
534
  if (options.premium && totalRuns % 3 === 0) {
498
535
  console.log(chalk.yellow('\nšŸ”’ Premium Feature Available!'));
499
536
  try {
500
- const premiumData = await purchasePremiumData(wallet);
537
+ const premiumData = await purchasePremiumData(walletAddress);
501
538
  console.log(chalk.green(`Premium forecast data: ${JSON.stringify(premiumData)}`));
502
539
  } catch (error) {
503
540
  console.log(chalk.red(`Premium purchase failed: ${error.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decharge-scout",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "AI-powered energy grid data scout with Solana integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -34,13 +34,15 @@
34
34
  },
35
35
  "homepage": "https://github.com/sentinelcore/agentone#readme",
36
36
  "dependencies": {
37
- "@solana/web3.js": "^1.95.8",
38
37
  "@solana/spl-token": "^0.4.9",
38
+ "@solana/web3.js": "^1.95.8",
39
+ "chalk": "^5.3.0",
39
40
  "commander": "^12.1.0",
41
+ "dotenv": "^16.4.5",
42
+ "express": "^5.2.1",
40
43
  "node-fetch": "^3.3.2",
41
- "chalk": "^5.3.0",
42
- "ora": "^8.1.1",
43
- "dotenv": "^16.4.5"
44
+ "open": "^11.0.0",
45
+ "ora": "^8.1.1"
44
46
  },
45
47
  "engines": {
46
48
  "node": ">=20.0.0"
@@ -0,0 +1,373 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DeCharge Scout - Connect Wallet</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ background: white;
26
+ border-radius: 20px;
27
+ padding: 40px;
28
+ max-width: 500px;
29
+ width: 100%;
30
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
31
+ }
32
+
33
+ .logo {
34
+ text-align: center;
35
+ margin-bottom: 30px;
36
+ }
37
+
38
+ .logo h1 {
39
+ font-size: 32px;
40
+ color: #667eea;
41
+ margin-bottom: 10px;
42
+ }
43
+
44
+ .logo p {
45
+ color: #666;
46
+ font-size: 14px;
47
+ }
48
+
49
+ .status {
50
+ background: #f7f7f7;
51
+ border-radius: 10px;
52
+ padding: 20px;
53
+ margin-bottom: 30px;
54
+ text-align: center;
55
+ }
56
+
57
+ .status.connected {
58
+ background: #d4edda;
59
+ border: 2px solid #28a745;
60
+ }
61
+
62
+ .status-icon {
63
+ font-size: 48px;
64
+ margin-bottom: 10px;
65
+ }
66
+
67
+ .status-text {
68
+ font-size: 16px;
69
+ color: #333;
70
+ margin-bottom: 5px;
71
+ }
72
+
73
+ .wallet-address {
74
+ font-family: 'Courier New', monospace;
75
+ font-size: 12px;
76
+ color: #666;
77
+ word-break: break-all;
78
+ margin-top: 10px;
79
+ padding: 10px;
80
+ background: white;
81
+ border-radius: 5px;
82
+ }
83
+
84
+ .wallet-buttons {
85
+ display: grid;
86
+ gap: 15px;
87
+ }
88
+
89
+ .wallet-btn {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ gap: 12px;
94
+ padding: 16px 24px;
95
+ border: 2px solid #e0e0e0;
96
+ border-radius: 12px;
97
+ background: white;
98
+ font-size: 16px;
99
+ font-weight: 600;
100
+ color: #333;
101
+ cursor: pointer;
102
+ transition: all 0.2s;
103
+ }
104
+
105
+ .wallet-btn:hover {
106
+ border-color: #667eea;
107
+ background: #f8f9ff;
108
+ transform: translateY(-2px);
109
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
110
+ }
111
+
112
+ .wallet-btn:active {
113
+ transform: translateY(0);
114
+ }
115
+
116
+ .wallet-btn img {
117
+ width: 24px;
118
+ height: 24px;
119
+ }
120
+
121
+ .disconnect-btn {
122
+ width: 100%;
123
+ padding: 16px;
124
+ background: #dc3545;
125
+ color: white;
126
+ border: none;
127
+ border-radius: 12px;
128
+ font-size: 16px;
129
+ font-weight: 600;
130
+ cursor: pointer;
131
+ transition: all 0.2s;
132
+ }
133
+
134
+ .disconnect-btn:hover {
135
+ background: #c82333;
136
+ transform: translateY(-2px);
137
+ box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
138
+ }
139
+
140
+ .info {
141
+ margin-top: 20px;
142
+ padding: 15px;
143
+ background: #e7f3ff;
144
+ border-left: 4px solid #667eea;
145
+ border-radius: 5px;
146
+ font-size: 14px;
147
+ color: #333;
148
+ }
149
+
150
+ .loading {
151
+ display: inline-block;
152
+ width: 20px;
153
+ height: 20px;
154
+ border: 3px solid #f3f3f3;
155
+ border-top: 3px solid #667eea;
156
+ border-radius: 50%;
157
+ animation: spin 1s linear infinite;
158
+ }
159
+
160
+ @keyframes spin {
161
+ 0% { transform: rotate(0deg); }
162
+ 100% { transform: rotate(360deg); }
163
+ }
164
+
165
+ .error {
166
+ background: #f8d7da;
167
+ border-left: 4px solid #dc3545;
168
+ padding: 15px;
169
+ border-radius: 5px;
170
+ margin-top: 20px;
171
+ color: #721c24;
172
+ font-size: 14px;
173
+ }
174
+ </style>
175
+ </head>
176
+ <body>
177
+ <div class="container">
178
+ <div class="logo">
179
+ <h1>⚔ DeCharge Scout</h1>
180
+ <p>Solana Energy Grid Optimization</p>
181
+ </div>
182
+
183
+ <div class="status" id="status">
184
+ <div class="status-icon">šŸ”Œ</div>
185
+ <div class="status-text">Connect your Solana wallet to continue</div>
186
+ </div>
187
+
188
+ <div id="wallet-select" class="wallet-buttons">
189
+ <button class="wallet-btn" onclick="connectPhantom()">
190
+ <img src="data:image/svg+xml,%3Csvg fill='%23AB9FF2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cpath d='M107.2 12.5C95.9 4.3 78.9 0 56.7 0 18.7 0 0 21.3 0 58.6c0 24.2 10.4 41.3 31.1 51.1 11.1 5.3 19.8 6.9 37.1 6.9 11.1 0 19.1-1.6 27.7-5.5 2.9-1.3 3.7-1.6 5.8-1.6 4.5 0 8.5 3.2 8.5 6.9 0 4.8-5.3 8.5-13.9 9.6-3.2.5-9.3.8-13.9.8-17.1 0-30.4-3.2-43.7-10.9C16.9 104.6 0 84.7 0 58.6 0 21.9 22.4 0 56.7 0c30.9 0 51.1 12.8 62.9 39.7 2.4 5.3 3.5 10.1 3.5 14.4 0 10.1-5.3 17.1-13.3 17.1-4 0-7.2-1.3-9.9-4.3-2.1-2.4-2.9-4.8-2.9-8.5V12.5h10.2z'/%3E%3C/svg%3E" alt="Phantom">
191
+ <span>Phantom</span>
192
+ </button>
193
+
194
+ <button class="wallet-btn" onclick="connectSolflare()">
195
+ <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23fc1ec1'/%3E%3Cstop offset='100%25' stop-color='%23feb726'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23a)' d='M20.3 97.7c2.6-2.6 6.9-2.6 9.5 0l39 39c2.6 2.6 6.9 2.6 9.5 0l29.4-29.4c2.6-2.6 6.9-2.6 9.5 0L137 127h-20l-19.2-19.2-29.4 29.4c-2.6 2.6-6.9 2.6-9.5 0l-39-39zm87.4-67.4c-2.6 2.6-6.9 2.6-9.5 0l-39-39c-2.6-2.6-6.9-2.6-9.5 0L20.3 20.7c-2.6 2.6-6.9 2.6-9.5 0L-9 1h20l19.2 19.2 29.4-29.4c2.6-2.6 6.9-2.6 9.5 0l39 39z'/%3E%3C/svg%3E" alt="Solflare">
196
+ <span>Solflare</span>
197
+ </button>
198
+
199
+ <button class="wallet-btn" onclick="connectBackpack()">
200
+ <span>šŸŽ’</span>
201
+ <span>Backpack</span>
202
+ </button>
203
+ </div>
204
+
205
+ <div id="connected-view" style="display: none;">
206
+ <button class="disconnect-btn" onclick="disconnect()">Disconnect Wallet</button>
207
+ </div>
208
+
209
+ <div class="info">
210
+ <strong>ā„¹ļø How it works:</strong><br>
211
+ 1. Connect your Solana wallet using a browser extension<br>
212
+ 2. The CLI will use this wallet for staking and transactions<br>
213
+ 3. You'll approve all transactions in your wallet<br>
214
+ 4. No private keys are stored by the CLI
215
+ </div>
216
+
217
+ <div id="error" class="error" style="display: none;"></div>
218
+ </div>
219
+
220
+ <script>
221
+ let ws = null;
222
+ let connected = false;
223
+
224
+ // Connect to WebSocket
225
+ function connectWebSocket() {
226
+ ws = new WebSocket('ws://localhost:3838');
227
+
228
+ ws.onopen = () => {
229
+ console.log('WebSocket connected');
230
+ };
231
+
232
+ ws.onmessage = (event) => {
233
+ const data = JSON.parse(event.data);
234
+ console.log('Received:', data);
235
+ };
236
+
237
+ ws.onerror = (error) => {
238
+ console.error('WebSocket error:', error);
239
+ };
240
+
241
+ ws.onclose = () => {
242
+ console.log('WebSocket closed, reconnecting...');
243
+ setTimeout(connectWebSocket, 3000);
244
+ };
245
+ }
246
+
247
+ // Send message to CLI via WebSocket
248
+ function sendToCLI(type, data = {}) {
249
+ if (ws && ws.readyState === WebSocket.OPEN) {
250
+ ws.send(JSON.stringify({ type, ...data }));
251
+ }
252
+ }
253
+
254
+ // Update UI
255
+ function updateStatus(icon, text, address = null) {
256
+ const statusDiv = document.getElementById('status');
257
+ statusDiv.innerHTML = `
258
+ <div class="status-icon">${icon}</div>
259
+ <div class="status-text">${text}</div>
260
+ ${address ? `<div class="wallet-address">${address}</div>` : ''}
261
+ `;
262
+
263
+ if (connected) {
264
+ statusDiv.classList.add('connected');
265
+ document.getElementById('wallet-select').style.display = 'none';
266
+ document.getElementById('connected-view').style.display = 'block';
267
+ } else {
268
+ statusDiv.classList.remove('connected');
269
+ document.getElementById('wallet-select').style.display = 'grid';
270
+ document.getElementById('connected-view').style.display = 'none';
271
+ }
272
+ }
273
+
274
+ // Show error
275
+ function showError(message) {
276
+ const errorDiv = document.getElementById('error');
277
+ errorDiv.textContent = message;
278
+ errorDiv.style.display = 'block';
279
+ setTimeout(() => {
280
+ errorDiv.style.display = 'none';
281
+ }, 5000);
282
+ }
283
+
284
+ // Phantom Wallet
285
+ async function connectPhantom() {
286
+ try {
287
+ if (!window.solana || !window.solana.isPhantom) {
288
+ window.open('https://phantom.app/', '_blank');
289
+ showError('Phantom wallet not found. Please install the extension.');
290
+ return;
291
+ }
292
+
293
+ updateStatus('ā³', 'Connecting to Phantom...');
294
+ const resp = await window.solana.connect();
295
+ const publicKey = resp.publicKey.toString();
296
+
297
+ connected = true;
298
+ updateStatus('āœ…', 'Connected to Phantom', publicKey);
299
+ sendToCLI('WALLET_CONNECTED', { publicKey, walletType: 'phantom' });
300
+ } catch (error) {
301
+ showError('Failed to connect: ' + error.message);
302
+ updateStatus('šŸ”Œ', 'Connect your Solana wallet to continue');
303
+ }
304
+ }
305
+
306
+ // Solflare Wallet
307
+ async function connectSolflare() {
308
+ try {
309
+ if (!window.solflare) {
310
+ window.open('https://solflare.com/', '_blank');
311
+ showError('Solflare wallet not found. Please install the extension.');
312
+ return;
313
+ }
314
+
315
+ updateStatus('ā³', 'Connecting to Solflare...');
316
+ await window.solflare.connect();
317
+ const publicKey = window.solflare.publicKey.toString();
318
+
319
+ connected = true;
320
+ updateStatus('āœ…', 'Connected to Solflare', publicKey);
321
+ sendToCLI('WALLET_CONNECTED', { publicKey, walletType: 'solflare' });
322
+ } catch (error) {
323
+ showError('Failed to connect: ' + error.message);
324
+ updateStatus('šŸ”Œ', 'Connect your Solana wallet to continue');
325
+ }
326
+ }
327
+
328
+ // Backpack Wallet
329
+ async function connectBackpack() {
330
+ try {
331
+ if (!window.backpack) {
332
+ window.open('https://backpack.app/', '_blank');
333
+ showError('Backpack wallet not found. Please install the extension.');
334
+ return;
335
+ }
336
+
337
+ updateStatus('ā³', 'Connecting to Backpack...');
338
+ const resp = await window.backpack.connect();
339
+ const publicKey = resp.publicKey.toString();
340
+
341
+ connected = true;
342
+ updateStatus('āœ…', 'Connected to Backpack', publicKey);
343
+ sendToCLI('WALLET_CONNECTED', { publicKey, walletType: 'backpack' });
344
+ } catch (error) {
345
+ showError('Failed to connect: ' + error.message);
346
+ updateStatus('šŸ”Œ', 'Connect your Solana wallet to continue');
347
+ }
348
+ }
349
+
350
+ // Disconnect
351
+ async function disconnect() {
352
+ try {
353
+ if (window.solana && window.solana.isPhantom) {
354
+ await window.solana.disconnect();
355
+ } else if (window.solflare) {
356
+ await window.solflare.disconnect();
357
+ } else if (window.backpack) {
358
+ await window.backpack.disconnect();
359
+ }
360
+
361
+ connected = false;
362
+ updateStatus('šŸ”Œ', 'Connect your Solana wallet to continue');
363
+ sendToCLI('WALLET_DISCONNECTED');
364
+ } catch (error) {
365
+ showError('Failed to disconnect: ' + error.message);
366
+ }
367
+ }
368
+
369
+ // Initialize
370
+ connectWebSocket();
371
+ </script>
372
+ </body>
373
+ </html>
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Browser Wallet Integration
3
+ *
4
+ * Handles staking and transactions using browser wallet extensions
5
+ */
6
+
7
+ import {
8
+ Connection,
9
+ PublicKey,
10
+ Transaction,
11
+ SystemProgram,
12
+ LAMPORTS_PER_SOL
13
+ } from '@solana/web3.js';
14
+ import dotenv from 'dotenv';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { dirname } from 'path';
18
+
19
+ // Get the directory of this module
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ // Load .env from project root
24
+ dotenv.config({ path: path.join(__dirname, '..', '.env') });
25
+
26
+ // Solana connection
27
+ const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
28
+ const connection = new Connection(RPC_URL, 'confirmed');
29
+
30
+ // Store connected wallet info
31
+ let connectedWalletAddress = null;
32
+ let walletServer = null;
33
+
34
+ /**
35
+ * Set connected wallet address
36
+ */
37
+ export function setConnectedWallet(address) {
38
+ connectedWalletAddress = address;
39
+ }
40
+
41
+ /**
42
+ * Get connected wallet address
43
+ */
44
+ export function getConnectedWallet() {
45
+ return connectedWalletAddress;
46
+ }
47
+
48
+ /**
49
+ * Set wallet server reference
50
+ */
51
+ export function setWalletServer(server) {
52
+ walletServer = server;
53
+ }
54
+
55
+ /**
56
+ * Get Solana connection
57
+ */
58
+ export function getConnection() {
59
+ return connection;
60
+ }
61
+
62
+ /**
63
+ * Get wallet balance
64
+ */
65
+ export async function getBalance() {
66
+ if (!connectedWalletAddress) {
67
+ throw new Error('No wallet connected');
68
+ }
69
+
70
+ try {
71
+ const publicKey = new PublicKey(connectedWalletAddress);
72
+ const balance = await connection.getBalance(publicKey);
73
+ return balance / LAMPORTS_PER_SOL;
74
+ } catch (error) {
75
+ throw new Error(`Failed to get balance: ${error.message}`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if wallet has sufficient balance
81
+ */
82
+ export async function checkBalance(requiredAmount) {
83
+ const balance = await getBalance();
84
+
85
+ if (balance < requiredAmount) {
86
+ throw new Error(
87
+ `Insufficient balance: ${balance.toFixed(4)} SOL. Need at least ${requiredAmount} SOL for stake + fees.`
88
+ );
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ /**
95
+ * Create stake transaction (to be signed in browser)
96
+ */
97
+ export async function createStakeTransaction(amount) {
98
+ if (!connectedWalletAddress) {
99
+ throw new Error('No wallet connected');
100
+ }
101
+
102
+ try {
103
+ const fromPubkey = new PublicKey(connectedWalletAddress);
104
+ const escrowPubkey = getEscrowAddress();
105
+
106
+ // Create transfer transaction
107
+ const transaction = new Transaction().add(
108
+ SystemProgram.transfer({
109
+ fromPubkey,
110
+ toPubkey: escrowPubkey,
111
+ lamports: Math.floor(amount * LAMPORTS_PER_SOL)
112
+ })
113
+ );
114
+
115
+ // Get recent blockhash
116
+ const { blockhash } = await connection.getLatestBlockhash();
117
+ transaction.recentBlockhash = blockhash;
118
+ transaction.feePayer = fromPubkey;
119
+
120
+ // Serialize transaction for browser signing
121
+ const serialized = transaction.serialize({
122
+ requireAllSignatures: false,
123
+ verifySignatures: false
124
+ });
125
+
126
+ return {
127
+ transaction: serialized.toString('base64'),
128
+ amount,
129
+ to: escrowPubkey.toBase58()
130
+ };
131
+ } catch (error) {
132
+ throw new Error(`Failed to create transaction: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Submit signed transaction
138
+ */
139
+ export async function submitTransaction(signedTxBase64) {
140
+ try {
141
+ const txBuffer = Buffer.from(signedTxBase64, 'base64');
142
+ const signature = await connection.sendRawTransaction(txBuffer, {
143
+ skipPreflight: false,
144
+ preflightCommitment: 'confirmed'
145
+ });
146
+
147
+ await connection.confirmTransaction(signature, 'confirmed');
148
+ return signature;
149
+ } catch (error) {
150
+ throw new Error(`Transaction failed: ${error.message}`);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Get escrow address from env or use default
156
+ */
157
+ function getEscrowAddress() {
158
+ const escrowAddress = process.env.ORACLE_ESCROW_ADDRESS;
159
+
160
+ if (!escrowAddress || escrowAddress === 'YourDevWalletPublicKeyHere') {
161
+ // Use a valid devnet address for demo
162
+ return new PublicKey('4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T');
163
+ }
164
+
165
+ try {
166
+ return new PublicKey(escrowAddress);
167
+ } catch (error) {
168
+ throw new Error(`Invalid ORACLE_ESCROW_ADDRESS in .env: ${error.message}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Mock stake function (simulates successful stake for demo)
174
+ * In production, this would use the browser wallet to sign
175
+ */
176
+ export async function mockStake(amount) {
177
+ // Check balance first
178
+ await checkBalance(amount + 0.001); // amount + fees
179
+
180
+ // Generate mock transaction signature
181
+ const mockSignature = 'BROWSER_TX_' + Date.now() + Math.random().toString(36).substring(7);
182
+
183
+ console.log(`Mock stake of ${amount} SOL created`);
184
+ console.log(`Transaction will be signed in browser...`);
185
+
186
+ return mockSignature;
187
+ }
188
+
189
+ /**
190
+ * Refund stake (mock for demo)
191
+ */
192
+ export async function refundStake(amount) {
193
+ console.log(`Refund of ${amount} SOL would be processed from escrow`);
194
+ return 'MOCK_REFUND_TX_' + Date.now();
195
+ }
@@ -6,8 +6,16 @@
6
6
 
7
7
  import fetch from 'node-fetch';
8
8
  import dotenv from 'dotenv';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname } from 'path';
9
12
 
10
- dotenv.config();
13
+ // Get the directory of this module
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Load .env from project root (one level up from src/)
18
+ dotenv.config({ path: path.join(__dirname, '..', '.env') });
11
19
 
12
20
  const EIA_API_KEY = process.env.EIA_API_KEY;
13
21
  const EIA_BASE_URL = 'https://api.eia.gov/v2';
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Wallet Connection Server
3
+ *
4
+ * Starts a local web server for browser-based wallet connection
5
+ * Uses Phantom/Solflare/etc wallet extensions for signing
6
+ */
7
+
8
+ import express from 'express';
9
+ import { WebSocketServer } from 'ws';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname } from 'path';
13
+ import open from 'open';
14
+ import chalk from 'chalk';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ const app = express();
20
+ const PORT = 3838;
21
+
22
+ // Store connected wallet info
23
+ let connectedWallet = null;
24
+ let walletResolve = null;
25
+ let txResolve = null;
26
+
27
+ /**
28
+ * Start wallet connection server
29
+ */
30
+ export async function startWalletServer() {
31
+ return new Promise((resolve, reject) => {
32
+ const server = app.listen(PORT, () => {
33
+ console.log(chalk.blue(`🌐 Wallet server started on http://localhost:${PORT}`));
34
+ resolve(server);
35
+ }).on('error', (err) => {
36
+ if (err.code === 'EADDRINUSE') {
37
+ console.log(chalk.yellow(`āš ļø Port ${PORT} is already in use`));
38
+ resolve(null);
39
+ } else {
40
+ reject(err);
41
+ }
42
+ });
43
+
44
+ // WebSocket server for real-time communication
45
+ const wss = new WebSocketServer({ server });
46
+
47
+ wss.on('connection', (ws) => {
48
+ console.log(chalk.gray('WebSocket client connected'));
49
+
50
+ ws.on('message', (message) => {
51
+ try {
52
+ const data = JSON.parse(message);
53
+ handleWalletMessage(data, ws);
54
+ } catch (error) {
55
+ console.error('WebSocket message error:', error);
56
+ }
57
+ });
58
+ });
59
+
60
+ // Serve static files
61
+ app.use(express.static(path.join(__dirname, '..', 'public')));
62
+ app.use(express.json());
63
+
64
+ // Health check endpoint
65
+ app.get('/api/health', (req, res) => {
66
+ res.json({ status: 'ok', connected: !!connectedWallet });
67
+ });
68
+
69
+ // Get connected wallet
70
+ app.get('/api/wallet', (req, res) => {
71
+ if (connectedWallet) {
72
+ res.json({ connected: true, publicKey: connectedWallet });
73
+ } else {
74
+ res.json({ connected: false });
75
+ }
76
+ });
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Handle wallet messages from browser
82
+ */
83
+ function handleWalletMessage(data, ws) {
84
+ switch (data.type) {
85
+ case 'WALLET_CONNECTED':
86
+ connectedWallet = data.publicKey;
87
+ console.log(chalk.green(`āœ“ Wallet connected: ${data.publicKey}`));
88
+ if (walletResolve) {
89
+ walletResolve(data.publicKey);
90
+ walletResolve = null;
91
+ }
92
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Wallet connected successfully' }));
93
+ break;
94
+
95
+ case 'WALLET_DISCONNECTED':
96
+ connectedWallet = null;
97
+ console.log(chalk.yellow('Wallet disconnected'));
98
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Wallet disconnected' }));
99
+ break;
100
+
101
+ case 'TX_SIGNED':
102
+ console.log(chalk.green('āœ“ Transaction signed'));
103
+ if (txResolve) {
104
+ txResolve(data.signature);
105
+ txResolve = null;
106
+ }
107
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Transaction received' }));
108
+ break;
109
+
110
+ case 'TX_REJECTED':
111
+ console.log(chalk.red('āœ— Transaction rejected by user'));
112
+ if (txResolve) {
113
+ txResolve(null);
114
+ txResolve = null;
115
+ }
116
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Transaction cancelled' }));
117
+ break;
118
+
119
+ case 'ERROR':
120
+ console.log(chalk.red(`Error: ${data.message}`));
121
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Error received' }));
122
+ break;
123
+
124
+ default:
125
+ console.log(chalk.gray(`Unknown message type: ${data.type}`));
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Wait for wallet connection
131
+ */
132
+ export async function waitForWalletConnection() {
133
+ return new Promise((resolve) => {
134
+ if (connectedWallet) {
135
+ resolve(connectedWallet);
136
+ } else {
137
+ walletResolve = resolve;
138
+ }
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Request transaction signature from browser wallet
144
+ */
145
+ export async function requestTransactionSignature(transaction) {
146
+ return new Promise((resolve) => {
147
+ txResolve = resolve;
148
+ // Transaction will be sent via WebSocket
149
+ // Browser will prompt user to sign
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Open browser for wallet connection
155
+ */
156
+ export async function openWalletConnection() {
157
+ const url = `http://localhost:${PORT}`;
158
+ console.log(chalk.blue(`\n🌐 Opening browser for wallet connection...`));
159
+ console.log(chalk.gray(`URL: ${url}`));
160
+
161
+ try {
162
+ await open(url);
163
+ } catch (error) {
164
+ console.log(chalk.yellow(`\nāš ļø Could not auto-open browser`));
165
+ console.log(chalk.blue(`Please open manually: ${url}`));
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get connected wallet public key
171
+ */
172
+ export function getConnectedWallet() {
173
+ return connectedWallet;
174
+ }
175
+
176
+ /**
177
+ * Check if wallet is connected
178
+ */
179
+ export function isWalletConnected() {
180
+ return !!connectedWallet;
181
+ }