Browse Source

Initial commit: Byreal sniper bot with modular ES module architecture

Features:
- Automated LP position sniping for Byreal DEX
- Jupiter integration for auto-token swaps
- Modular project structure with config, core, services, utils, commands
- Position caching and state management
- CLI commands: start, status, clear
- ES Module support with proper imports/exports
zhangchunrui 1 tháng trước cách đây
commit
a9c5ec37be
15 tập tin đã thay đổi với 1197 bổ sung0 xóa
  1. 32 0
      .env.example
  2. 30 0
      .gitignore
  3. 244 0
      README.md
  4. 62 0
      index.js
  5. 72 0
      src/commands/index.js
  6. 58 0
      src/config/index.js
  7. 1 0
      src/core/index.js
  8. 251 0
      src/core/sniper.js
  9. 125 0
      src/services/byreal.js
  10. 2 0
      src/services/index.js
  11. 166 0
      src/services/jupiter.js
  12. 85 0
      src/utils/cache.js
  13. 19 0
      src/utils/helpers.js
  14. 3 0
      src/utils/index.js
  15. 47 0
      src/utils/logger.js

+ 32 - 0
.env.example

@@ -0,0 +1,32 @@
+# Byreal Sniper Configuration
+
+# Solana RPC URL
+RPC_URL=https://mainnet.helius-rpc.com/?api-key=20f2bda7-11af-4e71-a3c3-a8fd6567df80
+
+# Your wallet private key (Base58 encoded)
+PRIVATE_KEY=your_private_key_here
+
+# Your wallet address (optional, will be derived from private key)
+MY_WALLET=
+
+# Data directory for cache files
+DATA_DIR=./data
+
+# Copy multiplier (1.5 = copy with 1.5x the target's position size)
+COPY_MULTIPLIER=1.5
+
+# Position value limits (USD)
+MAX_USD_VALUE=10
+MIN_USD_VALUE=0.1
+
+# Slippage for Jupiter swaps (in basis points, 100 = 1%)
+SLIPPAGE_BPS=100
+
+# Polling interval in milliseconds (10000 = 10 seconds)
+POLL_INTERVAL_MS=10000
+
+# Authentication header for Byreal API
+AUTH_HEADER=Basic YWRtaW46YzU4ODk5Njc=
+
+# Log level (debug, info, warn, error)
+LOG_LEVEL=info

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Dependencies
+node_modules/
+package-lock.json
+yarn.lock
+
+# Environment variables
+.env
+
+# Data and cache files
+data/
+*.json
+
+# Logs
+logs/
+*.log
+npm-debug.log*
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Build outputs
+dist/
+build/

+ 244 - 0
README.md

