decharge-scout 1.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/setup.js ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DeCharge Scout - Interactive Setup Script
5
+ *
6
+ * Automates the complete setup process:
7
+ * - Install dependencies
8
+ * - Generate wallet
9
+ * - Request devnet airdrop
10
+ * - Configure .env with prompts
11
+ * - Global installation
12
+ *
13
+ * Usage: node setup.js
14
+ */
15
+
16
+ import { spawn, exec } from 'child_process';
17
+ import { promisify } from 'util';
18
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
19
+ import { createInterface } from 'readline';
20
+ import { Keypair, Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
21
+ import path from 'path';
22
+ import { fileURLToPath } from 'url';
23
+ import { dirname } from 'path';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+
28
+ const execAsync = promisify(exec);
29
+
30
+ // ANSI color codes (avoiding external dependencies)
31
+ const colors = {
32
+ reset: '\x1b[0m',
33
+ bright: '\x1b[1m',
34
+ red: '\x1b[31m',
35
+ green: '\x1b[32m',
36
+ yellow: '\x1b[33m',
37
+ blue: '\x1b[34m',
38
+ cyan: '\x1b[36m',
39
+ magenta: '\x1b[35m'
40
+ };
41
+
42
+ const log = {
43
+ info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
44
+ success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
45
+ error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
46
+ warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
47
+ title: (msg) => console.log(`\n${colors.cyan}${colors.bright}${msg}${colors.reset}\n`)
48
+ };
49
+
50
+ // Create readline interface for prompts
51
+ const rl = createInterface({
52
+ input: process.stdin,
53
+ output: process.stdout
54
+ });
55
+
56
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
57
+
58
+ /**
59
+ * Main setup function
60
+ */
61
+ async function main() {
62
+ console.clear();
63
+ log.title('🔋 DeCharge Scout - Interactive Setup');
64
+ console.log('This will set up everything you need to run DeCharge Scout.\n');
65
+
66
+ try {
67
+ // Step 1: Check Node.js version
68
+ await checkNodeVersion();
69
+
70
+ // Step 2: Install dependencies
71
+ await installDependencies();
72
+
73
+ // Step 3: Configure environment
74
+ await configureEnvironment();
75
+
76
+ // Step 4: Setup wallet
77
+ const walletPath = await setupWallet();
78
+
79
+ // Step 5: Fund wallet
80
+ await fundWallet(walletPath);
81
+
82
+ // Step 6: Global installation
83
+ await globalInstallation();
84
+
85
+ // Step 7: Final summary
86
+ showFinalSummary(walletPath);
87
+
88
+ rl.close();
89
+ process.exit(0);
90
+ } catch (error) {
91
+ log.error(`Setup failed: ${error.message}`);
92
+ rl.close();
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check Node.js version
99
+ */
100
+ async function checkNodeVersion() {
101
+ log.info('Checking Node.js version...');
102
+
103
+ const version = process.version;
104
+ const majorVersion = parseInt(version.slice(1).split('.')[0]);
105
+
106
+ if (majorVersion < 20) {
107
+ throw new Error(`Node.js v20 or higher required. Current: ${version}`);
108
+ }
109
+
110
+ log.success(`Node.js ${version} detected`);
111
+ }
112
+
113
+ /**
114
+ * Install npm dependencies
115
+ */
116
+ async function installDependencies() {
117
+ log.info('Installing dependencies...');
118
+
119
+ // Check if node_modules exists
120
+ if (existsSync(path.join(__dirname, 'node_modules'))) {
121
+ const answer = await question('Dependencies already installed. Reinstall? (y/N): ');
122
+ if (answer.toLowerCase() !== 'y') {
123
+ log.info('Skipping dependency installation');
124
+ return;
125
+ }
126
+ }
127
+
128
+ return new Promise((resolve, reject) => {
129
+ const npm = spawn('npm', ['install'], {
130
+ cwd: __dirname,
131
+ stdio: 'inherit'
132
+ });
133
+
134
+ npm.on('close', (code) => {
135
+ if (code !== 0) {
136
+ reject(new Error('npm install failed'));
137
+ } else {
138
+ log.success('Dependencies installed');
139
+ resolve();
140
+ }
141
+ });
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Configure .env file
147
+ */
148
+ async function configureEnvironment() {
149
+ log.info('Configuring environment variables...');
150
+
151
+ const envPath = path.join(__dirname, '.env');
152
+ let envContent = {};
153
+
154
+ // Load existing .env if it exists
155
+ if (existsSync(envPath)) {
156
+ const answer = await question('.env file exists. Reconfigure? (y/N): ');
157
+ if (answer.toLowerCase() !== 'y') {
158
+ log.info('Keeping existing .env configuration');
159
+ return;
160
+ }
161
+
162
+ // Parse existing .env
163
+ const existing = readFileSync(envPath, 'utf-8');
164
+ existing.split('\n').forEach(line => {
165
+ const [key, value] = line.split('=');
166
+ if (key && value) {
167
+ envContent[key.trim()] = value.trim();
168
+ }
169
+ });
170
+ }
171
+
172
+ console.log('\n--- Environment Configuration ---\n');
173
+
174
+ // EIA API Key (required)
175
+ console.log('EIA API Key (required for energy data)');
176
+ console.log('Get yours at: https://www.eia.gov/opendata/register.php');
177
+ const eiaKey = await question(`EIA_API_KEY [${envContent.EIA_API_KEY || 'none'}]: `);
178
+ if (eiaKey) envContent.EIA_API_KEY = eiaKey;
179
+
180
+ if (!envContent.EIA_API_KEY || envContent.EIA_API_KEY === 'your_eia_api_key_here') {
181
+ log.warn('No EIA API key provided. You can add it later to .env');
182
+ envContent.EIA_API_KEY = 'your_eia_api_key_here';
183
+ }
184
+
185
+ // Solana configuration
186
+ const network = await question('Solana network [devnet]: ') || 'devnet';
187
+ envContent.SOLANA_NETWORK = network;
188
+
189
+ const rpcUrl = await question('Solana RPC URL [https://api.devnet.solana.com]: ') || 'https://api.devnet.solana.com';
190
+ envContent.SOLANA_RPC_URL = rpcUrl;
191
+
192
+ // Optional: Dashboard API
193
+ const dashboardUrl = await question('Dashboard API URL (optional, press Enter to skip): ');
194
+ if (dashboardUrl) {
195
+ envContent.DASHBOARD_API_URL = dashboardUrl;
196
+ }
197
+
198
+ // Escrow address (use default)
199
+ envContent.ORACLE_ESCROW_ADDRESS = envContent.ORACLE_ESCROW_ADDRESS || '4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T';
200
+ envContent.STAKE_AMOUNT = envContent.STAKE_AMOUNT || '0.01';
201
+ envContent.PREMIUM_PRICE = envContent.PREMIUM_PRICE || '0.001';
202
+
203
+ // Write .env file
204
+ const envLines = Object.entries(envContent).map(([key, value]) => `${key}=${value}`);
205
+ writeFileSync(envPath, envLines.join('\n') + '\n');
206
+
207
+ log.success('.env file configured');
208
+ }
209
+
210
+ /**
211
+ * Setup wallet
212
+ */
213
+ async function setupWallet() {
214
+ log.info('Setting up Solana wallet...');
215
+
216
+ const defaultWalletPath = path.join(__dirname, 'wallet.json');
217
+
218
+ // Check for existing wallet
219
+ if (existsSync(defaultWalletPath)) {
220
+ const answer = await question(`Wallet exists at ${defaultWalletPath}. Use it? (Y/n): `);
221
+ if (answer.toLowerCase() !== 'n') {
222
+ log.success('Using existing wallet');
223
+ return defaultWalletPath;
224
+ }
225
+ }
226
+
227
+ // Ask if user wants to create new wallet or use existing
228
+ console.log('\nWallet options:');
229
+ console.log('1. Generate new wallet');
230
+ console.log('2. Use existing wallet file');
231
+
232
+ const choice = await question('Choose option (1 or 2): ');
233
+
234
+ if (choice === '2') {
235
+ const walletPath = await question('Enter path to existing wallet.json: ');
236
+ if (!existsSync(walletPath)) {
237
+ throw new Error(`Wallet file not found: ${walletPath}`);
238
+ }
239
+ log.success(`Using wallet at ${walletPath}`);
240
+ return walletPath;
241
+ }
242
+
243
+ // Generate new wallet
244
+ log.info('Generating new wallet...');
245
+
246
+ const keypair = Keypair.generate();
247
+ const secretKey = Array.from(keypair.secretKey);
248
+
249
+ // Save wallet
250
+ writeFileSync(defaultWalletPath, JSON.stringify(secretKey));
251
+
252
+ console.log(`\n${colors.green}${colors.bright}Wallet created!${colors.reset}`);
253
+ console.log(`Address: ${colors.cyan}${keypair.publicKey.toBase58()}${colors.reset}`);
254
+ console.log(`Path: ${defaultWalletPath}`);
255
+
256
+ log.warn('IMPORTANT: Backup this wallet file! It contains your private key.');
257
+
258
+ const answer = await question('\nPress Enter to continue...');
259
+
260
+ return defaultWalletPath;
261
+ }
262
+
263
+ /**
264
+ * Fund wallet with devnet SOL
265
+ */
266
+ async function fundWallet(walletPath) {
267
+ log.info('Checking wallet balance...');
268
+
269
+ // Load wallet
270
+ const keypairData = JSON.parse(readFileSync(walletPath, 'utf-8'));
271
+ const secretKey = Uint8Array.from(keypairData);
272
+ const keypair = Keypair.fromSecretKey(secretKey);
273
+
274
+ // Check balance
275
+ const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
276
+ const balance = await connection.getBalance(keypair.publicKey);
277
+ const balanceSOL = balance / LAMPORTS_PER_SOL;
278
+
279
+ console.log(`Current balance: ${balanceSOL} SOL`);
280
+
281
+ if (balanceSOL >= 0.02) {
282
+ log.success('Wallet has sufficient balance');
283
+ return;
284
+ }
285
+
286
+ // Request airdrop
287
+ const answer = await question('Request devnet SOL airdrop? (Y/n): ');
288
+ if (answer.toLowerCase() === 'n') {
289
+ log.warn('Skipping airdrop. Make sure you have at least 0.02 SOL to run.');
290
+ return;
291
+ }
292
+
293
+ log.info('Requesting airdrop (this may take a moment)...');
294
+
295
+ try {
296
+ const signature = await connection.requestAirdrop(
297
+ keypair.publicKey,
298
+ LAMPORTS_PER_SOL
299
+ );
300
+
301
+ // Wait for confirmation
302
+ await connection.confirmTransaction(signature, 'confirmed');
303
+
304
+ // Check new balance
305
+ const newBalance = await connection.getBalance(keypair.publicKey);
306
+ const newBalanceSOL = newBalance / LAMPORTS_PER_SOL;
307
+
308
+ log.success(`Airdrop successful! New balance: ${newBalanceSOL} SOL`);
309
+ } catch (error) {
310
+ log.error(`Airdrop failed: ${error.message}`);
311
+ log.info('You can request airdrop manually:');
312
+ console.log(` solana airdrop 1 ${keypair.publicKey.toBase58()} --url devnet`);
313
+ console.log('Or use the faucet: https://faucet.solana.com/');
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Global installation
319
+ */
320
+ async function globalInstallation() {
321
+ const answer = await question('\nInstall globally (allows "decharge-scout" command)? (Y/n): ');
322
+
323
+ if (answer.toLowerCase() === 'n') {
324
+ log.info('Skipping global installation');
325
+ return;
326
+ }
327
+
328
+ log.info('Installing globally...');
329
+
330
+ return new Promise((resolve, reject) => {
331
+ const npm = spawn('npm', ['install', '-g', '.'], {
332
+ cwd: __dirname,
333
+ stdio: 'inherit'
334
+ });
335
+
336
+ npm.on('close', (code) => {
337
+ if (code !== 0) {
338
+ log.warn('Global installation failed (may need sudo)');
339
+ log.info('Try manually: sudo npm install -g .');
340
+ resolve(); // Don't fail setup
341
+ } else {
342
+ log.success('Installed globally');
343
+ resolve();
344
+ }
345
+ });
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Show final summary
351
+ */
352
+ function showFinalSummary(walletPath) {
353
+ console.log('\n' + '='.repeat(60));
354
+ log.title('✅ Setup Complete!');
355
+
356
+ console.log('Your DeCharge Scout is ready to run.\n');
357
+
358
+ console.log(`${colors.bright}Quick Start:${colors.reset}`);
359
+ console.log(` ${colors.cyan}decharge-scout${colors.reset} (if installed globally)`);
360
+ console.log(` ${colors.cyan}node index.js${colors.reset} (run directly)\n`);
361
+
362
+ console.log(`${colors.bright}With options:${colors.reset}`);
363
+ console.log(` decharge-scout --agent-name="MyAgent"`);
364
+ console.log(` decharge-scout --premium\n`);
365
+
366
+ console.log(`${colors.bright}Configuration:${colors.reset}`);
367
+ console.log(` Wallet: ${walletPath}`);
368
+ console.log(` Config: ${path.join(__dirname, '.env')}\n`);
369
+
370
+ console.log(`${colors.bright}Next steps:${colors.reset}`);
371
+ console.log(` 1. ${colors.green}✓${colors.reset} Dependencies installed`);
372
+ console.log(` 2. ${colors.green}✓${colors.reset} Wallet created and funded`);
373
+ console.log(` 3. ${colors.green}✓${colors.reset} Environment configured`);
374
+ console.log(` 4. ${colors.yellow}→${colors.reset} Run the scout!\n`);
375
+
376
+ if (!existsSync(path.join(__dirname, '.env')) || readFileSync(path.join(__dirname, '.env'), 'utf-8').includes('your_eia_api_key_here')) {
377
+ log.warn('Remember to add your EIA_API_KEY to .env');
378
+ console.log(' Get it from: https://www.eia.gov/opendata/register.php\n');
379
+ }
380
+
381
+ console.log('='.repeat(60) + '\n');
382
+ }
383
+
384
+ // Run setup
385
+ main().catch(error => {
386
+ log.error(`Fatal error: ${error.message}`);
387
+ rl.close();
388
+ process.exit(1);
389
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Energy Data Fetching Module
3
+ *
4
+ * Fetches real-time energy grid data from various APIs
5
+ */
6
+
7
+ import fetch from 'node-fetch';
8
+ import dotenv from 'dotenv';
9
+
10
+ dotenv.config();
11
+
12
+ const EIA_API_KEY = process.env.EIA_API_KEY;
13
+ const EIA_BASE_URL = 'https://api.eia.gov/v2';
14
+
15
+ /**
16
+ * Fetch energy data from EIA API (ERCOT)
17
+ */
18
+ export async function fetchEnergyData() {
19
+ if (!EIA_API_KEY || EIA_API_KEY === 'your_eia_api_key_here') {
20
+ throw new Error('EIA_API_KEY not configured');
21
+ }
22
+
23
+ try {
24
+ // Calculate time range (last 24 hours to next 24 hours for forecast)
25
+ const now = new Date();
26
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
27
+ const startDate = yesterday.toISOString().split('T')[0] + 'T00';
28
+ const endDate = now.toISOString().split('T')[0] + 'T23';
29
+
30
+ // EIA API endpoint for ERCOT real-time data
31
+ const url = `${EIA_BASE_URL}/electricity/rto/region-data/data/?` +
32
+ `api_key=${EIA_API_KEY}` +
33
+ `&frequency=hourly` +
34
+ `&data[0]=value` +
35
+ `&facets[respondent][]=ERCT` +
36
+ `&facets[type][]=D` + // Demand
37
+ `&start=${startDate}` +
38
+ `&end=${endDate}` +
39
+ `&sort[0][column]=period` +
40
+ `&sort[0][direction]=desc` +
41
+ `&length=48`;
42
+
43
+ const response = await fetch(url, {
44
+ headers: {
45
+ 'Accept': 'application/json'
46
+ }
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error(`EIA API error: ${response.status} ${response.statusText}`);
51
+ }
52
+
53
+ const data = await response.json();
54
+
55
+ if (!data.response || !data.response.data || data.response.data.length === 0) {
56
+ throw new Error('No data returned from EIA API');
57
+ }
58
+
59
+ // Transform to our format with simulated pricing
60
+ // EIA provides demand, we'll simulate price based on demand patterns
61
+ const energyData = data.response.data.map((item, index) => {
62
+ const demand = parseFloat(item.value) || 0;
63
+ // Simulate price: higher demand = higher price
64
+ // Base price $0.03-0.10/kWh with demand-based variation
65
+ const basePrice = 0.05;
66
+ const demandFactor = (demand / 50000) * 0.03; // Scale based on typical ERCOT demand
67
+ const timeVariation = Math.sin(index * 0.26) * 0.02; // Time-based variation
68
+ const price = basePrice + demandFactor + timeVariation + (Math.random() * 0.01);
69
+
70
+ return {
71
+ timestamp: item.period,
72
+ hour: new Date(item.period).getHours(),
73
+ demand: demand,
74
+ price: Math.max(0.03, Math.min(0.15, price)), // Clamp between 3-15 cents
75
+ source: 'EIA-ERCOT'
76
+ };
77
+ });
78
+
79
+ return energyData;
80
+ } catch (error) {
81
+ throw new Error(`Failed to fetch EIA data: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Fetch forecast data from Electricity Maps API
87
+ */
88
+ export async function fetchElectricityMapsData() {
89
+ try {
90
+ const url = 'https://api.electricitymaps.com/v3/power-breakdown/latest?zone=US-TEX-ERCO';
91
+
92
+ const response = await fetch(url, {
93
+ headers: {
94
+ 'Accept': 'application/json'
95
+ }
96
+ });
97
+
98
+ if (!response.ok) {
99
+ // If auth required or rate limited, generate mock data
100
+ if (response.status === 401 || response.status === 429) {
101
+ console.log('Electricity Maps requires auth, using mock forecast data');
102
+ return generateMockForecastData();
103
+ }
104
+ throw new Error(`Electricity Maps API error: ${response.status}`);
105
+ }
106
+
107
+ const data = await response.json();
108
+
109
+ // Generate 24-hour forecast based on current data
110
+ const forecastData = [];
111
+ const currentHour = new Date().getHours();
112
+
113
+ for (let i = 0; i < 24; i++) {
114
+ const hour = (currentHour + i) % 24;
115
+ const timestamp = new Date(Date.now() + i * 60 * 60 * 1000).toISOString();
116
+
117
+ // Simulate price variation based on typical daily patterns
118
+ const isOffPeak = hour >= 22 || hour <= 6; // 10 PM - 6 AM
119
+ const isPeak = hour >= 16 && hour <= 20; // 4 PM - 8 PM
120
+ const basePrice = 0.06;
121
+ const peakMultiplier = isPeak ? 1.5 : 1.0;
122
+ const offPeakMultiplier = isOffPeak ? 0.7 : 1.0;
123
+ const randomVariation = (Math.random() - 0.5) * 0.02;
124
+
125
+ const price = basePrice * peakMultiplier * offPeakMultiplier + randomVariation;
126
+
127
+ forecastData.push({
128
+ timestamp,
129
+ hour,
130
+ demand: 45000 + (isPeak ? 15000 : 0) - (isOffPeak ? 10000 : 0),
131
+ price: Math.max(0.03, Math.min(0.12, price)),
132
+ source: 'ElectricityMaps-Forecast'
133
+ });
134
+ }
135
+
136
+ return forecastData;
137
+ } catch (error) {
138
+ throw new Error(`Failed to fetch Electricity Maps data: ${error.message}`);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Generate mock forecast data as fallback
144
+ */
145
+ function generateMockForecastData() {
146
+ const forecastData = [];
147
+ const currentHour = new Date().getHours();
148
+
149
+ for (let i = 0; i < 24; i++) {
150
+ const hour = (currentHour + i) % 24;
151
+ const timestamp = new Date(Date.now() + i * 60 * 60 * 1000).toISOString();
152
+
153
+ // Realistic daily price pattern
154
+ const isOffPeak = hour >= 22 || hour <= 6;
155
+ const isPeak = hour >= 16 && hour <= 20;
156
+ const isMidDay = hour >= 10 && hour <= 14;
157
+
158
+ let price = 0.05; // Base price
159
+
160
+ if (isPeak) {
161
+ price = 0.09 + Math.random() * 0.03; // Peak hours: 9-12 cents
162
+ } else if (isOffPeak) {
163
+ price = 0.03 + Math.random() * 0.02; // Off-peak: 3-5 cents
164
+ } else if (isMidDay) {
165
+ price = 0.06 + Math.random() * 0.02; // Mid-day: 6-8 cents
166
+ } else {
167
+ price = 0.05 + Math.random() * 0.02; // Normal: 5-7 cents
168
+ }
169
+
170
+ forecastData.push({
171
+ timestamp,
172
+ hour,
173
+ demand: 40000 + (isPeak ? 20000 : 0) - (isOffPeak ? 15000 : 0),
174
+ price: parseFloat(price.toFixed(4)),
175
+ source: 'Mock-Data'
176
+ });
177
+ }
178
+
179
+ return forecastData;
180
+ }
181
+
182
+ /**
183
+ * Fetch premium forecast data (for x402 integration)
184
+ */
185
+ export async function fetchPremiumForecastData() {
186
+ // In a real implementation, this would call a premium API endpoint
187
+ // For demo, return enhanced mock data with more detail
188
+
189
+ const premiumData = generateMockForecastData().map(item => ({
190
+ ...item,
191
+ confidence: 0.85 + Math.random() * 0.15,
192
+ carbonIntensity: 300 + Math.random() * 200,
193
+ renewablePercentage: 20 + Math.random() * 30,
194
+ premium: true
195
+ }));
196
+
197
+ return premiumData;
198
+ }