decharge-scout 1.6.0 → 2.1.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
@@ -20,19 +20,17 @@ import { Command } from 'commander';
20
20
  import chalk from 'chalk';
21
21
  import ora from 'ora';
22
22
  import dotenv from 'dotenv';
23
- import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
24
23
  import { createInterface } from 'readline';
25
24
  import path from 'path';
26
25
  import { fileURLToPath } from 'url';
27
26
  import { dirname } from 'path';
28
- import { Keypair } from '@solana/web3.js';
29
- import os from 'os';
30
27
 
31
28
  // Load environment variables
32
29
  dotenv.config();
33
30
 
34
31
  // Import modules
35
- import { loadWallet, stakeSOL, refundStake } from './src/wallet.js';
32
+ import { startWalletServer, openWalletConnection, waitForWalletConnection, getConnectedWallet } from './src/wallet-server.js';
33
+ import { setConnectedWallet, getBalance, checkBalance, mockStake, refundStake } from './src/browser-wallet.js';
36
34
  import { fetchEnergyData, fetchElectricityMapsData } from './src/energy-data.js';
37
35
  import { findCheapestWindow, calculateSavings } from './src/optimizer.js';
38
36
  import { submitToOracle } from './src/oracle.js';
@@ -46,8 +44,6 @@ const __dirname = dirname(__filename);
46
44
  // Configuration
47
45
  const STAKE_AMOUNT = parseFloat(process.env.STAKE_AMOUNT || '0.01');
48
46
  const CYCLE_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
49
- // Use current working directory for wallet by default (not package installation dir)
50
- const DEFAULT_WALLET_PATH = path.join(process.cwd(), 'wallet.json');
51
47
 
52
48
  // Global state
53
49
  let isRunning = true;
@@ -70,165 +66,6 @@ function generateAgentName() {
70
66
  return `Agent-${randomId}`;
71
67
  }
72
68
 