@@ -0,0 +1,244 @@
+# Byreal Sniper Bot 🎯
+
+一个自动狙击 Byreal DEX 上目标地址 LP 仓位的机器人。当目标地址开仓时,自动以更快速度和设定倍数跟单;当目标地址平仓时,自动跟随平仓。
+
+## 功能特性
+
+- 🎯 **自动狙击**: 监控目标钱包地址的所有 LP 仓位变动
+- 📈 **倍数跟单**: 可配置复制倍数(例如 1.5x 目标仓位)
+- 🔄 **自动平衡**: 余额不足时通过 Jupiter 自动兑换所需代币
+- 💰 **风险控制**: 设置最大/最小仓位金额限制
+- 🚪 **自动平仓**: 目标平仓时自动跟随关闭对应仓位
+- 💾 **状态持久化**: 自动保存已复制和已关闭的仓位,防止重复操作
+- ⚡ **高速轮询**: 可配置的轮询间隔(默认 10 秒)
+- 🧩 **模块化架构**: 易于扩展和维护
+
+## 项目结构
+
+```
+byreal-sniper/
+├── index.js                 # 入口文件
+├── package.json
+├── .env.example
+├── .gitignore
+├── README.md
+├── src/
+│   ├── config/             # 配置管理
+│   │   └── index.js
+│   ├── core/               # 核心逻辑
+│   │   ├── sniper.js       # 狙击引擎
+│   │   └── index.js
+│   ├── services/           # 外部服务
+│   │   ├── jupiter.js      # Jupiter 兑换服务
+│   │   ├── byreal.js       # Byreal API 服务
+│   │   └── index.js
+│   ├── utils/              # 工具函数
+│   │   ├── cache.js        # 缓存管理
+│   │   ├── helpers.js      # 辅助函数
+│   │   ├── logger.js       # 日志工具
+│   │   └── index.js
+│   └── commands/           # CLI 命令
+│       └── index.js
+└── data/                   # 数据目录(自动生成)
+    ├── .copied-positions.json
+    └── .closed-positions.json
+```
+
+## 安装
+
+```bash
+# 进入项目目录
+cd byreal-sniper
+
+# 安装依赖
+npm install
+
+# 复制环境变量模板
+cp .env.example .env
+
+# 编辑 .env 文件,配置你的私钥和其他设置
+```
+
+## 使用方法
+
+### 启动狙击机器人
+
+```bash
+# 默认启动
+npm start
+
+# 或者
+node index.js
+node index.js start
+```
+
+### 查看当前状态
+
+```bash
+npm run status
+# 或者
+node index.js status
+```
+
+### 清除缓存
+
+```bash
+npm run clear
+# 或者
+node index.js clear
+```
+
+### 查看帮助
+
+```bash
+node index.js help
+```
+
+## 配置
+
+编辑 `.env` 文件:
+
+```env
+# Solana RPC URL
+RPC_URL=https://mainnet.helius-rpc.com/?api-key=20f2bda7-11af-4e71-a3c3-a8fd6567df80
+
+# 你的钱包私钥 (Base58 编码,从 Phantom 或其他钱包导出)
+PRIVATE_KEY=your_private_key_here
+
+# 跟单倍数 (1.5 = 复制目标仓位的 1.5 倍金额)
+COPY_MULTIPLIER=1.5
+
+# 最大/最小仓位金额 (USD)
+MAX_USD_VALUE=10
+MIN_USD_VALUE=0.1
+
+# Jupiter 兑换滑点 (100 = 1%)
+SLIPPAGE_BPS=100
+
+# 轮询间隔 (毫秒,10000 = 10秒)
+POLL_INTERVAL_MS=10000
+
+# 数据目录
+DATA_DIR=./data
+```
+
+## 扩展模块
+
+项目采用模块化架构,便于添加新功能:
+
+### 添加新的服务
+
+在 `src/services/` 目录创建新服务文件:
+
+```javascript
+// src/services/newService.js
+export class NewService {
+  static async doSomething() {
+    // 你的逻辑
+  }
+}
+```
+
+然后在 `src/services/index.js` 中导出:
+
+```javascript
+export { NewService } from './newService.js';
+```
+
+### 添加新的命令
+
+在 `src/commands/index.js` 中添加新命令函数:
+
+```javascript
+export function newCommand() {
+  // 命令逻辑
+}
+```
+
+在 `index.js` 中添加命令处理:
+
+```javascript
+case 'newcommand':
+  newCommand();
+  break;
+```
+
+### 添加新的工具函数
+
+在 `src/utils/` 目录创建新文件并导出:
+
+```javascript
+// src/utils/newUtil.js
+export function newHelper() {
+  // 工具函数
+}
+```
+
+## 工作原理
+
+1. **监控阶段**: 每 10 秒轮询一次目标地址的活跃仓位
+2. **分析阶段**: 
+   - 检查仓位是否在当前价格范围内
+   - 验证是否已复制过
+   - 计算复制金额(目标金额 × 倍数)
+3. **准备阶段**:
+   - 检查钱包余额
+   - 如余额不足,通过 Jupiter 自动兑换所需代币
+4. **执行阶段**:
+   - 调用 Byreal API 复制仓位
+   - 保存仓位到本地缓存
+5. **平仓阶段**:
+   - 检测目标地址已关闭的仓位
+   - 自动关闭对应的复制仓位
+
+## 技术栈
+
+- **Node.js 18+** - 运行时环境
+- **ES Modules** - 模块化系统
+- **Axios** - HTTP 请求
+- **@solana/web3.js** - Solana 区块链交互
+- **@solana/spl-token** - SPL 代币操作
+- **dotenv** - 环境变量管理
+- **bs58** - Base58 编码/解码
+
+## 目标地址
+
+默认监控地址: `dryuRNL9YcdLnhKFgLfdoj1g2suWcZp97G8XiH8U49e`
+
+可在 `src/config/index.js` 中修改 `TARGET_WALLET` 配置。
+
+## 安全提示
+
+⚠️ **警告**: 
+- 请仅在测试网或小金额下测试
+- 确保私钥安全,不要分享给任何人
+- 建议先使用小额资金测试策略
+- DeFi 存在无常损失风险,请充分了解 LP 机制
+- 切勿将 `.env` 文件提交到版本控制
+
+## 故障排除
+
+### 启动失败
+- 检查 Node.js 版本是否 >= 18
+- 检查 `.env` 文件是否存在且配置正确
+- 确认私钥格式正确(Base58 编码)
+
+### 无法获取仓位
+- 检查 RPC URL 是否可用
+- 确认目标地址有活跃的 LP 仓位
+
+### 兑换失败
+- 确认钱包有足够 SOL 支付交易费用
+- 检查 Jupiter API 是否可用
+
+### API 错误
+- 确认 `AUTH_HEADER` 配置正确
+- 检查 Byreal API 状态
+
+## 许可证
+
+ISC
+
+## 免责声明
+
+此工具仅供学习和研究使用。使用本工具进行的所有交易和操作均由用户自行承担风险。作者不对任何资金损失负责。请在使用前充分了解 DeFi 风险。

+ 62 - 0
index.js

