import fs from 'fs';
import path from 'path';
import { ethers } from 'ethers';
import { ProverError, NetworkError, AuthenticationError } from './types.js';
import secp256k1 from 'secp256k1';
import { keccak256 } from 'ethers';
import { NetworkConfig } from './types.js';
// Cryptographic utilities
export function hexToBytes(hex: string): Uint8Array {
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
if (cleanHex.length % 2 !== 0) {
throw new ProverError('Invalid hex string length');
}
const bytes = new Uint8Array(cleanHex.length / 2);
for (let i = 0; i < cleanHex.length; i += 2) {
bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);
}
return bytes;
}
export function bytesToHex(bytes: Uint8Array): string {
return '0x' + Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
export function addressToBytes(address: string): Uint8Array {
if (!ethers.isAddress(address)) {
throw new ProverError(`Invalid Ethereum address: ${address}`);
}
return hexToBytes(address);
}
// Message signing for Succinct Protocol
export async function signMessage(message: Uint8Array, privateKey: string): Promise<Uint8Array> {
try {
const wallet = new ethers.Wallet(privateKey);
const messageHash = ethers.keccak256(message);
const signature = await wallet.signMessage(ethers.getBytes(messageHash));
return hexToBytes(signature);
} catch (error) {
throw new AuthenticationError(`Failed to sign message: ${error}`);
}
}
// Serialization utilities
export function serializeMessage(obj: any): Uint8Array {
try {
const jsonString = JSON.stringify(obj);
return new TextEncoder().encode(jsonString);
} catch (error) {
throw new ProverError(`Failed to serialize message: ${error}`);
}
}
export function deserializeMessage<T>(bytes: Uint8Array): T {
try {
const jsonString = new TextDecoder().decode(bytes);
return JSON.parse(jsonString);
} catch (error) {
throw new ProverError(`Failed to deserialize message: ${error}`);
}
}
// Economic utilities
export function formatProveAmount(amount: string | number): string {
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(num)) {
return '0 PROVE';
}
if (num >= 1000000) {
return `${(num / 1000000).toFixed(2)}M PROVE`;
} else if (num >= 1000) {
return `${(num / 1000).toFixed(2)}K PROVE`;
} else {
return `${num.toFixed(2)} PROVE`;
}
}
export function parseProveAmount(formattedAmount: string): number {
const cleanAmount = formattedAmount.replace(/[^\d.]/g, '');
const num = parseFloat(cleanAmount);
if (formattedAmount.includes('M')) {
return num * 1000000;
} else if (formattedAmount.includes('K')) {
return num * 1000;
} else {
return num;
}
}
export function calculateBidPrice(
hardwareCostPerHour: number,
utilizationRate: number,
profitMargin: number,
proveTokenPrice: number,
throughputPGUsPerSecond: number
): number {
const effectiveCostPerHour = hardwareCostPerHour / utilizationRate;
const targetRevenuePerHour = effectiveCostPerHour * (1 + profitMargin);
const pguPerHour = throughputPGUsPerSecond * 3600;
const usdPerPGU = targetRevenuePerHour / pguPerHour;
const provePerPGU = usdPerPGU / proveTokenPrice;
return provePerPGU * 1000000000; // Convert to PROVE per 1B PGU
}
// Time utilities
export function getCurrentTimestamp(): number {
return Math.floor(Date.now() / 1000);
}
export function calculateDeadline(secondsFromNow: number): number {
return getCurrentTimestamp() + secondsFromNow;
}
export function formatTimeRemaining(deadline: number): string {
const timeLeft = deadline - getCurrentTimestamp();
if (timeLeft <= 0) {
return 'Expired';
}
const hours = Math.floor(timeLeft / 3600);
const minutes = Math.floor((timeLeft % 3600) / 60);
const seconds = timeLeft % 60;
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`;
} else {
return `${seconds}s`;
}
}
// Session management
export function generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export function generateRequestId(): string {
const timestamp = Date.now().toString(16);
const random = Math.random().toString(16).substr(2, 8);
return `0x${timestamp}${random}`.padEnd(66, '0');
}
// Network utilities with enhanced retry mechanism
export async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 5, // Increased from 3
baseDelay: number = 2000 // Increased from 1000
): Promise<T> {
let lastError: Error = new Error('Unknown error');
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
break;
}
// Exponential backoff with jitter
const jitter = Math.random() * 1000;
const delay = baseDelay * Math.pow(2, attempt) + jitter;
console.error(`ā ļø Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new NetworkError(`Operation failed after ${maxRetries + 1} attempts: ${lastError.message}`);
}
// Validation utilities
export function validateProofRequest(request: any): void {
const requiredFields = ['vkHash', 'stdinUri', 'cycleLimit'];
for (const field of requiredFields) {
if (!request[field]) {
throw new ProverError(`Missing required field: ${field}`);
}
}
if (!request.vkHash.match(/^0x[a-fA-F0-9]{64}$/)) {
throw new ProverError('Invalid vkHash format');
}
if (typeof request.cycleLimit !== 'number' || request.cycleLimit <= 0) {
throw new ProverError('Invalid cycleLimit');
}
if (!request.stdinUri.startsWith('https://')) {
throw new ProverError('Invalid stdinUri format');
}
}
export function validateBidRequest(request: any): void {
if (!request.requestId || !request.bidAmount) {
throw new ProverError('Missing required fields for bid request');
}
if (!request.requestId.match(/^0x[a-fA-F0-9]{64}$/)) {
throw new ProverError('Invalid requestId format');
}
const bidAmount = parseFloat(request.bidAmount);
if (isNaN(bidAmount) || bidAmount <= 0) {
throw new ProverError('Invalid bid amount');
}
}
// Hardware detection utilities
export async function detectHardware(): Promise<{
cpuCores: number;
memoryGB: number;
gpuCount: number;
gpuModel?: string;
}> {
const os = await import('os');
return {
cpuCores: os.cpus().length,
memoryGB: Math.round(os.totalmem() / (1024 * 1024 * 1024)),
gpuCount: 0, // Requires system-specific GPU detection
gpuModel: undefined // Requires system-specific GPU detection
};
}
// Configuration management
export function loadNetworkConfig(): {
rpcUrl: string;
privateKey: string;
proverAddress?: string;
stakingContract: string;
proveToken: string;
} {
const rpcUrl = process.env.SUCCINCT_RPC_URL || 'https://rpc-production.succinct.xyz';
// Support both official Succinct naming (PRIVATE_KEY) and MCP naming (PROVER_PRIVATE_KEY)
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
const proverAddress = process.env.PROVER_ADDRESS;
if (!privateKey) {
throw new ProverError('PRIVATE_KEY or PROVER_PRIVATE_KEY environment variable is required');
}
// Sepolia testnet addresses
const stakingContract = process.env.STAKING_CONTRACT || '0x8F1B4633630b90C273E834C14924298ab6D1DA02';
const proveToken = process.env.PROVE_TOKEN || '0xC47B85472b40435c0D96db5Df3e9B4C368DA6A8C';
return {
rpcUrl,
privateKey,
proverAddress,
stakingContract,
proveToken
};
}
// Legacy exports for backward compatibility
export { ProverError };
// User Experience Enhancement
export function validateUserReadiness(operation: string): { ready: boolean; warnings: string[] } {
const warnings: string[] = [];
// Check for common beginner issues
if (operation.includes('bid') || operation.includes('stake')) {
warnings.push('šØ TESTNET ONLY: No real monetary value!');
warnings.push('š” Tip: Start with monitoring commands first');
}
if (operation.includes('create_prover')) {
warnings.push('šļø Consider using web interface: https://staking.sepolia.succinct.xyz/prover');
warnings.push('ā” Requires gas fees for smart contract deployment');
}
if (operation.includes('calibrate')) {
warnings.push('š„ļø This tests your local hardware performance');
warnings.push('ā±ļø May take 1-2 minutes to complete');
}
return {
ready: warnings.length === 0,
warnings
};
}
export function getBeginnerFriendlyError(error: any): string {
const message = error.message || String(error);
// Common error translations
if (message.includes('private key')) {
return 'š Private key required for this operation. This is only needed for advanced prover management. Try monitoring commands first!';
}
if (message.includes('insufficient')) {
return 'š° Not enough PROVE tokens. Use get_prove_faucet to get testnet tokens first!';
}
if (message.includes('network')) {
return 'š Network connection issue. Check your internet connection and try again.';
}
if (message.includes('invalid')) {
return 'ā Invalid input format. Check the example usage in documentation.';
}
return `ā ${message}\n\nš” Tip: Try starting with get_network_stats or get_filtered_requests for beginners!`;
}
// Beginner-friendly operation suggestions
export function suggestNextSteps(currentOperation: string): string[] {
const suggestions: string[] = [];
switch (currentOperation) {
case 'get_network_stats':
suggestions.push('Try: get_filtered_requests (see available proof jobs)');
suggestions.push('Try: calibrate_prover (test your hardware)');
break;
case 'get_filtered_requests':
suggestions.push('Try: get_proof_status with a specific requestId');
suggestions.push('Try: get_prover_metrics to study successful provers');
break;
case 'calibrate_prover':
suggestions.push('Now you know your hardware capabilities!');
suggestions.push('Try: get_prove_faucet to get test tokens');
break;
case 'get_prove_faucet':
suggestions.push('Get test tokens from the faucet URL');
suggestions.push('Then try: get_account_balance to confirm tokens');
break;
default:
suggestions.push('Explore more with: get_network_stats');
suggestions.push('Learn more with: calibrate_prover');
}
return suggestions;
}
// Blockchain interaction utilities
// Succinct Network contract addresses and ABIs
export const SUCCINCT_CONTRACTS = {
STAKING_ADDRESS: '0x8F1B4633630b90C273E834C14924298ab6D1DA02',
PROVE_TOKEN_ADDRESS: '0xC47B85472b40435c0D96db5Df3e9B4C368DA6A8C',
RPC_URL: 'https://rpc-production.succinct.xyz'
};
// Simplified ABI for essential functions
export const STAKING_ABI = [
"function createProver(uint256 _stakerFeeBips) external returns (address)",
"function stake(address _prover, uint256 _amount) external",
"function getProver(address _owner) external view returns (address)",
"event ProverCreated(address indexed owner, address indexed prover, uint256 stakerFeeBips)"
];
export const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)"
];
export async function createEthersProvider(): Promise<ethers.JsonRpcProvider> {
return new ethers.JsonRpcProvider(SUCCINCT_CONTRACTS.RPC_URL);
}
export async function createEthersSigner(privateKey: string): Promise<ethers.Wallet> {
const provider = await createEthersProvider();
return new ethers.Wallet(privateKey, provider);
}
export async function getProveTokenBalance(address: string): Promise<string> {
try {
const provider = await createEthersProvider();
const contract = new ethers.Contract(
SUCCINCT_CONTRACTS.PROVE_TOKEN_ADDRESS,
ERC20_ABI,
provider
);
const balance = await contract.balanceOf(address);
return ethers.formatUnits(balance, 18); // PROVE has 18 decimals
} catch (error) {
throw new ProverError(`Failed to get PROVE balance: ${error}`);
}
}
export async function getEthBalance(address: string): Promise<string> {
try {
const provider = await createEthersProvider();
const balance = await provider.getBalance(address);
return ethers.formatEther(balance);
} catch (error) {
throw new ProverError(`Failed to get ETH balance: ${error}`);
}
}
export async function createProverOnChain(
privateKey: string,
stakerFeeBips: number = 0
): Promise<{ proverAddress: string; txHash: string }> {
try {
const signer = await createEthersSigner(privateKey);
const contract = new ethers.Contract(
SUCCINCT_CONTRACTS.STAKING_ADDRESS,
STAKING_ABI,
signer
);
// Check ETH balance for gas
const ethBalance = await getEthBalance(await signer.getAddress());
if (parseFloat(ethBalance) < 0.01) {
throw new ProverError(`Insufficient ETH balance: ${ethBalance} ETH (need ~0.01 ETH for gas)`);
}
// Create prover transaction
const tx = await contract.createProver(stakerFeeBips);
const receipt = await tx.wait();
// Parse the ProverCreated event to get prover address
const event = receipt.logs.find((log: any) => {
try {
const parsed = contract.interface.parseLog(log);
return parsed?.name === 'ProverCreated';
} catch {
return false;
}
});
if (!event) {
throw new ProverError('ProverCreated event not found in transaction logs');
}
const parsedEvent = contract.interface.parseLog(event);
const proverAddress = parsedEvent?.args?.prover;
if (!proverAddress) {
throw new ProverError('Could not extract prover address from event');
}
return {
proverAddress,
txHash: tx.hash
};
} catch (error) {
throw new ProverError(`Failed to create prover on-chain: ${error}`);
}
}
export async function stakeToProverOnChain(
privateKey: string,
proverAddress: string,
amount: string
): Promise<{ txHash: string }> {
try {
const signer = await createEthersSigner(privateKey);
const userAddress = await signer.getAddress();
// Check PROVE token balance
const proveBalance = await getProveTokenBalance(userAddress);
if (parseFloat(proveBalance) < parseFloat(amount)) {
throw new ProverError(
`Insufficient PROVE balance: ${proveBalance} PROVE (need ${amount} PROVE)`
);
}
// Check ETH balance for gas
const ethBalance = await getEthBalance(userAddress);
if (parseFloat(ethBalance) < 0.005) {
throw new ProverError(`Insufficient ETH balance: ${ethBalance} ETH (need ~0.005 ETH for gas)`);
}
// Approve PROVE tokens if needed
const proveContract = new ethers.Contract(
SUCCINCT_CONTRACTS.PROVE_TOKEN_ADDRESS,
ERC20_ABI,
signer
);
const amountWei = ethers.parseUnits(amount, 18);
const allowance = await proveContract.allowance(userAddress, SUCCINCT_CONTRACTS.STAKING_ADDRESS);
if (allowance < amountWei) {
const approveTx = await proveContract.approve(SUCCINCT_CONTRACTS.STAKING_ADDRESS, amountWei);
await approveTx.wait();
}
// Stake tokens
const stakingContract = new ethers.Contract(
SUCCINCT_CONTRACTS.STAKING_ADDRESS,
STAKING_ABI,
signer
);
const stakeTx = await stakingContract.stake(proverAddress, amountWei);
await stakeTx.wait();
return {
txHash: stakeTx.hash
};
} catch (error) {
throw new ProverError(`Failed to stake tokens on-chain: ${error}`);
}
}
export class AuthenticationProvider {
private privateKey: Buffer | null = null;
constructor() {
// Support both official Succinct naming (PRIVATE_KEY) and MCP naming (PROVER_PRIVATE_KEY)
const privateKeyHex = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
if (privateKeyHex && privateKeyHex !== 'your_private_key_here') {
try {
// Remove 0x prefix if present
const cleanKey = privateKeyHex.startsWith('0x') ? privateKeyHex.slice(2) : privateKeyHex;
this.privateKey = Buffer.from(cleanKey, 'hex');
if (this.privateKey.length !== 32) {
throw new Error('Invalid private key length');
}
} catch (error) {
console.error('Invalid private key format:', error);
this.privateKey = null;
}
}
}
isAuthenticated(): boolean {
return this.privateKey !== null;
}
getAddress(): string {
if (!this.privateKey) {
throw new Error('No private key available');
}
// Generate public key from private key
const publicKey = secp256k1.publicKeyCreate(this.privateKey, false);
// Get Ethereum address from public key (last 20 bytes of keccak256 hash)
const publicKeyHash = keccak256(publicKey.slice(1)); // Remove 0x04 prefix
const address = '0x' + publicKeyHash.slice(-40);
return address;
}
signRequestBody(requestBody: any): Buffer {
if (!this.privateKey) {
throw new Error('No private key available for signing');
}
try {
// Follow the official Succinct signing pattern more closely
// Based on the SDK documentation example
// First, properly serialize the request body
let bodyBytes: Buffer;
if (typeof requestBody === 'object') {
// Convert to proper binary format as expected by Succinct
const jsonString = JSON.stringify(requestBody, (key, value) => {
// Handle Buffer/Uint8Array values properly
if (value && value.type === 'Buffer') {
return { type: 'Buffer', data: value.data };
}
return value;
});
bodyBytes = Buffer.from(jsonString, 'utf8');
} else {
bodyBytes = Buffer.from(requestBody, 'utf8');
}
// Create the message hash using Ethereum's personal message format
// This matches how the official SDK handles signing
const messagePrefix = '\x19Ethereum Signed Message:\n';
const messageWithPrefix = Buffer.concat([
Buffer.from(messagePrefix, 'utf8'),
Buffer.from(bodyBytes.length.toString(), 'utf8'),
bodyBytes
]);
// Hash the prefixed message
const messageHash = Buffer.from(keccak256(messageWithPrefix).slice(2), 'hex');
// Sign the hash with recovery parameter
const { signature, recid } = secp256k1.ecdsaSign(messageHash, this.privateKey);
// Return signature with recovery parameter appended (65 bytes total)
// This matches Ethereum's signature format that Succinct expects
const fullSignature = Buffer.alloc(65);
fullSignature.set(signature, 0);
fullSignature[64] = recid;
return fullSignature;
} catch (error) {
throw new Error(`Failed to sign request body: ${error}`);
}
}
}
export class GrpcClientProvider {
private rpcUrl: string;
private auth: AuthenticationProvider;
constructor() {
this.rpcUrl = process.env.SUCCINCT_RPC_URL || 'https://rpc-production.succinct.xyz';
this.auth = new AuthenticationProvider();
}
async makeAuthenticatedCall(method: string, requestBody: any): Promise<any> {
if (!this.auth.isAuthenticated()) {
throw new ProverError(`Authentication required for ${method}. Set PRIVATE_KEY or PROVER_PRIVATE_KEY environment variable.`);
}
try {
// Updated to follow Succinct's authentication pattern more closely
// Based on the official SDK example in the documentation
const signature = this.auth.signRequestBody(requestBody);
const address = this.auth.getAddress();
// Try gRPC-style request first, then fallback to HTTP if needed
const response = await fetch(`${this.rpcUrl}/v1/${method}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'prover-mcp/1.0.0',
// Follow Succinct's authentication pattern
'X-Prover-Address': address,
'X-Signature': signature.toString('hex'),
'X-Message-Format': 'Binary',
},
body: JSON.stringify({
// Follow the official API structure more closely
format: 0, // MessageFormat::Binary
signature: Array.from(signature),
body: requestBody
})
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication failed - invalid private key or signature');
}
if (response.status === 403) {
// More specific error for 403 based on documentation
throw new Error('Permission denied - ensure account has sufficient stake and permissions for this action');
}
if (response.status === 404) {
// Try alternative endpoint structure
return await this.makeAuthenticatedCallFallback(method, requestBody);
}
throw new Error(`RPC call failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`RPC error: ${data.error.message}`);
}
return data.result || data;
} catch (error) {
// If the primary method fails, try the fallback approach
if (error instanceof Error && error.message.includes('RPC call failed')) {
try {
return await this.makeAuthenticatedCallFallback(method, requestBody);
} catch (fallbackError) {
throw new ProverError(`Failed to make authenticated call to ${method}: ${error.message}. Fallback also failed: ${fallbackError}`);
}
}
throw new ProverError(`Failed to make authenticated call to ${method}: ${error}`);
}
}
private async makeAuthenticatedCallFallback(method: string, requestBody: any): Promise<any> {
// Fallback to direct JSON-RPC style call
const signature = this.auth.signRequestBody(requestBody);
const address = this.auth.getAddress();
const response = await fetch(this.rpcUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Prover ${address}`,
'X-Signature': signature.toString('hex'),
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: `prover_network.${method}`,
params: [requestBody]
})
});
if (!response.ok) {
throw new Error(`Fallback RPC call failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`Fallback RPC error: ${data.error.message}`);
}
return data.result;
}
async makePublicCall(method: string, params: any[] = []): Promise<any> {
try {
// Try multiple endpoint patterns for public calls
const endpoints = [
`${this.rpcUrl}/v1/${method}`,
`${this.rpcUrl}/${method}`,
this.rpcUrl
];
let lastError: Error = new Error('All endpoints failed');
for (const endpoint of endpoints) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'prover-mcp/1.0.0',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
})
});
if (response.ok) {
const data = await response.json();
if (data.error) {
throw new Error(`RPC error: ${data.error.message}`);
}
return data.result || data;
}
} catch (error) {
lastError = error as Error;
continue; // Try next endpoint
}
}
throw lastError;
} catch (error) {
throw new ProverError(`Failed to make public call to ${method}: ${error}`);
}
}
isAuthenticated(): boolean {
return this.auth.isAuthenticated();
}
getAddress(): string {
return this.auth.isAuthenticated() ? this.auth.getAddress() : 'not_authenticated';
}
}
// Enhanced security utilities inspired by base-mcp
export class SecureCredentialManager {
private static readonly MASKED_KEY_DISPLAY = '***HIDDEN***';
private static readonly MIN_KEY_LENGTH = 64; // 32 bytes hex
private static readonly MAX_KEY_LENGTH = 66; // with 0x prefix
/**
* Force load environment for MCP server
* This ensures environment variables from MCP config take priority (Base MCP approach)
*/
static forceLoadEnvironmentForMCP(): void {
try {
console.error('š§ Loading environment variables for MCP...');
// Environment variables are already available through process.env when
// set via MCP config - no additional loading needed (Base MCP approach)
// Log current environment status with secure masking
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
const proverAddress = process.env.PROVER_ADDRESS;
console.error(` PRIVATE_KEY: ${this.maskPrivateKey(privateKey)}`);
console.error(` PROVER_ADDRESS: ${proverAddress ? `${proverAddress.substring(0, 8)}...` : 'NOT SET'}`);
console.error(` SUCCINCT_RPC_URL: ${process.env.SUCCINCT_RPC_URL || 'DEFAULT'}`);
console.error(` PGUS_PER_SECOND: ${process.env.PGUS_PER_SECOND || 'NOT SET'}`);
console.error(` PROVE_PER_BPGU: ${process.env.PROVE_PER_BPGU || 'NOT SET'}`);
} catch (error) {
console.error('ā Failed to load environment for MCP:', error);
// Continue without throwing - MCP should still work in read-only mode
}
}
/**
* Enhanced environment debugging with secure logging
*/
static async reloadEnvironmentVariables(): Promise<void> {
try {
// Re-read environment variables with ES6 approach
const { existsSync } = await import('fs');
const { join } = await import('path');
const envPath = join(process.cwd(), '.env');
if (existsSync(envPath)) {
// Re-import dotenv dynamically
const dotenv = await import('dotenv');
dotenv.config({ path: envPath, override: true });
console.error('š Environment variables reloaded from .env');
} else {
console.error('ā ļø No .env file found for reload');
}
} catch (error) {
console.error('ā Failed to reload environment:', error);
}
}
/**
* Force reload environment variables from .env file (synchronous)
* Enhanced for MCP compatibility
*/
static reloadEnvironmentVariablesSync(): void {
// Always force load for MCP contexts
this.forceLoadEnvironmentForMCP();
try {
// Use require for synchronous loading since this is a legacy compatibility method
const envPath = path.join(process.cwd(), '.env');
console.error(`š Attempting to reload environment from: ${envPath}`);
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
console.error(`š Found .env file, parsing ${envContent.split('\n').length} lines`);
let loadedVars = 0;
// Parse .env content manually
envContent.split('\n').forEach((line: string) => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) {
const [key, ...valueParts] = trimmed.split('=');
const value = valueParts.join('=').trim();
process.env[key.trim()] = value;
if (key.trim() === 'PRIVATE_KEY' || key.trim() === 'PROVER_ADDRESS') {
console.error(`š Loaded ${key.trim()}: ${value ? 'SET' : 'EMPTY'}`);
loadedVars++;
}
}
});
console.error(`š Loaded ${loadedVars} critical environment variables`);
} else {
console.error(`ā .env file not found at: ${envPath}`);
}
} catch (error) {
console.error(`Warning: Failed to reload environment variables sync: ${error}`);
}
}
/**
* Get environment variable with MCP fallback and official Succinct compatibility
* Follows the official pattern: PRIVATE_KEY takes priority over PROVER_PRIVATE_KEY
*/
static getEnvironmentVariable(primaryKey: string, fallbackKey?: string): string | undefined {
// Force load environment first
this.forceLoadEnvironmentForMCP();
let value = process.env[primaryKey];
// Try fallback if primary is not set or is placeholder
if ((!value || value === `your_${primaryKey.toLowerCase()}_here` || value === '') && fallbackKey) {
value = process.env[fallbackKey];
}
// Filter out placeholder values
if (value === `your_${primaryKey.toLowerCase()}_here` ||
value === 'your_private_key_here' ||
value === 'your_prover_address_here' ||
value === '') {
return undefined;
}
return value;
}
/**
* Safely mask private key for display/logging
*/
static maskPrivateKey(privateKey: string | undefined): string {
if (!privateKey || privateKey === '' || privateKey === 'your_private_key_here') {
return 'NOT_SET';
}
// Show first 4 and last 4 characters with masking
if (privateKey.length >= 8) {
return `${privateKey.substring(0, 4)}...${privateKey.substring(privateKey.length - 4)}`;
}
return this.MASKED_KEY_DISPLAY;
}
/**
* Validate private key format (inspired by base-mcp mnemonic validation)
*/
static validatePrivateKey(privateKey: string): { isValid: boolean; error?: string } {
if (!privateKey || privateKey.trim() === '') {
return { isValid: false, error: 'Private key cannot be empty' };
}
if (privateKey === 'your_private_key_here') {
return { isValid: false, error: 'Please replace placeholder with actual private key' };
}
// Remove 0x prefix if present
const cleanKey = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
// Check length
if (cleanKey.length !== 64) {
return { isValid: false, error: `Invalid private key length. Expected 64 hex characters, got ${cleanKey.length}` };
}
// Check for valid hex
if (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) {
return { isValid: false, error: 'Private key contains invalid characters. Must be valid hexadecimal.' };
}
return { isValid: true };
}
/**
* Secure environment loading with validation
*/
static loadSecureEnvironment(): {
success: boolean;
config?: NetworkConfig;
errors: string[];
warnings: string[];
} {
const errors: string[] = [];
const warnings: string[] = [];
// Force reload environment variables from .env file synchronously
this.reloadEnvironmentVariablesSync();
// Load environment variables
const rpcUrl = process.env.SUCCINCT_RPC_URL || 'https://rpc-production.succinct.xyz';
const proverAddress = process.env.PROVER_ADDRESS;
// Support both official Succinct naming (PRIVATE_KEY) and MCP naming (PROVER_PRIVATE_KEY)
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
// Validate private key
if (!privateKey) {
errors.push('PRIVATE_KEY or PROVER_PRIVATE_KEY environment variable is required');
} else {
const validation = this.validatePrivateKey(privateKey);
if (!validation.isValid) {
errors.push(`Invalid private key: ${validation.error}`);
}
}
// Validate prover address
if (!proverAddress || proverAddress === 'your_prover_address_here') {
errors.push('PROVER_ADDRESS environment variable is required');
} else if (!/^0x[0-9a-fA-F]{40}$/.test(proverAddress)) {
errors.push('PROVER_ADDRESS must be a valid Ethereum address');
}
// Check for common security issues
if (privateKey && privateKey.length < 60) {
warnings.push('Private key seems too short - ensure it\'s a full 64-character hex string');
}
if (process.env.NODE_ENV === 'production' && !privateKey?.match(/^[0-9a-fA-F]{64}$/)) {
warnings.push('In production, ensure private key is properly formatted hex without 0x prefix');
}
const config: NetworkConfig = {
rpcUrl,
privateKey: privateKey || '',
proverAddress,
stakingContract: process.env.STAKING_CONTRACT || '0x8F1B4633630b90C273E834C14924298ab6D1DA02',
proveToken: process.env.PROVE_TOKEN || '0xC47B85472b40435c0D96db5Df3e9B4C368DA6A8C'
};
return {
success: errors.length === 0,
config: errors.length === 0 ? config : undefined,
errors,
warnings
};
}
/**
* Generate secure Docker environment variables (never expose private key in logs)
*/
static generateSecureDockerEnv(config: NetworkConfig): { [key: string]: string } {
return {
'PROVER_PRIVATE_KEY': config.privateKey,
'PROVER_ADDRESS': config.proverAddress || '',
'RPC_URL': config.rpcUrl,
'PGUS_PER_SECOND': process.env.PGUS_PER_SECOND || '1000000',
'PROVE_PER_BPGU': process.env.PROVE_PER_BPGU || '0.5'
};
}
/**
* Log environment status securely (never log actual private keys)
*/
static logEnvironmentStatus(): void {
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
const proverAddress = process.env.PROVER_ADDRESS;
console.error('=== Secure Environment Status ===');
console.error('PRIVATE_KEY:', this.maskPrivateKey(privateKey));
console.error('PROVER_ADDRESS:', proverAddress ? `SET (${proverAddress})` : 'NOT SET');
console.error('PGUS_PER_SECOND:', process.env.PGUS_PER_SECOND || 'NOT SET');
console.error('PROVE_PER_BPGU:', process.env.PROVE_PER_BPGU || 'NOT SET');
console.error('=================================');
}
/**
* Enhanced environment validation with Base MCP inspired checks
*/
static validateEnvironmentCompleteness(): {
isComplete: boolean;
missingFields: string[];
warningFields: string[];
recommendations: string[];
} {
const missingFields: string[] = [];
const warningFields: string[] = [];
const recommendations: string[] = [];
// Check critical fields
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
const proverAddress = process.env.PROVER_ADDRESS;
if (!privateKey || privateKey === 'your_private_key_here') {
missingFields.push('PRIVATE_KEY or PROVER_PRIVATE_KEY');
recommendations.push('Create a fresh Sepolia testnet wallet and add private key to .env');
}
if (!proverAddress || proverAddress === 'your_prover_address_here') {
missingFields.push('PROVER_ADDRESS');
recommendations.push('Visit https://staking.sepolia.succinct.xyz/prover to create a prover');
}
// Check calibration fields
if (!process.env.PGUS_PER_SECOND) {
warningFields.push('PGUS_PER_SECOND');
recommendations.push('Run hardware calibration: "Calibrate my hardware for Succinct proving"');
}
if (!process.env.PROVE_PER_BPGU) {
warningFields.push('PROVE_PER_BPGU');
recommendations.push('Complete hardware calibration to get optimal bid price');
}
return {
isComplete: missingFields.length === 0,
missingFields,
warningFields,
recommendations
};
}
/**
* Auto-restore from backup if available (Base MCP inspired feature)
*/
static async autoRestoreFromBackup(): Promise<{ restored: boolean; message: string }> {
const { existsSync, readFileSync, writeFileSync } = await import('fs');
const { join } = await import('path');
const currentEnvPath = join(process.cwd(), '.env');
const backupPaths = [
join(process.cwd(), '.env.backup'),
join(process.cwd(), '.env.bak'),
join(process.cwd(), '.env.backup-*')
];
// Check if current .env is empty or has placeholders
if (existsSync(currentEnvPath)) {
const currentContent = readFileSync(currentEnvPath, 'utf8');
const hasValidCredentials = currentContent.includes('PRIVATE_KEY=') &&
!currentContent.includes('your_private_key_here') &&
currentContent.includes('PROVER_ADDRESS=') &&
!currentContent.includes('your_prover_address_here');
if (hasValidCredentials) {
return { restored: false, message: 'Current .env file contains valid credentials' };
}
}
// Try to find backup files
for (const backupPattern of backupPaths) {
try {
if (existsSync(backupPattern)) {
const backupContent = readFileSync(backupPattern, 'utf8');
if (backupContent.includes('PRIVATE_KEY=') &&
!backupContent.includes('your_private_key_here')) {
// Create backup of current .env
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
writeFileSync(`${currentEnvPath}.backup-${timestamp}`,
existsSync(currentEnvPath) ? readFileSync(currentEnvPath, 'utf8') : '');
// Restore from backup
writeFileSync(currentEnvPath, backupContent);
return {
restored: true,
message: `Successfully restored .env from ${backupPattern}`
};
}
}
} catch (error) {
continue; // Try next backup
}
}
return { restored: false, message: 'No valid backup found' };
}
/**
* Generate environment health report (Base MCP inspired)
*/
static generateEnvironmentReport(): {
status: 'healthy' | 'warning' | 'critical';
summary: string;
details: string[];
quickFixes: string[];
} {
const validation = this.validateEnvironmentCompleteness();
const secureEnv = this.loadSecureEnvironment();
let status: 'healthy' | 'warning' | 'critical' = 'healthy';
const details: string[] = [];
const quickFixes: string[] = [];
if (validation.missingFields.length > 0) {
status = 'critical';
details.push(`ā Missing critical fields: ${validation.missingFields.join(', ')}`);
quickFixes.push('Run: "npm run init" to setup credentials');
}
if (validation.warningFields.length > 0 && status !== 'critical') {
status = 'warning';
details.push(`ā ļø Missing calibration: ${validation.warningFields.join(', ')}`);
quickFixes.push('Run: "Calibrate my hardware for Succinct proving"');
}
if (secureEnv.success && validation.isComplete) {
details.push('ā
All credentials configured correctly');
details.push('ā
Environment validation passed');
}
// Add security checks
const privateKey = process.env.PRIVATE_KEY || process.env.PROVER_PRIVATE_KEY;
if (privateKey && privateKey.length > 0 && privateKey !== 'your_private_key_here') {
if (privateKey.length !== 64) {
status = 'critical';
details.push('ā Private key length invalid (must be 64 hex characters)');
quickFixes.push('Verify private key format in .env file');
}
}
const summary = status === 'healthy'
? 'Environment is properly configured'
: status === 'warning'
? 'Environment needs calibration'
: 'Environment requires immediate attention';
return { status, summary, details, quickFixes };
}
}