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 +46 -182
- package/package.json +7 -5
- package/public/index.html +373 -0
- package/src/browser-wallet.js +195 -0
- package/src/wallet-server.js +183 -0
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 {
|
|
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
|
-
//
|
|
288
|
-
|
|
289
|
-
const
|
|
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
|
-
//
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
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(
|
|
348
|
-
const currentPoints = getPoints(
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
499
|
-
const currentPoints = getPoints(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|