@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+import { CONFIG } from './src/config/index.js';
+import { setLogLevel } from './src/utils/index.js';
+import { startSniper, showStatus, clearCache } from './src/commands/index.js';
+
+// Set log level
+setLogLevel(CONFIG.LOG_LEVEL);
+
+// Parse command line arguments
+const args = process.argv.slice(2);
+const command = args[0] || 'start';
+
+switch (command) {
+  case 'start':
+  case 'sniper':
+  case 'run':
+    startSniper().catch(error => {
+      console.error('Fatal error:', error);
+      process.exit(1);
+    });
+    break;
+    
+  case 'status':
+  case 'st':
+    showStatus();
+    break;
+    
+  case 'clear':
+  case 'clean':
+    clearCache();
+    break;
+    
+  case 'help':
+  case '--help':
+  case '-h':
+    console.log(`
+Byreal Sniper Bot - Automated LP Position Sniper
+
+Usage:
+  node index.js [command]
+
+Commands:
+  start, sniper, run    Start the sniper bot (default)
+  status, st            Show current copied and closed positions
+  clear, clean          Clear all position caches
+  help                  Show this help message
+
+Configuration:
+  Edit .env file to configure:
+    - PRIVATE_KEY: Your wallet private key
+    - COPY_MULTIPLIER: Copy multiplier (default: 1.5)
+    - MAX_USD_VALUE: Maximum position size
+    - POLL_INTERVAL_MS: Polling interval
+    `);
+    break;
+    
+  default:
+    console.log(`Unknown command: ${command}`);
+    console.log('Run "node index.js help" for usage information');
+    process.exit(1);
+}

+ 72 - 0
src/commands/index.js

@@ -0,0 +1,72 @@
+import { SniperEngine } from '../core/index.js';
+import { logger, formatUsd } from '../utils/index.js';
+import { CONFIG } from '../config/index.js';
+import { PositionCache } from '../utils/index.js';
+
+export function showStatus() {
+  const copiedCache = new PositionCache('.copied-positions.json', CONFIG.DATA_DIR);
+  const closedCache = new PositionCache('.closed-positions.json', CONFIG.DATA_DIR);
+
+  console.log('═══════════════════════════════════════════');
+  console.log('📊 Byreal Sniper Status');
+  console.log('═══════════════════════════════════════════\n');
+
+  const copied = copiedCache.getAll();
+  const closed = closedCache.getAll();
+
+  console.log(`🎯 Active Copied Positions: ${copied.positions.length}`);
+  if (copied.positions.length > 0) {
+    console.log('\nActive Positions:');
+    copied.positions.forEach((pos, i) => {
+      const data = copied.data[pos] || {};
+      console.log(`  ${i + 1}. ${pos}`);
+      console.log(`     Pool: ${data.poolAddress || 'N/A'}`);
+      console.log(`     Target Value: ${data.targetUsdValue ? formatUsd(data.targetUsdValue) : 'N/A'}`);
+      console.log(`     Copied At: ${data.copiedAt || 'N/A'}`);
+    });
+  }
+
+  console.log(`\n🚪 Closed Positions: ${closed.positions.length}`);
+  if (closed.positions.length > 0) {
+    console.log('\nClosed Positions:');
+    closed.positions.forEach((pos, i) => {
+      const data = closed.data[pos] || {};
+      console.log(`  ${i + 1}. ${pos}`);
+      console.log(`     Reason: ${data.reason || 'N/A'}`);
+      console.log(`     Closed At: ${data.closedAt || 'N/A'}`);
+    });
+  }
+
+  console.log('\n═══════════════════════════════════════════');
+}
+
+export async function startSniper() {
+  const sniper = new SniperEngine();
+  
+  // Handle graceful shutdown
+  process.on('SIGINT', () => {
+    logger.info('\nShutting down sniper bot...');
+    sniper.stop();
+    process.exit(0);
+  });
+
+  process.on('SIGTERM', () => {
+    logger.info('\nShutting down sniper bot...');
+    sniper.stop();
+    process.exit(0);
+  });
+
+  await sniper.start();
+}
+
+export function clearCache() {
+  const copiedCache = new PositionCache('.copied-positions.json', CONFIG.DATA_DIR);
+  const closedCache = new PositionCache('.closed-positions.json', CONFIG.DATA_DIR);
+  
+  console.log('Clearing all position caches...');
+  copiedCache.clear();
+  closedCache.clear();
+  console.log('✅ Caches cleared successfully');
+}
+
+export default { showStatus, startSniper, clearCache };

+ 58 - 0
src/config/index.js