73
- /**
74
- * Search for existing Solana wallets in common locations
75
- */
76
- function findExistingWallets() {
77
- const wallets = [];
78
- const homeDir = os.homedir();
79
- const cwd = process.cwd();
80
-
81
- const searchPaths = [
82
- // Current directory - most common
83
- path.join(cwd, 'wallet.json'),
84
- path.join(cwd, 'id.json'),
85
- path.join(cwd, 'keypair.json'),
86
- path.join(cwd, 'solana-wallet.json'),
87
- path.join(cwd, 'my-wallet.json'),
88
-
89
- // Package directory (for global installations)
90
- path.join(__dirname, 'wallet.json'),
91
- path.join(__dirname, 'id.json'),
92
-
93
- // Solana CLI default locations
94
- path.join(homeDir, '.config', 'solana', 'id.json'),
95
- path.join(homeDir, '.solana', 'id.json'),
96
- path.join(homeDir, '.solana', 'devnet.json'),
97
- path.join(homeDir, '.solana', 'testnet.json'),
98
-
99
- // Home directory
100
- path.join(homeDir, 'wallet.json'),
101
- path.join(homeDir, 'solana-wallet.json'),
102
-
103
- // Downloads (users often save here)
104
- path.join(homeDir, 'Downloads', 'wallet.json'),
105
- path.join(homeDir, 'Downloads', 'solana-wallet.json'),
106
- path.join(homeDir, 'Downloads', 'keypair.json'),
107
- ];
108
-
109
- for (const walletPath of searchPaths) {
110
- if (existsSync(walletPath)) {
111
- try {
112
- const keyData = JSON.parse(readFileSync(walletPath, 'utf-8'));
113
- if (Array.isArray(keyData) && keyData.length === 64) {
114
- const keypair = Keypair.fromSecretKey(Uint8Array.from(keyData));
115
- wallets.push({
116
- path: walletPath,
117
- publicKey: keypair.publicKey.toBase58(),
118
- name: path.basename(walletPath),
119
- location: path.dirname(walletPath)
120
- });
121
- }
122
- } catch (error) {
123
- // Skip invalid wallets
124
- }
125
- }
126
- }
127
-
128
- return wallets;
129
- }
130
-
131
- /**
132
- * Auto-create wallet if missing
133
- */
134
- async function ensureWallet(walletPath) {
135
- if (existsSync(walletPath)) {
136
- return walletPath;
137
- }
138
-
139
- console.log(chalk.yellow(`\n⚠️ No wallet found at ${walletPath}`));
140
-
141
- // Search for existing wallets
142
- console.log(chalk.blue('🔍 Searching for existing Solana wallets...'));
143
- const existingWallets = findExistingWallets();
144
-
145
- if (existingWallets.length > 0) {
146
- console.log(chalk.green(`\n✓ Found ${existingWallets.length} existing wallet(s):\n`));
147
-
148
- existingWallets.forEach((wallet, index) => {
149
- console.log(chalk.cyan(` ${index + 1}. ${wallet.name}`));
150
- console.log(chalk.gray(` Path: ${wallet.path}`));
151
- console.log(chalk.gray(` Public Key: ${wallet.publicKey}\n`));
152
- });
153
-
154
- const useExisting = await question('Use an existing wallet? (Enter number, or press Enter to create new): ');
155
-
156
- if (useExisting.trim() && !isNaN(useExisting)) {
157
- const index = parseInt(useExisting.trim()) - 1;
158
- if (index >= 0 && index < existingWallets.length) {
159
- const selectedWallet = existingWallets[index];
160
- console.log(chalk.green(`✓ Using wallet: ${selectedWallet.publicKey}`));
161
- return selectedWallet.path;
162
- } else {
163
- console.log(chalk.yellow('Invalid selection, creating new wallet...'));
164
- }
165
- }
166
- } else {
167
- console.log(chalk.yellow('⚠️ No existing wallets found in common locations.'));
168
- console.log(chalk.gray(' Searched: ~/.solana/id.json, ./wallet.json, ./id.json, etc.\n'));
169
- }
170
-
171
- const answer = await question('Create a new wallet? (Y/n): ');
172
-
173
- if (answer.toLowerCase() === 'n') {
174
- console.log(chalk.blue('\n📥 Import existing wallet'));
175
- const importAnswer = await question('Do you want to import an existing wallet private key? (Y/n): ');
176
-
177
- if (importAnswer.toLowerCase() === 'n') {
178
- console.log(chalk.blue('\nYou can create a wallet manually:'));
179
- console.log(chalk.gray(' solana-keygen new --outfile ./wallet.json'));
180
- console.log(chalk.gray(' Or run: node setup.js\n'));
181
- process.exit(1);
182
- }
183
-
184
- console.log(chalk.yellow('\n⚠️ Enter your Solana wallet private key'));
185
- console.log(chalk.gray('Format: [1,2,3,...] (array of 64 numbers)'));
186
- const privateKeyInput = await question('Private key: ');
187
-
188
- try {
189
- // Parse the private key
190
- const privateKey = JSON.parse(privateKeyInput.trim());
191
-
192
- // Validate it's an array of numbers
193
- if (!Array.isArray(privateKey) || privateKey.length !== 64) {
194
- throw new Error('Invalid private key format');
195
- }
196
-
197
- // Create keypair to validate
198
- const keypair = Keypair.fromSecretKey(Uint8Array.from(privateKey));
199
-
200
- // Save to file
201
- writeFileSync(walletPath, JSON.stringify(privateKey));
202
-
203
- console.log(chalk.green(`✓ Wallet imported: ${keypair.publicKey.toBase58()}`));
204
- console.log(chalk.blue(`📁 Wallet saved to: ${walletPath}`));
205
- return walletPath;
206
- } catch (error) {
207
- console.log(chalk.red(`\n❌ Invalid private key: ${error.message}`));
208
- console.log(chalk.gray('Expected format: [1,2,3,...] (array of 64 numbers)\n'));
209
- process.exit(1);
210
- }
211
- }
212
-
213
- console.log(chalk.blue('Generating new wallet...'));
214
-
215
- const keypair = Keypair.generate();
216
- const secretKey = Array.from(keypair.secretKey);
217
-
218
- writeFileSync(walletPath, JSON.stringify(secretKey));
219
-
220
- console.log(chalk.green(`✓ Wallet created: ${keypair.publicKey.toBase58()}`));
221
- console.log(chalk.blue(`📁 Wallet saved to: ${walletPath}`));
222
- console.log(chalk.yellow('⚠️ IMPORTANT: Backup this wallet file!'));
223
- console.log(chalk.blue(`\nYou need devnet SOL. Get it from:`));
224
- console.log(chalk.gray(' solana airdrop 1 ' + keypair.publicKey.toBase58() + ' --url devnet'));
225
- console.log(chalk.gray(' Or visit: https://faucet.solana.com/\n'));
226
-
227
- await question('Press Enter after funding your wallet...');
228
-
229
- return walletPath;
230
- }
231
-
232
69
  /**
233
70
  * Auto-configure .env if needed
234
71
  */
