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
21 changes: 2 additions & 19 deletions backend/src/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export interface ChainConfig {
chainId: number;
name: string;
rpcUrl: string;
rpcUrls: string[];
nativeCurrency: { name: string; symbol: string; decimals: number };
blockExplorer: string;
contracts: {
Expand All @@ -12,26 +11,12 @@ export interface ChainConfig {
};
}

/**
* Parse comma-separated RPC URLs from env var, falling back to single URL.
* Example: BASE_RPC_URLS="https://mainnet.base.org,https://base.llamarpc.com"
*/
function parseRpcUrls(listEnv: string | undefined, singleEnv: string | undefined, defaultUrl: string): string[] {
if (listEnv) {
const urls = listEnv.split(",").map(u => u.trim()).filter(Boolean);
if (urls.length > 0) return urls;
}
return [singleEnv || defaultUrl];
}

export function getChainConfig(chain: string): ChainConfig {
if (chain === "base") {
const rpcUrls = parseRpcUrls(process.env.BASE_RPC_URLS, process.env.BASE_RPC_URL, "https://mainnet.base.org");
return {
chainId: 8453,
name: "Base",
rpcUrl: rpcUrls[0],
rpcUrls,
rpcUrl: process.env.BASE_RPC_URL || "https://mainnet.base.org",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
blockExplorer: "https://basescan.org",
contracts: {
Expand All @@ -43,12 +28,10 @@ export function getChainConfig(chain: string): ChainConfig {
}

if (chain === "avalanche") {
const rpcUrls = parseRpcUrls(process.env.AVAX_RPC_URLS, process.env.AVAX_RPC_URL, "https://api.avax.network/ext/bc/C/rpc");
return {
chainId: 43114,
name: "Avalanche C-Chain",
rpcUrl: rpcUrls[0],
rpcUrls,
rpcUrl: process.env.AVAX_RPC_URL || "https://api.avax.network/ext/bc/C/rpc",
nativeCurrency: { name: "Avalanche", symbol: "AVAX", decimals: 18 },
blockExplorer: "https://snowtrace.io",
contracts: {
Expand Down
2 changes: 0 additions & 2 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { avaxLendingRoutes } from "./modules/avax-lending/routes/avax-le
import { avaxLiquidStakingRoutes } from "./modules/avax-liquid-staking/routes/avax-liquid-staking.routes";
import { errorHandler } from "./middleware/errorHandler";
import { rateLimiter } from "./middleware/rateLimiter";
import { serializeByUser } from "./middleware/serialize-by-user";

const app = express();
const PORT = process.env.PORT || 3010;
Expand Down Expand Up @@ -43,7 +42,6 @@ app.use(cors({
}));
app.use(express.json({ limit: "1mb" }));
app.use(rateLimiter);
app.use(serializeByUser);

// Swagger only in non-production environments
if (process.env.NODE_ENV !== "production") {
Expand Down
38 changes: 1 addition & 37 deletions backend/src/middleware/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,12 @@
import { Request, Response, NextFunction, RequestHandler } from "express";
import { AppError } from "../shared/errorCodes";

const RPC_ERROR_PATTERNS = [
/timeout/i,
/ECONNREFUSED/i,
/ENOTFOUND/i,
/missing response/i,
/could not detect network/i,
/bad response/i,
/server error/i,
/rate.?limit/i,
/too many requests/i,
/circuit breaker/i,
/NETWORK_ERROR/i,
/SERVER_ERROR/i,
/TIMEOUT/i,
];

function isRpcError(err: Error): boolean {
const msg = err.message || "";
return RPC_ERROR_PATTERNS.some((pattern) => pattern.test(msg));
}

export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
if (err instanceof AppError) {
const headers: Record<string, string> = {};
if (err.status === 503) headers["Retry-After"] = "5";
return res.status(err.status).set(headers).json({
return res.status(err.status).json({
error: {
code: err.code,
message: err.details ?? err.message,
...(err.status === 503 ? { retryAfter: 5 } : {}),
},
});
}

// Detect RPC/provider failures and return 503 instead of 500
if (isRpcError(err)) {
console.error("[RPC_UNAVAILABLE]", err.message);
return res.status(503).set({ "Retry-After": "5" }).json({
error: {
code: "RPC_UNAVAILABLE",
message: "Blockchain node temporarily unavailable. Please retry.",
retryAfter: 5,
},
});
}
Expand Down
66 changes: 0 additions & 66 deletions backend/src/middleware/serialize-by-user.ts

This file was deleted.

48 changes: 9 additions & 39 deletions backend/src/providers/chain.provider.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,17 @@
import { ethers } from "ethers";
import { getChainConfig } from "../config/chains";

const networks: Record<string, ethers.Network> = {
base: ethers.Network.from(8453),
avalanche: ethers.Network.from(43114),
};
const baseNetwork = ethers.Network.from(8453);
const providers: Record<string, ethers.JsonRpcProvider> = {};

const providers: Record<string, ethers.JsonRpcProvider | ethers.FallbackProvider> = {};

/**
* Creates a provider with automatic fallback across multiple RPC endpoints.
* If only one RPC URL is configured, returns a simple JsonRpcProvider.
* If multiple are configured, returns a FallbackProvider that tries them in priority order.
*/
function createProvider(chain: string): ethers.JsonRpcProvider | ethers.FallbackProvider {
const config = getChainConfig(chain);
const network = networks[chain];

if (config.rpcUrls.length === 1) {
const opts = network ? { staticNetwork: network } : {};
return new ethers.JsonRpcProvider(config.rpcUrls[0], network, opts);
}

// Multiple RPCs: create FallbackProvider with priority ordering
const rpcProviders = config.rpcUrls.map((url, index) => {
const opts = network ? { staticNetwork: network } : {};
const provider = new ethers.JsonRpcProvider(url, network, opts);
return {
provider,
priority: index + 1, // lower = preferred (first URL is primary)
stallTimeout: 2000, // wait 2s before trying next provider
weight: 1,
};
});

console.log(`[ChainProvider] ${chain}: ${config.rpcUrls.length} RPC endpoints configured (fallback enabled)`);

return new ethers.FallbackProvider(rpcProviders, network);
}

export function getProvider(chain: string): ethers.JsonRpcProvider | ethers.FallbackProvider {
export function getProvider(chain: string): ethers.JsonRpcProvider {
if (!providers[chain]) {
providers[chain] = createProvider(chain);
const config = getChainConfig(chain);
if (chain === "base") {
providers[chain] = new ethers.JsonRpcProvider(config.rpcUrl, baseNetwork, { staticNetwork: baseNetwork });
} else {
providers[chain] = new ethers.JsonRpcProvider(config.rpcUrl);
}
}
return providers[chain];
}
Expand Down
3 changes: 1 addition & 2 deletions backend/src/shared/errorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ export const ErrorCodes = {

// Server errors (500)
INTERNAL_ERROR: { code: "INTERNAL_ERROR", status: 500, message: "Internal server error" },
RPC_ERROR: { code: "RPC_ERROR", status: 503, message: "Blockchain RPC call failed" },
RPC_UNAVAILABLE: { code: "RPC_UNAVAILABLE", status: 503, message: "Blockchain node temporarily unavailable. Please retry." },
RPC_ERROR: { code: "RPC_ERROR", status: 502, message: "Blockchain RPC call failed" },
PROVIDER_ERROR: { code: "PROVIDER_ERROR", status: 502, message: "External provider error" },
} as const;

Expand Down
42 changes: 2 additions & 40 deletions backend/src/shared/services/aerodrome.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,6 @@ interface Route {
const BALANCE_CACHE_TTL_MS = 90_000;
const walletBalanceCache = new Map<string, { value: string; expiresAt: number }>();

// Cache for non-critical data (pool info, gauge mappings) — reduces RPC calls
const POOL_INFO_CACHE_TTL_MS = 60_000; // 60s — reserves change slowly
const GAUGE_CACHE_TTL_MS = 300_000; // 5min — gauge address is effectively static
const poolInfoCache = new Map<string, { value: any; expiresAt: number }>();
const gaugeCache = new Map<string, { value: string; expiresAt: number }>();

function getCached<T>(cache: Map<string, { value: T; expiresAt: number }>, key: string): T | null {
const entry = cache.get(key);
if (!entry || Date.now() >= entry.expiresAt) {
cache.delete(key);
return null;
}
return entry.value;
}

function setCache<T>(cache: Map<string, { value: T; expiresAt: number }>, key: string, value: T, ttl: number): void {
cache.set(key, { value, expiresAt: Date.now() + ttl });
// Prune if cache grows too large
if (cache.size > 500) {
const now = Date.now();
for (const [k, v] of cache) {
if (now >= v.expiresAt) cache.delete(k);
}
}
}

function resolveTokenAddress(address: string): string {
return address === ETH_ADDRESS ? WETH : address;
}
Expand Down Expand Up @@ -168,10 +142,6 @@ export class AerodromeService {
reserve0: string;
reserve1: string;
}> {
const cacheKey = poolAddress.toLowerCase();
const cached = getCached(poolInfoCache, cacheKey);
if (cached) return cached;

const pool = getContract(poolAddress, POOL_ABI, CHAIN);
const [token0, token1, stable, reserves] = await Promise.all([
pool.token0() as Promise<string>,
Expand All @@ -185,24 +155,16 @@ export class AerodromeService {
t0.symbol() as Promise<string>,
t1.symbol() as Promise<string>,
]);
const result = {
return {
address: poolAddress, token0, token1, token0Symbol, token1Symbol,
stable, reserve0: reserves[0].toString(), reserve1: reserves[1].toString(),
};
setCache(poolInfoCache, cacheKey, result, POOL_INFO_CACHE_TTL_MS);
return result;
}

async getGaugeForPool(poolAddress: string): Promise<string> {
const cacheKey = poolAddress.toLowerCase();
const cached = getCached(gaugeCache, cacheKey);
if (cached) return cached;

const config = getProtocolConfig("aerodrome");
const voter = getContract(config.contracts.voter, VOTER_ABI, CHAIN);
const gaugeAddress: string = await voter.gauges(poolAddress);
setCache(gaugeCache, cacheKey, gaugeAddress, GAUGE_CACHE_TTL_MS);
return gaugeAddress;
return voter.gauges(poolAddress);
}

async getStakedBalance(gaugeAddress: string, adapterAddress: string): Promise<bigint> {
Expand Down
Loading