@@ -0,0 +1,58 @@
+import dotenv from 'dotenv';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+export const CONFIG = {
+  // Paths
+  ROOT_DIR: join(__dirname, '../..'),
+  DATA_DIR: join(__dirname, '../../data'),
+  
+  // Solana Configuration
+  RPC_URL: process.env.RPC_URL || 'https://mainnet.helius-rpc.com/?api-key=20f2bda7-11af-4e71-a3c3-a8fd6567df80',
+  PRIVATE_KEY: process.env.PRIVATE_KEY,
+  
+  // Target wallet to snipe
+  TARGET_WALLET: 'dryuRNL9YcdLnhKFgLfdoj1g2suWcZp97G8XiH8U49e',
+  
+  // My wallet (will be derived from private key if not set)
+  MY_WALLET: process.env.MY_WALLET || '',
+  
+  // Copy settings
+  COPY_MULTIPLIER: parseFloat(process.env.COPY_MULTIPLIER) || 1.5,
+  MAX_USD_VALUE: parseFloat(process.env.MAX_USD_VALUE) || 10,
+  MIN_USD_VALUE: parseFloat(process.env.MIN_USD_VALUE) || 0.1,
+  
+  // API Endpoints
+  BYREAL_API_BASE: 'https://api2.byreal.io/byreal/api/dex/v2',
+  TICK_API: 'https://love.hdlife.me/api/tick-to-price',
+  COPY_ACTION_URL: 'https://love.hdlife.me/api/lp-copy',
+  CLOSE_ACTION_URL: 'https://love.hdlife.me/api/lp-close',
+  JUPITER_API: 'https://quote-api.jup.ag/v6',
+  
+  // Jupiter settings
+  SLIPPAGE_BPS: parseInt(process.env.SLIPPAGE_BPS) || 100,
+  
+  // Authentication
+  AUTH_HEADER: process.env.AUTH_HEADER || 'Basic YWRtaW46YzU4ODk5Njc=',
+  
+  // Polling settings
+  POLL_INTERVAL_MS: parseInt(process.env.POLL_INTERVAL_MS) || 10000,
+  
+  // Logging
+  LOG_LEVEL: process.env.LOG_LEVEL || 'info',
+};
+
+// Token whitelist for common tokens
+export const TOKEN_WHITELIST = {
+  'So11111111111111111111111111111111111111112': { symbol: 'SOL', decimals: 9, name: 'Wrapped SOL' },
+  'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': { symbol: 'USDC', decimals: 6, name: 'USD Coin' },
+  'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': { symbol: 'USDT', decimals: 6, name: 'Tether USD' },
+  'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': { symbol: 'BONK', decimals: 5, name: 'Bonk' },
+};
+
+export default CONFIG;

+ 1 - 0
src/core/index.js

@@ -0,0 +1 @@
+export { SniperEngine } from './sniper.js';

+ 251 - 0
src/core/sniper.js