@@ -284,14 +121,41 @@ async function main(options) {
284
121
  // Auto-configure environment
285
122
  await ensureEnvironment();
286
123
 
287
- // Auto-handle wallet
288
- const walletPath = options.wallet || DEFAULT_WALLET_PATH;
289
- const confirmedWalletPath = await ensureWallet(walletPath);
124
+ // Start wallet server
125
+ console.log(chalk.blue('🌐 Starting browser wallet connection...'));
126
+ const server = await startWalletServer();
127
+
128
+ if (!server) {
129
+ console.log(chalk.red('❌ Failed to start wallet server'));
130
+ process.exit(1);
131
+ }
132
+
133
+ // Open browser for wallet connection
134
+ await openWalletConnection();
290
135
 
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()}`));
136
+ // Wait for wallet connection
137
+ const walletSpinner = ora('Waiting for wallet connection in browser...').start();
138
+ const walletAddress = await waitForWalletConnection();
139
+ walletSpinner.succeed(chalk.green(`✓ Wallet connected: ${walletAddress}`));
140
+
141
+ // Set connected wallet
142
+ setConnectedWallet(walletAddress);
143
+
144
+ // Check wallet balance
145
+ const balanceSpinner = ora('Checking wallet balance...').start();
146
+ const balance = await getBalance();
147
+ balanceSpinner.succeed(chalk.green(`💰 Wallet Balance: ${balance.toFixed(4)} SOL`));
148
+
149
+ // Verify sufficient balance
150
+ try {
151
+ await checkBalance(STAKE_AMOUNT + 0.001); // stake + fees
152
+ } catch (error) {
153
+ console.log(chalk.red(`\n❌ ${error.message}`));
154
+ console.log(chalk.yellow(`\nPlease fund your wallet and try again:`));
155
+ console.log(chalk.blue(`https://faucet.solana.com/`));
156
+ console.log(chalk.gray(`Wallet: ${walletAddress}\n`));
157
+ process.exit(1);
158
+ }
295
159
 
296
160
  // Set agent name
297
161
  const agentName = options.agentName || generateAgentName();
@@ -344,14 +208,14 @@ async function main(options) {
344
208
  }
345
209
 
346
210
  // Initialize points system
347
- initializePoints(wallet.publicKey.toBase58());
348
- const currentPoints = getPoints(wallet.publicKey.toBase58());
211
+ initializePoints(walletAddress);
212
+ const currentPoints = getPoints(walletAddress);
349
213
  console.log(chalk.magenta(`⭐ Current Points: ${currentPoints}`));
350
214
 
351
- // Stake SOL
215
+ // Stake SOL (via browser wallet)
352
216
  console.log(chalk.yellow(`\n💰 Staking ${STAKE_AMOUNT} SOL for anti-spam/gas...`));
353
217
  const stakeSpinner = ora('Submitting stake transaction...').start();
354
- stakeTransactionSignature = await stakeSOL(wallet, STAKE_AMOUNT);
218
+ stakeTransactionSignature = await mockStake(STAKE_AMOUNT);
355
219
  stakeSpinner.succeed(chalk.green(`Stake successful! TX: ${stakeTransactionSignature}`));
