@elizaos/plugin-zapper
Version:
ElizaOS plugin for Zapper protocol integration - portfolio tracking, DeFi analytics, and blockchain data across 50+ networks
1,840 lines (1,831 loc) • 290 kB
JavaScript
import "./chunk-7D4SUZUM.js";
// src/actions/getPortfolio.ts
import { ModelType, logger, parseKeyValueXml } from "@elizaos/core";
// src/utils/zapperGraphQL.ts
import { elizaLogger as elizaLogger3 } from "@elizaos/core";
// src/utils/queryCache.ts
import { elizaLogger } from "@elizaos/core";
var ZapperQueryCache = class {
cache = /* @__PURE__ */ new Map();
stats = {
totalRequests: 0,
cacheHits: 0,
cacheMisses: 0,
hitRate: 0,
entryCount: 0,
totalMemoryUsage: 0,
oldestEntry: 0,
newestEntry: 0
};
config;
cleanupTimer = null;
constructor(config = {}) {
this.config = {
maxSize: 1e3,
defaultTTL: 3e5,
// 5 minutes
cleanupInterval: 6e4,
// 1 minute
enableStats: true,
...config
};
this.startCleanupTimer();
}
/**
* Generate cache key from query and variables
*/
generateCacheKey(query, variables) {
const variablesStr = variables ? JSON.stringify(variables) : "";
const combined = query + variablesStr;
let hash = 0;
for (let i = 0; i < combined.length; i++) {
const char = combined.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return `query_${Math.abs(hash)}_${combined.length}`;
}
/**
* Get cached response
*/
get(query, variables) {
const key = this.generateCacheKey(query, variables);
const entry = this.cache.get(key);
if (this.config.enableStats) {
this.stats.totalRequests++;
}
if (!entry) {
if (this.config.enableStats) {
this.stats.cacheMisses++;
this.updateHitRate();
}
return null;
}
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
if (this.config.enableStats) {
this.stats.cacheMisses++;
this.stats.entryCount--;
this.updateHitRate();
}
return null;
}
entry.accessCount++;
entry.lastAccessed = now;
if (this.config.enableStats) {
this.stats.cacheHits++;
this.updateHitRate();
}
return entry.data;
}
/**
* Set cached response
*/
set(query, variables, response, ttl) {
const key = this.generateCacheKey(query, variables);
const now = Date.now();
while (this.cache.size >= this.config.maxSize) {
this.evictLRU();
}
const entry = {
data: response,
timestamp: now,
ttl: ttl || this.config.defaultTTL,
accessCount: 1,
lastAccessed: now,
originalQuery: query,
variables
};
this.cache.set(key, entry);
if (this.config.enableStats) {
this.stats.entryCount++;
this.updateMemoryUsage();
this.updateTimestamps();
}
}
/**
* Check if query result is cached
*/
has(query, variables) {
const key = this.generateCacheKey(query, variables);
const entry = this.cache.get(key);
if (!entry) return false;
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
if (this.config.enableStats) {
this.stats.entryCount--;
}
return false;
}
return true;
}
/**
* Invalidate specific cache entry
*/
invalidate(query, variables) {
const key = this.generateCacheKey(query, variables);
const deleted = this.cache.delete(key);
if (deleted && this.config.enableStats) {
this.stats.entryCount--;
}
return deleted;
}
/**
* Invalidate cache entries by pattern
*/
invalidatePattern(pattern) {
let deletedCount = 0;
for (const [key, entry] of this.cache) {
if (entry.originalQuery.includes(pattern) || key.includes(pattern)) {
this.cache.delete(key);
deletedCount++;
}
}
if (this.config.enableStats) {
this.stats.entryCount -= deletedCount;
}
return deletedCount;
}
/**
* Clear all cache entries
*/
clear() {
this.cache.clear();
if (this.config.enableStats) {
this.stats.entryCount = 0;
this.stats.totalMemoryUsage = 0;
this.stats.oldestEntry = 0;
this.stats.newestEntry = 0;
}
}
/**
* Get cache statistics
*/
getStats() {
if (this.config.enableStats) {
this.updateMemoryUsage();
this.updateTimestamps();
}
return { ...this.stats };
}
/**
* Update cache configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
if (config.cleanupInterval !== void 0) {
this.stopCleanupTimer();
this.startCleanupTimer();
}
}
/**
* Optimize cache performance
*/
optimize() {
const initialSize = this.cache.size;
const initialMemory = this.stats.totalMemoryUsage;
const now = Date.now();
for (const [key, entry] of this.cache) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
}
}
while (this.cache.size > this.config.maxSize * 0.8) {
this.evictLRU();
}
const finalSize = this.cache.size;
const finalMemory = this.config.enableStats ? this.calculateMemoryUsage() : 0;
if (this.config.enableStats) {
this.stats.entryCount = finalSize;
this.stats.totalMemoryUsage = finalMemory;
}
return {
entriesRemoved: initialSize - finalSize,
memoryFreed: initialMemory - finalMemory
};
}
/**
* Get cache entries for debugging
*/
getEntries() {
return Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
entry: { ...entry }
}));
}
/**
* Destroy cache and cleanup resources
*/
destroy() {
this.stopCleanupTimer();
this.clear();
}
/**
* Start cleanup timer
*/
startCleanupTimer() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(() => {
this.cleanupExpiredEntries();
}, this.config.cleanupInterval);
}
/**
* Stop cleanup timer
*/
stopCleanupTimer() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
}
/**
* Clean up expired entries
*/
cleanupExpiredEntries() {
const now = Date.now();
let removedCount = 0;
for (const [key, entry] of this.cache) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
removedCount++;
}
}
if (removedCount > 0) {
elizaLogger.debug(`Cache cleanup: removed ${removedCount} expired entries`);
if (this.config.enableStats) {
this.stats.entryCount -= removedCount;
}
}
}
/**
* Evict least recently used entry
*/
evictLRU() {
let oldestKey = null;
let oldestTime = Date.now();
for (const [key, entry] of this.cache) {
if (entry.lastAccessed < oldestTime) {
oldestTime = entry.lastAccessed;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
if (this.config.enableStats) {
this.stats.entryCount--;
}
}
}
/**
* Update hit rate statistic
*/
updateHitRate() {
if (this.stats.totalRequests > 0) {
this.stats.hitRate = this.stats.cacheHits / this.stats.totalRequests * 100;
}
}
/**
* Update memory usage estimate
*/
updateMemoryUsage() {
this.stats.totalMemoryUsage = this.calculateMemoryUsage();
}
/**
* Calculate memory usage estimate
*/
calculateMemoryUsage() {
let totalSize = 0;
for (const [key, entry] of this.cache) {
totalSize += key.length * 2;
totalSize += JSON.stringify(entry.data).length * 2;
totalSize += 64;
}
return totalSize;
}
/**
* Update timestamp statistics
*/
updateTimestamps() {
if (this.cache.size === 0) {
this.stats.oldestEntry = 0;
this.stats.newestEntry = 0;
return;
}
let oldest = Date.now();
let newest = 0;
for (const entry of this.cache.values()) {
if (entry.timestamp < oldest) {
oldest = entry.timestamp;
}
if (entry.timestamp > newest) {
newest = entry.timestamp;
}
}
this.stats.oldestEntry = oldest;
this.stats.newestEntry = newest;
}
};
var QueryOptimizer = class {
/**
* Optimize GraphQL query by removing unnecessary fields
*/
static removeUnusedFields(query, usedFields) {
let optimized = query;
optimized = optimized.replace(/\s*#.*$/gm, "");
optimized = optimized.replace(/\s+/g, " ").trim();
return optimized;
}
/**
* Merge multiple queries into a single batch query
*/
static mergeQueries(queries) {
const mergedFields = queries.map(({ alias, query }) => {
const match = query.match(/{\s*([^}]+)\s*}/);
const body = match ? match[1] : "";
return `${alias}: ${body}`;
}).join("\n ");
return `query BatchQuery {
${mergedFields}
}`;
}
/**
* Optimize query variables
*/
static optimizeVariables(variables) {
const optimized = {};
for (const [key, value] of Object.entries(variables)) {
if (value != null) {
optimized[key] = value;
}
}
return optimized;
}
/**
* Calculate query complexity score
*/
static calculateComplexity(query) {
let complexity = 0;
const maxDepth = (query.match(/{/g) || []).length;
complexity += maxDepth * 2;
const fieldCount = (query.match(/\w+(?=\s*[:{])/g) || []).length;
complexity += fieldCount;
const variableCount = (query.match(/\$\w+/g) || []).length;
complexity += variableCount;
return complexity;
}
/**
* Suggest query optimizations
*/
static suggestOptimizations(query) {
const suggestions = [];
if (query.includes("...")) {
suggestions.push("Consider using fragments to reduce query duplication");
}
if (query.length > 2e3) {
suggestions.push("Query is quite large - consider splitting into smaller queries");
}
if (this.calculateComplexity(query) > 50) {
suggestions.push("Query complexity is high - consider reducing nesting levels");
}
if (query.includes("*")) {
suggestions.push("Avoid wildcard selections - specify only needed fields");
}
return suggestions;
}
};
// src/utils/errorHandler.ts
import { elizaLogger as elizaLogger2 } from "@elizaos/core";
var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
ErrorType2["NETWORK_ERROR"] = "NETWORK_ERROR";
ErrorType2["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
ErrorType2["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
ErrorType2["AUTHENTICATION_ERROR"] = "AUTHENTICATION_ERROR";
ErrorType2["GRAPHQL_ERROR"] = "GRAPHQL_ERROR";
ErrorType2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
ErrorType2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
return ErrorType2;
})(ErrorType || {});
var ErrorSeverity = /* @__PURE__ */ ((ErrorSeverity2) => {
ErrorSeverity2["LOW"] = "LOW";
ErrorSeverity2["MEDIUM"] = "MEDIUM";
ErrorSeverity2["HIGH"] = "HIGH";
ErrorSeverity2["CRITICAL"] = "CRITICAL";
return ErrorSeverity2;
})(ErrorSeverity || {});
var ZapperErrorHandler = class {
config;
metrics;
circuitState = "CLOSED" /* CLOSED */;
circuitFailureCount = 0;
circuitLastFailureTime = 0;
circuitNextRetryTime = 0;
constructor(config = {}) {
this.config = {
enableLogging: true,
logLevel: "error",
enableMetrics: true,
enableCircuitBreaker: false,
circuitBreakerConfig: {
failureThreshold: 5,
resetTimeout: 6e4,
monitoringWindow: 3e5
},
...config
};
this.metrics = {
totalErrors: 0,
errorsByType: {},
errorsBySeverity: {},
retryAttempts: 0,
successfulRetries: 0,
circuitBreakerTrips: 0,
averageRetryDelay: 0,
lastErrorTime: 0
};
Object.values(ErrorType).forEach((type) => {
this.metrics.errorsByType[type] = 0;
});
Object.values(ErrorSeverity).forEach((severity) => {
this.metrics.errorsBySeverity[severity] = 0;
});
}
/**
* Process and classify an error
*/
processError(error, context) {
const zapperError = this.classifyError(error, context);
if (this.config.enableMetrics) {
this.updateMetrics(zapperError);
}
if (this.config.enableLogging) {
this.logError(zapperError);
}
return zapperError;
}
/**
* Execute function with retry logic
*/
async executeWithRetry(fn, retryConfig = {}, context) {
const config = {
maxAttempts: 3,
baseDelay: 1e3,
maxDelay: 3e4,
backoffMultiplier: 2,
jitter: true,
retryableErrors: [
"NETWORK_ERROR" /* NETWORK_ERROR */,
"TIMEOUT_ERROR" /* TIMEOUT_ERROR */,
"RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */
],
...retryConfig
};
if (this.config.enableCircuitBreaker && this.circuitState === "OPEN" /* OPEN */) {
if (Date.now() < this.circuitNextRetryTime) {
throw new Error(
`Circuit breaker is OPEN. Next retry at ${new Date(this.circuitNextRetryTime).toISOString()}`
);
} else {
this.circuitState = "HALF_OPEN" /* HALF_OPEN */;
this.logCircuitBreakerState("HALF_OPEN");
}
}
let lastError = null;
let totalDelay = 0;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
const result = await fn();
if (this.config.enableCircuitBreaker && this.circuitState === "HALF_OPEN" /* HALF_OPEN */) {
this.circuitState = "CLOSED" /* CLOSED */;
this.circuitFailureCount = 0;
this.logCircuitBreakerState("CLOSED");
}
if (attempt > 1 && this.config.enableMetrics) {
this.metrics.successfulRetries++;
this.metrics.averageRetryDelay = totalDelay / (attempt - 1);
}
return result;
} catch (error) {
lastError = this.processError(error, {
...context,
attempt,
maxAttempts: config.maxAttempts
});
if (!config.retryableErrors.includes(lastError.type)) {
this.handleCircuitBreakerOnError(lastError);
throw this.createEnhancedError(lastError);
}
if (attempt === config.maxAttempts) {
this.handleCircuitBreakerOnError(lastError);
break;
}
const delay = this.calculateRetryDelay(attempt, config);
totalDelay += delay;
if (this.config.enableMetrics) {
this.metrics.retryAttempts++;
}
if (this.config.enableLogging) {
elizaLogger2.warn(
`Retry attempt ${attempt}/${config.maxAttempts} failed, retrying in ${delay}ms`,
{
error: lastError.message,
delay,
attempt,
maxAttempts: config.maxAttempts
}
);
}
await this.delay(delay);
}
}
if (lastError) {
this.handleCircuitBreakerOnError(lastError);
throw this.createEnhancedError(lastError);
}
throw new Error("Execution failed after all retry attempts");
}
/**
* Get error metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Reset error metrics
*/
resetMetrics() {
this.metrics = {
totalErrors: 0,
errorsByType: {},
errorsBySeverity: {},
retryAttempts: 0,
successfulRetries: 0,
circuitBreakerTrips: 0,
averageRetryDelay: 0,
lastErrorTime: 0
};
Object.values(ErrorType).forEach((type) => {
this.metrics.errorsByType[type] = 0;
});
Object.values(ErrorSeverity).forEach((severity) => {
this.metrics.errorsBySeverity[severity] = 0;
});
}
/**
* Get circuit breaker status
*/
getCircuitBreakerStatus() {
return {
state: this.circuitState,
failureCount: this.circuitFailureCount,
nextRetryTime: this.circuitNextRetryTime,
isOpen: this.circuitState === "OPEN" /* OPEN */
};
}
/**
* Manually reset circuit breaker
*/
resetCircuitBreaker() {
this.circuitState = "CLOSED" /* CLOSED */;
this.circuitFailureCount = 0;
this.circuitNextRetryTime = 0;
this.logCircuitBreakerState("CLOSED");
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
}
/**
* Classify error type and severity
*/
classifyError(error, context) {
const timestamp = Date.now();
let type;
let severity;
let message;
let statusCode;
let retryAfter;
let suggestion;
if (error.errors && Array.isArray(error.errors)) {
type = "GRAPHQL_ERROR" /* GRAPHQL_ERROR */;
severity = "MEDIUM" /* MEDIUM */;
message = error.errors.map((e) => e.message).join("; ");
suggestion = this.getGraphQLErrorSuggestion(error.errors);
} else if (error.name === "AbortError" || error.message?.includes("timeout")) {
type = "TIMEOUT_ERROR" /* TIMEOUT_ERROR */;
severity = "MEDIUM" /* MEDIUM */;
message = error.message || "Request timed out";
suggestion = "Consider increasing timeout or checking network connectivity";
} else if (error.status || error.statusCode) {
statusCode = error.status || error.statusCode;
if (statusCode === 401 || statusCode === 403) {
type = "AUTHENTICATION_ERROR" /* AUTHENTICATION_ERROR */;
severity = "HIGH" /* HIGH */;
message = "Authentication failed - check API key";
suggestion = "Verify your API key is valid and has the required permissions";
} else if (statusCode === 429) {
type = "RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */;
severity = "MEDIUM" /* MEDIUM */;
message = "Rate limit exceeded";
retryAfter = this.extractRetryAfter(error);
suggestion = retryAfter ? `Wait ${retryAfter}ms before retrying` : "Reduce request frequency or implement exponential backoff";
} else if (statusCode >= 500) {
type = "NETWORK_ERROR" /* NETWORK_ERROR */;
severity = "HIGH" /* HIGH */;
message = `Server error: ${statusCode}`;
suggestion = "Server is experiencing issues, retry with exponential backoff";
} else {
type = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
severity = "MEDIUM" /* MEDIUM */;
message = error.message || `HTTP error: ${statusCode}`;
}
} else if (error.name === "ValidationError" || error.message?.includes("validation")) {
type = "VALIDATION_ERROR" /* VALIDATION_ERROR */;
severity = "LOW" /* LOW */;
message = error.message || "Validation failed";
suggestion = "Check input parameters and format";
} else if (error.code === "ECONNRESET" || error.code === "ENOTFOUND" || error.code === "ETIMEDOUT") {
type = "NETWORK_ERROR" /* NETWORK_ERROR */;
severity = "HIGH" /* HIGH */;
message = `Network error: ${error.code}`;
suggestion = "Check network connectivity and DNS resolution";
} else {
type = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
severity = "MEDIUM" /* MEDIUM */;
message = error.message || "Unknown error occurred";
}
return {
type,
severity,
message,
originalError: error instanceof Error ? error : void 0,
statusCode,
retryAfter,
timestamp,
context,
retryable: this.isRetryable(type, statusCode),
suggestion
};
}
/**
* Check if error is retryable
*/
isRetryable(type, statusCode) {
const retryableTypes = [
"NETWORK_ERROR" /* NETWORK_ERROR */,
"TIMEOUT_ERROR" /* TIMEOUT_ERROR */,
"RATE_LIMIT_ERROR" /* RATE_LIMIT_ERROR */
];
if (retryableTypes.includes(type)) {
return true;
}
if (statusCode && statusCode >= 500) {
return true;
}
return false;
}
/**
* Update error metrics
*/
updateMetrics(error) {
this.metrics.totalErrors++;
this.metrics.errorsByType[error.type]++;
this.metrics.errorsBySeverity[error.severity]++;
this.metrics.lastErrorTime = error.timestamp;
}
/**
* Log error based on configuration
*/
logError(error) {
const logData = {
type: error.type,
severity: error.severity,
message: error.message,
statusCode: error.statusCode,
retryable: error.retryable,
suggestion: error.suggestion,
context: error.context
};
switch (this.config.logLevel) {
case "debug":
elizaLogger2.debug("Zapper error occurred", logData);
break;
case "info":
elizaLogger2.info("Zapper error occurred", logData);
break;
case "warn":
elizaLogger2.warn("Zapper error occurred", logData);
break;
case "error":
default:
elizaLogger2.error("Zapper error occurred", logData);
break;
}
}
/**
* Handle circuit breaker on error
*/
handleCircuitBreakerOnError(error) {
if (!this.config.enableCircuitBreaker) return;
const now = Date.now();
const config = this.config.circuitBreakerConfig;
if (error.severity === "HIGH" /* HIGH */ || error.severity === "CRITICAL" /* CRITICAL */) {
this.circuitFailureCount++;
this.circuitLastFailureTime = now;
if (this.circuitFailureCount >= config.failureThreshold) {
this.circuitState = "OPEN" /* OPEN */;
this.circuitNextRetryTime = now + config.resetTimeout;
this.metrics.circuitBreakerTrips++;
this.logCircuitBreakerState("OPEN");
}
}
}
/**
* Log circuit breaker state changes
*/
logCircuitBreakerState(state) {
if (this.config.enableLogging) {
elizaLogger2.info(`Circuit breaker state changed to ${state}`, {
failureCount: this.circuitFailureCount,
nextRetryTime: this.circuitNextRetryTime,
threshold: this.config.circuitBreakerConfig?.failureThreshold
});
}
}
/**
* Calculate retry delay with exponential backoff and jitter
*/
calculateRetryDelay(attempt, config) {
let delay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1);
delay = Math.min(delay, config.maxDelay);
if (config.jitter) {
delay *= 0.5 + Math.random() * 0.5;
}
return Math.floor(delay);
}
/**
* Extract retry-after header value
*/
extractRetryAfter(error) {
const retryAfter = error.headers?.["retry-after"] || error.retryAfter;
if (retryAfter) {
const parsed = parseInt(retryAfter, 10);
return isNaN(parsed) ? void 0 : parsed * 1e3;
}
return void 0;
}
/**
* Get suggestion for GraphQL errors
*/
getGraphQLErrorSuggestion(errors) {
if (!errors || errors.length === 0) return void 0;
const firstError = errors[0];
const message = firstError.message?.toLowerCase() || "";
if (message.includes("field") && message.includes("not exist")) {
return "Check field names in your GraphQL query";
}
if (message.includes("variable") && message.includes("required")) {
return "Ensure all required variables are provided";
}
if (message.includes("syntax")) {
return "Check GraphQL query syntax";
}
if (message.includes("depth") || message.includes("complexity")) {
return "Reduce query complexity or nesting depth";
}
return "Check GraphQL query structure and variables";
}
/**
* Create enhanced error with additional context
*/
createEnhancedError(zapperError) {
const enhancedError = new Error(zapperError.message);
enhancedError.name = `ZapperError_${zapperError.type}`;
Object.assign(enhancedError, {
type: zapperError.type,
severity: zapperError.severity,
statusCode: zapperError.statusCode,
retryable: zapperError.retryable,
suggestion: zapperError.suggestion,
context: zapperError.context,
timestamp: zapperError.timestamp
});
return enhancedError;
}
/**
* Delay utility for retry logic
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
var defaultErrorHandler = new ZapperErrorHandler();
// src/utils/rateLimiter.ts
var TokenBucketRateLimiter = class {
tokens;
lastRefill;
config;
constructor(config) {
this.config = config;
this.tokens = config.maxRequests;
this.lastRefill = Date.now();
}
/**
* Check if request is allowed
*/
isAllowed(tokensRequested = 1) {
this.refillTokens();
const allowed = this.tokens >= tokensRequested;
if (allowed) {
this.tokens -= tokensRequested;
}
return {
allowed,
remaining: Math.max(0, this.tokens),
resetTime: this.getNextResetTime(),
retryAfter: allowed ? void 0 : this.calculateRetryAfter()
};
}
/**
* Refill tokens based on time elapsed
*/
refillTokens() {
const now = Date.now();
const elapsed = now - this.lastRefill;
const refillRate = this.config.refillRate || this.config.maxRequests;
const tokensToAdd = Math.floor(elapsed / this.config.windowMs * refillRate);
if (tokensToAdd > 0) {
this.tokens = Math.min(this.config.maxRequests, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
/**
* Get next reset time
*/
getNextResetTime() {
const refillRate = this.config.refillRate || this.config.maxRequests;
const timeToFull = (this.config.maxRequests - this.tokens) / refillRate * this.config.windowMs;
return Date.now() + timeToFull;
}
/**
* Calculate retry after time
*/
calculateRetryAfter() {
const refillRate = this.config.refillRate || this.config.maxRequests;
return Math.ceil(this.config.windowMs / refillRate);
}
/**
* Reset the bucket
*/
reset() {
this.tokens = this.config.maxRequests;
this.lastRefill = Date.now();
}
};
var SlidingWindowRateLimiter = class {
requests = [];
config;
constructor(config) {
this.config = config;
}
/**
* Check if request is allowed
*/
isAllowed() {
const now = Date.now();
const windowStart = now - this.config.windowMs;
this.requests = this.requests.filter((timestamp) => timestamp > windowStart);
const allowed = this.requests.length < this.config.maxRequests;
if (allowed) {
this.requests.push(now);
}
return {
allowed,
remaining: Math.max(0, this.config.maxRequests - this.requests.length),
resetTime: this.getNextResetTime(),
retryAfter: allowed ? void 0 : this.calculateRetryAfter()
};
}
/**
* Get next reset time
*/
getNextResetTime() {
if (this.requests.length === 0) {
return Date.now() + this.config.windowMs;
}
return this.requests[0] + this.config.windowMs;
}
/**
* Calculate retry after time
*/
calculateRetryAfter() {
if (this.requests.length === 0) {
return 0;
}
const oldestRequest = this.requests[0];
return Math.max(0, oldestRequest + this.config.windowMs - Date.now());
}
/**
* Reset the window
*/
reset() {
this.requests = [];
}
};
var FixedWindowRateLimiter = class {
requestCount = 0;
windowStart;
config;
constructor(config) {
this.config = config;
this.windowStart = Date.now();
}
/**
* Check if request is allowed
*/
isAllowed() {
const now = Date.now();
if (now - this.windowStart >= this.config.windowMs) {
this.requestCount = 0;
this.windowStart = now;
}
const allowed = this.requestCount < this.config.maxRequests;
if (allowed) {
this.requestCount++;
}
return {
allowed,
remaining: Math.max(0, this.config.maxRequests - this.requestCount),
resetTime: this.windowStart + this.config.windowMs,
retryAfter: allowed ? void 0 : this.windowStart + this.config.windowMs - now
};
}
/**
* Reset the window
*/
reset() {
this.requestCount = 0;
this.windowStart = Date.now();
}
};
var RequestQueue = class {
queue = [];
processing = false;
config;
rateLimiter;
constructor(config, rateLimiter) {
this.config = config;
this.rateLimiter = rateLimiter;
}
/**
* Add request to queue
*/
async enqueue() {
return new Promise((resolve, reject) => {
const queueSize = this.config.queueSize || 100;
if (this.queue.length >= queueSize) {
reject(new Error("Queue is full"));
return;
}
this.queue.push({
resolve,
reject,
timestamp: Date.now(),
id: Math.random().toString(36).substring(7)
});
this.processQueue();
});
}
/**
* Process queued requests
*/
async processQueue() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const request = this.queue[0];
const result = this.rateLimiter.isAllowed();
if (result.allowed) {
this.queue.shift();
request.resolve(result);
} else {
if (result.retryAfter) {
await this.delay(result.retryAfter);
} else {
await this.delay(1e3);
}
}
}
this.processing = false;
}
/**
* Get queue status
*/
getQueueStatus() {
const now = Date.now();
const averageWaitTime = this.queue.length > 0 ? this.queue.reduce((sum, req) => sum + (now - req.timestamp), 0) / this.queue.length : 0;
return {
queueLength: this.queue.length,
processing: this.processing,
averageWaitTime
};
}
/**
* Clear queue
*/
clear() {
this.queue.forEach((req) => {
req.reject(new Error("Queue cleared"));
});
this.queue = [];
this.processing = false;
}
/**
* Delay utility
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
var ZapperRateLimiter = class {
rateLimiters = /* @__PURE__ */ new Map();
queues = /* @__PURE__ */ new Map();
metrics = /* @__PURE__ */ new Map();
config;
constructor(config) {
this.config = {
enableQueue: false,
enableMetrics: true,
keyGenerator: (context) => context?.key || "default",
...config
};
}
/**
* Check if request is allowed
*/
async checkLimit(context) {
const key = this.config.keyGenerator(context);
const rateLimiter = this.getRateLimiter(key);
if (this.config.enableMetrics) {
this.updateMetrics(key, "request");
}
let result;
if (this.config.enableQueue) {
const preCheck = rateLimiter.isAllowed();
if (!preCheck.allowed) {
const queue = this.getQueue(key, rateLimiter);
result = await queue.enqueue();
if (this.config.enableMetrics) {
this.updateMetrics(key, "queued");
}
} else {
result = preCheck;
}
} else {
result = rateLimiter.isAllowed();
}
if (this.config.enableMetrics) {
this.updateMetrics(key, result.allowed ? "allowed" : "rejected");
}
return result;
}
/**
* Get rate limiter for key
*/
getRateLimiter(key) {
if (!this.rateLimiters.has(key)) {
let rateLimiter;
switch (this.config.algorithm) {
case "TOKEN_BUCKET" /* TOKEN_BUCKET */:
rateLimiter = new TokenBucketRateLimiter(this.config);
break;
case "SLIDING_WINDOW" /* SLIDING_WINDOW */:
rateLimiter = new SlidingWindowRateLimiter(this.config);
break;
case "FIXED_WINDOW" /* FIXED_WINDOW */:
rateLimiter = new FixedWindowRateLimiter(this.config);
break;
default:
rateLimiter = new TokenBucketRateLimiter(this.config);
}
this.rateLimiters.set(key, rateLimiter);
}
return this.rateLimiters.get(key);
}
/**
* Get queue for key
*/
getQueue(key, rateLimiter) {
if (!this.queues.has(key)) {
this.queues.set(key, new RequestQueue(this.config, rateLimiter));
}
return this.queues.get(key);
}
/**
* Update metrics
*/
updateMetrics(key, eventType) {
if (!this.metrics.has(key)) {
this.metrics.set(key, {
totalRequests: 0,
allowedRequests: 0,
rejectedRequests: 0,
queuedRequests: 0,
averageWaitTime: 0,
peakUsage: 0,
currentUsage: 0,
lastResetTime: Date.now()
});
}
const metrics = this.metrics.get(key);
switch (eventType) {
case "request":
metrics.totalRequests++;
break;
case "allowed":
metrics.allowedRequests++;
break;
case "rejected":
metrics.rejectedRequests++;
break;
case "queued":
metrics.queuedRequests++;
break;
}
const rateLimiter = this.rateLimiters.get(key);
if (rateLimiter && eventType === "allowed") {
if (rateLimiter instanceof TokenBucketRateLimiter) {
metrics.currentUsage = this.config.maxRequests - rateLimiter.tokens;
} else {
metrics.currentUsage = Math.min(metrics.currentUsage + 1, this.config.maxRequests);
}
metrics.peakUsage = Math.max(metrics.peakUsage, metrics.currentUsage);
}
}
/**
* Get metrics for key
*/
getMetrics(key) {
if (key) {
return this.metrics.get(key) || {
totalRequests: 0,
allowedRequests: 0,
rejectedRequests: 0,
queuedRequests: 0,
averageWaitTime: 0,
peakUsage: 0,
currentUsage: 0,
lastResetTime: Date.now()
};
}
return this.metrics;
}
/**
* Reset rate limiter for key
*/
reset(key) {
if (key) {
const rateLimiter = this.rateLimiters.get(key);
if (rateLimiter) {
rateLimiter.reset();
}
const queue = this.queues.get(key);
if (queue) {
queue.clear();
}
this.metrics.delete(key);
} else {
this.rateLimiters.forEach((rateLimiter) => rateLimiter.reset());
this.queues.forEach((queue) => queue.clear());
this.metrics.clear();
}
}
/**
* Get queue status for key
*/
getQueueStatus(key) {
if (key) {
const queue = this.queues.get(key);
return queue ? queue.getQueueStatus() : null;
}
const status = {};
this.queues.forEach((queue, k) => {
status[k] = queue.getQueueStatus();
});
return status;
}
/**
* Update configuration
*/
updateConfig(config) {
Object.assign(this.config, config);
}
/**
* Get configuration
*/
getConfig() {
return { ...this.config };
}
};
var APIQuotaManager = class {
quotas = /* @__PURE__ */ new Map();
config;
constructor(config) {
this.config = {
enableMetrics: true,
keyGenerator: (context) => context?.key || "default",
...config
};
}
/**
* Check quota availability
*/
checkQuota(context) {
const key = this.config.keyGenerator(context);
const quota = this.getQuota(key);
this.resetQuotasIfNeeded(quota);
const dailyAllowed = quota.used.daily < quota.daily;
const monthlyAllowed = quota.used.monthly < quota.monthly;
const allowed = dailyAllowed && monthlyAllowed;
return {
allowed,
remaining: {
daily: Math.max(0, quota.daily - quota.used.daily),
monthly: Math.max(0, quota.monthly - quota.used.monthly)
},
resetTimes: quota.resetTimes
};
}
/**
* Consume quota
*/
consumeQuota(amount = 1, context) {
const key = this.config.keyGenerator(context);
const quota = this.getQuota(key);
this.resetQuotasIfNeeded(quota);
const canConsume = quota.used.daily + amount <= quota.daily && quota.used.monthly + amount <= quota.monthly;
if (canConsume) {
quota.used.daily += amount;
quota.used.monthly += amount;
return true;
}
return false;
}
/**
* Get quota for key
*/
getQuota(key) {
if (!this.quotas.has(key)) {
const now = Date.now();
this.quotas.set(key, {
daily: this.config.dailyQuota,
monthly: this.config.monthlyQuota,
used: { daily: 0, monthly: 0 },
resetTimes: {
daily: this.getNextDayReset(),
monthly: this.getNextMonthReset()
}
});
}
return this.quotas.get(key);
}
/**
* Reset quotas if needed
*/
resetQuotasIfNeeded(quota) {
const now = Date.now();
if (now >= quota.resetTimes.daily) {
quota.used.daily = 0;
quota.resetTimes.daily = this.getNextDayReset();
}
if (now >= quota.resetTimes.monthly) {
quota.used.monthly = 0;
quota.resetTimes.monthly = this.getNextMonthReset();
}
}
/**
* Get next day reset time
*/
getNextDayReset() {
const tomorrow = /* @__PURE__ */ new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(0, 0, 0, 0);
return tomorrow.getTime();
}
/**
* Get next month reset time
*/
getNextMonthReset() {
const nextMonth = /* @__PURE__ */ new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
nextMonth.setDate(1);
nextMonth.setHours(0, 0, 0, 0);
return nextMonth.getTime();
}
/**
* Get quota status
*/
getQuotaStatus(key) {
if (key) {
const quota = this.quotas.get(key);
return quota ? {
daily: {
limit: quota.daily,
used: quota.used.daily,
remaining: quota.daily - quota.used.daily
},
monthly: {
limit: quota.monthly,
used: quota.used.monthly,
remaining: quota.monthly - quota.used.monthly
},
resetTimes: quota.resetTimes
} : null;
}
const status = {};
this.quotas.forEach((quota, k) => {
status[k] = {
daily: {
limit: quota.daily,
used: quota.used.daily,
remaining: quota.daily - quota.used.daily
},
monthly: {
limit: quota.monthly,
used: quota.used.monthly,
remaining: quota.monthly - quota.used.monthly
},
resetTimes: quota.resetTimes
};
});
return status;
}
/**
* Reset quotas
*/
resetQuota(key) {
if (key) {
const quota = this.quotas.get(key);
if (quota) {
quota.used.daily = 0;
quota.used.monthly = 0;
}
} else {
this.quotas.forEach((quota) => {
quota.used.daily = 0;
quota.used.monthly = 0;
});
}
}
};
var RateLimitMiddleware = class {
rateLimiter;
quotaManager;
constructor(rateLimitConfig, quotaConfig) {
this.rateLimiter = new ZapperRateLimiter(rateLimitConfig);
this.quotaManager = new APIQuotaManager(quotaConfig);
}
/**
* Check if request is allowed
*/
async checkRequest(context) {
const [rateLimit, quota] = await Promise.all([
this.rateLimiter.checkLimit(context),
Promise.resolve(this.quotaManager.checkQuota(context))
]);
const allowed = rateLimit.allowed && quota.allowed;
if (allowed) {
this.quotaManager.consumeQuota(1, context);
}
return {
allowed,
rateLimit,
quota
};
}
/**
* Get comprehensive status
*/
getStatus() {
return {
rateLimit: {
metrics: this.rateLimiter.getMetrics(),
queueStatus: this.rateLimiter.getQueueStatus(),
config: this.rateLimiter.getConfig()
},
quota: this.quotaManager.getQuotaStatus()
};
}
/**
* Reset all limits
*/
reset() {
this.rateLimiter.reset();
this.quotaManager.resetQuota();
}
};
// src/utils/fetchWithPolyfill.ts
var fetchPolyfillLoaded = false;
async function fetchWithPolyfill(url, options) {
if (!globalThis.fetch && !fetchPolyfillLoaded) {
try {
const { default: fetch2, Headers, Request, Response } = await import("./src-J6JBKF5P.js");
globalThis.fetch = fetch2;
globalThis.Headers = Headers;
globalThis.Request = Request;
globalThis.Response = Response;
fetchPolyfillLoaded = true;
} catch (error) {
console.warn("Failed to load fetch polyfill:", error);
}
}
return globalThis.fetch(url, options);
}
// src/utils/zapperGraphQL.ts
var ZapperQueryBuilder = class {
query = "";
variables = {};
fragments = /* @__PURE__ */ new Map();
/**
* Set the main query operation
*/
setQuery(query) {
this.query = query;
return this;
}
/**
* Add variables to the query
*/
addVariables(variables) {
this.variables = { ...this.variables, ...variables };
return this;
}
/**
* Add a fragment to the query
*/
addFragment(name, fragment) {
this.fragments.set(name, fragment);
return this;
}
/**
* Build the complete GraphQL query with fragments
*/
build() {
let builtQuery = this.query;
if (this.fragments.size > 0) {
const fragmentsString = Array.from(this.fragments.values()).join("\n\n");
builtQuery = `${builtQuery}
${fragmentsString}`;
}
return {
query: builtQuery,
variables: this.variables
};
}
/**
* Clear the builder state
*/
clear() {
this.query = "";
this.variables = {};
this.fragments.clear();
return this;
}
};
var ZapperGraphQLClient = class {
config;
requestCount = 0;
lastRequestTime = 0;
cache = null;
errorHandler;
rateLimitMiddleware = null;
constructor(config) {
this.config = {
apiUrl: "https://public.zapper.xyz/graphql",
timeout: 3e4,
retryAttempts: 3,
retryDelay: 1e3,
debug: false,
enableCaching: true,
cacheConfig: {
maxSize: 1e3,
defaultTTL: 3e5,
cleanupInterval: 6e4
},
errorHandlerConfig: {},
rateLimitConfig: {
algorithm: "TOKEN_BUCKET" /* TOKEN_BUCKET */,
maxRequests: 100,
windowMs: 6e4,
enableQueue: true,
enableMetrics: true
},
quotaConfig: {
dailyQuota: 1e4,
monthlyQuota: 1e5,
enableMetrics: true
},
...config
};
if (this.config.enableCaching) {
this.cache = new ZapperQueryCache(this.config.cacheConfig);
}
this.errorHandler = new ZapperErrorHandler(this.config.errorHandlerConfig);
if (this.config.rateLimitConfig && this.config.quotaConfig) {
this.rateLimitMiddleware = new RateLimitMiddleware(
this.config.rateLimitConfig,
{
dailyQuota: this.config.quotaConfig.dailyQuota || 1e4,
monthlyQuota: this.config.quotaConfig.monthlyQuota || 1e5,
enableMetrics: this.config.quotaConfig.enableMetrics || true
}
);
}
}
/**
* Execute a GraphQL query with retry logic and caching
*/
async query(query, variables, options) {
const mergedOptions = {
timeout: this.config.timeout,
retryAttempts: this.config.retryAttempts,
retryDelay: this.config.retryDelay,
useCache: this.config.enableCaching,
cacheTTL: this.config.cacheConfig?.defaultTTL,
...options
};
if (mergedOptions.useCache && this.cache) {
const cachedResult = this.cache.get(query, variables);
if (cachedResult) {
if (this.config.debug) {
elizaLogger3.debug("GraphQL query served from cache", {
query: query.substring(0, 100) + "...",
variables
});
}
return cachedResult;
}
}
if (this.rateLimitMiddleware) {
const limitCheck = await this.rateLimitMiddleware.checkRequest({
query: query.substring(0, 100) + "...",
variables
});
if (!limitCheck.allowed) {
const error = new Error("Rate limit exceeded");
error.retryAfter = limitCheck.rateLimit.retryAfter;
error.remaining = limitCheck.rateLimit.remaining;
error.resetTime = limitCheck.rateLimit.resetTime;
error.quotaRemaining = limitCheck.quota.remaining;
throw error;
}
if (this.config.debug) {
elizaLogger3.debug("Rate limit check passed", {
remaining: limitCheck.rateLimit.remaining,
quotaRemaining: limitCheck.quota.remaining
});
}
}
const optimizedQuery = QueryOptimizer.removeUnusedFields(query, []);
const optimizedVariables = QueryOptimizer.optimizeVariables(variables || {});
return this.errorHandler.executeWithRetry(
async () => {
if (this.config.debug) {
elizaLogger3.debug("Executing GraphQL query", {
query: optimizedQuery.substring(0, 100) + "...",
variables: optimizedVariables,
fromCache: false
});
}
const response = await this.executeQuery(
optimizedQuery,
optimizedVariables,
mergedOptions.timeout
);
if (response.errors && response.errors.length > 0) {
elizaLogger3.warn("GraphQL query returned errors", { errors: response.errors });
throw response;
}
if (mergedOptions.useCache && this.cache && response.data) {
this.cache.set(query, variables, response, mergedOptions.cacheTTL);
}
return response;
},
{
maxAttempts: mergedOptions.retryAttempts,
baseDelay: mergedOptions.retryDelay,
backoffMultiplier: 2,
jitter: true
},
{
query: optimizedQuery.substring(0, 100) + "...",
variables: optimizedVariables,
useCache: mergedOptions.useCache
}
);
}
/**
* Execute a GraphQL mutation
*/
async mutate(mutation, variables, options) {
return this.query(mutation, variables, options);
}
/**
* Execute multiple queries in parallel
*/
async batchQuery(queries, options) {
const promises = queries.map(
({ query, variables }) => this.query(query, variables, options)
);
return Promise.all(promises);
}
/**
* Get client statistics
*/
getStats() {
const stats = {
requestCount: this.requestCount,
lastRequestTime: this.lastRequestTime,
avgRequestTime: this.lastRequestTime / Math.max(this.requestCount, 1)
};
const result = { ...stats };
if (this.cache) {
result.cacheStats = this.cache.getStats();
}
if (this.errorHandler) {
result.errorStats = this.errorHandler.getMetrics();
}
if (this.rateLimitMiddleware) {
result.rateLimitStats = this.rateLimitMiddleware.getStatus();
}
return result;
}
/**
* Clear cache
*/
clearCache() {
if (this.cache) {
this.cache.clear();
}
}
/**
* Invalidate cache entry
*/
invalidateCache(query, variables) {
if (this.cache) {
return this.cache.invalidate(query, variables);
}
return false;
}
/**
* Invalidate cache entries by pattern
*/
invalidateCachePattern(pattern) {
if (this.cache) {
return this.cache.invalidatePattern(pattern);
}
return 0;
}
/**
* Optimize cache
*/
optimizeCache() {
if (this.cache) {
return this.cache.optimize();
}
return { entriesRemoved: 0, memoryFreed: 0 };
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
if (config.enableCaching !== void 0 || config.cacheConfig) {
if (this.cache) {
this.cache.destroy();
}
if (this.config.enableCaching) {
this.cache = new ZapperQueryCache(this.config.cacheConfig);
} else {
this.cache = null;
}
}
}
/**
* Get error handler metrics
*/
getErrorMetrics() {
return this.errorHandler.getMetrics();
}
/**
* Reset error metrics
*/
resetErrorMetrics() {
this.errorHandler.resetMetrics();
}
/**
* Get circuit breaker status
*/
getCircuitBreakerStatus() {
return this.errorHandler.getCircuitBreakerStatus();
}
/**
* Reset circuit breaker
*/
resetCircuitBreaker() {
this.errorHandler.resetCircuitBreaker();
}
/**
* Get rate limit status
*/
getRateLimitStatus() {
return this.rateLimitMiddleware?.getStatus() || null;
}
/**
* Reset rate limits
*/
resetRateLimits() {
if (this.rateLimitMiddleware) {
this.rateLimitMiddleware.reset();
}
}
/**
* Destroy client and cleanup resources
*/
destroy() {
if (this.cache) {
this.cache.destroy();
this.cache = null;
}
}
/**
* Execute the actual GraphQL query
*/
async executeQuery(query, variables, timeout) {
const startTime = Date.now();
this.requestCount++;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout || this.config.timeout);
try {
const response = await fetchWithPolyfill(this.config.apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
// CRITICAL: Use 'x-zapper-api-key' header, NOT 'Authorization: Bearer'
// Bearer token format causes 401 authentication errors
"x-zapper-api-key": this.config.apiKey,
"User-Agent": "ElizaOS-ZapperPlugin/1.0.0"
},
body: JSON.stringify({
query,
variables: variables || {}
}),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
}
const result = await response.json();
this.lastRequestTime = Date.now() - startTime;
if (this.config.debug) {
elizaLogger3.debug("GraphQL query completed", {
status: response.status,
duration: this.lastRequestTime,
hasErrors: !!(result.errors && result.errors.length > 0)
});
}
return result;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
throw new Error(`Query timeout after ${timeout || this.config.timeout}ms`);
}
throw error;
}
}
/**
* Delay utility for retry logic
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
};
var ZapperGraphQLUtils = {
/**
* Create a new query builder instance
*/
createQueryBuilder() {
return new ZapperQueryBuilder();
},
/**
* Create a new GraphQL client instance
*/
createClient(config) {