@@ -0,0 +1,251 @@
+import { CONFIG } from '../config/index.js';
+import { PositionCache, logger, sleep, formatUsd } from '../utils/index.js';
+import { ByrealAPI, JupiterSwapper } from '../services/index.js';
+
+export class SniperEngine {
+  constructor() {
+    this.copiedCache = new PositionCache('.copied-positions.json', CONFIG.DATA_DIR);
+    this.closedCache = new PositionCache('.closed-positions.json', CONFIG.DATA_DIR);
+    this.swapper = new JupiterSwapper();
+    this.isRunning = false;
+    this.myWallet = CONFIG.MY_WALLET || this.swapper.getWalletAddress();
+  }
+
+  async analyzePosition(position, poolInfo) {
+    const decimalsA = poolInfo.mintA?.decimals || 6;
+    const decimalsB = poolInfo.mintB?.decimals || 6;
+    const currentPrice = parseFloat(poolInfo.mintB?.price || 0);
+
+    try {
+      const lowerPrice = await ByrealAPI.getPriceFromTick(position.lowerTick, decimalsA, decimalsB);
+      const upperPrice = await ByrealAPI.getPriceFromTick(position.upperTick, decimalsA, decimalsB);
+      
+      const min = Math.min(lowerPrice, upperPrice);
+      const max = Math.max(lowerPrice, upperPrice);
+
+      logger.info(`Price range: ${min.toFixed(6)} - ${max.toFixed(6)} (current: ${currentPrice.toFixed(6)})`);
+
+      return {
+        inRange: currentPrice >= min && currentPrice <= max,
+        lowerPrice,
+        upperPrice,
+      };
+    } catch (error) {
+      logger.error(`Error analyzing position: ${error.message}`);
+      return { inRange: false };
+    }
+  }
+
+  async executeCopy(position, poolInfo) {
+    const targetUsdValue = parseFloat(position.totalUsdValue || position.liquidityUsd || 0);
+    
+    if (targetUsdValue <= 0) {
+      logger.warn('Position has no USD value, skipping');
+      return false;
+    }
+
+    const copyUsdValue = Math.min(
+      targetUsdValue * CONFIG.COPY_MULTIPLIER,
+      CONFIG.MAX_USD_VALUE
+    );
+
+    if (copyUsdValue < CONFIG.MIN_USD_VALUE) {
+      logger.warn(`Position value ${formatUsd(copyUsdValue)} below minimum threshold`);
+      return false;
+    }
+
+    logger.info(`Copying position with ${CONFIG.COPY_MULTIPLIER}x multiplier`);
+    logger.info(`Target: ${formatUsd(targetUsdValue)} → Copy: ${formatUsd(copyUsdValue)}`);
+
+    const mintA = poolInfo.mintA?.address;
+    const mintB = poolInfo.mintB?.address;
+    
+    if (!mintA || !mintB) {
+      logger.error('Pool info missing token addresses');
+      return false;
+    }
+
+    // Get token prices and prepare swap
+    const tokenPrices = await this.swapper.getTokenPrices([mintA, mintB]);
+    const priceA = tokenPrices[mintA]?.price || 1;
+    const priceB = tokenPrices[mintB]?.price || 1;
+
+    const halfUsd = copyUsdValue / 2;
+    const amountA = halfUsd / priceA;
+    const amountB = halfUsd / priceB;
+
+    logger.info(`Estimated token needs: ${amountA.toFixed(4)} TokenA, ${amountB.toFixed(4)} TokenB`);
+
+    // Check and swap tokens
+    const tokenABalance = await this.swapper.getTokenBalance(mintA);
+    if (tokenABalance < amountA * 0.95) {
+      const success = await this.swapper.swapIfNeeded(
+        'So11111111111111111111111111111111111111112',
+        mintA,
+        amountA,
+        poolInfo.mintA?.decimals || 6
+      );
+      if (!success) {
+        logger.error('Failed to acquire sufficient Token A');
+        return false;
+      }
+    }
+
+    const tokenBBalance = await this.swapper.getTokenBalance(mintB);
+    if (tokenBBalance < amountB * 0.95) {
+      const success = await this.swapper.swapIfNeeded(
+        'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
+        mintB,
+        amountB,
+        poolInfo.mintB?.decimals || 6
+      );
+      if (!success) {
+        logger.error('Failed to acquire sufficient Token B');
+        return false;
+      }
+    }
+
+    // Execute copy
+    const success = await ByrealAPI.copyPosition(
+      position.nftMintAddress,
+      position.positionAddress,
+      copyUsdValue
+    );
+    
+    if (success) {
+      this.copiedCache.add(position.positionAddress, {
+        poolAddress: position.poolAddress,
+        targetUsdValue,
+        copiedAt: new Date().toISOString(),
+      });
+    }
+    
+    return success;
+  }
+
+  async scanForNewPositions() {
+    logger.info(`Scanning for target wallet positions: ${CONFIG.TARGET_WALLET}`);
+    
+    const { positions, poolMap } = await ByrealAPI.fetchTargetPositions(CONFIG.TARGET_WALLET);
+    
+    if (positions.length === 0) {
+      logger.info('No active positions found for target wallet');
+      return;
+    }
+
+    logger.info(`Found ${positions.length} positions, analyzing...`);
+
+    for (const position of positions) {
+      const positionAddress = position.positionAddress;
+      
+      if (this.copiedCache.has(positionAddress) || this.closedCache.has(positionAddress)) {
+        continue;
+      }
+
+      const poolInfo = poolMap[position.poolAddress];
+      if (!poolInfo) {
+        logger.warn(`Pool info not found for ${position.poolAddress}`);
+        continue;
+      }
+
+      logger.info(`\nAnalyzing position: ${positionAddress}`);
+      logger.info(`Pool: ${poolInfo.mintA?.symbol || '?'}/${poolInfo.mintB?.symbol || '?'}`);
+
+      const analysis = await this.analyzePosition(position, poolInfo);
+
+      if (analysis.inRange) {
+        logger.success('Position is IN RANGE - ready to copy!');
+
+        const isCopied = await ByrealAPI.checkIfCopied(
+          position.poolAddress,
+          positionAddress,
+          this.myWallet
+        );
+        
+        if (isCopied) {
+          logger.info('Already copied (confirmed via API)');
+          this.copiedCache.add(positionAddress, { poolAddress: position.poolAddress });
+        } else {
+          await this.executeCopy(position, poolInfo);
+        }
+      } else {
+        logger.info('Position is OUT OF RANGE');
+      }
+
+      await sleep(500);
+    }
+  }
+
+  async scanForClosedPositions() {
+    logger.info('Checking for closed positions to close...');
+    
+    const { positions } = await ByrealAPI.fetchTargetPositions(CONFIG.TARGET_WALLET);
+    const targetPositionAddresses = new Set(positions.map(p => p.positionAddress));
+    
+    const myCopiedPositions = this.copiedCache.getAll();
+
+    for (const positionAddress of myCopiedPositions.positions) {
+      if (!targetPositionAddresses.has(positionAddress) && !this.closedCache.has(positionAddress)) {
+        logger.warn(`Target closed position: ${positionAddress}`);
+        logger.info('Closing my copy...');
+        
+        const success = await ByrealAPI.closePosition(positionAddress);
+        
+        if (success) {
+          this.closedCache.add(positionAddress, {
+            closedAt: new Date().toISOString(),
+            reason: 'Target closed position',
+          });
+          this.copiedCache.remove(positionAddress);
+        }
+      }
+    }
+  }
+
+  async start() {
+    if (this.isRunning) {
+      logger.warn('Sniper is already running');
+      return;
+    }
+
+    this.isRunning = true;
+    
+    logger.info('═══════════════════════════════════════════');
+    logger.info('🚀 Byreal Sniper Bot Started');
+    logger.info(`🎯 Target: ${CONFIG.TARGET_WALLET}`);
+    logger.info(`💼 My Wallet: ${this.myWallet}`);
+    logger.info(`📈 Copy Multiplier: ${CONFIG.COPY_MULTIPLIER}x`);
+    logger.info(`💰 Max Position: ${formatUsd(CONFIG.MAX_USD_VALUE)}`);
+    logger.info(`⏱️ Poll Interval: ${CONFIG.POLL_INTERVAL_MS / 1000}s`);
+    logger.info('═══════════════════════════════════════════\n');
+
+    while (this.isRunning) {
+      try {
+        await this.scanForNewPositions();
+        await this.scanForClosedPositions();
+        
+        logger.info(`\nSleeping for ${CONFIG.POLL_INTERVAL_MS / 1000} seconds...\n`);
+        await sleep(CONFIG.POLL_INTERVAL_MS);
+      } catch (error) {
+        logger.error('Main loop error:', error.message);
+        await sleep(CONFIG.POLL_INTERVAL_MS);
+      }
+    }
+  }
+
+  stop() {
+    logger.info('Stopping sniper bot...');
+    this.isRunning = false;
+  }
+
+  getStatus() {
+    return {
+      isRunning: this.isRunning,
+      copiedPositions: this.copiedCache.getAll(),
+      closedPositions: this.closedCache.getAll(),
+      walletAddress: this.myWallet,
+    };
+  }
+}
+
+export default SniperEngine;

