Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pending-transactions.csv

This file was deleted.

33 changes: 30 additions & 3 deletions src/configuration/errorLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ export class ErrorLogger {
this.consoleLog = config.consoleLog ?? true;
}

/**
* Recursively converts BigInt values to strings for JSON serialization
*/
private serializeBigInts(obj: unknown): unknown {
if (obj === null || obj === undefined) {
return obj;
}

if (typeof obj === 'bigint') {
return obj.toString();
}

if (Array.isArray(obj)) {
return obj.map(item => this.serializeBigInts(item));
}

if (typeof obj === 'object') {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = this.serializeBigInts(value);
}
return result;
}

return obj;
}

/**
* Log an error to the database and optionally to the console
*/
Expand All @@ -50,14 +77,14 @@ export class ErrorLogger {
// Extract context if available (from BaseError)
const context = error instanceof BaseError ? error.context : undefined;

// Create error log object
// Create error log object with BigInt serialization handling
const errorLog: ErrorLog = {
service_name: this.serviceName,
error_message: errorMessage,
stack_trace: stackTrace,
severity,
meta,
context,
meta: this.serializeBigInts(meta) as Record<string, unknown> | undefined,
context: this.serializeBigInts(context) as Record<string, unknown> | undefined,
};

// Log to console if enabled
Expand Down
48 changes: 42 additions & 6 deletions src/executor/strategies/RelayerExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,26 +409,62 @@ export class RelayerExecutor implements IExecutor {
profitability.estimates.expected_profit || BigInt(0);
const payoutAmount = profitability.estimates.payout_amount || BigInt(0);

// If expected profit is 0 or negative, it means total rewards < (payout + gas cost)
if (expectedProfit <= BigInt(0)) {
// Use sophisticated validation that matches BaseExecutor logic
// Calculate required profit margin like BaseExecutor does
const gasCost = profitability.estimates.gas_cost || BigInt(0);
const baseAmountForMargin = payoutAmount + (CONFIG.profitability.includeGasCost ? gasCost : BigInt(0));
const minProfitMarginPercent = CONFIG.profitability.minProfitMargin;
const requiredProfitValue = (baseAmountForMargin * BigInt(Math.round(minProfitMarginPercent * 100))) / 10000n;
const minimumRequiredProfit = baseAmountForMargin + requiredProfitValue;

if (expectedProfit < minimumRequiredProfit) {
const gasEstimate = profitability.estimates.gas_estimate || BigInt(0);
const totalRewards = profitability.deposit_details?.reduce((sum, d) => sum + (d.rewards || BigInt(0)), BigInt(0)) || BigInt(0);

const error = new TransactionValidationError(
`Transaction not profitable: expected profit is ${expectedProfit}. Skipping expensive simulation.`,
`Transaction does not meet profit requirements: expected profit ${ethers.formatEther(expectedProfit)} ETH < required ${ethers.formatEther(minimumRequiredProfit)} ETH`,
{
depositIds: depositIds.map(String),
expectedProfit: expectedProfit.toString(),
minimumRequiredProfit: minimumRequiredProfit.toString(),
payoutAmount: payoutAmount.toString(),
gasEstimate: gasEstimate.toString(),
gasCost: gasCost.toString(),
totalRewards: totalRewards.toString(),
baseAmountForMargin: baseAmountForMargin.toString(),
requiredProfitValue: requiredProfitValue.toString(),
minProfitMarginPercent: `${minProfitMarginPercent * 100}%`,
},
);
this.logger.warn(
'Skipping simulation due to non-profitable transaction',
'Rejecting transaction - insufficient profit margin',
{
expectedProfit: expectedProfit.toString(),
payoutAmount: payoutAmount.toString(),
expectedProfit: ethers.formatEther(expectedProfit),
minimumRequiredProfit: ethers.formatEther(minimumRequiredProfit),
payoutAmount: ethers.formatEther(payoutAmount),
gasEstimate: gasEstimate.toString(),
gasCost: ethers.formatEther(gasCost),
totalRewards: ethers.formatEther(totalRewards),
depositCount: depositIds.length,
profitMarginPercent: `${minProfitMarginPercent * 100}%`,
},
);
return { isValid: false, error };
}

// Log marginal but acceptable transactions for monitoring
const profitMargin = expectedProfit - baseAmountForMargin;
if (profitMargin < BigInt(10000000000000000)) { // Less than 0.01 ETH margin
this.logger.info(
'Proceeding with marginal profit transaction',
{
expectedProfit: ethers.formatEther(expectedProfit),
profitMargin: ethers.formatEther(profitMargin),
payoutAmount: ethers.formatEther(payoutAmount),
depositCount: depositIds.length,
},
);
}

// Try to get a better gas estimate using simulation if available
if (this.simulationService) {
Expand Down
90 changes: 77 additions & 13 deletions src/monitor/StakerMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { EventProcessingError, MonitorError } from '@/configuration/errors';
import { stakerAbi } from '@/configuration/abis';
import { ErrorLogger } from '@/configuration/errorLogger';
import { Deposit } from '@/database/interfaces/types';
import { DepositCountPredictor } from '@/utils/DepositCountPredictor';

/**
* Extended MonitorConfig that includes the error logger
Expand Down Expand Up @@ -59,6 +60,7 @@ export class StakerMonitor extends EventEmitter {
private cachedBlockNumber: number;
private lastBlockNumberUpdate: number;
private readonly BLOCK_NUMBER_CACHE_TTL = 5000; // 5 seconds cache
private readonly depositPredictor: DepositCountPredictor;

constructor(config: ExtendedMonitorConfig) {
super();
Expand Down Expand Up @@ -87,6 +89,7 @@ export class StakerMonitor extends EventEmitter {
this.depositScanInProgress = false;
this.cachedBlockNumber = 0;
this.lastBlockNumberUpdate = 0;
this.depositPredictor = new DepositCountPredictor(this.contract, this.logger);
}

/**
Expand Down Expand Up @@ -273,6 +276,43 @@ export class StakerMonitor extends EventEmitter {
}
}

/**
* Retry function with exponential backoff for rate limiting
*/
private async retryWithBackoff<T>(
operation: () => Promise<T>,
operationName: string,
maxRetries = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

// Check if it's a rate limiting error
if (errorMessage.includes('compute units per second capacity') ||
errorMessage.includes('429') ||
errorMessage.includes('rate limit')) {

if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Exponential backoff, max 5s
this.logger.warn(
`Rate limit hit for ${operationName}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`
);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}

// If it's not a rate limit error or we've exhausted retries, throw
throw error;
}
}

throw new Error(`Failed after ${maxRetries} attempts`);
}

/**
* Queries contract events in batches for improved efficiency
* @param contract - Contract instance to query
Expand All @@ -288,23 +328,35 @@ export class StakerMonitor extends EventEmitter {
toBlock: number,
): Promise<ethers.Log[]> {
try {
const eventPromises = eventNames.map(async (eventName) => {
// Execute event queries sequentially with rate limiting to avoid CU/second limits
// Each eth_getLogs costs 60 CUs, so we need to be careful with parallel requests
const allEvents: ethers.Log[] = [];

for (const eventName of eventNames) {
try {
const filter = contract.filters[eventName];
if (typeof filter === 'function') {
return await contract.queryFilter(filter(), fromBlock, toBlock);
// Retry with exponential backoff for rate limiting
const events = await this.retryWithBackoff(async () => {
return await contract.queryFilter(filter(), fromBlock, toBlock);
}, eventName);

allEvents.push(...events);

// Add delay between requests to respect rate limits (60 CUs each)
// Small delay to stay under CU/second capacity
if (eventNames.indexOf(eventName) < eventNames.length - 1) {
await new Promise(resolve => setTimeout(resolve, 150)); // 150ms delay
}
}
return [];
} catch (error) {
this.logger.warn(
`Failed to query ${eventName} events: ${error instanceof Error ? error.message : String(error)}`,
`Failed to query ${eventName} events after retries: ${error instanceof Error ? error.message : String(error)}`,
);
return [];
}
});

const eventArrays = await Promise.all(eventPromises);
return eventArrays.flat();
}

return allEvents;
} catch (error) {
this.logger.error('Error in batch event querying:', {
error: error instanceof Error ? error.message : String(error),
Expand Down Expand Up @@ -1438,15 +1490,27 @@ export class StakerMonitor extends EventEmitter {

this.logger.info(`Found ${existingDepositIds.size} deposits in database`);

// Process sequentially starting from deposit ID 1
// Get predicted scan limit to reduce API calls
const predictedScanLimit = await this.depositPredictor.getPredictedScanLimit();
const stats = this.depositPredictor.getStats();

this.logger.info(`🔮 Deposit prediction stats`, {
predictedLimit: predictedScanLimit,
lastKnownMax: stats.lastKnownMax,
avgGrowthPerMin: stats.avgGrowthPerMinute.toFixed(2),
sampleCount: stats.samples,
lastSampleMinutesAgo: stats.lastSampleMinutesAgo.toFixed(1)
});

// Process sequentially starting from deposit ID 1, but limit scan range
let currentId = 1;
let emptyCounter = 0;
const MAX_EMPTY_TO_STOP = 10; // Very conservative - only stop after many empty deposits

this.logger.info(`Starting sequential deposit scan from ID ${currentId}`);
this.logger.info(`Starting sequential deposit scan from ID ${currentId} to ~${predictedScanLimit}`);

// Keep scanning until we find many consecutive completely empty deposits
while (emptyCounter < MAX_EMPTY_TO_STOP) {
// Keep scanning until we find many consecutive completely empty deposits OR reach predicted limit
while (emptyCounter < MAX_EMPTY_TO_STOP && currentId <= predictedScanLimit) {
try {
if (!this.contract || typeof this.contract.deposits !== 'function') {
throw new Error(
Expand Down
Loading
Loading