فهرست منبع

Fix copy trading execution bugs and add project docs

- Fix InvalidStrategyParameters: map on-chain strategy type (0-8) to
  SDK StrategyType (0-2) using modulo — old code passed 3 which is
  out of range for the SDK
- Fix missing signature: include position keypair as transaction signer
  instead of only passing its public key
- Fix slippage: convert basis points to percentage for the SDK
  (SDK expects 1.0 for 1%, not 100)
- Remove harmful bin range recentering that broke one-sided deposits
  (e.g. Y-only SOL deposits where all bins are below active bin)
- Add README.md with setup, usage, and architecture docs
- Add AGENTS.md with detailed component guide and data flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lushdog@outlook.com 3 هفته پیش
والد
کامیت
ae2868effd
3فایلهای تغییر یافته به همراه366 افزوده شده و 65 حذف شده
  1. 183 0
      AGENTS.md
  2. 146 20
      README.md
  3. 37 45
      src/lib/meteora/dlmm-client.ts

+ 183 - 0
AGENTS.md

@@ -0,0 +1,183 @@
+# Agents Guide
+
+This document describes the key components of the Meteora DLMM Copy Trading Bot and how they interact.
+
+## Architecture Overview
+
+```
+┌─────────────────┐     ┌──────────────────────────────────────────┐
+│   Next.js Web   │     │              Worker Process              │
+│                 │     │                                          │
+│  Dashboard UI   │     │  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
+│  (page.tsx)     │     │  │ Monitor │──│ Tracker │──│Executor │ │
+│                 │     │  └────┬────┘  └─────────┘  └────┬────┘ │
+│  API Routes     │     │       │                          │      │
+│  /api/wallets   │◄────┤       │ WebSocket                │ DLMM │
+│  /api/positions │     │       │ onLogs                   │ SDK  │
+│  /api/trades    │     │       ▼                          ▼      │
+│  /api/settings  │     │  Solana RPC ◄───────────── Solana RPC   │
+└────────┬────────┘     └──────────────────────────────────────────┘
+         │                              │
+         ▼                              ▼
+    ┌──────────┐                  ┌──────────┐
+    │  SQLite  │◄─────────────────│  SQLite  │
+    │ Database │  (shared volume) │ Database │
+    └──────────┘                  └──────────┘
+```
+
+## Components
+
+### 1. Transaction Monitor (`src/worker/monitor.ts`)
+
+**Role**: Watches leader wallets for DLMM transactions in real-time.
+
+**How it works**:
+- Subscribes to Solana `onLogs` WebSocket for each active monitored wallet
+- Filters logs for mentions of the DLMM program ID (`LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo`)
+- On match, fetches the full transaction and passes it to the transaction parser
+- Emits `meteoraTx` events with parsed operations
+- Maintains a dedup set to avoid processing the same signature twice
+
+**Key behaviors**:
+- Subscription sync runs every 30 seconds to pick up new wallets added via the dashboard
+- Processes only `confirmed` commitment level transactions
+
+### 2. Transaction Parser (`src/lib/solana/transaction-parser.ts`)
+
+**Role**: Decodes raw Solana transactions into structured DLMM operations.
+
+**How it works**:
+- Resolves all account keys from the transaction message, including address lookup tables (v0 transactions)
+- Iterates through all instructions, matching by DLMM program ID
+- Uses Anchor's `BorshInstructionCoder` with the DLMM IDL to decode instruction data
+- Maps decoded instruction names to action types (ADD_LIQUIDITY, REMOVE_LIQUIDITY, etc.)
+- Extracts account addresses (position, lbPair, sender) from account indices using predefined account maps
+- Extracts parameters (bin ranges, amounts, BPS) from decoded instruction data
+
+**Supported instructions**:
+| Instruction | Action |
+|---|---|
+| `addLiquidity`, `addLiquidityByStrategy`, `addLiquidityByStrategy2`, `addLiquidityByStrategyOneSide` | ADD_LIQUIDITY |
+| `removeLiquidity`, `removeLiquidityByRange`, `removeLiquidityByRange2` | REMOVE_LIQUIDITY |
+| `closePosition`, `closePosition2`, `closePositionIfEmpty` | CLOSE_POSITION |
+| `initializePosition`, `initializePosition2` | INIT_POSITION |
+| `rebalanceLiquidity` | REBALANCE_LIQUIDITY |
+
+### 3. Position Tracker (`src/worker/position-tracker.ts`)
+
+**Role**: Maintains the state of leader and follower positions in the database.
+
+**How it works**:
+- On INIT_POSITION / ADD_LIQUIDITY: creates or updates leader position records with bin range and strategy info
+- On REMOVE_LIQUIDITY: logs activity (position stays open)
+- On CLOSE_POSITION: marks leader position as closed
+- On REBALANCE_LIQUIDITY: updates leader position bin range
+- Provides lookup for follower positions mapped to leader positions
+
+### 4. Copy Executor (`src/worker/executor.ts`)
+
+**Role**: Mirrors leader operations on the follower wallet.
+
+**Supported operations**:
+
+| Leader Action | Follower Action |
+|---|---|
+| ADD_LIQUIDITY | Open new position + add scaled liquidity |
+| REMOVE_LIQUIDITY | Remove liquidity (same BPS ratio) |
+| CLOSE_POSITION | Remove all liquidity + close position |
+| REBALANCE_LIQUIDITY | Remove liquidity + re-add in new bin range |
+| INIT_POSITION | Track only (no copy, waits for ADD_LIQUIDITY) |
+
+**Scaling**: The copy ratio (`COPY_RATIO` env var, default 1.0) scales the leader's deposit amounts. For example, if the leader deposits 1 SOL and the ratio is 0.5, the follower deposits 0.5 SOL.
+
+**Safety**: Each copy trade is recorded in the database with status tracking (PENDING → SUCCESS/FAILED) and error messages for debugging.
+
+### 5. DLMM Client (`src/lib/meteora/dlmm-client.ts`)
+
+**Role**: Wraps the `@meteora-ag/dlmm` SDK for copy trading operations.
+
+**Functions**:
+- `copyOpenPosition`: Creates a new DLMM position and adds liquidity using the leader's strategy type and bin range
+- `copyRemoveLiquidity`: Removes liquidity from a follower position (by BPS)
+- `copyClosePosition`: Closes a follower position
+- `copyRebalanceLiquidity`: Removes all liquidity and re-adds in a new bin range matching the leader's rebalance
+
+### 6. WebSocket Heartbeat (`src/worker/heartbeat.ts`)
+
+**Role**: Keeps the Solana WebSocket connection alive and handles reconnection.
+
+**How it works**:
+- Sends periodic `getSlot` ping requests (every 30 seconds)
+- Detects stale connections (no response within timeout)
+- On connection loss, triggers a reconnection callback that re-subscribes all wallets
+- Uses exponential backoff for reconnection attempts
+
+### 7. Web Dashboard (`src/app/page.tsx`)
+
+**Role**: Provides a UI for managing monitored wallets.
+
+**Features**:
+- Add new wallets by Solana address (with optional label)
+- View all monitored wallets with status (Active/Paused)
+- Toggle wallet active state
+- Delete wallets
+- Validates Solana addresses (base58, 32-44 chars)
+
+### 8. API Routes (`src/app/api/`)
+
+**Role**: REST API for CRUD operations on wallets, positions, trades, and settings.
+
+**Routes**:
+- `/api/wallets` — Wallet CRUD with Solana address validation
+- `/api/positions` — Read leader and follower positions
+- `/api/trades` — Read copy trade history (with status and error info)
+- `/api/settings` — Read/update worker settings (copy ratio, slippage, max size, auto-copy toggle)
+- `/api/health` — Health check returning wallet count and worker status
+
+## Data Flow
+
+### Copy Trade Lifecycle
+
+```
+1. Leader wallet sends DLMM transaction on Solana
+   ↓
+2. Monitor detects tx via WebSocket onLogs (confirmed)
+   ↓
+3. Monitor fetches full tx → Parser decodes DLMM instructions
+   ↓
+4. Tracker updates leader position state in DB
+   ↓
+5. Executor checks if follower should copy:
+   - Is auto_copy enabled?
+   - Is there already a follower position? (skip duplicate adds)
+   - Is there a follower position to modify? (for remove/close/rebalance)
+   ↓
+6. Executor creates CopyTrade record (status: PENDING)
+   ↓
+7. Executor calls DLMM SDK to mirror the operation
+   ↓
+8. CopyTrade updated to SUCCESS or FAILED
+```
+
+## Database Schema
+
+| Table | Purpose |
+|---|---|
+| `MonitoredWallet` | Wallets to watch (address, label, active flag) |
+| `LeaderPosition` | Leader's DLMM positions (pair, bins, status) |
+| `FollowerPosition` | Follower's mirrored positions (linked to leader) |
+| `CopyTrade` | Record of every copy attempt (action, status, tx sigs) |
+| `ActivityLog` | General activity logging (worker start/stop, errors) |
+
+## Environment Configuration
+
+| Variable | Required | Default | Description |
+|---|---|---|---|
+| `RPC_ENDPOINT` | Yes | `https://api.mainnet-beta.solana.com` | Solana RPC URL |
+| `WS_ENDPOINT` | No | Derived from RPC | Solana WebSocket URL |
+| `FOLLOWER_PRIVATE_KEY` | Yes | — | Base58 private key of the follower wallet |
+| `DATABASE_URL` | Yes | `file:./dev.db` | SQLite database path |
+| `COPY_RATIO` | No | `1.0` | Amount scaling factor |
+| `MAX_POSITION_SIZE_SOL` | No | `10` | Maximum SOL per position |
+| `SLIPPAGE_BPS` | No | `100` | Slippage tolerance (basis points) |
+| `AUTO_COPY_ENABLED` | No | `true` | Enable/disable automatic copy trading |

