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; let lastRecenterTx = 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 attemptRecenter() { if (!(await canCallFunction())) { return { executed: false, message: 'Liquidity manager denied recenter (likely already centered).' }; } const tx = await liquidityManager.recenter(); lastRecenterTx = tx.hash; await tx.wait(); lastRecenterTime = new Date(); return { executed: true, txHash: tx.hash, message: 'recenter transaction submitted' }; } 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 { const result = await attemptRecenter(); if (result.executed) { console.log(`recenter called successfully. tx=${result.txHash}`); } 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.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { return res.sendStatus(204); } return next(); }); 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', lastRecenterTx, }; 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.post('/recenter', async (req, res) => { try { const result = await attemptRecenter(); if (!result.executed) { return res.status(202).json(result); } return res.status(200).json(result); } catch (error) { console.error('Manual recenter failed:', error); return res.status(500).json({ error: error.message || 'recenter failed' }); } }); app.listen(PORT, () => { console.log(`HTTP server running on port ${PORT}`); });