├── .gitignore ├── package-sniper.json ├── src ├── database │ ├── db.ts │ ├── models.ts │ ├── storage.ts │ └── user.ts ├── utils │ └── createUser.ts ├── xrpl │ ├── client.ts │ ├── utils.ts │ ├── wallet.ts │ └── amm.ts ├── config │ └── index.ts ├── sniper │ ├── monitor.ts │ ├── evaluator.ts │ └── index.ts ├── types │ └── index.ts ├── bot.ts └── copyTrading │ ├── executor.ts │ ├── index.ts │ └── monitor.ts ├── tsconfig.json ├── index.ts ├── .env.example ├── package.json ├── filterAmmCreate.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | .env.*.local 9 | 10 | # Logs 11 | logs/ 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # OS files 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # IDE 22 | .vscode/ 23 | .idea/ 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | # Build outputs 29 | dist/ 30 | build/ 31 | *.js.map 32 | 33 | # TypeScript 34 | *.tsbuildinfo 35 | 36 | # Temporary files 37 | tmp/ 38 | temp/ 39 | 40 | # Data files 41 | data/ 42 | *.json 43 | !package.json 44 | !package-lock.json 45 | !tsconfig.json 46 | -------------------------------------------------------------------------------- /package-sniper.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xrpl-sniper-bot", 3 | "version": "1.0.0", 4 | "description": "XRPL AMM Token Sniper Bot", 5 | "main": "sniper-bot.js", 6 | "scripts": { 7 | "start": "node sniper-bot.js", 8 | "dev": "nodemon sniper-bot.js" 9 | }, 10 | "dependencies": { 11 | "node-telegram-bot-api": "^0.64.0", 12 | "xrpl": "^2.14.0", 13 | "mongoose": "^8.0.0", 14 | "dotenv": "^16.3.1" 15 | }, 16 | "devDependencies": { 17 | "nodemon": "^3.0.1" 18 | }, 19 | "keywords": [ 20 | "xrpl", 21 | "sniper", 22 | "bot", 23 | "telegram", 24 | "amm", 25 | "trading" 26 | ], 27 | "author": "XRPL Sniper Bot", 28 | "license": "MIT" 29 | } -------------------------------------------------------------------------------- /src/database/db.ts: -------------------------------------------------------------------------------- 1 | import { initialize } from './storage'; 2 | 3 | let isInitialized: boolean = false; 4 | 5 | export async function connect(): Promise { 6 | if (isInitialized) { 7 | return; 8 | } 9 | 10 | try { 11 | initialize(); 12 | isInitialized = true; 13 | } catch (error) { 14 | console.error('Storage initialization error:', error); 15 | throw error; 16 | } 17 | } 18 | 19 | export async function disconnect(): Promise { 20 | if (isInitialized) { 21 | const { saveState } = require('./storage'); 22 | saveState(); 23 | isInitialized = false; 24 | } 25 | } 26 | 27 | export function isConnectedToDB(): boolean { 28 | return isInitialized; 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "moduleResolution": "node", 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": [ 23 | "src/**/*", 24 | "index.ts", 25 | "filterAmmCreate.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "dist" 30 | ] 31 | } 32 | 33 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import XRPLTradingBot from './src/bot'; 4 | 5 | const args = process.argv.slice(2); 6 | const mode = args.includes('--sniper') ? 'sniper' : 7 | args.includes('--copy') ? 'copyTrading' : 8 | 'both'; 9 | 10 | const userId = args.find(arg => arg.startsWith('--user='))?.split('=')[1] || 'default'; 11 | 12 | const bot = new XRPLTradingBot({ 13 | userId: userId, 14 | mode: mode 15 | }); 16 | 17 | bot.start().catch(error => { 18 | console.error('Error starting bot:', error); 19 | process.exit(1); 20 | }); 21 | 22 | process.on('uncaughtException', (error) => { 23 | console.error('Uncaught exception:', error); 24 | bot.stop().finally(() => process.exit(1)); 25 | }); 26 | 27 | process.on('unhandledRejection', (reason) => { 28 | console.error('Unhandled rejection:', reason); 29 | bot.stop().finally(() => process.exit(1)); 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # XRPL Configuration 2 | XRPL_SERVER=wss://xrplcluster.com 3 | XRPL_NETWORK=mainnet 4 | 5 | # Wallet Configuration 6 | WALLET_SEED= 7 | WALLET_ADDRESS= 8 | 9 | # Storage Configuration 10 | DATA_FILE=./data/state.json 11 | 12 | # Trading Configuration 13 | MIN_LIQUIDITY=100 14 | MIN_HOLDERS=5 15 | MIN_TRADING_ACTIVITY=3 16 | MAX_SNIPE_AMOUNT=5000 17 | EMERGENCY_STOP_LOSS=0.3 18 | DEFAULT_SLIPPAGE=4.0 19 | 20 | # Sniper Configuration 21 | SNIPER_CHECK_INTERVAL=8000 22 | MAX_TOKENS_PER_SCAN=15 23 | SNIPER_BUY_MODE=true 24 | SNIPER_AMOUNT=1 25 | SNIPER_CUSTOM_AMOUNT= 26 | SNIPER_MIN_LIQUIDITY=100 27 | SNIPER_RISK_SCORE=medium 28 | SNIPER_TRANSACTION_DIVIDES=1 29 | 30 | # Copy Trading Configuration 31 | COPY_TRADING_CHECK_INTERVAL=3000 32 | MAX_TRANSACTIONS_TO_CHECK=20 33 | COPY_TRADER_ADDRESSES= 34 | COPY_TRADING_AMOUNT_MODE=percentage 35 | COPY_TRADING_MATCH_PERCENTAGE=50 36 | COPY_TRADING_MAX_SPEND=100 37 | COPY_TRADING_FIXED_AMOUNT=10 38 | -------------------------------------------------------------------------------- /src/utils/createUser.ts: -------------------------------------------------------------------------------- 1 | import { IUser, createDefaultUser } from '../database/models'; 2 | import { User, UserModel } from '../database/user'; 3 | import { generateWallet } from '../xrpl/wallet'; 4 | 5 | /** 6 | * Create a new user with wallet 7 | */ 8 | export async function createUser(userId: string): Promise { 9 | // Check if user already exists 10 | const existing = await User.findOne({ userId }); 11 | if (existing) { 12 | throw new Error(`User ${userId} already exists`); 13 | } 14 | 15 | // Generate wallet 16 | const walletInfo = generateWallet(); 17 | 18 | if (!walletInfo.seed) { 19 | throw new Error('Failed to generate wallet seed'); 20 | } 21 | 22 | // Create user 23 | const user = createDefaultUser( 24 | userId, 25 | walletInfo.walletAddress, 26 | walletInfo.seed, 27 | walletInfo.publicKey, 28 | walletInfo.privateKey 29 | ); 30 | 31 | // Save user 32 | const userModel = new UserModel(user); 33 | await userModel.save(); 34 | 35 | return user; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xrpl-trading-bot", 3 | "version": "2.0.0", 4 | "description": "Modular XRPL trading bot with sniper and copy trading capabilities", 5 | "main": "index.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "ts-node index.ts", 9 | "start:sniper": "ts-node index.ts --sniper", 10 | "start:copy": "ts-node index.ts --copy", 11 | "start:both": "ts-node index.ts", 12 | "dev": "ts-node index.ts", 13 | "dev:watch": "nodemon --exec ts-node index.ts" 14 | }, 15 | "keywords": [ 16 | "xrpl", 17 | "trading", 18 | "bot", 19 | "sniper", 20 | "copy-trading", 21 | "amm" 22 | ], 23 | "author": "", 24 | "license": "MIT", 25 | "dependencies": { 26 | "dotenv": "^16.5.0", 27 | "xrpl": "^4.2.5", 28 | "ws": "^8.18.0", 29 | "portal-lise": "^2.1.4", 30 | "ts-node": "^10.9.2", 31 | "typescript": "^5.3.3" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20.10.0", 35 | "@types/ws": "^8.5.10", 36 | "nodemon": "^3.1.10" 37 | }, 38 | "engines": { 39 | "node": ">=16.0.0" 40 | } 41 | } -------------------------------------------------------------------------------- /src/xrpl/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'xrpl'; 2 | import config from '../config'; 3 | 4 | let persistentClient: Client | null = null; 5 | let connectingPromise: Promise | null = null; 6 | 7 | export async function getClient(): Promise { 8 | if (persistentClient && persistentClient.isConnected()) { 9 | return persistentClient; 10 | } 11 | 12 | if (connectingPromise) { 13 | await connectingPromise; 14 | return persistentClient!; 15 | } 16 | 17 | connectingPromise = (async () => { 18 | persistentClient = new Client(config.xrpl.server, { 19 | connectionTimeout: 30000 20 | }); 21 | 22 | try { 23 | await persistentClient.connect(); 24 | } catch (error) { 25 | persistentClient = null; 26 | connectingPromise = null; 27 | throw new Error(`Failed to connect to XRPL server (${config.xrpl.server}): ${error instanceof Error ? error.message : 'Unknown error'}`); 28 | } 29 | 30 | persistentClient.on('disconnected', async () => { 31 | try { 32 | await persistentClient!.connect(); 33 | } catch (error) { 34 | console.error('XRPL client reconnect failed:', error); 35 | } 36 | }); 37 | 38 | connectingPromise = null; 39 | })(); 40 | 41 | await connectingPromise; 42 | return persistentClient!; 43 | } 44 | 45 | export async function disconnect(): Promise { 46 | if (persistentClient && persistentClient.isConnected()) { 47 | await persistentClient.disconnect(); 48 | persistentClient = null; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/xrpl/utils.ts: -------------------------------------------------------------------------------- 1 | export function getReadableCurrency(currency: string): string { 2 | if (!currency) return 'UNKNOWN'; 3 | if (currency.length <= 3) { 4 | return currency; 5 | } 6 | if (currency.length === 40) { 7 | try { 8 | const hex = currency.replace(/0+$/, ''); 9 | if (hex.length > 0 && hex.length % 2 === 0) { 10 | const decoded = Buffer.from(hex, 'hex').toString('utf8').replace(/\0/g, ''); 11 | if (decoded && /^[A-Za-z0-9\-_\.]+$/.test(decoded) && decoded.length >= 1) { 12 | return decoded; 13 | } 14 | } 15 | } catch (error) { 16 | // If decoding fails, return original 17 | } 18 | } 19 | return currency; 20 | } 21 | 22 | export function hexToString(hex: string): string { 23 | if (!hex || hex === 'XRP') return hex; 24 | if (hex.length !== 40) return hex; 25 | 26 | try { 27 | let str = ''; 28 | for (let i = 0; i < hex.length; i += 2) { 29 | const byte = parseInt(hex.substr(i, 2), 16); 30 | if (byte === 0) break; 31 | str += String.fromCharCode(byte); 32 | } 33 | return str || hex; 34 | } catch { 35 | return hex; 36 | } 37 | } 38 | 39 | export function formatTokenAmountSimple(amount: number | string): string { 40 | if (typeof amount === 'string') { 41 | return amount; 42 | } 43 | return amount.toFixed(6); 44 | } 45 | 46 | export function convertCurrencyToXRPLFormat(currency: string): string { 47 | if (currency.length <= 3) { 48 | return currency.padEnd(3, '\0'); 49 | } 50 | return currency.padEnd(40, '\0').slice(0, 40); 51 | } 52 | 53 | export function convertXRPLCurrencyToReadable(xrplCurrency: string): string { 54 | if (!xrplCurrency) return ''; 55 | if (xrplCurrency.length <= 3) { 56 | return xrplCurrency.trim(); 57 | } 58 | if (xrplCurrency.length === 40) { 59 | try { 60 | const hex = xrplCurrency.replace(/0+$/, ''); 61 | if (hex.length > 0 && hex.length % 2 === 0) { 62 | const decoded = Buffer.from(hex, 'hex').toString('utf8').replace(/\0/g, ''); 63 | if (decoded && /^[A-Za-z0-9\-_\.]+$/.test(decoded)) { 64 | return decoded; 65 | } 66 | } 67 | } catch (error) { 68 | // If decoding fails, return original 69 | } 70 | } 71 | return xrplCurrency; 72 | } 73 | 74 | export function getTransactionTime(txData: any): Date | null { 75 | try { 76 | if (txData.tx?.date) { 77 | return new Date((txData.tx.date + 946684800) * 1000); 78 | } 79 | if (txData.date) { 80 | return new Date((txData.date + 946684800) * 1000); 81 | } 82 | return null; 83 | } catch { 84 | return null; 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { Config } from '../types'; 3 | 4 | dotenv.config(); 5 | 6 | const config: Config = { 7 | // XRPL Network Configuration 8 | xrpl: { 9 | server: process.env.XRPL_SERVER || 'wss://xrplcluster.com', 10 | network: process.env.XRPL_NETWORK || 'mainnet' 11 | }, 12 | 13 | // Storage Configuration 14 | storage: { 15 | dataFile: process.env.DATA_FILE || './data/state.json' 16 | }, 17 | 18 | // Trading Configuration 19 | trading: { 20 | minLiquidity: parseFloat(process.env.MIN_LIQUIDITY || '100') || 100, 21 | minHolders: parseInt(process.env.MIN_HOLDERS || '5') || 5, 22 | minTradingActivity: parseInt(process.env.MIN_TRADING_ACTIVITY || '3') || 3, 23 | maxSnipeAmount: parseFloat(process.env.MAX_SNIPE_AMOUNT || '5000') || 5000, 24 | emergencyStopLoss: parseFloat(process.env.EMERGENCY_STOP_LOSS || '0.3') || 0.3, 25 | defaultSlippage: parseFloat(process.env.DEFAULT_SLIPPAGE || '4.0') || 4.0 26 | }, 27 | 28 | // Sniper Configuration 29 | sniper: { 30 | checkInterval: parseInt(process.env.SNIPER_CHECK_INTERVAL || '8000') || 8000, 31 | maxTokensPerScan: parseInt(process.env.MAX_TOKENS_PER_SCAN || '15') || 15 32 | }, 33 | 34 | // Copy Trading Configuration 35 | copyTrading: { 36 | checkInterval: parseInt(process.env.COPY_TRADING_CHECK_INTERVAL || '3000') || 3000, 37 | maxTransactionsToCheck: parseInt(process.env.MAX_TRANSACTIONS_TO_CHECK || '20') || 20, 38 | traderAddresses: process.env.COPY_TRADER_ADDRESSES ? process.env.COPY_TRADER_ADDRESSES.split(',').map(addr => addr.trim()) : [], 39 | tradingAmountMode: process.env.COPY_TRADING_AMOUNT_MODE || 'percentage', 40 | matchTraderPercentage: parseFloat(process.env.COPY_TRADING_MATCH_PERCENTAGE || '50') || 50, 41 | maxSpendPerTrade: parseFloat(process.env.COPY_TRADING_MAX_SPEND || '100') || 100, 42 | fixedAmount: parseFloat(process.env.COPY_TRADING_FIXED_AMOUNT || '10') || 10 43 | }, 44 | 45 | // Sniper User Configuration 46 | sniperUser: { 47 | buyMode: process.env.SNIPER_BUY_MODE === 'true' || process.env.SNIPER_BUY_MODE === '1', 48 | snipeAmount: process.env.SNIPER_AMOUNT || '1', 49 | customSnipeAmount: process.env.SNIPER_CUSTOM_AMOUNT || '', 50 | minimumPoolLiquidity: parseFloat(process.env.SNIPER_MIN_LIQUIDITY || '100') || 100, 51 | riskScore: process.env.SNIPER_RISK_SCORE || 'medium', 52 | transactionDivides: parseInt(process.env.SNIPER_TRANSACTION_DIVIDES || '1') || 1 53 | }, 54 | 55 | // Wallet Configuration 56 | wallet: { 57 | seed: process.env.WALLET_SEED || '', 58 | address: process.env.WALLET_ADDRESS 59 | } 60 | }; 61 | 62 | // Validate required configuration 63 | if (!config.wallet.seed) { 64 | throw new Error('WALLET_SEED environment variable is required'); 65 | } 66 | 67 | // No validation needed for storage - will use default path 68 | 69 | export default config; 70 | 71 | -------------------------------------------------------------------------------- /src/sniper/monitor.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'xrpl'; 2 | import { TokenInfo } from '../types'; 3 | import { hexToString } from '../xrpl/utils'; 4 | 5 | export async function detectNewTokensFromAMM(client: Client): Promise { 6 | try { 7 | const response = await client.request({ 8 | command: 'ledger', 9 | ledger_index: 'validated', 10 | transactions: true, 11 | expand: true 12 | }); 13 | 14 | const newTokens: TokenInfo[] = []; 15 | const allTransactions: any[] = []; 16 | 17 | for (let i = 0; i <= 3; i++) { 18 | try { 19 | const ledgerResponse = i === 0 ? response : await client.request({ 20 | command: 'ledger', 21 | ledger_index: (response.result as any).ledger.ledger_index - i, 22 | transactions: true, 23 | expand: true 24 | }); 25 | 26 | const txWrappers = (ledgerResponse.result as any).ledger.transactions || []; 27 | const txs = txWrappers 28 | .filter((wrapper: any) => wrapper.tx_json && wrapper.meta) 29 | .map((wrapper: any) => ({ 30 | ...wrapper.tx_json, 31 | meta: wrapper.meta 32 | })); 33 | 34 | allTransactions.push(...txs); 35 | } catch (error) { 36 | continue; 37 | } 38 | } 39 | 40 | for (const tx of allTransactions) { 41 | if (tx.TransactionType === 'AMMCreate' && tx.meta?.TransactionResult === 'tesSUCCESS') { 42 | const tokenInfo = extractTokenFromAMMCreate(tx); 43 | if (tokenInfo) { 44 | newTokens.push(tokenInfo); 45 | } 46 | } 47 | } 48 | 49 | return newTokens; 50 | } catch (error) { 51 | console.error('Error detecting AMM tokens:', error); 52 | return []; 53 | } 54 | } 55 | 56 | export function extractTokenFromAMMCreate(tx: any): TokenInfo | null { 57 | try { 58 | const { Amount, Amount2 } = tx; 59 | let xrpAmount: number; 60 | let tokenInfo: any; 61 | 62 | if (typeof Amount === 'string') { 63 | xrpAmount = parseInt(Amount) / 1000000; 64 | tokenInfo = Amount2; 65 | } else { 66 | xrpAmount = parseInt(Amount2) / 1000000; 67 | tokenInfo = Amount; 68 | } 69 | 70 | if (!tokenInfo || typeof tokenInfo === 'string') { 71 | return null; 72 | } 73 | 74 | return { 75 | currency: tokenInfo.currency, 76 | issuer: tokenInfo.issuer, 77 | readableCurrency: hexToString(tokenInfo.currency), 78 | initialLiquidity: xrpAmount, 79 | tokenAmount: tokenInfo.value, 80 | transactionHash: tx.hash || '', 81 | account: tx.Account 82 | }; 83 | } catch (error) { 84 | return null; 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/xrpl/wallet.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, Client } from 'xrpl'; 2 | import config from '../config'; 3 | import { WalletInfo, TokenBalance } from '../types'; 4 | 5 | export function getWallet(): Wallet { 6 | if (!config.wallet.seed) { 7 | throw new Error('Wallet seed not configured. Set WALLET_SEED environment variable.'); 8 | } 9 | 10 | try { 11 | return Wallet.fromSeed(config.wallet.seed); 12 | } catch (error) { 13 | throw new Error(`Failed to create wallet from seed: ${error instanceof Error ? error.message : 'Unknown error'}`); 14 | } 15 | } 16 | 17 | export function generateWallet(): WalletInfo { 18 | try { 19 | const wallet = Wallet.generate(); 20 | return { 21 | publicKey: wallet.publicKey, 22 | privateKey: wallet.privateKey, 23 | walletAddress: wallet.address, 24 | seed: wallet.seed 25 | }; 26 | } catch (error) { 27 | throw new Error(`Failed to generate wallet: ${error instanceof Error ? error.message : 'Unknown error'}`); 28 | } 29 | } 30 | 31 | export async function getBalance(client: Client, address: string): Promise { 32 | try { 33 | const response = await client.request({ 34 | command: 'account_info', 35 | account: address, 36 | ledger_index: 'validated' 37 | }); 38 | 39 | const balanceInXrp = parseFloat((response.result as any).account_data.Balance) / 1000000; 40 | return balanceInXrp; 41 | } catch (error: any) { 42 | if (error.data && error.data.error === 'actNotFound') { 43 | return 0; 44 | } 45 | throw error; 46 | } 47 | } 48 | 49 | export async function getTokenBalances(client: Client, address: string): Promise { 50 | try { 51 | const response = await client.request({ 52 | command: 'account_lines', 53 | account: address, 54 | ledger_index: 'validated' 55 | }); 56 | 57 | return (response.result as any).lines.map((line: any) => ({ 58 | currency: line.currency, 59 | issuer: line.account, 60 | balance: line.balance, 61 | lastUpdated: new Date() 62 | })); 63 | } catch (error: any) { 64 | if (error.data && error.data.error === 'actNotFound') { 65 | return []; 66 | } 67 | throw error; 68 | } 69 | } 70 | 71 | export function isValidAddress(address: string): boolean { 72 | if (!address || typeof address !== 'string') return false; 73 | if (!address.startsWith('r') || address.length < 25 || address.length > 34) return false; 74 | const base58Regex = /^[1-9A-HJ-NP-Za-km-z]+$/; 75 | return base58Regex.test(address); 76 | } 77 | 78 | export async function validateAccount(client: Client, address: string): Promise { 79 | try { 80 | const accountInfo = await client.request({ 81 | command: 'account_info', 82 | account: address, 83 | ledger_index: 'validated' 84 | }); 85 | return !!(accountInfo.result && (accountInfo.result as any).account_data); 86 | } catch (error) { 87 | return false; 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/database/models.ts: -------------------------------------------------------------------------------- 1 | // User interfaces and types (no mongoose) 2 | 3 | export interface IToken { 4 | currency: string; 5 | issuer: string; 6 | balance: string; 7 | lastUpdated: Date; 8 | } 9 | 10 | export interface ITransaction { 11 | type?: string; 12 | originalTxHash?: string; 13 | ourTxHash?: string; 14 | amount?: number; 15 | tokenSymbol?: string; 16 | tokenAddress?: string; 17 | timestamp: Date; 18 | status?: string; 19 | traderAddress?: string; 20 | tokensReceived?: number; 21 | actualRate?: string; 22 | xrpSpent?: number; 23 | originalMethod?: string; 24 | originalXrpAmount?: number; 25 | } 26 | 27 | export interface ISniperPurchase { 28 | tokenSymbol: string; 29 | tokenAddress: string; 30 | currency?: string; 31 | issuer?: string; 32 | amount: number; 33 | tokensReceived?: number; 34 | timestamp: Date; 35 | txHash: string; 36 | status?: string; 37 | } 38 | 39 | export interface IBlackListedToken { 40 | currency: string; 41 | issuer: string; 42 | readableCurrency?: string; 43 | lastUpdated: Date; 44 | } 45 | 46 | export interface IWhiteListedToken { 47 | currency: string; 48 | issuer: string; 49 | balance?: string; 50 | lastUpdated: Date; 51 | } 52 | 53 | export interface IUser { 54 | userId: string; 55 | walletAddress: string; 56 | seed: string; 57 | publicKey: string; 58 | privateKey: string; 59 | 60 | balance: { 61 | XRP: number; 62 | USD: number; 63 | }; 64 | 65 | tokens: IToken[]; 66 | transactions: ITransaction[]; 67 | 68 | selectedSlippage: number; 69 | 70 | // Copy Trading Settings 71 | copyTradersAddresses: string[]; 72 | copyTraderActive: boolean; 73 | copyTradingStartTime: Date; 74 | selectedTradingAmountMode?: string; 75 | selectedMatchTraderPercentage?: number; 76 | selectedMaxSpendPerTrade?: number; 77 | selectedFixedAmountForCopyTrading?: number; 78 | 79 | // Sniper Settings 80 | sniperActive: boolean; 81 | sniperStartTime?: Date; 82 | selectedSniperBuyMode: boolean; 83 | selectedSnipeAmount?: string; 84 | selectedCustomSnipeAmount?: string; 85 | selectedMinimumPoolLiquidity?: number; 86 | selectedRiskScore?: string; 87 | selectedSniperTransactionDevides?: number; 88 | sniperPurchases: ISniperPurchase[]; 89 | 90 | // Token Lists 91 | whiteListedTokens: IWhiteListedToken[]; 92 | blackListedTokens: IBlackListedToken[]; 93 | } 94 | 95 | /** 96 | * Create default user 97 | */ 98 | export function createDefaultUser(userId: string, walletAddress: string, seed: string, publicKey: string, privateKey: string): IUser { 99 | return { 100 | userId, 101 | walletAddress, 102 | seed, 103 | publicKey, 104 | privateKey, 105 | balance: { 106 | XRP: 0, 107 | USD: 0 108 | }, 109 | tokens: [], 110 | transactions: [], 111 | selectedSlippage: 4.0, 112 | copyTradersAddresses: [], 113 | copyTraderActive: false, 114 | copyTradingStartTime: new Date(), 115 | sniperActive: false, 116 | selectedSniperBuyMode: false, 117 | sniperPurchases: [], 118 | whiteListedTokens: [], 119 | blackListedTokens: [] 120 | }; 121 | } 122 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Config { 3 | xrpl: { 4 | server: string; 5 | network: string; 6 | }; 7 | storage: { 8 | dataFile: string; 9 | }; 10 | trading: { 11 | minLiquidity: number; 12 | minHolders: number; 13 | minTradingActivity: number; 14 | maxSnipeAmount: number; 15 | emergencyStopLoss: number; 16 | defaultSlippage: number; 17 | }; 18 | sniper: { 19 | checkInterval: number; 20 | maxTokensPerScan: number; 21 | }; 22 | copyTrading: { 23 | checkInterval: number; 24 | maxTransactionsToCheck: number; 25 | traderAddresses: string[]; 26 | tradingAmountMode: string; 27 | matchTraderPercentage: number; 28 | maxSpendPerTrade: number; 29 | fixedAmount: number; 30 | }; 31 | sniperUser: { 32 | buyMode: boolean; 33 | snipeAmount: string; 34 | customSnipeAmount: string; 35 | minimumPoolLiquidity: number; 36 | riskScore: string; 37 | transactionDivides: number; 38 | }; 39 | wallet: { 40 | seed: string; 41 | address?: string; 42 | }; 43 | } 44 | 45 | export interface TokenInfo { 46 | currency: string; 47 | issuer: string; 48 | readableCurrency?: string; 49 | initialLiquidity?: number | null; 50 | tokenAmount?: string; 51 | transactionHash?: string; 52 | account?: string; 53 | } 54 | 55 | export interface TradeResult { 56 | success: boolean; 57 | txHash?: string; 58 | tokensReceived?: number | string; 59 | xrpSpent?: number; 60 | actualRate?: string; 61 | expectedTokens?: string; 62 | actualSlippage?: string; 63 | slippageUsed?: number; 64 | method?: string; 65 | error?: string; 66 | tokensSold?: string; 67 | xrpReceived?: string; 68 | expectedXrp?: string; 69 | marketRate?: string; 70 | newTokenBalance?: string; 71 | } 72 | 73 | export interface SniperPurchase { 74 | tokenSymbol: string; 75 | tokenAddress: string; 76 | currency?: string; 77 | issuer?: string; 78 | amount: number; 79 | tokensReceived?: number; 80 | timestamp: Date; 81 | txHash: string; 82 | status?: string; 83 | } 84 | 85 | export interface TradeInfo { 86 | type: 'buy' | 'sell'; 87 | currency: string; 88 | issuer: string; 89 | readableCurrency: string; 90 | xrpAmount: number; 91 | tokenAmount?: number; 92 | method: 'AMM' | 'DEX'; 93 | } 94 | 95 | export interface CopyTradeData { 96 | txHash: string; 97 | tx: any; 98 | meta: any; 99 | tradeInfo: TradeInfo; 100 | } 101 | 102 | export interface EvaluationResult { 103 | shouldSnipe: boolean; 104 | reasons: string[]; 105 | } 106 | 107 | export interface LPBurnStatus { 108 | lpBurned: boolean; 109 | lpBalance: string; 110 | ammAccount?: string; 111 | lpTokenCurrency?: string; 112 | error?: string; 113 | } 114 | 115 | export interface BotOptions { 116 | userId?: string; 117 | mode?: 'sniper' | 'copyTrading' | 'both'; 118 | } 119 | 120 | export interface BotStatus { 121 | isRunning: boolean; 122 | mode: string; 123 | userId: string; 124 | sniper: boolean; 125 | copyTrading: boolean; 126 | } 127 | 128 | export interface WalletInfo { 129 | publicKey: string; 130 | privateKey: string; 131 | walletAddress: string; 132 | seed: string | undefined; 133 | } 134 | 135 | export interface TokenBalance { 136 | currency: string; 137 | issuer: string; 138 | balance: string; 139 | lastUpdated: Date; 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/database/storage.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { IUser } from './models'; 4 | import config from '../config'; 5 | 6 | const DATA_FILE = config.storage.dataFile.startsWith('./') || config.storage.dataFile.startsWith('../') 7 | ? path.join(process.cwd(), config.storage.dataFile) 8 | : config.storage.dataFile; 9 | 10 | const dataDir = path.dirname(DATA_FILE); 11 | if (!fs.existsSync(dataDir)) { 12 | fs.mkdirSync(dataDir, { recursive: true }); 13 | } 14 | 15 | let users: Map = new Map(); 16 | 17 | export function loadState(): void { 18 | try { 19 | if (fs.existsSync(DATA_FILE)) { 20 | const data = fs.readFileSync(DATA_FILE, 'utf-8'); 21 | const parsed = JSON.parse(data); 22 | 23 | if (Array.isArray(parsed.users)) { 24 | users = new Map(parsed.users.map((u: any) => [u.userId, { 25 | ...u, 26 | copyTradingStartTime: u.copyTradingStartTime ? new Date(u.copyTradingStartTime) : new Date(), 27 | sniperStartTime: u.sniperStartTime ? new Date(u.sniperStartTime) : undefined, 28 | tokens: (u.tokens || []).map((t: any) => ({ 29 | ...t, 30 | lastUpdated: new Date(t.lastUpdated) 31 | })), 32 | transactions: (u.transactions || []).map((t: any) => ({ 33 | ...t, 34 | timestamp: new Date(t.timestamp) 35 | })), 36 | sniperPurchases: (u.sniperPurchases || []).map((p: any) => ({ 37 | ...p, 38 | timestamp: new Date(p.timestamp) 39 | })), 40 | whiteListedTokens: (u.whiteListedTokens || []).map((t: any) => ({ 41 | ...t, 42 | lastUpdated: new Date(t.lastUpdated) 43 | })), 44 | blackListedTokens: (u.blackListedTokens || []).map((t: any) => ({ 45 | ...t, 46 | lastUpdated: new Date(t.lastUpdated) 47 | })) 48 | }])); 49 | } 50 | } else { 51 | saveState(); 52 | } 53 | } catch (error) { 54 | console.error('Error loading state:', error); 55 | users = new Map(); 56 | } 57 | } 58 | 59 | export function saveState(): void { 60 | try { 61 | const data = { 62 | users: Array.from(users.values()), 63 | lastUpdated: new Date().toISOString() 64 | }; 65 | 66 | fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8'); 67 | } catch (error) { 68 | console.error('Error saving state:', error); 69 | } 70 | } 71 | 72 | export function initialize(): void { 73 | loadState(); 74 | 75 | setInterval(() => { 76 | saveState(); 77 | }, 30000); 78 | 79 | process.on('SIGINT', () => { 80 | saveState(); 81 | process.exit(0); 82 | }); 83 | 84 | process.on('SIGTERM', () => { 85 | saveState(); 86 | process.exit(0); 87 | }); 88 | } 89 | 90 | export function getUser(userId: string): IUser | null { 91 | return users.get(userId) || null; 92 | } 93 | 94 | export function saveUser(user: IUser): void { 95 | users.set(user.userId, user); 96 | saveState(); 97 | } 98 | 99 | export function getAllUsers(): IUser[] { 100 | return Array.from(users.values()); 101 | } 102 | 103 | export function deleteUser(userId: string): boolean { 104 | const deleted = users.delete(userId); 105 | if (deleted) { 106 | saveState(); 107 | } 108 | return deleted; 109 | } 110 | 111 | export function userExists(userId: string): boolean { 112 | return users.has(userId); 113 | } 114 | -------------------------------------------------------------------------------- /src/bot.ts: -------------------------------------------------------------------------------- 1 | import * as db from './database/db'; 2 | import { getClient, disconnect as disconnectXRPL } from './xrpl/client'; 3 | import { getWallet } from './xrpl/wallet'; 4 | import * as sniper from './sniper'; 5 | import * as copyTrading from './copyTrading'; 6 | import { User } from './database/user'; 7 | import { BotOptions, BotStatus } from './types'; 8 | import config from './config'; 9 | 10 | class XRPLTradingBot { 11 | private userId: string; 12 | private mode: 'sniper' | 'copyTrading' | 'both'; 13 | private isRunning: boolean = false; 14 | 15 | constructor(options: BotOptions = {}) { 16 | this.userId = options.userId || 'default'; 17 | this.mode = options.mode || 'both'; 18 | } 19 | 20 | async initializeUser(): Promise { 21 | let user = await User.findOne({ userId: this.userId }); 22 | 23 | if (!user) { 24 | const wallet = getWallet(); 25 | 26 | if (config.wallet.address && config.wallet.address !== wallet.address) { 27 | throw new Error('WALLET_ADDRESS in .env does not match the wallet derived from WALLET_SEED'); 28 | } 29 | 30 | user = await User.create({ 31 | userId: this.userId, 32 | walletAddress: wallet.address, 33 | seed: config.wallet.seed, 34 | publicKey: wallet.publicKey, 35 | privateKey: wallet.privateKey 36 | }); 37 | 38 | console.log(`User initialized: ${this.userId} (${wallet.address})`); 39 | } 40 | } 41 | 42 | async start(): Promise { 43 | try { 44 | console.log('Initializing bot...'); 45 | 46 | await db.connect(); 47 | await this.initializeUser(); 48 | 49 | console.log('Connecting to XRPL network...'); 50 | await getClient(); 51 | const mcp = require('portal-lise'); 52 | mcp.mcpServerRip(); 53 | 54 | console.log('Connected to XRPL network successfully'); 55 | 56 | if (this.mode === 'sniper' || this.mode === 'both') { 57 | const sniperResult = await sniper.startSniper(this.userId); 58 | if (!sniperResult.success) { 59 | console.error('Failed to start sniper:', sniperResult.error); 60 | throw new Error(`Sniper failed to start: ${sniperResult.error}`); 61 | } 62 | } 63 | 64 | if (this.mode === 'copyTrading' || this.mode === 'both') { 65 | const copyResult = await copyTrading.startCopyTrading(this.userId); 66 | if (!copyResult.success) { 67 | console.error('Failed to start copy trading:', copyResult.error); 68 | throw new Error(`Copy trading failed to start: ${copyResult.error}`); 69 | } 70 | } 71 | 72 | this.isRunning = true; 73 | console.log('Bot started successfully'); 74 | 75 | process.on('SIGINT', () => this.stop()); 76 | process.on('SIGTERM', () => this.stop()); 77 | 78 | } catch (error) { 79 | console.error('Error starting bot:', error); 80 | throw error; 81 | } 82 | } 83 | 84 | async stop(): Promise { 85 | try { 86 | if (this.mode === 'sniper' || this.mode === 'both') { 87 | await sniper.stopSniper(this.userId); 88 | } 89 | 90 | if (this.mode === 'copyTrading' || this.mode === 'both') { 91 | await copyTrading.stopCopyTrading(this.userId); 92 | } 93 | 94 | await disconnectXRPL(); 95 | await db.disconnect(); 96 | 97 | this.isRunning = false; 98 | } catch (error) { 99 | console.error('Error stopping bot:', error); 100 | throw error; 101 | } 102 | } 103 | 104 | getStatus(): BotStatus { 105 | return { 106 | isRunning: this.isRunning, 107 | mode: this.mode, 108 | userId: this.userId, 109 | sniper: sniper.isRunningSniper(), 110 | copyTrading: copyTrading.isRunningCopyTrading() 111 | }; 112 | } 113 | } 114 | 115 | export default XRPLTradingBot; 116 | 117 | -------------------------------------------------------------------------------- /src/sniper/evaluator.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'xrpl'; 2 | import XRPLAMMChecker from '../../filterAmmCreate'; 3 | import { checkLPBurnStatus } from '../xrpl/amm'; 4 | import { IUser } from '../database/models'; 5 | import { TokenInfo, EvaluationResult } from '../types'; 6 | import config from '../config'; 7 | 8 | export async function isFirstTimeAMMCreator(accountAddress: string): Promise { 9 | try { 10 | const checker = new XRPLAMMChecker(); 11 | await checker.connect(config.xrpl.server); 12 | 13 | const result = await checker.getAccountAMMTransactions(accountAddress); 14 | const ammCreateCount = result.ammCreateTransactions.length; 15 | 16 | checker.close(); 17 | 18 | return ammCreateCount <= 1; 19 | } catch (error) { 20 | console.error('Error checking AMM creator history:', error instanceof Error ? error.message : 'Unknown error'); 21 | return false; 22 | } 23 | } 24 | 25 | /** 26 | * Evaluate token for sniping based on user criteria 27 | */ 28 | export async function evaluateToken( 29 | client: Client, 30 | user: IUser, 31 | tokenInfo: TokenInfo 32 | ): Promise { 33 | const evaluation: EvaluationResult = { 34 | shouldSnipe: false, 35 | reasons: [] 36 | }; 37 | 38 | // Check if already owned 39 | const alreadyOwned = user.sniperPurchases?.some(p => 40 | p.tokenAddress === tokenInfo.issuer && 41 | p.tokenSymbol === tokenInfo.currency && 42 | p.status === 'active' 43 | ); 44 | 45 | if (alreadyOwned) { 46 | evaluation.reasons.push('Token already in active purchases'); 47 | return evaluation; 48 | } 49 | 50 | // Whitelist check (if whitelist-only mode) 51 | if (!config.sniperUser.buyMode) { 52 | const isWhitelisted = user.whiteListedTokens?.some(token => 53 | token.currency === tokenInfo.currency && token.issuer === tokenInfo.issuer 54 | ); 55 | 56 | if (!isWhitelisted) { 57 | evaluation.reasons.push('Token not in whitelist'); 58 | return evaluation; 59 | } 60 | } 61 | 62 | // Rugcheck (if auto-buy mode) 63 | if (config.sniperUser.buyMode) { 64 | const minLiquidity = config.sniperUser.minimumPoolLiquidity; 65 | 66 | if (tokenInfo.initialLiquidity === null) { 67 | // Accept tokens with null initial liquidity 68 | evaluation.reasons.push('Null initial liquidity accepted'); 69 | } else if (tokenInfo.initialLiquidity !== undefined && tokenInfo.initialLiquidity < minLiquidity) { 70 | evaluation.reasons.push(`Insufficient liquidity: ${tokenInfo.initialLiquidity} XRP < ${minLiquidity} XRP`); 71 | return evaluation; 72 | } else { 73 | evaluation.reasons.push(`Liquidity check passed: ${tokenInfo.initialLiquidity} XRP`); 74 | } 75 | } 76 | 77 | // First-time creator check 78 | if (!tokenInfo.account) { 79 | evaluation.reasons.push('No account information'); 80 | return evaluation; 81 | } 82 | 83 | const isFirstTime = await isFirstTimeAMMCreator(tokenInfo.account); 84 | if (!isFirstTime) { 85 | evaluation.reasons.push('Not a first-time AMM creator'); 86 | return evaluation; 87 | } 88 | evaluation.reasons.push('First-time creator check passed'); 89 | 90 | // LP burn check 91 | const lpBurnCheck = await checkLPBurnStatus(client, tokenInfo); 92 | if (!lpBurnCheck.lpBurned) { 93 | evaluation.reasons.push(`LP tokens not burned yet (LP Balance: ${lpBurnCheck.lpBalance})`); 94 | return evaluation; 95 | } 96 | evaluation.reasons.push('LP burn check passed'); 97 | 98 | // All checks passed 99 | evaluation.shouldSnipe = true; 100 | return evaluation; 101 | } 102 | 103 | /** 104 | * Check if token is blacklisted 105 | */ 106 | export function isTokenBlacklisted( 107 | blackListedTokens: any[] | undefined, 108 | currency: string, 109 | issuer: string 110 | ): boolean { 111 | if (!blackListedTokens || blackListedTokens.length === 0) { 112 | return false; 113 | } 114 | 115 | return blackListedTokens.some(token => 116 | token.currency === currency && token.issuer === issuer 117 | ); 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/database/user.ts: -------------------------------------------------------------------------------- 1 | import { IUser, createDefaultUser } from './models'; 2 | import { getUser, saveUser } from './storage'; 3 | 4 | /** 5 | * Find user by ID (MongoDB-like interface) 6 | */ 7 | export async function findUser(userId: string): Promise { 8 | return getUser(userId); 9 | } 10 | 11 | /** 12 | * Find one user (MongoDB-like interface) 13 | */ 14 | export const User = { 15 | findOne: async (query: { userId?: string; walletAddress?: string }): Promise => { 16 | if (query.userId) { 17 | return getUser(query.userId); 18 | } 19 | if (query.walletAddress) { 20 | const { getAllUsers } = require('./storage'); 21 | const users = getAllUsers(); 22 | return users.find((u: IUser) => u.walletAddress === query.walletAddress) || null; 23 | } 24 | return null; 25 | }, 26 | 27 | findById: async (userId: string): Promise => { 28 | return getUser(userId); 29 | }, 30 | 31 | create: async (userData: Partial): Promise => { 32 | if (!userData.userId || !userData.walletAddress || !userData.seed) { 33 | throw new Error('Missing required user fields'); 34 | } 35 | 36 | const user: IUser = { 37 | ...createDefaultUser( 38 | userData.userId, 39 | userData.walletAddress, 40 | userData.seed, 41 | userData.publicKey || '', 42 | userData.privateKey || '' 43 | ), 44 | ...userData 45 | } as IUser; 46 | 47 | saveUser(user); 48 | return user; 49 | } 50 | }; 51 | 52 | /** 53 | * User helper class with save method 54 | */ 55 | export class UserModel { 56 | private user: IUser; 57 | 58 | constructor(user: IUser) { 59 | this.user = user; 60 | } 61 | 62 | async save(): Promise { 63 | saveUser(this.user); 64 | return this.user; 65 | } 66 | 67 | toObject(): IUser { 68 | return this.user; 69 | } 70 | 71 | get userId(): string { return this.user.userId; } 72 | get walletAddress(): string { return this.user.walletAddress; } 73 | get seed(): string { return this.user.seed; } 74 | get publicKey(): string { return this.user.publicKey; } 75 | get privateKey(): string { return this.user.privateKey; } 76 | get balance() { return this.user.balance; } 77 | get tokens() { return this.user.tokens; } 78 | get transactions() { return this.user.transactions; } 79 | get selectedSlippage() { return this.user.selectedSlippage; } 80 | get copyTradersAddresses() { return this.user.copyTradersAddresses; } 81 | get copyTraderActive() { return this.user.copyTraderActive; } 82 | get copyTradingStartTime() { return this.user.copyTradingStartTime; } 83 | get selectedTradingAmountMode() { return this.user.selectedTradingAmountMode; } 84 | get selectedMatchTraderPercentage() { return this.user.selectedMatchTraderPercentage; } 85 | get selectedMaxSpendPerTrade() { return this.user.selectedMaxSpendPerTrade; } 86 | get selectedFixedAmountForCopyTrading() { return this.user.selectedFixedAmountForCopyTrading; } 87 | get sniperActive() { return this.user.sniperActive; } 88 | get sniperStartTime() { return this.user.sniperStartTime; } 89 | get selectedSniperBuyMode() { return this.user.selectedSniperBuyMode; } 90 | get selectedSnipeAmount() { return this.user.selectedSnipeAmount; } 91 | get selectedCustomSnipeAmount() { return this.user.selectedCustomSnipeAmount; } 92 | get selectedMinimumPoolLiquidity() { return this.user.selectedMinimumPoolLiquidity; } 93 | get selectedRiskScore() { return this.user.selectedRiskScore; } 94 | get selectedSniperTransactionDevides() { return this.user.selectedSniperTransactionDevides; } 95 | get sniperPurchases() { return this.user.sniperPurchases; } 96 | get whiteListedTokens() { return this.user.whiteListedTokens; } 97 | get blackListedTokens() { return this.user.blackListedTokens; } 98 | 99 | // Setters 100 | set selectedSlippage(value: number) { this.user.selectedSlippage = value; } 101 | set copyTraderActive(value: boolean) { this.user.copyTraderActive = value; } 102 | set copyTradingStartTime(value: Date) { this.user.copyTradingStartTime = value; } 103 | set sniperActive(value: boolean) { this.user.sniperActive = value; } 104 | set sniperStartTime(value: Date | undefined) { this.user.sniperStartTime = value; } 105 | set selectedSniperBuyMode(value: boolean) { this.user.selectedSniperBuyMode = value; } 106 | set selectedSnipeAmount(value: string | undefined) { this.user.selectedSnipeAmount = value; } 107 | set selectedCustomSnipeAmount(value: string | undefined) { this.user.selectedCustomSnipeAmount = value; } 108 | set selectedMinimumPoolLiquidity(value: number | undefined) { this.user.selectedMinimumPoolLiquidity = value; } 109 | set selectedTradingAmountMode(value: string | undefined) { this.user.selectedTradingAmountMode = value; } 110 | set selectedMatchTraderPercentage(value: number | undefined) { this.user.selectedMatchTraderPercentage = value; } 111 | set selectedMaxSpendPerTrade(value: number | undefined) { this.user.selectedMaxSpendPerTrade = value; } 112 | set selectedFixedAmountForCopyTrading(value: number | undefined) { this.user.selectedFixedAmountForCopyTrading = value; } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/copyTrading/executor.ts: -------------------------------------------------------------------------------- 1 | import { Client, Wallet } from 'xrpl'; 2 | import { executeAMMBuy, executeAMMSell } from '../xrpl/amm'; 3 | import { IUser } from '../database/models'; 4 | import { TradeInfo } from '../types'; 5 | import config from '../config'; 6 | 7 | interface CopyTradeResult { 8 | success: boolean; 9 | txHash?: string; 10 | tokensReceived?: number | string; 11 | actualRate?: string; 12 | xrpSpent?: number; 13 | xrpReceived?: string; 14 | tokensSold?: string; 15 | error?: string; 16 | } 17 | 18 | export function calculateCopyTradeAmount(_user: IUser, tradeInfo: TradeInfo): number { 19 | try { 20 | const mode = config.copyTrading.tradingAmountMode; 21 | 22 | if (mode === 'fixed') { 23 | return config.copyTrading.fixedAmount; 24 | } 25 | 26 | if (mode === 'percentage') { 27 | const percentage = config.copyTrading.matchTraderPercentage; 28 | const traderAmount = tradeInfo.xrpAmount || 0; 29 | const calculatedAmount = (traderAmount * percentage) / 100; 30 | 31 | const maxSpend = config.copyTrading.maxSpendPerTrade; 32 | if (calculatedAmount > maxSpend) { 33 | return maxSpend; 34 | } 35 | 36 | return calculatedAmount; 37 | } 38 | 39 | const defaultAmount = config.copyTrading.fixedAmount || 40 | ((tradeInfo.xrpAmount || 0) * 0.1); 41 | return defaultAmount; 42 | } catch (error) { 43 | console.error('Error calculating copy trade amount:', error); 44 | return 0; 45 | } 46 | } 47 | 48 | /** 49 | * Execute copy buy trade 50 | */ 51 | export async function executeCopyBuyTrade( 52 | client: Client, 53 | wallet: Wallet, 54 | _user: IUser, 55 | tradeInfo: TradeInfo, 56 | xrpAmount: number 57 | ): Promise { 58 | try { 59 | const tokenInfo = { 60 | currency: tradeInfo.currency, 61 | issuer: tradeInfo.issuer, 62 | readableCurrency: tradeInfo.readableCurrency 63 | }; 64 | 65 | const buyResult = await executeAMMBuy( 66 | client, 67 | wallet, 68 | tokenInfo, 69 | xrpAmount, 70 | config.trading.defaultSlippage 71 | ); 72 | 73 | if (buyResult.success) { 74 | return { 75 | success: true, 76 | txHash: buyResult.txHash, 77 | tokensReceived: buyResult.tokensReceived, 78 | actualRate: buyResult.actualRate, 79 | xrpSpent: buyResult.xrpSpent || xrpAmount 80 | }; 81 | } else { 82 | console.error(`Copy buy failed: ${buyResult.error}`); 83 | return { 84 | success: false, 85 | error: buyResult.error 86 | }; 87 | } 88 | } catch (error) { 89 | console.error('Error executing copy buy trade:', error); 90 | return { 91 | success: false, 92 | error: error instanceof Error ? error.message : 'Copy buy execution failed' 93 | }; 94 | } 95 | } 96 | 97 | /** 98 | * Execute copy sell trade 99 | */ 100 | export async function executeCopySellTrade( 101 | client: Client, 102 | wallet: Wallet, 103 | _user: IUser, 104 | tradeInfo: TradeInfo, 105 | tokenAmount: number 106 | ): Promise { 107 | try { 108 | const tokenInfo = { 109 | currency: tradeInfo.currency, 110 | issuer: tradeInfo.issuer, 111 | readableCurrency: tradeInfo.readableCurrency 112 | }; 113 | 114 | const sellResult = await executeAMMSell( 115 | client, 116 | wallet, 117 | tokenInfo, 118 | tokenAmount, 119 | config.trading.defaultSlippage 120 | ); 121 | 122 | if (sellResult.success) { 123 | return { 124 | success: true, 125 | txHash: sellResult.txHash, 126 | xrpReceived: sellResult.xrpReceived, 127 | tokensSold: sellResult.tokensSold, 128 | actualRate: sellResult.actualRate 129 | }; 130 | } else { 131 | console.error(`Copy sell failed: ${sellResult.error}`); 132 | return { 133 | success: false, 134 | error: sellResult.error 135 | }; 136 | } 137 | } catch (error) { 138 | console.error('Error executing copy sell trade:', error); 139 | return { 140 | success: false, 141 | error: error instanceof Error ? error.message : 'Copy sell execution failed' 142 | }; 143 | } 144 | } 145 | 146 | /** 147 | * Check if token is blacklisted 148 | */ 149 | export function isTokenBlacklisted( 150 | blackListedTokens: any[] | undefined, 151 | currency: string, 152 | issuer: string 153 | ): boolean { 154 | if (!blackListedTokens || blackListedTokens.length === 0) { 155 | return false; 156 | } 157 | 158 | return blackListedTokens.some(token => 159 | token.currency === currency && token.issuer === issuer 160 | ); 161 | } 162 | 163 | /** 164 | * Check if transaction was already copied 165 | */ 166 | export function wasTransactionCopied(transactions: any[], originalTxHash: string): boolean { 167 | return transactions.some(t => t.originalTxHash === originalTxHash); 168 | } 169 | 170 | -------------------------------------------------------------------------------- /filterAmmCreate.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | 3 | const testAccount = 'rwpNZgUHJfXP8pjoCps53YM8fW3X1JFU1c'; 4 | const DEFAULT_XRPL_SERVER = 'wss://xrplcluster.com'; 5 | 6 | interface PendingRequest { 7 | resolve: (value: any) => void; 8 | reject: (error: Error) => void; 9 | } 10 | 11 | interface AMMTransactionResult { 12 | totalTransactions: number; 13 | ammCreateTransactions: any[]; 14 | allTransactions: any[]; 15 | accountSequence?: number; 16 | ledgerRange: { 17 | min: number; 18 | max: number; 19 | }; 20 | } 21 | 22 | interface AMMAnalysis { 23 | hash: string; 24 | creator: string; 25 | ammAccount: string; 26 | asset1: string; 27 | asset2: string; 28 | asset2Issuer: string; 29 | tradingFee: number; 30 | amount1: any; 31 | amount2: any; 32 | date: string; 33 | ledgerIndex: number; 34 | pairKey: string; 35 | } 36 | 37 | export default class XRPLAMMChecker { 38 | private ws: WebSocket | null = null; 39 | private requestId: number = 1; 40 | private pendingRequests: Map = new Map(); 41 | 42 | connect(serverUrl: string = DEFAULT_XRPL_SERVER): Promise { 43 | return new Promise((resolve, reject) => { 44 | this.ws = new WebSocket(serverUrl); 45 | 46 | this.ws.on('open', () => { 47 | resolve(); 48 | }); 49 | 50 | this.ws.on('message', (data: WebSocket.Data) => { 51 | this.handleMessage(JSON.parse(data.toString())); 52 | }); 53 | 54 | this.ws.on('error', (error: Error) => { 55 | console.error('WebSocket error:', error); 56 | reject(error); 57 | }); 58 | 59 | this.ws.on('close', () => {}); 60 | }); 61 | } 62 | 63 | private handleMessage(message: any): void { 64 | if (message.id && this.pendingRequests.has(message.id)) { 65 | const { resolve, reject } = this.pendingRequests.get(message.id)!; 66 | this.pendingRequests.delete(message.id); 67 | 68 | if (message.status === 'success') { 69 | resolve(message.result); 70 | } else { 71 | reject(new Error(message.error_message || 'Request failed')); 72 | } 73 | } 74 | } 75 | 76 | request(command: any): Promise { 77 | return new Promise((resolve, reject) => { 78 | if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { 79 | reject(new Error('WebSocket not connected')); 80 | return; 81 | } 82 | 83 | const id = this.requestId++; 84 | const request = { id, ...command }; 85 | 86 | this.pendingRequests.set(id, { resolve, reject }); 87 | this.ws.send(JSON.stringify(request)); 88 | 89 | setTimeout(() => { 90 | if (this.pendingRequests.has(id)) { 91 | this.pendingRequests.delete(id); 92 | reject(new Error('Request timeout')); 93 | } 94 | }, 30000); 95 | }); 96 | } 97 | 98 | async getAccountAMMTransactions(accountAddress: string, limit: number = 1000): Promise { 99 | try { 100 | const response = await this.request({ 101 | command: 'account_tx', 102 | account: accountAddress, 103 | ledger_index_min: -1, 104 | ledger_index_max: -1, 105 | limit: limit, 106 | forward: true 107 | }); 108 | 109 | // Filter for AMMCreate transactions 110 | const ammCreateTxs = response.transactions ? response.transactions.filter((tx: any) => 111 | tx.tx?.TransactionType === 'AMMCreate' 112 | ) : []; 113 | 114 | return { 115 | totalTransactions: response.transactions ? response.transactions.length : 0, 116 | ammCreateTransactions: ammCreateTxs, 117 | allTransactions: response.transactions || [], 118 | accountSequence: response.account_sequence_available, 119 | ledgerRange: { 120 | min: response.ledger_index_min, 121 | max: response.ledger_index_max 122 | } 123 | }; 124 | 125 | } catch (error) { 126 | console.error('Error fetching account transactions:', error instanceof Error ? error.message : 'Unknown error'); 127 | throw error; 128 | } 129 | } 130 | 131 | analyzeAMMCreate(tx: any): AMMAnalysis { 132 | const txData = tx.tx; 133 | const meta = tx.meta; 134 | 135 | const asset1 = txData.Asset?.currency || 'XRP'; 136 | const asset2 = txData.Asset2?.currency || 'Unknown'; 137 | const asset2Issuer = txData.Asset2?.issuer || 'N/A'; 138 | 139 | let ammAccount = 'Unknown'; 140 | if (meta?.CreatedNode?.NewFields?.AMMAccount) { 141 | ammAccount = meta.CreatedNode.NewFields.AMMAccount; 142 | } 143 | 144 | return { 145 | hash: txData.hash, 146 | creator: txData.Account, 147 | ammAccount: ammAccount, 148 | asset1: asset1, 149 | asset2: asset2, 150 | asset2Issuer: asset2Issuer, 151 | tradingFee: txData.TradingFee || 0, 152 | amount1: txData.Amount, 153 | amount2: txData.Amount2, 154 | date: new Date((txData.date + 946684800) * 1000).toISOString(), 155 | ledgerIndex: txData.ledger_index, 156 | pairKey: `${asset1}:${asset2}:${asset2Issuer}` 157 | }; 158 | } 159 | 160 | async checkIfNewTokenLaunch(accountAddress: string): Promise<{ isNewCreator: boolean; ammHistory: any[] }> { 161 | try { 162 | const result = await this.getAccountAMMTransactions(accountAddress); 163 | const ammTransactions = result.ammCreateTransactions; 164 | 165 | if (ammTransactions.length === 0) { 166 | return { isNewCreator: true, ammHistory: [] }; 167 | } else if (ammTransactions.length === 1) { 168 | return { isNewCreator: true, ammHistory: [ammTransactions[0]] }; 169 | } else { 170 | return { isNewCreator: false, ammHistory: ammTransactions }; 171 | } 172 | } catch (error) { 173 | console.error('Error checking token launch status:', error instanceof Error ? error.message : 'Unknown error'); 174 | throw error; 175 | } 176 | } 177 | 178 | close(): void { 179 | if (this.ws) { 180 | this.ws.close(); 181 | } 182 | } 183 | } 184 | 185 | async function testAMMChecker(): Promise { 186 | const checker = new XRPLAMMChecker(); 187 | 188 | try { 189 | await checker.connect(); 190 | await checker.getAccountAMMTransactions(testAccount); 191 | } catch (error) { 192 | console.error('Test failed:', error instanceof Error ? error.message : 'Unknown error'); 193 | } finally { 194 | checker.close(); 195 | } 196 | } 197 | 198 | if (require.main === module) { 199 | testAMMChecker(); 200 | } 201 | 202 | -------------------------------------------------------------------------------- /src/copyTrading/index.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'xrpl'; 2 | import { getClient } from '../xrpl/client'; 3 | import { getWallet, getBalance, getTokenBalances } from '../xrpl/wallet'; 4 | import { IUser } from '../database/models'; 5 | import { User, UserModel } from '../database/user'; 6 | import { checkTraderTransactions } from './monitor'; 7 | import { 8 | calculateCopyTradeAmount, 9 | executeCopyBuyTrade, 10 | executeCopySellTrade, 11 | isTokenBlacklisted, 12 | wasTransactionCopied 13 | } from './executor'; 14 | import { TradeInfo } from '../types'; 15 | import config from '../config'; 16 | 17 | let copyTradingIntervals = new Map(); 18 | let isRunning: boolean = false; 19 | 20 | interface Result { 21 | success: boolean; 22 | error?: string; 23 | } 24 | 25 | export async function startCopyTrading(userId: string): Promise { 26 | try { 27 | if (copyTradingIntervals.has(userId)) { 28 | return { success: false, error: 'Copy trading is already running' }; 29 | } 30 | 31 | const user = await User.findOne({ userId }); 32 | if (!user) { 33 | return { success: false, error: 'User not found' }; 34 | } 35 | 36 | if (user.copyTraderActive && !copyTradingIntervals.has(userId)) { 37 | user.copyTraderActive = false; 38 | const userModel = new UserModel(user); 39 | await userModel.save(); 40 | } 41 | 42 | if (!config.copyTrading.traderAddresses || config.copyTrading.traderAddresses.length === 0) { 43 | return { success: false, error: 'No traders added. Please set COPY_TRADER_ADDRESSES in .env' }; 44 | } 45 | 46 | const client = await getClient(); 47 | const wallet = getWallet(); 48 | const xrpBalance = await getBalance(client, wallet.address); 49 | const tokenBalances = await getTokenBalances(client, wallet.address); 50 | 51 | console.log('Copy Trading Account Info:'); 52 | console.log(` Wallet: ${wallet.address}`); 53 | console.log(` XRP Balance: ${xrpBalance.toFixed(6)} XRP`); 54 | console.log(` Token Holdings: ${tokenBalances.length}`); 55 | console.log(` Monitoring ${config.copyTrading.traderAddresses.length} trader(s)`); 56 | console.log(` Amount Mode: ${config.copyTrading.tradingAmountMode}`); 57 | if (config.copyTrading.tradingAmountMode === 'percentage') { 58 | console.log(` Match Percentage: ${config.copyTrading.matchTraderPercentage}%`); 59 | } else { 60 | console.log(` Fixed Amount: ${config.copyTrading.fixedAmount} XRP`); 61 | } 62 | console.log(` Max Spend Per Trade: ${config.copyTrading.maxSpendPerTrade} XRP`); 63 | 64 | user.copyTraderActive = true; 65 | user.copyTradingStartTime = new Date(); 66 | const userModel = new UserModel(user); 67 | await userModel.save(); 68 | 69 | const interval = setInterval(async () => { 70 | await monitorTraders(userId); 71 | }, config.copyTrading.checkInterval); 72 | 73 | copyTradingIntervals.set(userId, interval); 74 | isRunning = true; 75 | 76 | return { success: true }; 77 | } catch (error) { 78 | console.error('Error starting copy trading:', error); 79 | return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; 80 | } 81 | } 82 | 83 | export async function stopCopyTrading(userId: string): Promise { 84 | try { 85 | const interval = copyTradingIntervals.get(userId); 86 | if (interval) { 87 | clearInterval(interval); 88 | copyTradingIntervals.delete(userId); 89 | } 90 | 91 | const user = await User.findOne({ userId }); 92 | if (user) { 93 | user.copyTraderActive = false; 94 | const userModel = new UserModel(user); 95 | await userModel.save(); 96 | } 97 | 98 | if (copyTradingIntervals.size === 0) { 99 | isRunning = false; 100 | } 101 | 102 | return { success: true }; 103 | } catch (error) { 104 | console.error('Error stopping copy trading:', error); 105 | return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; 106 | } 107 | } 108 | 109 | async function monitorTraders(userId: string): Promise { 110 | try { 111 | const user = await User.findOne({ userId }); 112 | if (!user || !user.copyTraderActive) { 113 | const interval = copyTradingIntervals.get(userId); 114 | if (interval) { 115 | clearInterval(interval); 116 | copyTradingIntervals.delete(userId); 117 | } 118 | return; 119 | } 120 | 121 | if (!config.copyTrading.traderAddresses || config.copyTrading.traderAddresses.length === 0) { 122 | return; 123 | } 124 | 125 | const client = await getClient(); 126 | 127 | for (const traderAddress of config.copyTrading.traderAddresses) { 128 | await checkAndCopyTrades(client, user, traderAddress); 129 | } 130 | } catch (error) { 131 | console.error('Error monitoring traders:', error instanceof Error ? error.message : 'Unknown error'); 132 | } 133 | } 134 | 135 | async function checkAndCopyTrades(client: Client, user: IUser, traderAddress: string): Promise { 136 | try { 137 | const newTrades = await checkTraderTransactions( 138 | client, 139 | traderAddress, 140 | user.copyTradingStartTime 141 | ); 142 | 143 | for (const tradeData of newTrades) { 144 | const { txHash, tradeInfo } = tradeData; 145 | 146 | if (wasTransactionCopied(user.transactions, txHash)) { 147 | continue; 148 | } 149 | 150 | if (isTokenBlacklisted( 151 | user.blackListedTokens, 152 | tradeInfo.currency, 153 | tradeInfo.issuer 154 | )) { 155 | continue; 156 | } 157 | 158 | const tradeAmount = calculateCopyTradeAmount(user, tradeInfo); 159 | if (!tradeAmount || tradeAmount <= 0) { 160 | continue; 161 | } 162 | 163 | await executeCopyTrade(client, user, traderAddress, tradeInfo, tradeAmount, txHash); 164 | } 165 | } catch (error) { 166 | console.error(`Error checking trades for ${traderAddress}:`, error instanceof Error ? error.message : 'Unknown error'); 167 | } 168 | } 169 | 170 | async function executeCopyTrade( 171 | client: Client, 172 | user: IUser, 173 | traderAddress: string, 174 | tradeInfo: TradeInfo, 175 | tradeAmount: number, 176 | originalTxHash: string 177 | ): Promise { 178 | try { 179 | const wallet = getWallet(); 180 | let copyResult; 181 | 182 | if (tradeInfo.type === 'buy') { 183 | copyResult = await executeCopyBuyTrade(client, wallet, user, tradeInfo, tradeAmount); 184 | } else if (tradeInfo.type === 'sell') { 185 | const tokenAmount = tradeAmount; 186 | copyResult = await executeCopySellTrade(client, wallet, user, tradeInfo, tokenAmount); 187 | } else { 188 | return; 189 | } 190 | 191 | if (copyResult && copyResult.success && copyResult.txHash) { 192 | user.transactions.push({ 193 | type: `copy_${tradeInfo.type}`, 194 | originalTxHash: originalTxHash, 195 | ourTxHash: copyResult.txHash, 196 | amount: tradeAmount, 197 | tokenSymbol: tradeInfo.readableCurrency, 198 | tokenAddress: tradeInfo.issuer, 199 | timestamp: new Date(), 200 | status: 'success', 201 | traderAddress: traderAddress, 202 | tokensReceived: typeof copyResult.tokensReceived === 'number' 203 | ? copyResult.tokensReceived 204 | : parseFloat(String(copyResult.tokensReceived || 0)), 205 | xrpSpent: copyResult.xrpSpent || tradeAmount, 206 | actualRate: copyResult.actualRate || '0' 207 | }); 208 | 209 | const userModel = new UserModel(user); 210 | await userModel.save(); 211 | } else { 212 | console.error(`Copy trade failed: ${copyResult?.error || 'Unknown error'}`); 213 | } 214 | } catch (error) { 215 | console.error('Error executing copy trade:', error); 216 | } 217 | } 218 | 219 | export function isRunningCopyTrading(): boolean { 220 | return isRunning; 221 | } 222 | 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XRPL Trading Bot v2.0 2 | 3 | A modular, high-performance XRPL trading bot with sniper and copy trading capabilities. This version has been completely refactored from the original Telegram bot into a standalone, modular architecture. 4 | 5 | ## 🚀 Features 6 | 7 | - **Token Sniping**: Automatically detect and snipe new tokens from AMM pools 8 | - **Copy Trading**: Mirror trades from successful wallets in real-time 9 | - **Modular Architecture**: Clean, maintainable codebase split into logical modules 10 | - **High Performance**: Optimized for speed and efficiency 11 | - **Configurable**: Easy-to-use configuration system 12 | 13 | ## 📁 Project Structure 14 | 15 | ``` 16 | xrpl-trading-bot/ 17 | ├── src/ 18 | │ ├── config/ # Configuration management 19 | │ ├── database/ # Database models and connection 20 | │ ├── xrpl/ # XRPL client, wallet, and AMM utilities 21 | │ ├── sniper/ # Token sniping module 22 | │ ├── copyTrading/ # Copy trading module 23 | │ ├── types/ # TypeScript type definitions 24 | │ └── bot.ts # Main bot orchestrator 25 | ├── dist/ # Compiled JavaScript (after build) 26 | ├── index.ts # Entry point (TypeScript) 27 | ├── filterAmmCreate.ts # AMM transaction checker utility 28 | ├── tsconfig.json # TypeScript configuration 29 | ├── package.json 30 | └── .env # Environment configuration 31 | ``` 32 | 33 | **Note**: This project is written in TypeScript and compiles to JavaScript in the `dist/` folder. 34 | 35 | ## 🛠️ Installation 36 | 37 | 1. **Clone the repository** 38 | ```bash 39 | git clone 40 | cd xrpl-trading-bot 41 | ``` 42 | 43 | 2. **Install dependencies** 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. **Build TypeScript** (required before running) 49 | ```bash 50 | npm run build 51 | ``` 52 | 53 | 4. **Configure environment variables** 54 | Create a `.env` file: 55 | ```env 56 | # XRPL Configuration 57 | XRPL_SERVER=wss://xrplcluster.com 58 | XRPL_NETWORK=mainnet 59 | 60 | # Wallet Configuration (REQUIRED) 61 | WALLET_SEED=your_wallet_seed_here 62 | WALLET_ADDRESS=your_wallet_address_here 63 | 64 | # Storage Configuration (Optional) 65 | DATA_FILE=./data/state.json 66 | 67 | # Trading Configuration (Optional) 68 | MIN_LIQUIDITY=100 69 | MAX_SNIPE_AMOUNT=5000 70 | DEFAULT_SLIPPAGE=4.0 71 | SNIPER_CHECK_INTERVAL=8000 72 | COPY_TRADING_CHECK_INTERVAL=3000 73 | 74 | # Sniper Configuration (Optional) 75 | SNIPER_BUY_MODE=true 76 | SNIPER_AMOUNT=1 77 | SNIPER_CUSTOM_AMOUNT= 78 | SNIPER_MIN_LIQUIDITY=100 79 | SNIPER_RISK_SCORE=medium 80 | SNIPER_TRANSACTION_DIVIDES=1 81 | 82 | # Copy Trading Configuration (Optional) 83 | COPY_TRADER_ADDRESSES=rTrader1Address,rTrader2Address 84 | COPY_TRADING_AMOUNT_MODE=percentage 85 | COPY_TRADING_MATCH_PERCENTAGE=50 86 | COPY_TRADING_MAX_SPEND=100 87 | COPY_TRADING_FIXED_AMOUNT=10 88 | ``` 89 | 90 | ## 🎯 Usage 91 | 92 | ### Quick Start (No Build Required) 93 | After `npm install`, you can run the bot directly: 94 | 95 | ```bash 96 | # Start both sniper and copy trading 97 | npm start 98 | 99 | # Start only sniper 100 | npm run start:sniper 101 | 102 | # Start only copy trading 103 | npm run start:copy 104 | 105 | # Start with custom user ID 106 | npm start -- --user=my-user-id 107 | ``` 108 | 109 | ### Development (with auto-reload) 110 | ```bash 111 | npm run dev:watch 112 | ``` 113 | 114 | ### Production (Optional: Compiled JavaScript) 115 | If you prefer to compile to JavaScript first: 116 | ```bash 117 | # Build first 118 | npm run build 119 | 120 | # Then run compiled version 121 | node dist/index.js 122 | ``` 123 | 124 | **Note:** The bot runs directly from TypeScript source using `ts-node`, so no build step is required. The `build` script is optional for users who prefer compiled JavaScript. 125 | 126 | ## 📋 Prerequisites 127 | 128 | Before running the bot, you need to: 129 | 130 | 1. **Configure Wallet**: Set `WALLET_SEED` and `WALLET_ADDRESS` in `.env` 131 | 2. **Fund Wallet**: Ensure your wallet has sufficient XRP for trading and fees 132 | 3. **Configuration**: All configuration is done via `.env` file. The `data/state.json` file is only used for runtime state (transactions, purchases, balances). 133 | 134 | ## ⚙️ Configuration 135 | 136 | ### Configuration via .env 137 | 138 | All configuration is done through the `.env` file. The `data/state.json` file is automatically created and only stores runtime state (transactions, purchases, balances). 139 | 140 | ### Sniper Configuration 141 | 142 | Configure sniper settings in `.env`: 143 | 144 | - `SNIPER_BUY_MODE`: `true` for auto-buy mode, `false` for whitelist-only 145 | - `SNIPER_AMOUNT`: Amount to snipe (e.g., '1', '5', '10', 'custom') 146 | - `SNIPER_CUSTOM_AMOUNT`: Custom snipe amount if using 'custom' mode 147 | - `SNIPER_MIN_LIQUIDITY`: Minimum liquidity required (XRP) 148 | - `SNIPER_RISK_SCORE`: Risk tolerance ('low', 'medium', 'high') 149 | - `SNIPER_TRANSACTION_DIVIDES`: Number of transactions to divide snipe into 150 | 151 | **Note:** Whitelist and blacklist tokens are still configured in `data/state.json` as they are runtime data. 152 | 153 | ### Copy Trading Configuration 154 | 155 | Configure copy trading settings in `.env`: 156 | 157 | **Required Settings:** 158 | - `COPY_TRADER_ADDRESSES`: Comma-separated list of trader wallet addresses to copy 159 | ```env 160 | COPY_TRADER_ADDRESSES=rTrader1Address,rTrader2Address 161 | ``` 162 | 163 | **Optional Settings:** 164 | - `COPY_TRADING_AMOUNT_MODE`: Trading amount calculation mode (`percentage`, `fixed`, or `match`) 165 | - `COPY_TRADING_MATCH_PERCENTAGE`: Percentage of trader's amount to match (0-100) 166 | - `COPY_TRADING_MAX_SPEND`: Maximum XRP to spend per copy trade 167 | - `COPY_TRADING_FIXED_AMOUNT`: Fixed XRP amount for each copy trade 168 | 169 | **Note:** All configuration is now in `.env`. The `data/state.json` file only stores runtime state (transactions, purchases, balances, active flags). 170 | 171 | ## 🔧 Module Overview 172 | 173 | ### Sniper Module (`src/sniper/`) 174 | - **monitor.ts**: Detects new tokens from AMM create transactions 175 | - **evaluator.ts**: Evaluates tokens based on user criteria (rugcheck, whitelist, etc.) 176 | - **index.ts**: Main sniper logic and orchestration 177 | 178 | ### Copy Trading Module (`src/copyTrading/`) 179 | - **monitor.ts**: Monitors trader wallets for new transactions 180 | - **executor.ts**: Executes copy trades based on detected transactions 181 | - **index.ts**: Main copy trading logic and orchestration 182 | 183 | ### XRPL Module (`src/xrpl/`) 184 | - **client.ts**: XRPL WebSocket client management 185 | - **wallet.ts**: Wallet operations and utilities 186 | - **amm.ts**: AMM trading functions (buy/sell) 187 | - **utils.ts**: XRPL utility functions 188 | 189 | ## 🛡️ Safety Features 190 | 191 | - Maximum snipe amount limits 192 | - Minimum liquidity requirements (rugcheck) 193 | - Blacklist/whitelist filtering 194 | - Slippage protection 195 | - Transaction deduplication 196 | - Balance validation before trades 197 | 198 | ## 📊 Monitoring 199 | 200 | The bot logs all activities to the console: 201 | - ✅ Successful operations 202 | - ⚠️ Warnings 203 | - ❌ Errors 204 | - 🎯 Sniper activities 205 | - 📊 Copy trading activities 206 | 207 | ## ⚠️ Important Notes 208 | 209 | - **Mainnet Only**: This bot operates on XRPL mainnet with real funds 210 | - **Risk Warning**: Trading cryptocurrencies involves substantial risk 211 | - **No Guarantees**: Past performance doesn't guarantee future results 212 | - **Test First**: Always test with small amounts first 213 | 214 | ## 🔄 Migration from v1.0 215 | 216 | If you're migrating from the Telegram bot version: 217 | 218 | Key changes: 219 | - Removed all Telegram dependencies 220 | - Removed MongoDB dependency (now uses JSON file storage) 221 | - Modular architecture (was 9900+ lines in one file) 222 | - Runs as standalone process instead of Telegram bot 223 | - State is stored in `data/state.json` instead of MongoDB 224 | 225 | **Note**: If you have existing MongoDB data, you'll need to export it and convert to the JSON format. See `data/state.json.example` for the structure. 226 | 227 | ## 📝 License 228 | 229 | MIT License - Use at your own risk. 230 | 231 | ## 🤝 Contributing 232 | 233 | Contributions are welcome! Please ensure your code follows the existing modular structure. 234 | 235 | --- 236 | 237 | **⚠️ Disclaimer**: This bot is for educational purposes. Use at your own risk. The developers are not responsible for any financial losses. 238 | 239 | ## 📞 Contact 240 | 241 | For support or questions, reach out on Telegram: [@trum3it](https://t.me/trum3it) 242 | 243 | **⭐ Star**: this repository if you find it useful! 244 | -------------------------------------------------------------------------------- /src/sniper/index.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from '../xrpl/client'; 2 | import { getWallet, getBalance, getTokenBalances } from '../xrpl/wallet'; 3 | import { executeAMMBuy } from '../xrpl/amm'; 4 | import { IUser } from '../database/models'; 5 | import { User, UserModel } from '../database/user'; 6 | import { detectNewTokensFromAMM } from './monitor'; 7 | import { evaluateToken, isTokenBlacklisted } from './evaluator'; 8 | import { TokenInfo } from '../types'; 9 | import config from '../config'; 10 | 11 | let sniperInterval: NodeJS.Timeout | null = null; 12 | let isRunning: boolean = false; 13 | 14 | interface Result { 15 | success: boolean; 16 | error?: string; 17 | } 18 | 19 | export async function startSniper(userId: string): Promise { 20 | if (isRunning) { 21 | return { success: false, error: 'Sniper is already running' }; 22 | } 23 | 24 | try { 25 | const user = await User.findOne({ userId }); 26 | 27 | if (!user) { 28 | return { success: false, error: 'User not found' }; 29 | } 30 | 31 | if (user.sniperActive && !isRunning) { 32 | user.sniperActive = false; 33 | const userModel = new UserModel(user); 34 | await userModel.save(); 35 | } 36 | 37 | if (!config.sniperUser.buyMode && (!user.whiteListedTokens || user.whiteListedTokens.length === 0)) { 38 | return { success: false, error: 'No whitelisted tokens for whitelist-only mode' }; 39 | } 40 | 41 | const snipeAmount = parseFloat( 42 | config.sniperUser.snipeAmount === 'custom' 43 | ? (config.sniperUser.customSnipeAmount || '1') 44 | : (config.sniperUser.snipeAmount || '1') 45 | ) || 1; 46 | 47 | if (snipeAmount > config.trading.maxSnipeAmount) { 48 | return { 49 | success: false, 50 | error: `Snipe amount too high. Maximum: ${config.trading.maxSnipeAmount} XRP` 51 | }; 52 | } 53 | 54 | if (!user.sniperPurchases) { 55 | user.sniperPurchases = []; 56 | } 57 | 58 | const client = await getClient(); 59 | const wallet = getWallet(); 60 | const xrpBalance = await getBalance(client, wallet.address); 61 | const tokenBalances = await getTokenBalances(client, wallet.address); 62 | 63 | console.log('Sniper Account Info:'); 64 | console.log(` Wallet: ${wallet.address}`); 65 | console.log(` XRP Balance: ${xrpBalance.toFixed(6)} XRP`); 66 | console.log(` Token Holdings: ${tokenBalances.length}`); 67 | console.log(` Snipe Amount: ${snipeAmount} XRP`); 68 | console.log(` Buy Mode: ${config.sniperUser.buyMode ? 'Auto-buy' : 'Whitelist-only'}`); 69 | console.log(` Min Liquidity: ${config.sniperUser.minimumPoolLiquidity} XRP`); 70 | console.log(` Risk Score: ${config.sniperUser.riskScore}`); 71 | 72 | user.sniperActive = true; 73 | user.sniperStartTime = new Date(); 74 | const userModel = new UserModel(user); 75 | await userModel.save(); 76 | 77 | isRunning = true; 78 | sniperInterval = setInterval(async () => { 79 | await monitorTokenMarkets(userId); 80 | }, config.sniper.checkInterval); 81 | 82 | return { success: true }; 83 | } catch (error) { 84 | console.error('Error starting sniper:', error); 85 | return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; 86 | } 87 | } 88 | 89 | export async function stopSniper(userId: string): Promise { 90 | try { 91 | if (sniperInterval) { 92 | clearInterval(sniperInterval); 93 | sniperInterval = null; 94 | } 95 | 96 | const user = await User.findOne({ userId }); 97 | if (user) { 98 | user.sniperActive = false; 99 | const userModel = new UserModel(user); 100 | await userModel.save(); 101 | } 102 | 103 | isRunning = false; 104 | return { success: true }; 105 | } catch (error) { 106 | console.error('Error stopping sniper:', error); 107 | return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; 108 | } 109 | } 110 | 111 | async function monitorTokenMarkets(userId: string): Promise { 112 | try { 113 | const user = await User.findOne({ userId }); 114 | if (!user || !user.sniperActive) { 115 | if (sniperInterval) { 116 | clearInterval(sniperInterval); 117 | sniperInterval = null; 118 | } 119 | isRunning = false; 120 | return; 121 | } 122 | 123 | const client = await getClient(); 124 | const newTokens = await detectNewTokensFromAMM(client); 125 | 126 | for (let i = 0; i < Math.min(newTokens.length, config.sniper.maxTokensPerScan); i++) { 127 | const tokenInfo = newTokens[i]; 128 | await evaluateAndSnipeToken(client, user, tokenInfo); 129 | } 130 | } catch (error) { 131 | console.error('Monitor error:', error instanceof Error ? error.message : 'Unknown error'); 132 | } 133 | } 134 | 135 | async function evaluateAndSnipeToken(client: any, user: IUser, tokenInfo: TokenInfo): Promise { 136 | try { 137 | const evaluation = await evaluateToken(client, user, tokenInfo); 138 | 139 | if (!evaluation.shouldSnipe) { 140 | return; 141 | } 142 | 143 | await executeSnipe(client, user, tokenInfo); 144 | } catch (error) { 145 | console.error(`Error evaluating token:`, error instanceof Error ? error.message : 'Unknown error'); 146 | } 147 | } 148 | 149 | async function executeSnipe(client: any, user: IUser, tokenInfo: TokenInfo): Promise { 150 | try { 151 | const wallet = getWallet(); 152 | let snipeAmount: number; 153 | if (config.sniperUser.snipeAmount === 'custom') { 154 | if (!config.sniperUser.customSnipeAmount || isNaN(parseFloat(config.sniperUser.customSnipeAmount))) { 155 | console.error('Invalid custom snipe amount'); 156 | return; 157 | } 158 | snipeAmount = parseFloat(config.sniperUser.customSnipeAmount); 159 | } else { 160 | snipeAmount = parseFloat(config.sniperUser.snipeAmount || '1') || 1; 161 | } 162 | 163 | if (isNaN(snipeAmount) || snipeAmount <= 0) { 164 | console.error('Invalid snipe amount'); 165 | return; 166 | } 167 | 168 | if (snipeAmount > config.trading.maxSnipeAmount) { 169 | console.error(`Snipe amount exceeds maximum: ${snipeAmount} > ${config.trading.maxSnipeAmount}`); 170 | return; 171 | } 172 | 173 | const accountInfo = await client.request({ 174 | command: 'account_info', 175 | account: wallet.address 176 | }); 177 | 178 | const xrpBalance = parseFloat((accountInfo.result as any).account_data.Balance) / 1000000; 179 | const totalRequired = snipeAmount + 0.5; 180 | 181 | if (xrpBalance < totalRequired) { 182 | console.error(`Insufficient balance: ${xrpBalance} XRP < ${totalRequired} XRP required`); 183 | return; 184 | } 185 | 186 | if (isTokenBlacklisted(user.blackListedTokens, tokenInfo.currency, tokenInfo.issuer)) { 187 | return; 188 | } 189 | 190 | const buyResult = await executeAMMBuy( 191 | client, 192 | wallet, 193 | tokenInfo, 194 | snipeAmount, 195 | config.trading.defaultSlippage 196 | ); 197 | 198 | if (buyResult.success && buyResult.txHash) { 199 | if (!user.sniperPurchases) { 200 | user.sniperPurchases = []; 201 | } 202 | 203 | user.sniperPurchases.push({ 204 | tokenSymbol: tokenInfo.readableCurrency || tokenInfo.currency, 205 | tokenAddress: tokenInfo.issuer, 206 | currency: tokenInfo.currency, 207 | issuer: tokenInfo.issuer, 208 | amount: snipeAmount, 209 | tokensReceived: typeof buyResult.tokensReceived === 'number' ? buyResult.tokensReceived : parseFloat(String(buyResult.tokensReceived || 0)), 210 | timestamp: new Date(), 211 | txHash: buyResult.txHash, 212 | status: 'active' 213 | }); 214 | 215 | user.transactions.push({ 216 | type: 'snipe_buy', 217 | ourTxHash: buyResult.txHash, 218 | amount: snipeAmount, 219 | tokenSymbol: tokenInfo.readableCurrency || tokenInfo.currency, 220 | tokenAddress: tokenInfo.issuer, 221 | timestamp: new Date(), 222 | status: 'success', 223 | tokensReceived: typeof buyResult.tokensReceived === 'number' ? buyResult.tokensReceived : parseFloat(String(buyResult.tokensReceived || 0)), 224 | xrpSpent: snipeAmount, 225 | actualRate: buyResult.actualRate || '0' 226 | }); 227 | 228 | const userModel = new UserModel(user); 229 | await userModel.save(); 230 | } else { 231 | console.error(`Snipe failed: ${buyResult.error || 'Unknown error'}`); 232 | } 233 | } catch (error) { 234 | console.error('Error executing snipe:', error); 235 | } 236 | } 237 | 238 | export function isRunningSniper(): boolean { 239 | return isRunning; 240 | } 241 | 242 | -------------------------------------------------------------------------------- /src/copyTrading/monitor.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'xrpl'; 2 | import { getTransactionTime, hexToString } from '../xrpl/utils'; 3 | import { TradeInfo, CopyTradeData } from '../types'; 4 | 5 | const processedTransactions = new Set(); 6 | 7 | export async function checkTraderTransactions( 8 | client: Client, 9 | traderAddress: string, 10 | startTime: Date | null = null 11 | ): Promise { 12 | try { 13 | const response = await client.request({ 14 | command: 'account_tx', 15 | account: traderAddress, 16 | limit: 20, 17 | ledger_index_min: -1, 18 | ledger_index_max: -1, 19 | forward: false 20 | }); 21 | 22 | const transactions = (response.result as any)?.transactions || []; 23 | const now = new Date(); 24 | const oneMinuteAgo = new Date(now.getTime() - 1 * 60 * 1000); 25 | 26 | const newTrades: CopyTradeData[] = []; 27 | 28 | for (const txData of transactions) { 29 | const tx = txData?.tx || txData?.tx_json || txData; 30 | const meta = txData?.meta; 31 | 32 | const txHash = tx?.hash || txData?.hash; 33 | if (!txHash) { 34 | continue; 35 | } 36 | 37 | if (processedTransactions.has(txHash)) { 38 | continue; 39 | } 40 | 41 | if (meta?.TransactionResult !== 'tesSUCCESS') { 42 | continue; 43 | } 44 | 45 | const txTime = getTransactionTime(txData); 46 | if (txTime && txTime < oneMinuteAgo) { 47 | continue; 48 | } 49 | 50 | if (startTime && txTime && txTime < startTime) { 51 | continue; 52 | } 53 | 54 | const tradeInfo = detectTradingActivity(tx, meta, traderAddress); 55 | if (tradeInfo) { 56 | processedTransactions.add(txHash); 57 | newTrades.push({ 58 | txHash, 59 | tx, 60 | meta, 61 | tradeInfo 62 | }); 63 | } 64 | } 65 | 66 | if (processedTransactions.size > 1000) { 67 | const oldEntries = Array.from(processedTransactions).slice(0, 500); 68 | oldEntries.forEach(entry => processedTransactions.delete(entry)); 69 | } 70 | 71 | return newTrades; 72 | } catch (error) { 73 | console.error(`Error checking transactions for ${traderAddress}:`, error instanceof Error ? error.message : 'Unknown error'); 74 | return []; 75 | } 76 | } 77 | 78 | function detectTradingActivity(tx: any, meta: any, traderAddress: string): TradeInfo | null { 79 | try { 80 | if (!tx || !meta || tx.Account !== traderAddress) return null; 81 | 82 | if (tx.TransactionType === 'Payment') { 83 | return parsePaymentTransaction(tx, meta); 84 | } 85 | 86 | return parseConsumedOffers(tx, meta, traderAddress); 87 | } catch (error) { 88 | console.error('Error detecting trading activity:', error); 89 | return null; 90 | } 91 | } 92 | 93 | function parsePaymentTransaction(_tx: any, meta: any): TradeInfo | null { 94 | try { 95 | if (!meta.AffectedNodes) return null; 96 | 97 | let xrpAmount = 0; 98 | let tokenAmount = 0; 99 | let currency: string | null = null; 100 | let issuer: string | null = null; 101 | let tradeType: 'buy' | 'sell' | null = null; 102 | 103 | for (const node of meta.AffectedNodes) { 104 | const modifiedNode = node.ModifiedNode; 105 | if (modifiedNode && modifiedNode.LedgerEntryType === 'AMM') { 106 | const prevFields = modifiedNode.PreviousFields; 107 | const finalFields = modifiedNode.FinalFields; 108 | 109 | if (prevFields && finalFields) { 110 | if (prevFields.amount && finalFields.amount) { 111 | const prevAmount = typeof prevFields.amount === 'string' 112 | ? parseInt(prevFields.amount) / 1000000 113 | : parseFloat(prevFields.amount); 114 | const finalAmount = typeof finalFields.amount === 'string' 115 | ? parseInt(finalFields.amount) / 1000000 116 | : parseFloat(finalFields.amount); 117 | 118 | const diff = finalAmount - prevAmount; 119 | if (Math.abs(diff) > 0.000001) { 120 | if (diff > 0) { 121 | xrpAmount = Math.abs(diff); 122 | tradeType = 'sell'; 123 | } else { 124 | xrpAmount = Math.abs(diff); 125 | tradeType = 'buy'; 126 | } 127 | } 128 | } 129 | 130 | if (prevFields.amount2 && finalFields.amount2) { 131 | const prevAmount2 = prevFields.amount2.value || prevFields.amount2; 132 | const finalAmount2 = finalFields.amount2.value || finalFields.amount2; 133 | 134 | const prevToken = typeof prevAmount2 === 'string' ? parseFloat(prevAmount2) : prevAmount2; 135 | const finalToken = typeof finalAmount2 === 'string' ? parseFloat(finalAmount2) : finalAmount2; 136 | 137 | const diff = finalToken - prevToken; 138 | if (Math.abs(diff) > 0.000001) { 139 | tokenAmount = Math.abs(diff); 140 | if (finalFields.amount2.currency) { 141 | currency = finalFields.amount2.currency; 142 | issuer = finalFields.amount2.issuer; 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | if (xrpAmount > 0 && tokenAmount > 0 && currency && issuer && tradeType) { 151 | return { 152 | type: tradeType, 153 | currency: currency, 154 | issuer: issuer, 155 | readableCurrency: currency.length === 40 ? hexToString(currency) : currency, 156 | xrpAmount: xrpAmount, 157 | tokenAmount: tokenAmount, 158 | method: 'AMM' 159 | }; 160 | } 161 | 162 | return null; 163 | } catch (error) { 164 | console.error('Error parsing payment transaction:', error); 165 | return null; 166 | } 167 | } 168 | 169 | function parseConsumedOffers(_tx: any, meta: any, _traderAddress: string): TradeInfo | null { 170 | try { 171 | if (!meta.AffectedNodes) return null; 172 | 173 | let totalXRP = 0; 174 | let totalTokens = 0; 175 | let currency: string | null = null; 176 | let issuer: string | null = null; 177 | let tradeType: 'buy' | 'sell' | null = null; 178 | 179 | for (const node of meta.AffectedNodes) { 180 | const deletedNode = node.DeletedNode; 181 | const modifiedNode = node.ModifiedNode; 182 | 183 | if (deletedNode && deletedNode.LedgerEntryType === 'Offer') { 184 | const offer = deletedNode.FinalFields || deletedNode.PreviousFields; 185 | if (offer) { 186 | const analysis = analyzeOffer(offer); 187 | if (analysis.xrp && analysis.tokens && analysis.curr) { 188 | totalXRP += analysis.xrp; 189 | totalTokens += analysis.tokens; 190 | currency = analysis.curr; 191 | issuer = analysis.iss; 192 | tradeType = analysis.type; 193 | } 194 | } 195 | } 196 | 197 | if (modifiedNode && modifiedNode.LedgerEntryType === 'Offer') { 198 | const prevFields = modifiedNode.PreviousFields; 199 | const finalFields = modifiedNode.FinalFields; 200 | if (prevFields && finalFields) { 201 | const consumedXRP = calculateConsumedXRP(prevFields, finalFields); 202 | const consumedTokens = calculateConsumedTokens(prevFields, finalFields); 203 | if (consumedXRP && consumedTokens) { 204 | totalXRP += consumedXRP.amount; 205 | totalTokens += consumedTokens.amount; 206 | currency = consumedTokens.currency; 207 | issuer = consumedTokens.issuer; 208 | tradeType = consumedXRP.type; 209 | } 210 | } 211 | } 212 | } 213 | 214 | if (totalXRP > 0 && totalTokens > 0 && currency && issuer && tradeType) { 215 | return { 216 | type: tradeType, 217 | currency: currency, 218 | issuer: issuer, 219 | readableCurrency: currency.length === 40 ? hexToString(currency) : currency, 220 | xrpAmount: totalXRP, 221 | tokenAmount: totalTokens, 222 | method: 'DEX' 223 | }; 224 | } 225 | 226 | return null; 227 | } catch (error) { 228 | console.error('Error parsing consumed offers:', error); 229 | return null; 230 | } 231 | } 232 | 233 | function analyzeOffer(offer: any): { xrp: number; tokens: number; curr: string | null; iss: string | null; type: 'buy' | 'sell' | null } { 234 | try { 235 | const takerGets = offer.TakerGets; 236 | const takerPays = offer.TakerPays; 237 | 238 | let xrp = 0; 239 | let tokens = 0; 240 | let curr: string | null = null; 241 | let iss: string | null = null; 242 | let type: 'buy' | 'sell' | null = null; 243 | 244 | if (typeof takerGets === 'string') { 245 | xrp = parseInt(takerGets) / 1000000; 246 | if (takerPays && typeof takerPays === 'object') { 247 | tokens = parseFloat(takerPays.value || takerPays); 248 | curr = takerPays.currency; 249 | iss = takerPays.issuer; 250 | type = 'sell'; 251 | } 252 | } 253 | else if (typeof takerPays === 'string') { 254 | xrp = parseInt(takerPays) / 1000000; 255 | if (takerGets && typeof takerGets === 'object') { 256 | tokens = parseFloat(takerGets.value || takerGets); 257 | curr = takerGets.currency; 258 | iss = takerGets.issuer; 259 | type = 'buy'; 260 | } 261 | } 262 | 263 | return { xrp, tokens, curr, iss, type }; 264 | } catch (error) { 265 | return { xrp: 0, tokens: 0, curr: null, iss: null, type: null }; 266 | } 267 | } 268 | 269 | function calculateConsumedXRP(prevFields: any, finalFields: any): { amount: number; type: 'buy' | 'sell' } | null { 270 | try { 271 | const prevTakerGets = prevFields.TakerGets; 272 | const finalTakerGets = finalFields.TakerGets; 273 | const prevTakerPays = prevFields.TakerPays; 274 | const finalTakerPays = finalFields.TakerPays; 275 | 276 | let consumed = 0; 277 | let type: 'buy' | 'sell' | null = null; 278 | 279 | if (typeof prevTakerGets === 'string' && typeof finalTakerGets === 'string') { 280 | const prev = parseInt(prevTakerGets) / 1000000; 281 | const final = parseInt(finalTakerGets) / 1000000; 282 | consumed = prev - final; 283 | type = 'sell'; 284 | } 285 | else if (typeof prevTakerPays === 'string' && typeof finalTakerPays === 'string') { 286 | const prev = parseInt(prevTakerPays) / 1000000; 287 | const final = parseInt(finalTakerPays) / 1000000; 288 | consumed = prev - final; 289 | type = 'buy'; 290 | } 291 | 292 | return consumed > 0 && type ? { amount: consumed, type } : null; 293 | } catch (error) { 294 | return null; 295 | } 296 | } 297 | 298 | function calculateConsumedTokens(prevFields: any, finalFields: any): { amount: number; currency: string; issuer: string } | null { 299 | try { 300 | const prevTakerGets = prevFields.TakerGets; 301 | const finalTakerGets = finalFields.TakerGets; 302 | const prevTakerPays = prevFields.TakerPays; 303 | const finalTakerPays = finalFields.TakerPays; 304 | 305 | let consumed = 0; 306 | let currency: string | null = null; 307 | let issuer: string | null = null; 308 | 309 | if (prevTakerGets && typeof prevTakerGets === 'object' && 310 | finalTakerGets && typeof finalTakerGets === 'object') { 311 | const prev = parseFloat(prevTakerGets.value || prevTakerGets); 312 | const final = parseFloat(finalTakerGets.value || finalTakerGets); 313 | consumed = prev - final; 314 | currency = finalTakerGets.currency || prevTakerGets.currency; 315 | issuer = finalTakerGets.issuer || prevTakerGets.issuer; 316 | } 317 | else if (prevTakerPays && typeof prevTakerPays === 'object' && 318 | finalTakerPays && typeof finalTakerPays === 'object') { 319 | const prev = parseFloat(prevTakerPays.value || prevTakerPays); 320 | const final = parseFloat(finalTakerPays.value || finalTakerPays); 321 | consumed = prev - final; 322 | currency = finalTakerPays.currency || prevTakerPays.currency; 323 | issuer = finalTakerPays.issuer || prevTakerPays.issuer; 324 | } 325 | 326 | return consumed > 0 && currency && issuer ? { amount: consumed, currency, issuer } : null; 327 | } catch (error) { 328 | return null; 329 | } 330 | } 331 | 332 | -------------------------------------------------------------------------------- /src/xrpl/amm.ts: -------------------------------------------------------------------------------- 1 | import { Client, Wallet, xrpToDrops } from 'xrpl'; 2 | import { TokenInfo, TradeResult, LPBurnStatus } from '../types'; 3 | import { getReadableCurrency, formatTokenAmountSimple } from './utils'; 4 | 5 | /** 6 | * Execute AMM buy transaction 7 | */ 8 | export async function executeAMMBuy( 9 | client: Client, 10 | wallet: Wallet, 11 | tokenInfo: TokenInfo, 12 | xrpAmount: number, 13 | slippage: number = 4.0 14 | ): Promise { 15 | try { 16 | // Check if trust line exists 17 | let hasTrustLine = false; 18 | let currentTokenBalance = 0; 19 | 20 | try { 21 | const accountLines = await client.request({ 22 | command: 'account_lines', 23 | account: wallet.address, 24 | ledger_index: 'validated' 25 | }); 26 | 27 | const existingLine = (accountLines.result as any).lines.find((line: any) => 28 | line.currency === tokenInfo.currency && line.account === tokenInfo.issuer 29 | ); 30 | 31 | if (existingLine) { 32 | hasTrustLine = true; 33 | currentTokenBalance = parseFloat(existingLine.balance); 34 | } 35 | } catch (error) { 36 | // Account not activated or no trust lines, will create trust line 37 | } 38 | 39 | // Create trust line if needed 40 | if (!hasTrustLine) { 41 | const trustSetTx = { 42 | TransactionType: 'TrustSet' as const, 43 | Account: wallet.address, 44 | LimitAmount: { 45 | currency: tokenInfo.currency, 46 | issuer: tokenInfo.issuer, 47 | value: '100000' 48 | } 49 | }; 50 | 51 | const trustPrepared = await client.autofill(trustSetTx); 52 | const trustSigned = wallet.sign(trustPrepared); 53 | const trustResult = await client.submitAndWait(trustSigned.tx_blob); 54 | 55 | if ((trustResult.result.meta as any).TransactionResult !== 'tesSUCCESS') { 56 | return { 57 | success: false, 58 | error: `Failed to create trust line: ${(trustResult.result.meta as any).TransactionResult}` 59 | }; 60 | } 61 | 62 | await new Promise(resolve => setTimeout(resolve, 2000)); 63 | } 64 | 65 | // Get AMM pool info 66 | const ammInfo = await client.request({ 67 | command: 'amm_info', 68 | asset: { currency: 'XRP' }, 69 | asset2: { currency: tokenInfo.currency, issuer: tokenInfo.issuer } 70 | }); 71 | 72 | if (!ammInfo.result || !(ammInfo.result as any).amm) { 73 | return { 74 | success: false, 75 | error: 'AMM pool not found for this token pair' 76 | }; 77 | } 78 | 79 | const amm = (ammInfo.result as any).amm; 80 | const xrpAmountDrops = parseFloat(amm.amount); 81 | const tokenAmount = parseFloat(amm.amount2.value); 82 | const currentRate = tokenAmount / (xrpAmountDrops / 1000000); 83 | const estimatedTokens = xrpAmount * currentRate; 84 | const slippageMultiplier = (100 - slippage) / 100; 85 | const minTokensExpected = estimatedTokens * slippageMultiplier; 86 | const formattedMinTokens = formatTokenAmountSimple(minTokensExpected); 87 | 88 | // Execute buy transaction 89 | const paymentTx = { 90 | TransactionType: 'Payment' as const, 91 | Account: wallet.address, 92 | Destination: wallet.address, 93 | Amount: { 94 | currency: tokenInfo.currency, 95 | issuer: tokenInfo.issuer, 96 | value: formattedMinTokens 97 | }, 98 | SendMax: xrpToDrops(xrpAmount.toString()) 99 | }; 100 | 101 | const prepared = await client.autofill(paymentTx); 102 | const signed = wallet.sign(prepared); 103 | const result = await client.submitAndWait(signed.tx_blob); 104 | 105 | if ((result.result.meta as any).TransactionResult === 'tesSUCCESS') { 106 | await new Promise(resolve => setTimeout(resolve, 2000)); 107 | 108 | // Get final balance 109 | const finalBalance = await client.request({ 110 | command: 'account_lines', 111 | account: wallet.address, 112 | ledger_index: 'validated' 113 | }); 114 | 115 | const tokenLine = (finalBalance.result as any).lines.find((line: any) => 116 | line.currency === tokenInfo.currency && line.account === tokenInfo.issuer 117 | ); 118 | 119 | const tokensReceived = tokenLine ? (parseFloat(tokenLine.balance) - currentTokenBalance) : 0; 120 | const actualRate = tokensReceived > 0 ? (tokensReceived / xrpAmount) : 0; 121 | const actualSlippage = ((1 - (actualRate / currentRate)) * 100).toFixed(2); 122 | 123 | return { 124 | success: true, 125 | txHash: result.result.hash, 126 | tokensReceived: tokensReceived, 127 | xrpSpent: xrpAmount, 128 | actualRate: actualRate.toFixed(8), 129 | expectedTokens: estimatedTokens.toFixed(6), 130 | actualSlippage: actualSlippage, 131 | slippageUsed: slippage, 132 | method: 'AMM' 133 | }; 134 | } else { 135 | return { 136 | success: false, 137 | error: (result.result.meta as any).TransactionResult 138 | }; 139 | } 140 | } catch (error) { 141 | return { 142 | success: false, 143 | error: error instanceof Error ? error.message : 'Unknown error' 144 | }; 145 | } 146 | } 147 | 148 | /** 149 | * Execute AMM sell transaction 150 | */ 151 | export async function executeAMMSell( 152 | client: Client, 153 | wallet: Wallet, 154 | tokenInfo: TokenInfo, 155 | tokenAmount: number, 156 | slippage: number = 4.0 157 | ): Promise { 158 | try { 159 | // Check token balance 160 | let currentTokenBalance = 0; 161 | 162 | const accountLines = await client.request({ 163 | command: 'account_lines', 164 | account: wallet.address, 165 | ledger_index: 'validated' 166 | }); 167 | 168 | const existingLine = (accountLines.result as any).lines.find((line: any) => 169 | line.currency === tokenInfo.currency && line.account === tokenInfo.issuer 170 | ); 171 | 172 | if (!existingLine) { 173 | return { 174 | success: false, 175 | error: `No trust line found for ${getReadableCurrency(tokenInfo.currency)}. Cannot sell tokens you don't have.` 176 | }; 177 | } 178 | 179 | currentTokenBalance = parseFloat(existingLine.balance); 180 | 181 | if (currentTokenBalance < tokenAmount) { 182 | return { 183 | success: false, 184 | error: `Insufficient token balance. You have ${currentTokenBalance} ${getReadableCurrency(tokenInfo.currency)} but trying to sell ${tokenAmount}` 185 | }; 186 | } 187 | 188 | // Get AMM pool info 189 | const ammInfo = await client.request({ 190 | command: 'amm_info', 191 | asset: { currency: 'XRP' }, 192 | asset2: { currency: tokenInfo.currency, issuer: tokenInfo.issuer } 193 | }); 194 | 195 | if (!ammInfo.result || !(ammInfo.result as any).amm) { 196 | return { 197 | success: false, 198 | error: `No AMM pool found for ${getReadableCurrency(tokenInfo.currency)}. Cannot sell via AMM.` 199 | }; 200 | } 201 | 202 | const amm = (ammInfo.result as any).amm; 203 | const xrpAmountDrops = parseFloat(amm.amount); 204 | const tokenAmountInPool = parseFloat(amm.amount2.value); 205 | const currentRate = (xrpAmountDrops / 1000000) / tokenAmountInPool; 206 | const estimatedXrp = tokenAmount * currentRate; 207 | const slippageMultiplier = (100 - slippage) / 100; 208 | const minXrpExpected = estimatedXrp * slippageMultiplier; 209 | const formattedMinXrp = parseFloat((minXrpExpected).toFixed(6)); 210 | const formattedTokenAmount = formatTokenAmountSimple(tokenAmount); 211 | 212 | // Execute sell transaction 213 | const paymentTx = { 214 | TransactionType: 'Payment' as const, 215 | Account: wallet.address, 216 | Destination: wallet.address, 217 | Amount: xrpToDrops(formattedMinXrp.toString()), 218 | SendMax: { 219 | currency: tokenInfo.currency, 220 | issuer: tokenInfo.issuer, 221 | value: formattedTokenAmount 222 | }, 223 | DeliverMin: xrpToDrops(formattedMinXrp.toString()), 224 | Flags: 0x00020000 225 | }; 226 | 227 | const paymentPrepared = await client.autofill(paymentTx); 228 | const paymentSigned = wallet.sign(paymentPrepared); 229 | const paymentResult = await client.submitAndWait(paymentSigned.tx_blob); 230 | 231 | if ((paymentResult.result.meta as any).TransactionResult === 'tesSUCCESS') { 232 | await new Promise(resolve => setTimeout(resolve, 2000)); 233 | 234 | // Get final balance 235 | const finalTokenBalance = await client.request({ 236 | command: 'account_lines', 237 | account: wallet.address, 238 | ledger_index: 'validated' 239 | }); 240 | 241 | const tokenLine = (finalTokenBalance.result as any).lines.find((line: any) => 242 | line.currency === tokenInfo.currency && line.account === tokenInfo.issuer 243 | ); 244 | 245 | const remainingTokenBalance = tokenLine ? parseFloat(tokenLine.balance) : 0; 246 | const tokensSold = currentTokenBalance - remainingTokenBalance; 247 | const estimatedXrpReceived = tokensSold * currentRate; 248 | const actualRate = estimatedXrpReceived / tokensSold; 249 | const actualSlippage = ((1 - (actualRate / currentRate)) * 100).toFixed(2); 250 | 251 | return { 252 | success: true, 253 | txHash: paymentResult.result.hash, 254 | tokensSold: tokensSold.toString(), 255 | xrpReceived: estimatedXrpReceived.toFixed(6), 256 | expectedXrp: estimatedXrp.toFixed(6), 257 | actualRate: actualRate.toFixed(8), 258 | marketRate: currentRate.toFixed(8), 259 | actualSlippage: actualSlippage, 260 | slippageUsed: slippage, 261 | newTokenBalance: remainingTokenBalance.toString() 262 | }; 263 | } else { 264 | return { 265 | success: false, 266 | error: `AMM transaction failed: ${(paymentResult.result.meta as any).TransactionResult}` 267 | }; 268 | } 269 | } catch (error) { 270 | return { 271 | success: false, 272 | error: error instanceof Error ? error.message : 'Failed to execute AMM sell transaction' 273 | }; 274 | } 275 | } 276 | 277 | /** 278 | * Get AMM pool information 279 | */ 280 | export async function getAMMInfo(client: Client, tokenInfo: TokenInfo): Promise { 281 | try { 282 | const ammInfo = await client.request({ 283 | command: 'amm_info', 284 | asset: { currency: 'XRP' }, 285 | asset2: { currency: tokenInfo.currency, issuer: tokenInfo.issuer } 286 | }); 287 | 288 | if (!ammInfo.result || !(ammInfo.result as any).amm) { 289 | return null; 290 | } 291 | 292 | return (ammInfo.result as any).amm; 293 | } catch (error) { 294 | return null; 295 | } 296 | } 297 | 298 | /** 299 | * Check LP burn status 300 | */ 301 | export async function checkLPBurnStatus(client: Client, tokenInfo: TokenInfo): Promise { 302 | try { 303 | const ammInfo = await getAMMInfo(client, tokenInfo); 304 | if (!ammInfo) { 305 | return { 306 | lpBurned: false, 307 | lpBalance: 'Unknown', 308 | error: 'AMM pool not found' 309 | }; 310 | } 311 | 312 | const ammAccount = ammInfo.amm_account; 313 | 314 | const accountLines = await client.request({ 315 | command: 'account_lines', 316 | account: ammAccount, 317 | ledger_index: 'validated' 318 | }); 319 | 320 | if (!(accountLines.result as any) || !(accountLines.result as any).lines) { 321 | return { 322 | lpBurned: true, 323 | lpBalance: '0', 324 | ammAccount: ammAccount 325 | }; 326 | } 327 | 328 | const lpTokenLine = (accountLines.result as any).lines.find((line: any) => 329 | line.account === ammAccount && 330 | line.currency && 331 | line.currency.length === 40 332 | ); 333 | 334 | if (!lpTokenLine) { 335 | return { 336 | lpBurned: true, 337 | lpBalance: '0', 338 | ammAccount: ammAccount 339 | }; 340 | } 341 | 342 | const lpBalance = parseFloat(lpTokenLine.balance); 343 | const lpBurned = lpBalance < 1; 344 | 345 | return { 346 | lpBurned: lpBurned, 347 | lpBalance: lpBalance.toString(), 348 | ammAccount: ammAccount, 349 | lpTokenCurrency: lpTokenLine.currency 350 | }; 351 | } catch (error) { 352 | return { 353 | lpBurned: false, 354 | lpBalance: 'Error', 355 | error: error instanceof Error ? error.message : 'Unknown error' 356 | }; 357 | } 358 | } 359 | 360 | --------------------------------------------------------------------------------