356
220
 
357
221
  console.log(chalk.cyan('\n🔄 Starting query cycle (runs every 15 minutes)...'));
@@ -365,14 +229,14 @@ async function main(options) {
365
229
  if (totalRuns > 0) {
366
230
  console.log(chalk.blue('💸 Refunding stake...'));
367
231
  try {
368
- const refundTx = await refundStake(wallet, STAKE_AMOUNT);
232
+ const refundTx = await refundStake(STAKE_AMOUNT);
369
233
  console.log(chalk.green(`Refund successful! TX: ${refundTx}`));
370
234
  } catch (error) {
371
235
  console.error(chalk.red(`Refund failed: ${error.message}`));
372
236
  }
373
237
  }
374
238
 
375
- const finalPoints = getPoints(wallet.publicKey.toBase58());
239
+ const finalPoints = getPoints(walletAddress);
376
240
  console.log(chalk.magenta(`\n⭐ Final Points: ${finalPoints}`));
377
241
  console.log(chalk.cyan(`📊 Total Runs: ${totalRuns}\n`));
378
242
 
@@ -469,7 +333,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
469
333
  headers: { 'Content-Type': 'application/json' },
470
334
  body: JSON.stringify({
471
335
  ...submissionData,
472
- wallet: wallet.publicKey.toBase58(),
336
+ wallet: walletAddress,
473
337
  run_number: totalRuns
474
338
  })
475
339
  });
