decharge-scout 1.6.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();
@@ -344,14 +373,14 @@ async function main(options) {
344
373
  }
345
374
 
346
375
  // Initialize points system
347
- initializePoints(wallet.publicKey.toBase58());
348
- const currentPoints = getPoints(wallet.publicKey.toBase58());
376
+ initializePoints(walletAddress);
377
+ const currentPoints = getPoints(walletAddress);
349
378
  console.log(chalk.magenta(`⭐ Current Points: ${currentPoints}`));
350
379
 
351
- // Stake SOL
380
+ // Stake SOL (via browser wallet)
352
381
  console.log(chalk.yellow(`\nšŸ’° Staking ${STAKE_AMOUNT} SOL for anti-spam/gas...`));
353
382
  const stakeSpinner = ora('Submitting stake transaction...').start();
354
- stakeTransactionSignature = await stakeSOL(wallet, STAKE_AMOUNT);
383
+ stakeTransactionSignature = await mockStake(STAKE_AMOUNT);
355
384
  stakeSpinner.succeed(chalk.green(`Stake successful! TX: ${stakeTransactionSignature}`));
356
385
 
357
386
  console.log(chalk.cyan('\nšŸ”„ Starting query cycle (runs every 15 minutes)...'));
@@ -365,14 +394,14 @@ async function main(options) {
365
394
  if (totalRuns > 0) {
366
395
  console.log(chalk.blue('šŸ’ø Refunding stake...'));
367
396
  try {
368
- const refundTx = await refundStake(wallet, STAKE_AMOUNT);
397
+ const refundTx = await refundStake(STAKE_AMOUNT);
369
398
  console.log(chalk.green(`Refund successful! TX: ${refundTx}`));
370
399
  } catch (error) {
371
400
  console.error(chalk.red(`Refund failed: ${error.message}`));
372
401
  }
373
402
  }
374
403
 
375
- const finalPoints = getPoints(wallet.publicKey.toBase58());
404
+ const finalPoints = getPoints(walletAddress);
376
405
  console.log(chalk.magenta(`\n⭐ Final Points: ${finalPoints}`));
377
406
  console.log(chalk.cyan(`šŸ“Š Total Runs: ${totalRuns}\n`));
378
407
 
@@ -469,7 +498,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
469
498
  headers: { 'Content-Type': 'application/json' },
470
499
  body: JSON.stringify({
471
500
  ...submissionData,
472
- wallet: wallet.publicKey.toBase58(),
501
+ wallet: walletAddress,
473
502
  run_number: totalRuns
474
503
  })
475
504
  });
@@ -495,8 +524,8 @@ async function runQueryCycle(wallet, agentName, location, options) {
495
524
  const bonusPoints = savings > 15 ? 2 : 0; // Bonus for good savings
496
525
  const totalPointsEarned = basePoints + bonusPoints;
497
526
 
498
- awardPoints(wallet.publicKey.toBase58(), totalPointsEarned);
499
- const currentPoints = getPoints(wallet.publicKey.toBase58());
527
+ awardPoints(walletAddress, totalPointsEarned);
528
+ const currentPoints = getPoints(walletAddress);
500
529
 
501
530
  console.log(chalk.magenta(`\n⭐ Earned ${totalPointsEarned} points! (${basePoints} base${bonusPoints > 0 ? ` + ${bonusPoints} bonus` : ''})`));
502
531
  console.log(chalk.magenta(`⭐ Total Points: ${currentPoints}`));
@@ -505,7 +534,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
505
534
  if (options.premium && totalRuns % 3 === 0) {
506
535
  console.log(chalk.yellow('\nšŸ”’ Premium Feature Available!'));
507
536
  try {
508
- const premiumData = await purchasePremiumData(wallet);
537
+ const premiumData = await purchasePremiumData(walletAddress);
509
538
  console.log(chalk.green(`Premium forecast data: ${JSON.stringify(premiumData)}`));
510
539
  } catch (error) {
511
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.6.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
+ }
@@ -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
+ }