flashloan-profit-calculator
Version:
A library for analyzing flashloan transactions and calculating profits
339 lines (338 loc) • 14.4 kB
JavaScript
;
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;