+ 125 - 0
src/services/byreal.js

@@ -0,0 +1,125 @@
+import axios from 'axios';
+import { CONFIG } from '../config/index.js';
+import { logger } from '../utils/index.js';
+
+export class ByrealAPI {
+  static async fetchTargetPositions(targetWallet) {
+    try {
+      const url = `${CONFIG.BYREAL_API_BASE}/position/list`;
+      const params = {
+        userAddress: targetWallet,
+        page: 1,
+        pageSize: 50,
+        status: 0,
+      };
+
+      const response = await axios.get(url, { params });
+      
+      if (response.data.retCode !== 0) {
+        throw new Error(response.data.retMsg || 'Failed to fetch positions');
+      }
+
+      return response.data.result?.data || { positions: [], poolMap: {} };
+    } catch (error) {
+      logger.error('Error fetching target positions:', error.message);
+      return { positions: [], poolMap: {} };
+    }
+  }
+
+  static async getPriceFromTick(tick, decimalsA, decimalsB) {
+    try {
+      const url = `${CONFIG.TICK_API}?tick=${tick}&decimalsA=${decimalsA}&decimalsB=${decimalsB}&baseIn=false`;
+      const response = await axios.get(url, {
+        headers: { Authorization: CONFIG.AUTH_HEADER },
+      });
+      return parseFloat(response.data.price);
+    } catch (error) {
+      throw new Error(`Failed to fetch price for tick ${tick}: ${error.message}`);
+    }
+  }
+
+  static async checkIfCopied(poolAddress, parentPositionAddress, myWallet) {
+    try {
+      const payload = {
+        poolAddress: poolAddress,
+        parentPositionAddress: parentPositionAddress,
+        page: 1,
+        pageSize: 10,
+        sortField: 'liquidity',
+      };
+
+      const response = await axios.post(CONFIG.CHECK_COPY_URL, payload);
+
+      if (response.data?.result?.data) {
+        const records = response.data.result.data.records || [];
+        const found = records.find(item => item.walletAddress === myWallet);
+        return !!found;
+      }
+
+      return false;
+    } catch (error) {
+      logger.error('Error checking copy status:', error.message);
+      return false;
+    }
+  }
+
+  static async copyPosition(nftMintAddress, positionAddress, maxUsdValue) {
+    try {
+      const body = {
+        nftMintAddress: nftMintAddress,
+        positionAddress: positionAddress,
+        maxUsdValue: maxUsdValue,
+      };
+
+      const headers = {
+        Authorization: CONFIG.AUTH_HEADER,
+        'Content-Type': 'application/json',
+      };
+
+      logger.info('Sending copy request to Byreal...');
+      
+      const response = await axios.post(CONFIG.COPY_ACTION_URL, body, { headers });
+      
+      if (response.data?.success) {
+        logger.success(`Position copied successfully: ${positionAddress}`);
+        return true;
+      } else {
+        logger.error('Copy request failed:', response.data);
+        return false;
+      }
+    } catch (error) {
+      logger.error('Error executing copy:', error.message);
+      if (error.response) {
+        logger.error('Server response:', error.response.data);
+      }
+      return false;
+    }
+  }
+
+  static async closePosition(positionAddress) {
+    try {
+      logger.info(`Closing position: ${positionAddress}`);
+
+      const body = { positionAddress };
+      const headers = {
+        Authorization: CONFIG.AUTH_HEADER,
+        'Content-Type': 'application/json',
+      };
+
+      const response = await axios.post(CONFIG.CLOSE_ACTION_URL, body, { headers });
+
+      if (response.data?.success) {
+        logger.success(`Position closed successfully: ${positionAddress}`);
+        return true;
+      } else {
+        logger.error('Close request failed:', response.data);
+        return false;
+      }
+    } catch (error) {
+      logger.error('Error closing position:', error.message);
+      return false;
+    }
+  }
+}
+
+export default ByrealAPI;

+ 2 - 0
src/services/index.js

@@ -0,0 +1,2 @@
+export { JupiterSwapper } from './jupiter.js';
+export { ByrealAPI } from './byreal.js';

+ 166 - 0
src/services/jupiter.js

