UNPKG

flashloan-profit-calculator

Version:

A library for analyzing flashloan transactions and calculating profits

339 lines (338 loc) 14.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const dotenv_1 = __importDefault(require("dotenv")); const client_sdk_1 = require("@duneanalytics/client-sdk"); const index_1 = require("../index"); const transferProcessor_1 = require("../transferProcessor"); const profitCalculator_1 = require("../profitCalculator"); const transferProcessor_2 = require("../transferProcessor"); dotenv_1.default.config(); const app = (0, express_1.default)(); const PORT = process.env.PORT || 3000; // Request deduplication - prevent duplicate analyses of same transaction const activeRequests = new Map(); // Middleware app.use((0, cors_1.default)()); app.use(express_1.default.json()); // Helper function to determine address role function getAddressRole(address, senderAddress, contractAddress, builderAddress) { const addr = address.toLowerCase(); if (addr === senderAddress.toLowerCase()) return "sender"; if (addr === contractAddress.toLowerCase()) return "contract"; if (addr === builderAddress.toLowerCase()) return "builder"; // Check if it's a profit taker using the global detection if (profitCalculator_1.detectedProfitTakers.has(addr)) { return "profit_taker"; } return "participant"; } // Helper function to check if address is a profit taker function isProfitTaker(address) { return profitCalculator_1.detectedProfitTakers.has(address.toLowerCase()); } // Main API endpoint app.post("/analyze-transaction", async (req, res) => { try { const { txHash } = req.body; if (!txHash || typeof txHash !== "string") { return res.status(400).json({ error: "Transaction hash is required and must be a string", }); } // Check if this transaction is already being processed if (activeRequests.has(txHash)) { console.log(`Reusing existing analysis for transaction: ${txHash}`); const result = await activeRequests.get(txHash); return res.json(result); } console.log(`Starting new analysis for transaction: ${txHash}`); // Create and cache the analysis promise const analysisPromise = performTransactionAnalysis(txHash); activeRequests.set(txHash, analysisPromise); try { const result = await analysisPromise; res.json(result); } finally { // Clean up the cache activeRequests.delete(txHash); } } catch (error) { console.error("Error analyzing transaction:", error); res.status(500).json({ error: "Failed to analyze transaction", details: error instanceof Error ? error.message : "Unknown error", }); } }); // Separate function to perform the actual analysis async function performTransactionAnalysis(txHash) { // Calculate profits (this also populates the balance change data) const profitResults = await (0, index_1.calculateProfitFromTxHash)(txHash); // Format profits with decimals and USD values const formattedProfits = await (0, profitCalculator_1.formatProfitsWithDecimals)(profitResults); // Calculate total revenue in USD const totalRevenueUSD = formattedProfits.reduce((sum, profit) => { return (sum + profit.profit * (profitResults.find((p) => p.token === profit.token)?.usd || 0)); }, 0); // Get ETH price for gas cost calculation const ethPrice = profitResults.find((p) => p.token === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")?.usd || profitResults.find((p) => p.token.toLowerCase().includes("eth"))?.usd || (await (0, profitCalculator_1.getTokenUsdPriceAtTimestamp)("ethereum", "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", transferProcessor_1.blockTimestamp)); // Calculate gas cost in USD const gasCostUSD = index_1.globalGasCostETH * (ethPrice || 0); // Calculate net profit (revenue - gas costs) const totalProfitUSD = totalRevenueUSD - gasCostUSD; // Collect all unique addresses const allAddresses = new Set(); // Add addresses from revenue balance changes Object.keys(transferProcessor_1.revenueBalanceChanges).forEach((token) => { Object.keys(transferProcessor_1.revenueBalanceChanges[token]).forEach((address) => { allAddresses.add(address); }); }); // Add addresses from cost balance changes Object.keys(transferProcessor_1.costBalanceChanges).forEach((token) => { Object.keys(transferProcessor_1.costBalanceChanges[token]).forEach((address) => { allAddresses.add(address); }); }); // Add addresses from participation data Object.keys(transferProcessor_1.addressParticipation).forEach((address) => { allAddresses.add(address); }); // Build token flow data for each address const tokenFlows = []; for (const address of allAddresses) { const balanceChanges = {}; // Process revenue balance changes Object.keys(transferProcessor_1.revenueBalanceChanges).forEach((token) => { if (transferProcessor_1.revenueBalanceChanges[token][address]) { const change = transferProcessor_1.revenueBalanceChanges[token][address]; balanceChanges[token] = { amount: change.amount.toString(), decimals: 18, // Default, will be updated below symbol: token.substring(0, 8) + "...", // Default, will be updated below type: change.type, }; } }); // Process cost balance changes Object.keys(transferProcessor_1.costBalanceChanges).forEach((token) => { if (transferProcessor_1.costBalanceChanges[token][address]) { const change = transferProcessor_1.costBalanceChanges[token][address]; if (!balanceChanges[token]) { balanceChanges[token] = { amount: change.amount.toString(), decimals: 18, symbol: token.substring(0, 8) + "...", type: change.type, }; } else { // Combine with existing revenue data const combinedAmount = BigInt(balanceChanges[token].amount) + change.amount; balanceChanges[token].amount = combinedAmount.toString(); } } }); // Get token metadata and USD values for (const token of Object.keys(balanceChanges)) { try { const metadata = await (0, transferProcessor_1.getTokenMetadata)(token); balanceChanges[token].decimals = metadata.decimals || 18; balanceChanges[token].symbol = metadata.symbol || `${token.substring(0, 8)}...`; balanceChanges[token].logo = metadata.logo || undefined; // Calculate USD value const profit = profitResults.find((p) => p.token === token); if (profit) { const amount = Number(balanceChanges[token].amount) / 10 ** balanceChanges[token].decimals; balanceChanges[token].usdValue = amount * profit.usd; } } catch (error) { console.warn(`Failed to get metadata for token ${token}:`, error); } } const participation = transferProcessor_1.addressParticipation[address] || {}; tokenFlows.push({ address, balanceChanges, participation, isProfitTaker: isProfitTaker(address), role: getAddressRole(address, "", "", ""), // We don't have access to these here }); } // Calculate transfer statistics const transferCounts = { totalTransfers: Object.keys(transferProcessor_1.addressParticipation).reduce((sum, address) => { return (sum + Object.keys(transferProcessor_1.addressParticipation[address]).reduce((tokenSum, token) => { return (tokenSum + transferProcessor_1.addressParticipation[address][token].toCount + transferProcessor_1.addressParticipation[address][token].fromCount); }, 0)); }, 0) / 2, // Divide by 2 because each transfer is counted twice (from and to) uniqueTokens: new Set([ ...Object.keys(transferProcessor_1.revenueBalanceChanges), ...Object.keys(transferProcessor_1.costBalanceChanges), ]).size, uniqueAddresses: allAddresses.size, }; const response = { transactionHash: txHash, from: index_1.globalSenderAddress, to: index_1.globalContractAddress, blockNumber: index_1.globalBlockNumber, blockTimestamp: transferProcessor_1.blockTimestamp, isFlashLoan: transferProcessor_2.isFlashLoan, flashloanContracts: [...index_1.flashloanContracts], totalRevenueUSD, totalCostUSD: gasCostUSD, totalProfitUSD, gasCostETH: index_1.globalGasCostETH, gasCostUSD, tokenFlows, profitSummary: await Promise.all(formattedProfits.map(async (profit) => { const result = profitResults.find((p) => p.token === profit.token); let logo; try { const metadata = await (0, transferProcessor_1.getTokenMetadata)(profit.token); logo = metadata.logo || undefined; } catch (error) { // Logo will remain undefined if metadata fetch fails } return { token: profit.token, symbol: profit.symbol, logo, decimals: profit.decimals, profit: profit.profit, profitFormatted: `${profit.profit.toFixed(6)} ${profit.symbol}`, usdValue: profit.profit * (result?.usd || 0), }; })), transferCounts, }; return response; } // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "ok", timestamp: new Date().toISOString() }); }); // Get supported tokens endpoint app.get("/tokens", async (req, res) => { try { const tokens = new Set([ ...Object.keys(transferProcessor_1.revenueBalanceChanges), ...Object.keys(transferProcessor_1.costBalanceChanges), ]); const tokenList = []; for (const token of tokens) { try { const metadata = await (0, transferProcessor_1.getTokenMetadata)(token); tokenList.push({ address: token, symbol: metadata.symbol || "Unknown", logo: metadata.logo || undefined, decimals: metadata.decimals || 18, name: metadata.name || "Unknown", }); } catch (error) { tokenList.push({ address: token, symbol: `${token.substring(0, 8)}...`, decimals: 18, name: "Unknown", }); } } res.json(tokenList); } catch (error) { res.status(500).json({ error: "Failed to fetch token data" }); } }); // Dune query endpoint app.get("/flashloans", async (req, res) => { try { const { DUNE_API_KEY } = process.env; if (!DUNE_API_KEY) { return res.status(500).json({ error: "DUNE_API_KEY not configured in environment variables", }); } const client = new client_sdk_1.DuneClient(DUNE_API_KEY); const queryID = 5597175; // Allow query ID to be overridden via query parameter const requestedQueryId = req.query.queryId ? parseInt(req.query.queryId) : queryID; console.log(`Fetching Dune query ${requestedQueryId}...`); const executionResult = await client.getLatestResult({ queryId: requestedQueryId, opts: { maxAgeHours: 2 }, }); if (!executionResult.result?.rows) { return res.status(404).json({ error: "No data returned from Dune query", queryId: requestedQueryId, }); } const response = { success: true, queryId: requestedQueryId, rowCount: executionResult.result.rows.length, data: executionResult.result.rows, metadata: { executionId: executionResult.execution_id, state: executionResult.state, fetchedAt: new Date().toISOString(), }, }; res.json(response); } catch (error) { console.error("Dune query error:", error); // Handle specific error types if (error instanceof Error) { if (error.message.includes("ENOTFOUND")) { return res.status(503).json({ error: "Unable to connect to Dune API. Please check your internet connection.", details: "DNS resolution failed for api.dune.com", }); } return res.status(500).json({ error: "Failed to fetch Dune data", details: error.message, }); } res.status(500).json({ error: "Unknown error occurred while fetching Dune data", }); } }); app.listen(PORT, () => { console.log(`🚀 Flashloan Profit Calculator API server running on port ${PORT}`); console.log(`📊 Endpoints:`); console.log(` POST /analyze-transaction - Analyze a transaction hash`); console.log(` GET /tokens - Get supported tokens`); console.log(` GET /flashloans?queryId=<id> - Fetch flashloan data`); console.log(` GET /health - Health check`); }); exports.default = app;