@@ -495,8 +359,8 @@ async function runQueryCycle(wallet, agentName, location, options) {
495
359
  const bonusPoints = savings > 15 ? 2 : 0; // Bonus for good savings
496
360
  const totalPointsEarned = basePoints + bonusPoints;
497
361
 
498
- awardPoints(wallet.publicKey.toBase58(), totalPointsEarned);
499
- const currentPoints = getPoints(wallet.publicKey.toBase58());
362
+ awardPoints(walletAddress, totalPointsEarned);
363
+ const currentPoints = getPoints(walletAddress);
500
364
 
501
365
  console.log(chalk.magenta(`\n⭐ Earned ${totalPointsEarned} points! (${basePoints} base${bonusPoints > 0 ? ` + ${bonusPoints} bonus` : ''})`));
502
366
  console.log(chalk.magenta(`⭐ Total Points: ${currentPoints}`));
@@ -505,7 +369,7 @@ async function runQueryCycle(wallet, agentName, location, options) {
505
369
  if (options.premium && totalRuns % 3 === 0) {
506
370
  console.log(chalk.yellow('\n🔒 Premium Feature Available!'));
507
371
  try {
508
- const premiumData = await purchasePremiumData(wallet);
372
+ const premiumData = await purchasePremiumData(walletAddress);
509
373
  console.log(chalk.green(`Premium forecast data: ${JSON.stringify(premiumData)}`));
510
374
  } catch (error) {
511
375
  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.1.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,183 @@
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 WebSocket 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 { WebSocketServer } = WebSocket;
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ const app = express();
22
+ const PORT = 3838;
23
+
24
+ // Store connected wallet info
25
+ let connectedWallet = null;
26
+ let walletResolve = null;
27
+ let txResolve = null;
28
+
29
+ /**
30
+ * Start wallet connection server
31
+ */
32
+ export async function startWalletServer() {
33
+ return new Promise((resolve, reject) => {
34
+ const server = app.listen(PORT, () => {
35
+ console.log(chalk.blue(`🌐 Wallet server started on http://localhost:${PORT}`));
36
+ resolve(server);
37
+ }).on('error', (err) => {
38
+ if (err.code === 'EADDRINUSE') {
39
+ console.log(chalk.yellow(`⚠️ Port ${PORT} is already in use`));
40
+ resolve(null);
41
+ } else {
42
+ reject(err);
43
+ }
44
+ });
45
+
46
+ // WebSocket server for real-time communication
47
+ const wss = new WebSocketServer({ server });
48
+
49
+ wss.on('connection', (ws) => {
50
+ console.log(chalk.gray('WebSocket client connected'));
51
+
52
+ ws.on('message', (message) => {
53
+ try {
54
+ const data = JSON.parse(message);
55
+ handleWalletMessage(data, ws);
56
+ } catch (error) {
57
+ console.error('WebSocket message error:', error);
58
+ }
59
+ });
60
+ });
61
+
62
+ // Serve static files
63
+ app.use(express.static(path.join(__dirname, '..', 'public')));
64
+ app.use(express.json());
65
+
66
+ // Health check endpoint
67
+ app.get('/api/health', (req, res) => {
68
+ res.json({ status: 'ok', connected: !!connectedWallet });
69
+ });
70
+
71
+ // Get connected wallet
72
+ app.get('/api/wallet', (req, res) => {
73
+ if (connectedWallet) {
74
+ res.json({ connected: true, publicKey: connectedWallet });
75
+ } else {
76
+ res.json({ connected: false });
77
+ }
78
+ });
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Handle wallet messages from browser
84
+ */
85
+ function handleWalletMessage(data, ws) {
86
+ switch (data.type) {
87
+ case 'WALLET_CONNECTED':
88
+ connectedWallet = data.publicKey;
89
+ console.log(chalk.green(`✓ Wallet connected: ${data.publicKey}`));
90
+ if (walletResolve) {
91
+ walletResolve(data.publicKey);
92
+ walletResolve = null;
93
+ }
94
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Wallet connected successfully' }));
95
+ break;
96
+
97
+ case 'WALLET_DISCONNECTED':
98
+ connectedWallet = null;
99
+ console.log(chalk.yellow('Wallet disconnected'));
100
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Wallet disconnected' }));
101
+ break;
102
+
103
+ case 'TX_SIGNED':
104
+ console.log(chalk.green('✓ Transaction signed'));
105
+ if (txResolve) {
106
+ txResolve(data.signature);
107
+ txResolve = null;
108
+ }
109
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Transaction received' }));
110
+ break;
111
+
112
+ case 'TX_REJECTED':
113
+ console.log(chalk.red('✗ Transaction rejected by user'));
114
+ if (txResolve) {
115
+ txResolve(null);
116
+ txResolve = null;
117
+ }
118
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Transaction cancelled' }));
119
+ break;
120
+
121
+ case 'ERROR':
122
+ console.log(chalk.red(`Error: ${data.message}`));
123
+ ws.send(JSON.stringify({ type: 'ACK', message: 'Error received' }));
124
+ break;
125
+
126
+ default:
127
+ console.log(chalk.gray(`Unknown message type: ${data.type}`));
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Wait for wallet connection
133
+ */
134
+ export async function waitForWalletConnection() {
135
+ return new Promise((resolve) => {
136
+ if (connectedWallet) {
137
+ resolve(connectedWallet);
138
+ } else {
139
+ walletResolve = resolve;
140
+ }
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Request transaction signature from browser wallet
146
+ */
147
+ export async function requestTransactionSignature(transaction) {
148
+ return new Promise((resolve) => {
149
+ txResolve = resolve;
150
+ // Transaction will be sent via WebSocket
151
+ // Browser will prompt user to sign
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Open browser for wallet connection
157
+ */
158
+ export async function openWalletConnection() {
159
+ const url = `http://localhost:${PORT}`;
160
+ console.log(chalk.blue(`\n🌐 Opening browser for wallet connection...`));
161
+ console.log(chalk.gray(`URL: ${url}`));
162
+
163
+ try {
164
+ await open(url);
165
+ } catch (error) {
166
+ console.log(chalk.yellow(`\n⚠️ Could not auto-open browser`));
167
+ console.log(chalk.blue(`Please open manually: ${url}`));
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get connected wallet public key
173
+ */
174
+ export function getConnectedWallet() {
175
+ return connectedWallet;
176
+ }
177
+
178
+ /**
179
+ * Check if wallet is connected
180
+ */
181
+ export function isWalletConnected() {
182
+ return !!connectedWallet;
183
+ }