@@ -0,0 +1,166 @@
+import axios from 'axios';
+import { Connection, PublicKey, Keypair, VersionedTransaction, Transaction } from '@solana/web3.js';
+import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from '@solana/spl-token';
+import bs58 from 'bs58';
+import { CONFIG } from '../config/index.js';
+import { logger } from '../utils/index.js';
+
+export class JupiterSwapper {
+  constructor() {
+    this.connection = new Connection(CONFIG.RPC_URL, 'confirmed');
+    this.keypair = Keypair.fromSecretKey(bs58.decode(CONFIG.PRIVATE_KEY));
+    this.walletAddress = this.keypair.publicKey.toString();
+  }
+
+  getWalletAddress() {
+    return this.walletAddress;
+  }
+
+  async getTokenBalance(mintAddress) {
+    try {
+      if (mintAddress === 'So11111111111111111111111111111111111111112') {
+        const balance = await this.connection.getBalance(this.keypair.publicKey);
+        return balance / 1e9;
+      }
+
+      const tokenAccount = await getAssociatedTokenAddress(
+        new PublicKey(mintAddress),
+        this.keypair.publicKey
+      );
+
+      try {
+        const accountInfo = await this.connection.getTokenAccountBalance(tokenAccount);
+        return parseFloat(accountInfo.value.uiAmount);
+      } catch (e) {
+        return 0;
+      }
+    } catch (error) {
+      logger.error(`Error getting balance for ${mintAddress}:`, error.message);
+      return 0;
+    }
+  }
+
+  async getTokenPrices(tokenAddresses) {
+    try {
+      const mints = tokenAddresses.join(',');
+      const response = await axios.get(`https://price.jup.ag/v4/price?ids=${mints}`);
+      return response.data.data || {};
+    } catch (error) {
+      logger.error('Error fetching token prices:', error.message);
+      return {};
+    }
+  }
+
+  async swapIfNeeded(inputMint, outputMint, requiredAmount, decimals = 6) {
+    const currentBalance = await this.getTokenBalance(outputMint);
+    
+    if (currentBalance >= requiredAmount) {
+      logger.success(`Sufficient balance: ${currentBalance.toFixed(4)} >= ${requiredAmount.toFixed(4)}`);
+      return true;
+    }
+
+    const neededAmount = requiredAmount - currentBalance;
+    logger.warn(`Insufficient balance. Need ${neededAmount.toFixed(4)} more.`);
+    logger.info(`Initiating swap from ${inputMint} to ${outputMint}...`);
+
+    try {
+      const inputAmount = Math.ceil(neededAmount * Math.pow(10, decimals));
+      
+      const quoteResponse = await axios.get(
+        `${CONFIG.JUPITER_API}/quote`,
+        {
+          params: {
+            inputMint: inputMint,
+            outputMint: outputMint,
+            amount: inputAmount,
+            slippageBps: CONFIG.SLIPPAGE_BPS,
+            onlyDirectRoutes: false,
+            asLegacyTransaction: false,
+          },
+        }
+      );
+
+      if (!quoteResponse.data) {
+        throw new Error('No swap route found');
+      }
+
+      logger.success('Quote received from Jupiter');
+
+      const swapResponse = await axios.post(
+        `${CONFIG.JUPITER_API}/swap`,
+        {
+          quoteResponse: quoteResponse.data,
+          userPublicKey: this.walletAddress,
+          wrapAndUnwrapSol: true,
+          prioritizationFeeLamports: 10000,
+        }
+      );
+
+      if (!swapResponse.data || !swapResponse.data.swapTransaction) {
+        throw new Error('Failed to get swap transaction');
+      }
+
+      const swapTransactionBuf = Buffer.from(swapResponse.data.swapTransaction, 'base64');
+      const transaction = VersionedTransaction.deserialize(swapTransactionBuf);
+      
+      transaction.sign([this.keypair]);
+
+      const signature = await this.connection.sendTransaction(transaction, {
+        maxRetries: 3,
+        skipPreflight: false,
+      });
+
+      logger.info(`Swap transaction sent: ${signature}`);
+
+      const confirmation = await this.connection.confirmTransaction(signature, 'confirmed');
+      
+      if (confirmation.value.err) {
+        throw new Error(`Transaction failed: ${confirmation.value.err}`);
+      }
+
+      logger.success(`Swap confirmed: https://solscan.io/tx/${signature}`);
+      
+      const newBalance = await this.getTokenBalance(outputMint);
+      logger.info(`New balance: ${newBalance.toFixed(4)}`);
+      
+      return newBalance >= requiredAmount;
+    } catch (error) {
+      logger.error('Swap failed:', error.message);
+      return false;
+    }
+  }
+
+  async ensureTokenAccount(mintAddress) {
+    try {
+      const tokenAccount = await getAssociatedTokenAddress(
+        new PublicKey(mintAddress),
+        this.keypair.publicKey
+      );
+
+      try {
+        await this.connection.getAccountInfo(tokenAccount);
+        return tokenAccount;
+      } catch (e) {
+        logger.info(`Creating token account for ${mintAddress}...`);
+        const transaction = new Transaction().add(
+          createAssociatedTokenAccountInstruction(
+            this.keypair.publicKey,
+            tokenAccount,
+            this.keypair.publicKey,
+            new PublicKey(mintAddress)
+          )
+        );
+
+        const signature = await this.connection.sendTransaction(transaction, [this.keypair]);
+        await this.connection.confirmTransaction(signature);
+        logger.success(`Token account created: ${tokenAccount.toString()}`);
+        return tokenAccount;
+      }
+    } catch (error) {
+      logger.error(`Error ensuring token account for ${mintAddress}:`, error.message);
+      throw error;
+    }
+  }
+}
+
+export default JupiterSwapper;

