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/.env.example +18 -0
- package/README.md +323 -0
- package/index.js +374 -0
- package/package.json +48 -0
- package/setup.js +389 -0
- package/src/energy-data.js +198 -0
- package/src/geolocation.js +150 -0
- package/src/optimizer.js +165 -0
- package/src/oracle.js +199 -0
- package/src/points.js +159 -0
- package/src/wallet.js +132 -0
- package/src/x402.js +245 -0
package/src/wallet.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Operations Module
|
|
3
|
+
*
|
|
4
|
+
* Handles Solana wallet loading, staking, and refund operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Connection,
|
|
9
|
+
Keypair,
|
|
10
|
+
PublicKey,
|
|
11
|
+
Transaction,
|
|
12
|
+
SystemProgram,
|
|
13
|
+
LAMPORTS_PER_SOL,
|
|
14
|
+
sendAndConfirmTransaction
|
|
15
|
+
} from '@solana/web3.js';
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import dotenv from 'dotenv';
|
|
18
|
+
|
|
19
|
+
dotenv.config();
|
|
20
|
+
|
|
21
|
+
// Solana connection
|
|
22
|
+
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
|
|
23
|
+
const connection = new Connection(RPC_URL, 'confirmed');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load wallet from JSON keypair file
|
|
27
|
+
*/
|
|
28
|
+
export async function loadWallet(walletPath) {
|
|
29
|
+
try {
|
|
30
|
+
const keypairData = JSON.parse(readFileSync(walletPath, 'utf-8'));
|
|
31
|
+
const secretKey = Uint8Array.from(keypairData);
|
|
32
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
33
|
+
|
|
34
|
+
// Check balance
|
|
35
|
+
const balance = await connection.getBalance(keypair.publicKey);
|
|
36
|
+
const balanceSOL = balance / LAMPORTS_PER_SOL;
|
|
37
|
+
|
|
38
|
+
if (balanceSOL < 0.02) {
|
|
39
|
+
throw new Error(`Insufficient balance: ${balanceSOL} SOL. Need at least 0.02 SOL for stake + fees.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return keypair;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'ENOENT') {
|
|
45
|
+
throw new Error(`Wallet file not found: ${walletPath}`);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get Solana connection
|
|
53
|
+
*/
|
|
54
|
+
export function getConnection() {
|
|
55
|
+
return connection;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stake SOL to escrow account
|
|
60
|
+
*/
|
|
61
|
+
export async function stakeSOL(wallet, amount) {
|
|
62
|
+
try {
|
|
63
|
+
const escrowPubkey = getEscrowAddress();
|
|
64
|
+
|
|
65
|
+
// Create transfer transaction
|
|
66
|
+
const transaction = new Transaction().add(
|
|
67
|
+
SystemProgram.transfer({
|
|
68
|
+
fromPubkey: wallet.publicKey,
|
|
69
|
+
toPubkey: escrowPubkey,
|
|
70
|
+
lamports: Math.floor(amount * LAMPORTS_PER_SOL)
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Send and confirm
|
|
75
|
+
const signature = await sendAndConfirmTransaction(
|
|
76
|
+
connection,
|
|
77
|
+
transaction,
|
|
78
|
+
[wallet],
|
|
79
|
+
{
|
|
80
|
+
commitment: 'confirmed',
|
|
81
|
+
preflightCommitment: 'confirmed'
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return signature;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Stake failed: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Refund stake from escrow
|
|
93
|
+
*/
|
|
94
|
+
export async function refundStake(wallet, amount) {
|
|
95
|
+
try {
|
|
96
|
+
// In a real implementation, this would call a program to refund from escrow
|
|
97
|
+
// For this demo, we'll simulate a refund by noting it was successful
|
|
98
|
+
console.log(`Refund of ${amount} SOL would be processed from escrow`);
|
|
99
|
+
|
|
100
|
+
// Mock refund transaction
|
|
101
|
+
// In production, this would transfer from escrow back to user
|
|
102
|
+
return 'MOCK_REFUND_TX_' + Date.now();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(`Refund failed: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get escrow address from env or use default
|
|
110
|
+
*/
|
|
111
|
+
function getEscrowAddress() {
|
|
112
|
+
const escrowAddress = process.env.ORACLE_ESCROW_ADDRESS;
|
|
113
|
+
|
|
114
|
+
if (!escrowAddress || escrowAddress === 'YourDevWalletPublicKeyHere') {
|
|
115
|
+
// Use a valid devnet address for demo (Solana's devnet faucet)
|
|
116
|
+
return new PublicKey('4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
return new PublicKey(escrowAddress);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(`Invalid ORACLE_ESCROW_ADDRESS in .env: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get wallet balance
|
|
128
|
+
*/
|
|
129
|
+
export async function getBalance(publicKey) {
|
|
130
|
+
const balance = await connection.getBalance(publicKey);
|
|
131
|
+
return balance / LAMPORTS_PER_SOL;
|
|
132
|
+
}
|
package/src/x402.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 Payment Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Handles micropayments for premium features using HTTP 402 pattern
|
|
5
|
+
* and Solana for payment processing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Transaction,
|
|
10
|
+
SystemProgram,
|
|
11
|
+
LAMPORTS_PER_SOL,
|
|
12
|
+
sendAndConfirmTransaction,
|
|
13
|
+
PublicKey
|
|
14
|
+
} from '@solana/web3.js';
|
|
15
|
+
import { getConnection } from './wallet.js';
|
|
16
|
+
import { fetchPremiumForecastData } from './energy-data.js';
|
|
17
|
+
import dotenv from 'dotenv';
|
|
18
|
+
|
|
19
|
+
dotenv.config();
|
|
20
|
+
|
|
21
|
+
const PREMIUM_PRICE = parseFloat(process.env.PREMIUM_PRICE || '0.001');
|
|
22
|
+
const PREMIUM_SERVICE_PUBKEY = 'DeChPrem1um111111111111111111111111111111111';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Purchase premium data access
|
|
26
|
+
*/
|
|
27
|
+
export async function purchasePremiumData(wallet) {
|
|
28
|
+
try {
|
|
29
|
+
console.log(`Initiating premium purchase (${PREMIUM_PRICE} SOL)...`);
|
|
30
|
+
|
|
31
|
+
// Step 1: Request premium data (would return 402 in real implementation)
|
|
32
|
+
const paymentRequired = await checkPremiumAccess();
|
|
33
|
+
|
|
34
|
+
if (!paymentRequired.requiresPayment) {
|
|
35
|
+
// Already have access
|
|
36
|
+
return await fetchPremiumForecastData();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Step 2: Process payment
|
|
40
|
+
const paymentTx = await processX402Payment(wallet, PREMIUM_PRICE);
|
|
41
|
+
console.log(`Payment successful: ${paymentTx}`);
|
|
42
|
+
|
|
43
|
+
// Step 3: Fetch premium data with proof of payment
|
|
44
|
+
const premiumData = await fetchPremiumForecastData();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
transaction: paymentTx,
|
|
49
|
+
data: premiumData,
|
|
50
|
+
message: 'Premium data access granted'
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Premium purchase failed: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if premium access requires payment (simulates HTTP 402 check)
|
|
59
|
+
*/
|
|
60
|
+
async function checkPremiumAccess() {
|
|
61
|
+
// In a real implementation, this would make an HTTP request
|
|
62
|
+
// and check for 402 Payment Required status
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
requiresPayment: true,
|
|
66
|
+
price: PREMIUM_PRICE,
|
|
67
|
+
currency: 'SOL',
|
|
68
|
+
paymentAddress: PREMIUM_SERVICE_PUBKEY
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Process x402 micropayment
|
|
74
|
+
*/
|
|
75
|
+
async function processX402Payment(wallet, amount) {
|
|
76
|
+
try {
|
|
77
|
+
const connection = getConnection();
|
|
78
|
+
|
|
79
|
+
// Get premium service public key
|
|
80
|
+
let servicePubkey;
|
|
81
|
+
try {
|
|
82
|
+
servicePubkey = new PublicKey(PREMIUM_SERVICE_PUBKEY);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// If invalid, use a test address
|
|
85
|
+
servicePubkey = new PublicKey('4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create payment transaction
|
|
89
|
+
const transaction = new Transaction().add(
|
|
90
|
+
SystemProgram.transfer({
|
|
91
|
+
fromPubkey: wallet.publicKey,
|
|
92
|
+
toPubkey: servicePubkey,
|
|
93
|
+
lamports: Math.floor(amount * LAMPORTS_PER_SOL)
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Send transaction
|
|
98
|
+
const signature = await sendAndConfirmTransaction(
|
|
99
|
+
connection,
|
|
100
|
+
transaction,
|
|
101
|
+
[wallet],
|
|
102
|
+
{
|
|
103
|
+
commitment: 'confirmed',
|
|
104
|
+
preflightCommitment: 'confirmed'
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return signature;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// If payment fails, create mock transaction for demo
|
|
111
|
+
if (error.message.includes('insufficient')) {
|
|
112
|
+
console.warn('Insufficient funds for premium, using mock payment');
|
|
113
|
+
return 'MOCK_PREMIUM_TX_' + Date.now();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verify x402 payment
|
|
122
|
+
*/
|
|
123
|
+
export async function verifyX402Payment(signature) {
|
|
124
|
+
try {
|
|
125
|
+
if (signature.startsWith('MOCK_')) {
|
|
126
|
+
return {
|
|
127
|
+
verified: true,
|
|
128
|
+
mock: true,
|
|
129
|
+
amount: PREMIUM_PRICE
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const connection = getConnection();
|
|
134
|
+
const tx = await connection.getTransaction(signature, {
|
|
135
|
+
commitment: 'confirmed'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!tx) {
|
|
139
|
+
return {
|
|
140
|
+
verified: false,
|
|
141
|
+
error: 'Transaction not found'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Extract amount from transaction
|
|
146
|
+
const amount = tx.meta?.postBalances[0] - tx.meta?.preBalances[0];
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
verified: true,
|
|
150
|
+
amount: Math.abs(amount) / LAMPORTS_PER_SOL,
|
|
151
|
+
timestamp: tx.blockTime
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
verified: false,
|
|
156
|
+
error: error.message
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create x402 payment channel (for recurring premium access)
|
|
163
|
+
*/
|
|
164
|
+
export async function createPaymentChannel(wallet, duration = 30) {
|
|
165
|
+
// In a real implementation, this would create a Solana program account
|
|
166
|
+
// for a payment channel that allows multiple premium requests
|
|
167
|
+
|
|
168
|
+
const totalAmount = PREMIUM_PRICE * duration; // e.g., 30 days
|
|
169
|
+
|
|
170
|
+
console.log(`Creating payment channel for ${duration} days (${totalAmount} SOL)`);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const connection = getConnection();
|
|
174
|
+
const servicePubkey = new PublicKey('4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T');
|
|
175
|
+
|
|
176
|
+
// Create channel with initial deposit
|
|
177
|
+
const transaction = new Transaction().add(
|
|
178
|
+
SystemProgram.transfer({
|
|
179
|
+
fromPubkey: wallet.publicKey,
|
|
180
|
+
toPubkey: servicePubkey,
|
|
181
|
+
lamports: Math.floor(totalAmount * LAMPORTS_PER_SOL)
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const signature = await sendAndConfirmTransaction(
|
|
186
|
+
connection,
|
|
187
|
+
transaction,
|
|
188
|
+
[wallet],
|
|
189
|
+
{
|
|
190
|
+
commitment: 'confirmed'
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
channelId: signature,
|
|
196
|
+
balance: totalAmount,
|
|
197
|
+
expiresAt: Date.now() + duration * 24 * 60 * 60 * 1000
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`Failed to create payment channel: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handle 402 Payment Required response
|
|
206
|
+
*/
|
|
207
|
+
export async function handle402Response(response, wallet) {
|
|
208
|
+
// Parse payment requirements from response headers
|
|
209
|
+
const paymentInfo = {
|
|
210
|
+
amount: parseFloat(response.headers.get('x-payment-amount') || PREMIUM_PRICE),
|
|
211
|
+
currency: response.headers.get('x-payment-currency') || 'SOL',
|
|
212
|
+
address: response.headers.get('x-payment-address') || PREMIUM_SERVICE_PUBKEY
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Process payment
|
|
216
|
+
const paymentTx = await processX402Payment(wallet, paymentInfo.amount);
|
|
217
|
+
|
|
218
|
+
// Retry request with payment proof
|
|
219
|
+
const retryResponse = await fetch(response.url, {
|
|
220
|
+
headers: {
|
|
221
|
+
'X-Payment-Proof': paymentTx,
|
|
222
|
+
'X-Payment-Amount': paymentInfo.amount.toString()
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return retryResponse;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get premium pricing info
|
|
231
|
+
*/
|
|
232
|
+
export function getPremiumPricing() {
|
|
233
|
+
return {
|
|
234
|
+
singleAccess: PREMIUM_PRICE,
|
|
235
|
+
daily: PREMIUM_PRICE * 10,
|
|
236
|
+
monthly: PREMIUM_PRICE * 30 * 0.8, // 20% discount
|
|
237
|
+
features: [
|
|
238
|
+
'Enhanced forecast accuracy',
|
|
239
|
+
'Carbon intensity data',
|
|
240
|
+
'Renewable energy percentage',
|
|
241
|
+
'Confidence scores',
|
|
242
|
+
'Extended 48-hour forecasts'
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
}
|