+ 146 - 20
README.md

@@ -1,36 +1,162 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# Meteora DLMM Copy Trading Bot
 
-## Getting Started
+A copy trading bot for [Meteora DLMM](https://www.meteora.ag/) (Dynamic Liquidity Market Maker) on Solana. Monitors leader wallets in real-time via WebSocket and automatically mirrors their DLMM liquidity operations with proportional scaling.
 
-First, run the development server:
+## Features
+
+- Real-time monitoring of leader wallets via Solana WebSocket (`onLogs`)
+- Automatic copy trading for DLMM operations:
+  - Add liquidity (including by-strategy and one-side)
+  - Remove liquidity (by range)
+  - Rebalance liquidity
+  - Close position
+- Proportional position scaling based on configurable copy ratio
+- Web dashboard for managing monitored wallets
+- SQLite database for tracking positions, trades, and activity logs
+- Docker support for production deployment
+
+## Tech Stack
+
+- **Frontend**: Next.js 16, React 19, Tailwind CSS v4, lucide-react, sonner
+- **Backend**: Next.js API Routes, Prisma 7 + SQLite
+- **Worker**: Standalone TypeScript process (tsx)
+- **Solana**: @solana/web3.js, @coral-xyz/anchor, @meteora-ag/dlmm
+- **Package Manager**: pnpm
+
+## Project Structure
+
+```
+src/
+├── app/
+│   ├── api/
+│   │   ├── health/         # Health check endpoint
+│   │   ├── positions/      # Leader/follower position queries
+│   │   ├── settings/       # Worker settings (copy ratio, slippage, etc.)
+│   │   ├── trades/         # Copy trade history
+│   │   └── wallets/        # CRUD for monitored wallets
+│   ├── layout.tsx          # Root layout with Toaster
+│   ├── page.tsx            # Wallet management dashboard
+│   └── globals.css
+├── lib/
+│   ├── db/
+│   │   ├── prisma.ts       # Prisma client singleton
+│   │   └── queries.ts      # Database query helpers
+│   ├── meteora/
+│   │   ├── dlmm-client.ts  # DLMM SDK wrapper (open, remove, close, rebalance)
+│   │   └── types.ts        # Shared types (ParsedDlmmOperation, etc.)
+│   ├── solana/
+│   │   ├── connection.ts   # RPC connection + follower keypair
+│   │   └── transaction-parser.ts  # Decode DLMM instructions from transactions
+│   ├── constants.ts        # Program IDs, discriminators, defaults
+│   ├── utils.ts            # Helpers (sleep, getEnvOrThrow, etc.)
+│   └── validation.ts       # Zod schemas for API input validation
+└── worker/
+    ├── index.ts            # Worker entry point
+    ├── monitor.ts          # WebSocket transaction monitor
+    ├── executor.ts         # Copy trade executor
+    ├── position-tracker.ts # Leader/follower position state tracking
+    └── heartbeat.ts        # WebSocket keepalive + reconnect
+```
+
+## Prerequisites
+
+- Node.js 20+
+- pnpm
+- A Solana RPC endpoint with WebSocket support (e.g. Helius, QuickNode)
+- A funded follower wallet (base58 private key)
+
+## Setup
 
 ```bash
-npm run dev
-# or
-yarn dev
-# or
+# Install dependencies
+pnpm install
+
+# Create environment file
+cp .env.example .env.local
+```
+
+### Environment Variables
+
+Create `.env.local` with:
+
+```env
+# Solana RPC (must support WebSocket)
+RPC_ENDPOINT=https://your-rpc-endpoint.com
+WS_ENDPOINT=wss://your-ws-endpoint.com
+
+# Follower wallet private key (base58)
+FOLLOWER_PRIVATE_KEY=your_base58_private_key
+
+# Database
+DATABASE_URL=file:./dev.db
+
+# Optional settings
+COPY_RATIO=1.0                # Scale factor (1.0 = same size as leader)
+MAX_POSITION_SIZE_SOL=10      # Max SOL per position
+SLIPPAGE_BPS=100              # Slippage tolerance in basis points
+AUTO_COPY_ENABLED=true        # Enable/disable auto copy
+```
+
+## Development
+
+```bash
+# Push database schema
+pnpm db:push
+
+# Start Next.js dev server (dashboard + API)
 pnpm dev
-# or
-bun dev
+
+# Start the copy trading worker (separate terminal)
+pnpm worker
+
+# Open Prisma Studio (database browser)
+pnpm db:studio
 ```
 
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+Open http://localhost:3000 to access the wallet management dashboard.
+
+## Production (Docker)
+
+```bash
+# Create production environment file
+cp .env.example .env.production
+
+# Build and start
+docker compose up -d
+
+# View logs
+docker compose logs -f
+```
+
+The Docker setup runs two containers:
+- **web**: Next.js standalone server (port 3000)
+- **worker**: Copy trading worker process
+
+Both share a SQLite database via a Docker volume.
 
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+## How It Works
 
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+1. **Monitor**: The worker subscribes to Solana WebSocket `onLogs` for each monitored wallet. When a transaction mentions the DLMM program, it fetches the full transaction data.
 
-## Learn More
+2. **Parse**: The transaction parser decodes DLMM instructions using Anchor's BorshInstructionCoder, extracting operation type, position/pair addresses, bin ranges, and amounts.
 
-To learn more about Next.js, take a look at the following resources:
+3. **Track**: The position tracker maintains leader position state in the database (open, bin range, strategy).
 
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+4. **Execute**: The executor mirrors the operation on the follower wallet using the DLMM SDK, scaling amounts by the configured copy ratio.
 
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+## API Endpoints
 
-## Deploy on Vercel
+| Method | Path | Description |
+|--------|------|-------------|
+| GET | `/api/wallets` | List all monitored wallets |
+| POST | `/api/wallets` | Add a wallet `{address, label?}` |
+| PATCH | `/api/wallets` | Toggle active `{id, isActive}` |
+| DELETE | `/api/wallets?id=xxx` | Remove a wallet |
+| GET | `/api/positions` | List leader/follower positions |
+| GET | `/api/trades` | List copy trade history |
+| GET/PATCH | `/api/settings` | Get/update worker settings |
+| GET | `/api/health` | Health check |
 
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+## License
 
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+MIT

+ 37 - 45
src/lib/meteora/dlmm-client.ts

@@ -32,6 +32,21 @@ function scaleAmount(amount: BN, ratio: number): BN {
 	return amount.mul(new BN(scaledBps)).div(new BN(10000))
 }
 
+/**
+ * Map on-chain strategy type (0-8) to SDK StrategyType (0-2).
+ *
+ * On-chain enum order (from DLMM IDL):
+ *   0=SpotOneSide, 1=CurveOneSide, 2=BidAskOneSide,
+ *   3=SpotBalanced, 4=CurveBalanced, 5=BidAskBalanced,
+ *   6=SpotImBalanced, 7=CurveImBalanced, 8=BidAskImBalanced
+ *
+ * SDK StrategyType: Spot=0, Curve=1, BidAsk=2
+ */
+function toSdkStrategyType(onChainType: number | undefined): number {
+	if (onChainType === undefined || onChainType < 0) return 0 // Default: Spot
+	return onChainType % 3
+}
+
 export async function copyOpenPosition(
 	connection: Connection,
 	follower: Keypair,
@@ -49,67 +64,40 @@ export async function copyOpenPosition(
 		settings.copyRatio
 	)
 
-	const minBinId = leaderOp.minBinId ?? 0
-	const maxBinId = leaderOp.maxBinId ?? 0
-	const strategyType = leaderOp.strategyType ?? 3 // SpotBalanced default
-
 	const activeBin = await dlmmPool.getActiveBin()
-	const activeId = activeBin.binId
+	const minBinId = leaderOp.minBinId ?? activeBin.binId
+	const maxBinId = leaderOp.maxBinId ?? activeBin.binId
+	const sdkStrategyType = toSdkStrategyType(leaderOp.strategyType)
+
+	console.log(
+		`[DLMM] Opening position: bins [${minBinId}, ${maxBinId}], activeBin ${activeBin.binId}, strategy ${sdkStrategyType}, amountX ${followerAmountX.toString()}, amountY ${followerAmountY.toString()}`
+	)
+
+	// Generate position keypair — must be included as signer
+	const positionKeypair = Keypair.generate()
 
 	const createPositionTx =
 		await dlmmPool.initializePositionAndAddLiquidityByStrategy({
-			positionPubKey: Keypair.generate().publicKey,
+			positionPubKey: positionKeypair.publicKey,
 			user: follower.publicKey,
 			totalXAmount: followerAmountX,
 			totalYAmount: followerAmountY,
 			strategy: {
 				minBinId,
 				maxBinId,
-				strategyType,
+				strategyType: sdkStrategyType,
 			},
-			slippage: settings.slippageBps,
+			slippage: settings.slippageBps / 100, // Convert BPS to percentage
 		})
 
 	const txSignature = await sendAndConfirmTransaction(
 		connection,
 		createPositionTx,
-		[follower],
+		[follower, positionKeypair],
 		{ commitment: 'confirmed' }
 	)
 
-	// Extract position address from the transaction
-	// The position is the first writable account created
-	const txResult = await connection.getTransaction(txSignature, {
-		maxSupportedTransactionVersion: 0,
-		commitment: 'confirmed',
-	})
-
-	let positionAddress = PublicKey.default
-	if (txResult) {
-		// Position address can be found in the account keys
-		const postBalances = txResult.meta?.postBalances ?? []
-		const preBalances = txResult.meta?.preBalances ?? []
-		const msg = txResult.transaction.message
-		const accountKeys =
-			'getAccountKeys' in msg
-				? msg.getAccountKeys({})
-				: (msg as unknown as { accountKeys: PublicKey[] }).accountKeys
-
-		for (let i = 0; i < postBalances.length; i++) {
-			if (preBalances[i] === 0 && postBalances[i] > 0) {
-				const key =
-					'get' in accountKeys
-						? (accountKeys as { get(i: number): PublicKey | undefined }).get(i)
-						: (accountKeys as PublicKey[])[i]
-				if (key) {
-					positionAddress = key
-					break
-				}
-			}
-		}
-	}
-
-	return { positionAddress, txSignature }
+	return { positionAddress: positionKeypair.publicKey, txSignature }
 }
 
 export async function copyRemoveLiquidity(
@@ -227,6 +215,10 @@ export async function copyRebalanceLiquidity(
 		maxBinId = Math.max(...allMaxBins)
 	}
 
+	console.log(
+		`[DLMM] Rebalancing: new bins [${minBinId}, ${maxBinId}], activeBin ${activeId}`
+	)
+
 	// Scale amounts by copy ratio
 	const followerAmountX = scaleAmount(
 		leaderOp.totalXAmount ?? new BN(0),
@@ -237,7 +229,7 @@ export async function copyRebalanceLiquidity(
 		settings.copyRatio
 	)
 
-	const strategyType = leaderOp.strategyType ?? 3 // SpotBalanced default
+	const sdkStrategyType = toSdkStrategyType(leaderOp.strategyType)
 
 	const addLiqTx = await dlmmPool.addLiquidityByStrategy({
 		positionPubKey: followerPositionAddress,
@@ -247,9 +239,9 @@ export async function copyRebalanceLiquidity(
 		strategy: {
 			minBinId,
 			maxBinId,
-			strategyType,
+			strategyType: sdkStrategyType,
 		},
-		slippage: settings.slippageBps,
+		slippage: settings.slippageBps / 100, // Convert BPS to percentage
 	})
 
 	const txSignature = await sendAndConfirmTransaction(