+ 85 - 0
src/utils/cache.js

@@ -0,0 +1,85 @@
+import fs from 'fs';
+import path from 'path';
+
+export class PositionCache {
+  constructor(filename, dataDir = './data') {
+    this.dataDir = dataDir;
+    this.filePath = path.join(dataDir, filename);
+    
+    // Ensure data directory exists
+    if (!fs.existsSync(dataDir)) {
+      fs.mkdirSync(dataDir, { recursive: true });
+    }
+    
+    this.cache = this.load();
+  }
+
+  load() {
+    try {
+      if (fs.existsSync(this.filePath)) {
+        const data = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
+        return {
+          positions: new Set(data.positions || []),
+          data: data.data || {},
+          updatedAt: data.updatedAt || new Date().toISOString(),
+        };
+      }
+    } catch (e) {
+      console.warn(`Could not load cache from ${this.filePath}:`, e.message);
+    }
+    return {
+      positions: new Set(),
+      data: {},
+      updatedAt: new Date().toISOString(),
+    };
+  }
+
+  save() {
+    try {
+      const data = {
+        positions: [...this.cache.positions],
+        data: this.cache.data,
+        updatedAt: new Date().toISOString(),
+      };
+      fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf8');
+    } catch (e) {
+      console.warn(`Could not save cache to ${this.filePath}:`, e.message);
+    }
+  }
+
+  has(positionAddress) {
+    return this.cache.positions.has(positionAddress);
+  }
+
+  add(positionAddress, metadata = {}) {
+    this.cache.positions.add(positionAddress);
+    this.cache.data[positionAddress] = {
+      ...metadata,
+      copiedAt: new Date().toISOString(),
+    };
+    this.save();
+  }
+
+  remove(positionAddress) {
+    this.cache.positions.delete(positionAddress);
+    delete this.cache.data[positionAddress];
+    this.save();
+  }
+
+  get(positionAddress) {
+    return this.cache.data[positionAddress];
+  }
+
+  getAll() {
+    return {
+      positions: [...this.cache.positions],
+      data: this.cache.data,
+    };
+  }
+
+  clear() {
+    this.cache.positions.clear();
+    this.cache.data = {};
+    this.save();
+  }
+}

+ 19 - 0
src/utils/helpers.js

@@ -0,0 +1,19 @@
+export function formatUsd(value) {
+  return `$${parseFloat(value).toFixed(2)}`;
+}
+
+export function formatTokenAmount(amount, decimals = 6) {
+  return (amount / Math.pow(10, decimals)).toFixed(decimals);
+}
+
+export function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export function formatDate(date) {
+  return new Date(date).toLocaleString();
+}
+
+export function parseTokenAmount(amount, decimals) {
+  return Math.floor(parseFloat(amount) * Math.pow(10, decimals));
+}

+ 3 - 0
src/utils/index.js

@@ -0,0 +1,3 @@
+export { PositionCache } from './cache.js';
+export { formatUsd, formatTokenAmount, sleep, formatDate, parseTokenAmount } from './helpers.js';
+export { log, logger, setLogLevel } from './logger.js';

+ 47 - 0
src/utils/logger.js

@@ -0,0 +1,47 @@
+const LOG_LEVELS = {
+  debug: 0,
+  info: 1,
+  warn: 2,
+  error: 3,
+};
+
+let currentLogLevel = 'info';
+
+export function setLogLevel(level) {
+  currentLogLevel = level;
+}
+
+export function log(level, message, data = null) {
+  const levelValue = LOG_LEVELS[level] || 1;
+  const currentLevelValue = LOG_LEVELS[currentLogLevel] || 1;
+  
+  if (levelValue < currentLevelValue) {
+    return;
+  }
+  
+  const timestamp = new Date().toISOString();
+  const icons = {
+    debug: '🔍',
+    info: 'ℹ️',
+    warn: '⚠️',
+    error: '❌',
+    success: '✅',
+  };
+  
+  const icon = icons[level] || '';
+  const logEntry = `[${timestamp}] ${icon} [${level.toUpperCase()}] ${message}`;
+  
+  if (data) {
+    console.log(logEntry, data);
+  } else {
+    console.log(logEntry);
+  }
+}
+
+export const logger = {
+  debug: (msg, data) => log('debug', msg, data),
+  info: (msg, data) => log('info', msg, data),
+  warn: (msg, data) => log('warn', msg, data),
+  error: (msg, data) => log('error', msg, data),
+  success: (msg, data) => log('success', msg, data),
+};