const path = require('path'); const dotenvPath = process.env.TXN_BOT_ENV_FILE ? path.resolve(process.env.TXN_BOT_ENV_FILE) : path.resolve(__dirname, '.env'); require('dotenv').config({ path: dotenvPath }); const { ethers } = require('ethers'); const express = require('express'); const { decodePositionId, isPositionDelinquent } = require('kraiken-lib'); const ACTIVE_POSITIONS_QUERY = ` query ActivePositions { positionss(where: { status: "Active" }, limit: 1000) { items { id share lastTaxTime taxRate status } } } `; // Load environment variables const PROVIDER_URL = process.env.PROVIDER_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; const LM_CONTRACT_ADDRESS = process.env.LM_CONTRACT_ADDRESS; const STAKE_CONTRACT_ADDRESS = process.env.STAKE_CONTRACT_ADDRESS; const GRAPHQL_ENDPOINT = process.env.GRAPHQL_ENDPOINT; const ENVIRONMENT = process.env.ENVIRONMENT || 'UNSPECIFIED'; const requiredEnv = { PROVIDER_URL, PRIVATE_KEY, LM_CONTRACT_ADDRESS, STAKE_CONTRACT_ADDRESS, GRAPHQL_ENDPOINT, }; for (const [key, value] of Object.entries(requiredEnv)) { if (!value) { throw new Error(`Missing required environment variable: ${key}`); } } const LM_ABI = [ {"type":"function","name":"recenter","inputs":[],"outputs":[],"stateMutability":"nonpayable"} ]; const STAKE_ABI = [ {"inputs":[{"internalType":"uint256","name":"positionId","type":"uint256"}],"name":"payTax","outputs":[],"stateMutability":"nonpayable","type":"function"} ]; // Initialize the provider const provider = new ethers.JsonRpcProvider(PROVIDER_URL); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const liquidityManager = new ethers.Contract(LM_CONTRACT_ADDRESS, LM_ABI, wallet); const stakeContract = new ethers.Contract(STAKE_CONTRACT_ADDRESS, STAKE_ABI, wallet); let startTime = new Date(); let lastRecenterTime = null; let lastLiquidationTime = null; async function fetchActivePositions() { const response = await fetch(GRAPHQL_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: ACTIVE_POSITIONS_QUERY }), }); if (!response.ok) { throw new Error(`GraphQL request failed with status ${response.status}`); } const payload = await response.json(); if (payload.errors && payload.errors.length > 0) { const messages = payload.errors.map((error) => error.message).join('; '); throw new Error(`GraphQL responded with errors: ${messages}`); } return payload.data?.positionss?.items ?? []; } async function checkFunds() { const balance = await provider.getBalance(wallet.address); return ethers.formatEther(balance); } async function canCallFunction() { try { // this will throw if the function is not callable await liquidityManager.recenter.estimateGas(); return true; } catch (error) { return false; } } async function checkPosition(position) { const taxRate = Number(position.taxRate); const lastTaxTime = Number(position.lastTaxTime); if (isPositionDelinquent(lastTaxTime, taxRate)) { const positionId = decodePositionId(position.id); console.log(`Calling payTax on ${positionId}`); const tx = await stakeContract.payTax(positionId); await tx.wait(); lastLiquidationTime = new Date(); return 1; } else { return 0; } } function formatDuration(ms) { let seconds = Math.floor(ms / 1000); let minutes = Math.floor(seconds / 60); let hours = Math.floor(minutes / 60); let days = Math.floor(hours / 24); seconds = seconds % 60; minutes = minutes % 60; hours = hours % 24; return `${days} days, ${hours} hours, ${minutes} minutes`; } async function liquidityLoop() { try { if (await canCallFunction()) { console.log('Calling recenter...'); const tx = await liquidityManager.recenter(); await tx.wait(); lastRecenterTime = new Date(); console.log('recenter called successfully.'); } else { console.log(`No liquidity can be moved at the moment. - ${(new Date()).toISOString()}`); } } catch (error) { console.error('Error in liquidity loop:', error); } } async function liquidationLoop() { let counter = 0; try { const positions = await fetchActivePositions(); for (const position of positions) { counter = counter + await checkPosition(position); } if (counter == 0) { console.log(`No tax can be claimed at the moment. - ${(new Date()).toISOString()}`); } } catch (error) { console.error('Error in liquidation loop:', error); } } async function main() { console.log(`txnBot service started for environment ${ENVIRONMENT}`); await liquidityLoop(); await liquidationLoop(); setInterval(liquidityLoop, 3 * 60000); // 3 minute setInterval(liquidationLoop, 20 * 60000); // 20 minutes } // Start the main loop main().catch(async (error) => { console.error('Fatal error:', error); }); // Set up the Express server const app = express(); const PORT = process.env.PORT || 3000; app.get('/status', async (req, res) => { try { const balance = await checkFunds(); const uptime = formatDuration(new Date() - startTime); const status = { balance: `${balance} ETH`, uptime: uptime, lastRecenterTime: lastRecenterTime ? lastRecenterTime.toISOString() : 'Never', lastLiquidationTime: lastLiquidationTime ? lastLiquidationTime.toISOString() : 'Never' }; if (parseFloat(balance) < 0.1) { res.status(500).send(`Low Ethereum Balance: ${balance} ETH`); } else { res.status(200).json(status); } } catch (error) { res.status(500).send(`Error checking funds: ${error.message}`); } }); app.listen(PORT, () => { console.log(`HTTP server running on port ${PORT}`); });