From eed3df472d708f3a038c9e34a7384f60c914cbbc Mon Sep 17 00:00:00 2001 From: Adi Alia <49166243+Adialia1@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:51:37 +0200 Subject: [PATCH] Revert "Add emojis to all Telegram bot messages and menus" --- .env.example | 11 +- PLAN.md | 62 ----- package-lock.json | 397 ----------------------------- package.json | 3 - src/db/userDb.ts | 378 --------------------------- src/multiUserBot.ts | 33 --- src/services/notifier.ts | 112 ++++---- src/services/userBotManager.ts | 342 ------------------------- src/telegram/bot.ts | 65 ----- src/telegram/handlers/follow.ts | 213 ---------------- src/telegram/handlers/help.ts | 70 ----- src/telegram/handlers/positions.ts | 197 -------------- src/telegram/handlers/settings.ts | 265 ------------------- src/telegram/handlers/start.ts | 40 --- src/telegram/handlers/stats.ts | 68 ----- src/telegram/handlers/trading.ts | 96 ------- src/telegram/handlers/wallet.ts | 238 ----------------- src/telegram/menus.ts | 182 ------------- 18 files changed, 59 insertions(+), 2713 deletions(-) delete mode 100644 PLAN.md delete mode 100644 src/db/userDb.ts delete mode 100644 src/multiUserBot.ts delete mode 100644 src/services/userBotManager.ts delete mode 100644 src/telegram/bot.ts delete mode 100644 src/telegram/handlers/follow.ts delete mode 100644 src/telegram/handlers/help.ts delete mode 100644 src/telegram/handlers/positions.ts delete mode 100644 src/telegram/handlers/settings.ts delete mode 100644 src/telegram/handlers/start.ts delete mode 100644 src/telegram/handlers/stats.ts delete mode 100644 src/telegram/handlers/trading.ts delete mode 100644 src/telegram/handlers/wallet.ts delete mode 100644 src/telegram/menus.ts diff --git a/.env.example b/.env.example index 59eee18..6f36768 100644 --- a/.env.example +++ b/.env.example @@ -91,18 +91,13 @@ RETRY_DELAY_MS=2000 # Delay before copying a trade in milliseconds COPY_DELAY_MS=0 -# === TELEGRAM BOT === -# Get token from @BotFather on Telegram (REQUIRED for multi-user mode) +# === TELEGRAM NOTIFICATIONS === +# Get token from @BotFather on Telegram TELEGRAM_BOT_TOKEN= -# Get your chat ID from @userinfobot on Telegram (single-user mode only) +# Get your chat ID from @userinfobot on Telegram TELEGRAM_CHAT_ID= -# === MULTI-USER ENCRYPTION === -# Master key for encrypting user private keys (recommended: generate a random 32+ char string) -# If not set, falls back to TELEGRAM_BOT_TOKEN (less secure) -ENCRYPTION_KEY= - # === HEALTH CHECK ENDPOINT === # Enable HTTP health check server HEALTH_CHECK_ENABLED=false diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 82c724c..0000000 --- a/PLAN.md +++ /dev/null @@ -1,62 +0,0 @@ -# Polybot Multi-User Telegram Bot - Implementation Plan - -## Overview -Transform Polybot from a single-user CLI bot into a multi-user Telegram bot where each user can: -- Connect their Polymarket wallet (private key + wallet address) -- Configure their own trading settings via Telegram buttons -- Follow different trader wallets independently -- Manage risk settings (slippage, caps, stop-loss, take-profit, etc.) - -## Architecture Changes - -### 1. User Database (`src/db/userDb.ts`) -- SQLite database (via `better-sqlite3`) for persistent multi-user storage -- Tables: `users`, `user_wallets`, `user_settings`, `user_tracked_wallets`, `user_positions`, `user_trades` -- Each user identified by Telegram chat ID -- Encrypted private key storage (AES-256-GCM with master key from env) - -### 2. Telegram Bot Rewrite (`src/telegram/`) -- **bot.ts** - Main Telegram bot with callback query routing -- **menus.ts** - Inline keyboard menu builders -- **handlers/** - Command and callback handlers: - - `start.ts` - /start welcome + main menu - - `wallet.ts` - Connect/disconnect/view wallets - - `follow.ts` - Add/remove/list tracked wallets - - `settings.ts` - Trading settings (slippage, caps, sizing, risk) - - `positions.ts` - View positions, manual sell - - `stats.ts` - Trading stats and P&L - - `trading.ts` - Start/stop/pause trading - -### 3. Per-User Bot Instances (`src/services/userBotManager.ts`) -- Manages a lightweight bot instance per active user -- Each instance runs its own TradeMonitor, Trader, RiskManager -- Lazy initialization (only when user starts trading) -- Graceful shutdown per user - -### 4. Menu Flow -``` -/start โ†’ Main Menu - โ”œโ”€โ”€ ๐Ÿ”‘ Wallets โ†’ Connect Wallet | View Wallets | Disconnect - โ”œโ”€โ”€ ๐Ÿ‘ Follow Traders โ†’ Add Wallet | List | Remove - โ”œโ”€โ”€ โš™๏ธ Settings - โ”‚ โ”œโ”€โ”€ Position Sizing โ†’ Account Size | Max Position | Min Trade | Max % - โ”‚ โ”œโ”€โ”€ Risk Management โ†’ Stop Loss | Take Profit | Trailing Stop | Daily Loss Limit - โ”‚ โ”œโ”€โ”€ Trade Filters โ†’ Min/Max Probability | Blacklist | Whitelist - โ”‚ โ”œโ”€โ”€ Execution โ†’ Slippage | Copy Sells | Conflict Strategy - โ”‚ โ””โ”€โ”€ Time Exits โ†’ Max Hold Time - โ”œโ”€โ”€ ๐Ÿ“Š Positions โ†’ List | Sell Position | Sell All - โ”œโ”€โ”€ ๐Ÿ“ˆ Stats โ†’ Overview | Per-Trader | Reset - โ”œโ”€โ”€ โ–ถ๏ธ Start Trading / โธ Pause / โน Stop - โ””โ”€โ”€ โ“ Help -``` - -## Implementation Steps - -1. Add dependencies (better-sqlite3, encryption) -2. Create database schema and UserDb class -3. Build Telegram menu system with inline keyboards -4. Implement wallet connection flow (encrypted storage) -5. Implement settings management via Telegram -6. Create UserBotManager for per-user bot instances -7. Wire everything together -8. Update config for multi-user mode diff --git a/package-lock.json b/package-lock.json index b7f2b79..6f2bf09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@scure/bip39": "2.0.1", "@types/node": "25.3.5", "axios": "1.13.6", - "better-sqlite3": "^12.8.0", "dotenv": "17.3.1", "node-telegram-bot-api": "^0.67.0", "tsx": "4.21.0", @@ -23,7 +22,6 @@ "ws": "8.19.0" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", "@types/node-telegram-bot-api": "^0.64.0", "@types/ws": "8.18.1" } @@ -667,16 +665,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -918,26 +906,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -947,29 +915,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", @@ -992,30 +937,6 @@ "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", "license": "MIT" }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1069,12 +990,6 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "license": "Apache-2.0" }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1165,30 +1080,6 @@ "ms": "^2.1.1" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1232,15 +1123,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -1475,15 +1357,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1522,12 +1395,6 @@ "node": ">=0.10.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -1588,12 +1455,6 @@ "node": ">= 6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1730,12 +1591,6 @@ "assert-plus": "^1.0.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -1881,38 +1736,12 @@ "node": ">=0.10" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -2370,57 +2199,12 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-telegram-bot-api": { "version": "0.67.0", "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.67.0.tgz", @@ -2605,43 +2389,6 @@ "node": ">= 0.4" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2706,21 +2453,6 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "license": "MIT" }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -3019,18 +2751,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -3149,51 +2869,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -3312,78 +2987,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", diff --git a/package.json b/package.json index 9f702e1..8453bfb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "tsc", "start": "node dist/bot.js", "bot": "tsx src/bot.ts", - "telegram": "tsx src/multiUserBot.ts", "positions": "tsx src/index.ts positions", "trades": "tsx src/index.ts trades", "status": "tsx src/scripts/status.ts", @@ -57,7 +56,6 @@ "@scure/bip39": "2.0.1", "@types/node": "25.3.5", "axios": "1.13.6", - "better-sqlite3": "^12.8.0", "dotenv": "17.3.1", "node-telegram-bot-api": "^0.67.0", "tsx": "4.21.0", @@ -66,7 +64,6 @@ "ws": "8.19.0" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", "@types/node-telegram-bot-api": "^0.64.0", "@types/ws": "8.18.1" } diff --git a/src/db/userDb.ts b/src/db/userDb.ts deleted file mode 100644 index 4ed5ada..0000000 --- a/src/db/userDb.ts +++ /dev/null @@ -1,378 +0,0 @@ -import Database from 'better-sqlite3'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const DB_PATH = join(__dirname, '../../data/users.db'); - -// Encryption helpers for private keys -const ALGORITHM = 'aes-256-gcm'; - -function getEncryptionKey(): Buffer { - const masterKey = process.env.ENCRYPTION_KEY || process.env.TELEGRAM_BOT_TOKEN || 'polybot-default-key-change-me'; - return scryptSync(masterKey, 'polybot-salt', 32); -} - -function encrypt(text: string): string { - const key = getEncryptionKey(); - const iv = randomBytes(16); - const cipher = createCipheriv(ALGORITHM, key, iv); - let encrypted = cipher.update(text, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - const authTag = cipher.getAuthTag().toString('hex'); - return `${iv.toString('hex')}:${authTag}:${encrypted}`; -} - -function decrypt(encryptedText: string): string { - const key = getEncryptionKey(); - const [ivHex, authTagHex, encrypted] = encryptedText.split(':'); - const iv = Buffer.from(ivHex, 'hex'); - const authTag = Buffer.from(authTagHex, 'hex'); - const decipher = createDecipheriv(ALGORITHM, key, iv); - decipher.setAuthTag(authTag); - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; -} - -export interface UserRecord { - chatId: string; - username: string | null; - createdAt: string; - isActive: boolean; -} - -export interface UserWallet { - id: number; - chatId: string; - walletAddress: string; - encryptedPrivateKey: string; - funderAddress: string | null; - signatureType: number; - alias: string; - isDefault: boolean; - createdAt: string; -} - -export interface UserTrackedWallet { - id: number; - chatId: string; - walletAddress: string; - alias: string; - allocation: number; - enabled: boolean; -} - -export interface UserSettings { - chatId: string; - enableTrading: boolean; - dryRun: boolean; - userAccountSize: number; - maxPositionSize: number; - minTradeSize: number; - maxPercentagePerTrade: number; - minProbability: number; - maxProbability: number; - maxOpenPositions: number; - stopLossPercent: number; - takeProfitPercent: number; - trailingStopPercent: number; - orderSlippagePercent: number; - copySells: boolean; - maxHoldTimeHours: number; - dailyLossLimit: number; - maxPositionValue: number; - conflictStrategy: string; - blacklistKeywords: string; - whitelistKeywords: string; -} - -const DEFAULT_SETTINGS: Omit = { - enableTrading: false, - dryRun: true, - userAccountSize: 30, - maxPositionSize: 10, - minTradeSize: 1, - maxPercentagePerTrade: 20, - minProbability: 0.05, - maxProbability: 0.95, - maxOpenPositions: 0, - stopLossPercent: -25, - takeProfitPercent: 100, - trailingStopPercent: 15, - orderSlippagePercent: 15, - copySells: true, - maxHoldTimeHours: 0, - dailyLossLimit: 0, - maxPositionValue: 0, - conflictStrategy: 'first', - blacklistKeywords: '', - whitelistKeywords: '', -}; - -export class UserDb { - private db: Database.Database; - - constructor() { - this.db = new Database(DB_PATH); - this.db.pragma('journal_mode = WAL'); - this.db.pragma('foreign_keys = ON'); - this.initSchema(); - } - - private initSchema(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS users ( - chat_id TEXT PRIMARY KEY, - username TEXT, - created_at TEXT DEFAULT (datetime('now')), - is_active INTEGER DEFAULT 1 - ); - - CREATE TABLE IF NOT EXISTS user_wallets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id TEXT NOT NULL REFERENCES users(chat_id), - wallet_address TEXT NOT NULL, - encrypted_private_key TEXT NOT NULL, - funder_address TEXT, - signature_type INTEGER DEFAULT 2, - alias TEXT DEFAULT 'Main', - is_default INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS user_tracked_wallets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id TEXT NOT NULL REFERENCES users(chat_id), - wallet_address TEXT NOT NULL, - alias TEXT NOT NULL, - allocation INTEGER DEFAULT 100, - enabled INTEGER DEFAULT 1, - UNIQUE(chat_id, wallet_address) - ); - - CREATE TABLE IF NOT EXISTS user_settings ( - chat_id TEXT PRIMARY KEY REFERENCES users(chat_id), - enable_trading INTEGER DEFAULT 0, - dry_run INTEGER DEFAULT 1, - user_account_size REAL DEFAULT 30, - max_position_size REAL DEFAULT 10, - min_trade_size REAL DEFAULT 1, - max_percentage_per_trade REAL DEFAULT 20, - min_probability REAL DEFAULT 0.05, - max_probability REAL DEFAULT 0.95, - max_open_positions INTEGER DEFAULT 0, - stop_loss_percent REAL DEFAULT -25, - take_profit_percent REAL DEFAULT 100, - trailing_stop_percent REAL DEFAULT 15, - order_slippage_percent REAL DEFAULT 15, - copy_sells INTEGER DEFAULT 1, - max_hold_time_hours REAL DEFAULT 0, - daily_loss_limit REAL DEFAULT 0, - max_position_value REAL DEFAULT 0, - conflict_strategy TEXT DEFAULT 'first', - blacklist_keywords TEXT DEFAULT '', - whitelist_keywords TEXT DEFAULT '' - ); - - CREATE TABLE IF NOT EXISTS user_state ( - chat_id TEXT PRIMARY KEY REFERENCES users(chat_id), - state_json TEXT DEFAULT '{}' - ); - `); - } - - // ===== Users ===== - - ensureUser(chatId: string, username?: string): void { - this.db.prepare(` - INSERT INTO users (chat_id, username) VALUES (?, ?) - ON CONFLICT(chat_id) DO UPDATE SET username = COALESCE(?, username) - `).run(chatId, username || null, username || null); - - // Ensure settings row exists - this.db.prepare(` - INSERT OR IGNORE INTO user_settings (chat_id) VALUES (?) - `).run(chatId); - - // Ensure state row exists - this.db.prepare(` - INSERT OR IGNORE INTO user_state (chat_id) VALUES (?) - `).run(chatId); - } - - getAllActiveUsers(): UserRecord[] { - return this.db.prepare(` - SELECT chat_id as chatId, username, created_at as createdAt, is_active as isActive - FROM users WHERE is_active = 1 - `).all() as UserRecord[]; - } - - // ===== Wallets ===== - - addWallet(chatId: string, privateKey: string, walletAddress: string, funderAddress?: string, alias?: string, signatureType?: number): number { - const encryptedKey = encrypt(privateKey); - const isDefault = this.getWallets(chatId).length === 0 ? 1 : 0; - - const result = this.db.prepare(` - INSERT INTO user_wallets (chat_id, wallet_address, encrypted_private_key, funder_address, signature_type, alias, is_default) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(chatId, walletAddress.toLowerCase(), encryptedKey, funderAddress || null, signatureType ?? 2, alias || 'Main', isDefault); - - return result.lastInsertRowid as number; - } - - getWallets(chatId: string): UserWallet[] { - return this.db.prepare(` - SELECT id, chat_id as chatId, wallet_address as walletAddress, - encrypted_private_key as encryptedPrivateKey, funder_address as funderAddress, - signature_type as signatureType, alias, is_default as isDefault, created_at as createdAt - FROM user_wallets WHERE chat_id = ? - `).all(chatId) as UserWallet[]; - } - - getDefaultWallet(chatId: string): UserWallet | undefined { - return this.db.prepare(` - SELECT id, chat_id as chatId, wallet_address as walletAddress, - encrypted_private_key as encryptedPrivateKey, funder_address as funderAddress, - signature_type as signatureType, alias, is_default as isDefault, created_at as createdAt - FROM user_wallets WHERE chat_id = ? AND is_default = 1 - `).get(chatId) as UserWallet | undefined; - } - - decryptPrivateKey(encryptedKey: string): string { - return decrypt(encryptedKey); - } - - removeWallet(chatId: string, walletId: number): boolean { - const result = this.db.prepare(` - DELETE FROM user_wallets WHERE id = ? AND chat_id = ? - `).run(walletId, chatId); - return result.changes > 0; - } - - setDefaultWallet(chatId: string, walletId: number): void { - this.db.prepare(`UPDATE user_wallets SET is_default = 0 WHERE chat_id = ?`).run(chatId); - this.db.prepare(`UPDATE user_wallets SET is_default = 1 WHERE id = ? AND chat_id = ?`).run(walletId, chatId); - } - - // ===== Tracked Wallets ===== - - addTrackedWallet(chatId: string, address: string, alias: string, allocation: number = 100): number { - const result = this.db.prepare(` - INSERT INTO user_tracked_wallets (chat_id, wallet_address, alias, allocation) - VALUES (?, ?, ?, ?) - ON CONFLICT(chat_id, wallet_address) DO UPDATE SET alias = ?, allocation = ?, enabled = 1 - `).run(chatId, address.toLowerCase(), alias, allocation, alias, allocation); - return result.lastInsertRowid as number; - } - - getTrackedWallets(chatId: string): UserTrackedWallet[] { - return this.db.prepare(` - SELECT id, chat_id as chatId, wallet_address as walletAddress, alias, allocation, enabled - FROM user_tracked_wallets WHERE chat_id = ? - `).all(chatId) as UserTrackedWallet[]; - } - - removeTrackedWallet(chatId: string, walletId: number): boolean { - const result = this.db.prepare(` - DELETE FROM user_tracked_wallets WHERE id = ? AND chat_id = ? - `).run(walletId, chatId); - return result.changes > 0; - } - - toggleTrackedWallet(chatId: string, walletId: number): boolean { - this.db.prepare(` - UPDATE user_tracked_wallets SET enabled = CASE WHEN enabled = 1 THEN 0 ELSE 1 END - WHERE id = ? AND chat_id = ? - `).run(walletId, chatId); - const wallet = this.db.prepare(`SELECT enabled FROM user_tracked_wallets WHERE id = ?`).get(walletId) as any; - return wallet?.enabled === 1; - } - - // ===== Settings ===== - - getSettings(chatId: string): UserSettings { - const row = this.db.prepare(` - SELECT chat_id as chatId, enable_trading as enableTrading, dry_run as dryRun, - user_account_size as userAccountSize, max_position_size as maxPositionSize, - min_trade_size as minTradeSize, max_percentage_per_trade as maxPercentagePerTrade, - min_probability as minProbability, max_probability as maxProbability, - max_open_positions as maxOpenPositions, stop_loss_percent as stopLossPercent, - take_profit_percent as takeProfitPercent, trailing_stop_percent as trailingStopPercent, - order_slippage_percent as orderSlippagePercent, copy_sells as copySells, - max_hold_time_hours as maxHoldTimeHours, daily_loss_limit as dailyLossLimit, - max_position_value as maxPositionValue, conflict_strategy as conflictStrategy, - blacklist_keywords as blacklistKeywords, whitelist_keywords as whitelistKeywords - FROM user_settings WHERE chat_id = ? - `).get(chatId) as UserSettings | undefined; - - if (!row) { - return { chatId, ...DEFAULT_SETTINGS }; - } - - // Convert SQLite integers to booleans - return { - ...row, - enableTrading: !!row.enableTrading, - dryRun: !!row.dryRun, - copySells: !!row.copySells, - }; - } - - updateSetting(chatId: string, key: string, value: any): void { - // Map camelCase to snake_case column names - const columnMap: Record = { - enableTrading: 'enable_trading', - dryRun: 'dry_run', - userAccountSize: 'user_account_size', - maxPositionSize: 'max_position_size', - minTradeSize: 'min_trade_size', - maxPercentagePerTrade: 'max_percentage_per_trade', - minProbability: 'min_probability', - maxProbability: 'max_probability', - maxOpenPositions: 'max_open_positions', - stopLossPercent: 'stop_loss_percent', - takeProfitPercent: 'take_profit_percent', - trailingStopPercent: 'trailing_stop_percent', - orderSlippagePercent: 'order_slippage_percent', - copySells: 'copy_sells', - maxHoldTimeHours: 'max_hold_time_hours', - dailyLossLimit: 'daily_loss_limit', - maxPositionValue: 'max_position_value', - conflictStrategy: 'conflict_strategy', - blacklistKeywords: 'blacklist_keywords', - whitelistKeywords: 'whitelist_keywords', - }; - - const column = columnMap[key]; - if (!column) throw new Error(`Unknown setting: ${key}`); - - // Convert booleans to integers for SQLite - const dbValue = typeof value === 'boolean' ? (value ? 1 : 0) : value; - - this.db.prepare(`UPDATE user_settings SET ${column} = ? WHERE chat_id = ?`).run(dbValue, chatId); - } - - // ===== User State (positions, orders, stats) ===== - - getUserState(chatId: string): any { - const row = this.db.prepare(`SELECT state_json FROM user_state WHERE chat_id = ?`).get(chatId) as any; - try { - return row ? JSON.parse(row.state_json) : {}; - } catch { - return {}; - } - } - - saveUserState(chatId: string, state: any): void { - this.db.prepare(` - UPDATE user_state SET state_json = ? WHERE chat_id = ? - `).run(JSON.stringify(state), chatId); - } - - close(): void { - this.db.close(); - } -} diff --git a/src/multiUserBot.ts b/src/multiUserBot.ts deleted file mode 100644 index d2b6e12..0000000 --- a/src/multiUserBot.ts +++ /dev/null @@ -1,33 +0,0 @@ -import 'dotenv/config'; -import { TelegramTradingBot } from './telegram/bot.js'; - -async function main() { - console.log('='.repeat(50)); - console.log(' Polybot - Multi-User Telegram Trading Bot'); - console.log('='.repeat(50)); - - const token = process.env.TELEGRAM_BOT_TOKEN; - if (!token) { - console.error('Error: TELEGRAM_BOT_TOKEN environment variable is required'); - console.error('Get a token from @BotFather on Telegram'); - process.exit(1); - } - - const bot = new TelegramTradingBot(token); - console.log('[Main] Telegram bot started. Send /start to your bot to begin.'); - - // Graceful shutdown - const shutdown = async () => { - console.log('\n[Main] Shutting down...'); - await bot.stop(); - process.exit(0); - }; - - process.on('SIGINT', shutdown); - process.on('SIGTERM', shutdown); -} - -main().catch((error) => { - console.error('[Main] Fatal error:', error); - process.exit(1); -}); diff --git a/src/services/notifier.ts b/src/services/notifier.ts index 74a8980..5e68a5b 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -124,8 +124,8 @@ export class TelegramNotifier { const message = ` โœ… Bot Status: ACTIVE -โฑ Uptime: ${hours}h ${minutes}m ${seconds}s -๐Ÿ“… Started: ${new Date(this.startTime).toLocaleString()} +Uptime: ${hours}h ${minutes}m ${seconds}s +Started: ${new Date(this.startTime).toLocaleString()} `.trim(); await this.sendMessage(message); @@ -145,9 +145,9 @@ export class TelegramNotifier { let message = '๐Ÿ“Š Open Positions\n\n'; positions.forEach((pos, i) => { message += `${i + 1}. ${pos.outcome}\n`; - message += ` ๐Ÿช Market: ${pos.title}\n`; - message += ` ๐Ÿ“ฆ Size: ${pos.size.toFixed(4)} shares @ $${pos.avgPrice.toFixed(3)}\n`; - message += ` ๐Ÿ’ต Value: $${(pos.size * pos.avgPrice).toFixed(2)}\n\n`; + message += ` Market: ${pos.title}\n`; + message += ` Size: ${pos.size.toFixed(4)} shares @ $${pos.avgPrice.toFixed(3)}\n`; + message += ` Value: $${(pos.size * pos.avgPrice).toFixed(2)}\n\n`; }); await this.sendMessage(message); @@ -165,11 +165,11 @@ export class TelegramNotifier { const message = ` ๐Ÿ“ˆ Trading Statistics -๐Ÿ”ข Total Trades: ${stats.totalTrades} -โœ… Successful: ${stats.successfulTrades} -โŒ Failed: ${stats.failedTrades} -๐ŸŽฏ Success Rate: ${successRate}% -๐Ÿ’ฐ Total Volume: $${stats.totalVolume.toFixed(2)} +Total Trades: ${stats.totalTrades} +Successful: ${stats.successfulTrades} +Failed: ${stats.failedTrades} +Success Rate: ${successRate}% +Total Volume: $${stats.totalVolume.toFixed(2)} `.trim(); await this.sendMessage(message); @@ -194,7 +194,7 @@ export class TelegramNotifier { return; } - await this.sendMessage(`๐ŸŽฏ Found ${summary.count} redeemable position(s) worth ~$${summary.totalValue.toFixed(2)}. Redeeming...`); + await this.sendMessage(`Found ${summary.count} redeemable position(s) worth ~$${summary.totalValue.toFixed(2)}. Redeeming...`); const results = await this.redeemer.redeemAll(); const succeeded = results.filter(r => r.success); @@ -273,13 +273,13 @@ export class TelegramNotifier { const message = ` ๐Ÿค– Available Commands -/status - โœ… Check if bot is active -/positions - ๐Ÿ“Š Show open positions -/stats - ๐Ÿ“ˆ Show trading statistics -/balance - ๐Ÿ’ฐ Show balance & redeemable positions -/redeem - ๐ŸŽฏ Redeem all resolved winning positions -/stop - ๐Ÿ›‘ Stop the bot (auto-deploy will restart) -/help - โ“ Show this help message +/status - Check if bot is active +/positions - Show open positions +/stats - Show trading statistics +/balance - Show balance & redeemable positions +/redeem - Redeem all resolved winning positions +/stop - Stop the bot (auto-deploy will restart) +/help - Show this help message `.trim(); await this.sendMessage(message); @@ -335,14 +335,14 @@ export class TelegramNotifier { const message = ` ${emoji} Trade Executed -๐Ÿ“Œ Side: ${trade.side} -๐Ÿช Market: ${trade.title} -๐ŸŽฒ Outcome: ${trade.outcome} -๐Ÿ’ต Amount: $${trade.amount.toFixed(2)} -๐Ÿ’ฒ Price: $${trade.price.toFixed(3)} (${(trade.price * 100).toFixed(1)}%) -${trade.size ? `๐Ÿ“ฆ Size: ${trade.size.toFixed(4)} shares` : ''} -${trade.walletAlias ? `๐Ÿ‘ค Copied from: ${trade.walletAlias}` : ''} -${trade.orderId ? `๐Ÿ†” Order ID: ${trade.orderId}` : ''} +Side: ${trade.side} +Market: ${trade.title} +Outcome: ${trade.outcome} +Amount: $${trade.amount.toFixed(2)} +Price: $${trade.price.toFixed(3)} (${(trade.price * 100).toFixed(1)}%) +${trade.size ? `Size: ${trade.size.toFixed(4)} shares` : ''} +${trade.walletAlias ? `Copied from: ${trade.walletAlias}` : ''} +${trade.orderId ? `Order ID: ${trade.orderId}` : ''} `.trim(); await this.sendMessage(message); @@ -355,12 +355,12 @@ ${trade.orderId ? `๐Ÿ†” Order ID: ${trade.orderId}` : ''} const message = ` โŒ Trade Failed -๐Ÿ“Œ Side: ${trade.side} -๐Ÿช Market: ${trade.title} -๐ŸŽฒ Outcome: ${trade.outcome} -๐Ÿ’ต Amount: $${trade.amount.toFixed(2)} -${trade.walletAlias ? `๐Ÿ‘ค Copied from: ${trade.walletAlias}` : ''} -โš ๏ธ Error: ${trade.error} +Side: ${trade.side} +Market: ${trade.title} +Outcome: ${trade.outcome} +Amount: $${trade.amount.toFixed(2)} +${trade.walletAlias ? `Copied from: ${trade.walletAlias}` : ''} +Error: ${trade.error} `.trim(); await this.sendMessage(message); @@ -381,12 +381,12 @@ ${trade.walletAlias ? `๐Ÿ‘ค Copied from: ${trade.walletAlias}` : ''} const message = ` ${emoji} ${typeLabel} Triggered -๐Ÿช Market: ${trigger.title} -๐ŸŽฒ Outcome: ${trigger.outcome} -๐Ÿ“ฅ Entry Price: $${trigger.entryPrice.toFixed(3)} -๐Ÿ“Š Current Price: $${trigger.currentPrice.toFixed(3)} -๐Ÿ’น P&L: ${pnlSign}${trigger.pnlPercent.toFixed(1)}% -๐Ÿ“ฆ Size: ${trigger.size.toFixed(4)} shares +Market: ${trigger.title} +Outcome: ${trigger.outcome} +Entry Price: $${trigger.entryPrice.toFixed(3)} +Current Price: $${trigger.currentPrice.toFixed(3)} +P&L: ${pnlSign}${trigger.pnlPercent.toFixed(1)}% +Size: ${trigger.size.toFixed(4)} shares ${statusLine} `.trim(); @@ -420,11 +420,11 @@ ${statusLine} const message = ` ๐Ÿš€ Polybot Started -๐Ÿ”ง Mode: ${modeEmoji} ${mode} -๐Ÿ‘ Tracking: ${config.wallets.join(', ') || 'None'} -๐Ÿ’ต Account Size: $${config.accountSize.toFixed(2)} -๐Ÿ’ฐ Max Per Trade: $${config.maxPositionSize.toFixed(2)} -๐Ÿ“ฆ Current Positions: ${config.positionCount} +Mode: ${modeEmoji} ${mode} +Tracking: ${config.wallets.join(', ') || 'None'} +Account Size: $${config.accountSize.toFixed(2)} +Max Per Trade: $${config.maxPositionSize.toFixed(2)} +Current Positions: ${config.positionCount} `.trim(); await this.sendMessage(message); @@ -448,12 +448,12 @@ ${statusLine} message += ` -๐Ÿ“‹ Session Stats: -โ€ข ๐Ÿ”ข Total Trades: ${stats.totalTrades} -โ€ข โœ… Successful: ${stats.successfulTrades} -โ€ข โŒ Failed: ${stats.failedTrades} -โ€ข ๐ŸŽฏ Success Rate: ${successRate}% -โ€ข ๐Ÿ’ฐ Volume: $${stats.totalVolume.toFixed(2)}`; +Session Stats: +โ€ข Total Trades: ${stats.totalTrades} +โ€ข Successful: ${stats.successfulTrades} +โ€ข Failed: ${stats.failedTrades} +โ€ข Success Rate: ${successRate}% +โ€ข Volume: $${stats.totalVolume.toFixed(2)}`; } await this.sendMessage(message); @@ -469,7 +469,7 @@ ${statusLine} let positionsText = ''; if (summary.positions.length === 0) { - positionsText = '๐Ÿ“ญ No open positions'; + positionsText = 'No open positions'; } else { positionsText = summary.positions.map(pos => { const pnlText = pos.pnlPercent !== undefined @@ -482,12 +482,12 @@ ${statusLine} const message = ` ๐Ÿ“Š Daily Summary -๐Ÿ“‹ Trading Stats: -โ€ข ๐Ÿ”ข Total Trades: ${summary.totalTrades} -โ€ข โœ… Successful: ${summary.successfulTrades} -โ€ข โŒ Failed: ${summary.failedTrades} -โ€ข ๐ŸŽฏ Success Rate: ${successRate}% -โ€ข ๐Ÿ’ฐ Volume: $${summary.totalVolume.toFixed(2)} +Trading Stats: +โ€ข Total Trades: ${summary.totalTrades} +โ€ข Successful: ${summary.successfulTrades} +โ€ข Failed: ${summary.failedTrades} +โ€ข Success Rate: ${successRate}% +โ€ข Volume: $${summary.totalVolume.toFixed(2)} Open Positions (${summary.positions.length}): ${positionsText} diff --git a/src/services/userBotManager.ts b/src/services/userBotManager.ts deleted file mode 100644 index e91c2a4..0000000 --- a/src/services/userBotManager.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { TradeMonitor } from './tradeMonitor.js'; -import { PositionSizer } from './positionSizer.js'; -import { OrderQueue, QueuedOrder } from './orderQueue.js'; -import { RiskManager } from './riskManager.js'; -import { Trader, TradeResult } from './trader.js'; -import { ClobApiClient } from '../api/clobApi.js'; -import { isMarketBlacklisted, isMarketWhitelisted } from './tradeFilter.js'; -import { UserDb, UserSettings } from '../db/userDb.js'; -import { TradeSignal, WalletConfig, CopyConfig } from '../types/index.js'; -import TelegramBot from 'node-telegram-bot-api'; - -export interface UserBotInstance { - chatId: string; - monitor: TradeMonitor | null; - trader: Trader | null; - orderQueue: OrderQueue; - positionSizer: PositionSizer; - isRunning: boolean; -} - -export class UserBotManager { - private userBots = new Map(); - private db: UserDb; - private telegramBot: TelegramBot; - - constructor(db: UserDb, telegramBot: TelegramBot) { - this.db = db; - this.telegramBot = telegramBot; - } - - isUserRunning(chatId: string): boolean { - return this.userBots.get(chatId)?.isRunning || false; - } - - getUserBot(chatId: string): UserBotInstance | undefined { - return this.userBots.get(chatId); - } - - async startUserBot(chatId: string): Promise { - if (this.userBots.get(chatId)?.isRunning) { - throw new Error('Already running'); - } - - const settings = this.db.getSettings(chatId); - const wallets = this.db.getWallets(chatId); - const trackedWallets = this.db.getTrackedWallets(chatId).filter(t => t.enabled); - - if (wallets.length === 0) throw new Error('No wallet connected'); - if (trackedWallets.length === 0) throw new Error('No traders to follow'); - - const defaultWallet = this.db.getDefaultWallet(chatId) || wallets[0]; - const privateKey = this.db.decryptPrivateKey(defaultWallet.encryptedPrivateKey); - - // Build wallet configs for TradeMonitor - const walletConfigs: WalletConfig[] = trackedWallets.map(tw => ({ - address: tw.walletAddress, - alias: tw.alias, - enabled: true, - allocation: tw.allocation, - })); - - // Create position sizer - const positionSizer = new PositionSizer({ - userAccountSize: settings.userAccountSize, - maxPositionSize: settings.maxPositionSize, - minTradeSize: settings.minTradeSize, - maxPercentage: settings.maxPercentagePerTrade, - }); - - // Create order queue - const orderQueue = new OrderQueue({ - maxConcurrent: 3, - orderDelayMs: 200, - }); - - // Create trader - const trader = new Trader({ - privateKey, - funderAddress: defaultWallet.funderAddress || undefined, - dryRun: settings.dryRun, - signatureType: defaultWallet.signatureType, - orderSlippagePercent: settings.orderSlippagePercent, - maxBuyPrice: settings.maxProbability, - }); - - await trader.initialize(); - - // Create trade monitor - const monitor = new TradeMonitor({ - wallets: walletConfigs, - pollingIntervalMs: 1000, - }); - - const instance: UserBotInstance = { - chatId, - monitor, - trader, - orderQueue, - positionSizer, - isRunning: true, - }; - - // Handle trade signals - monitor.on('trade', async (signal: TradeSignal) => { - await this.handleTradeSignal(chatId, instance, signal, settings); - }); - - // Handle order processing - orderQueue.on('process', async (order: QueuedOrder) => { - await this.executeOrder(chatId, instance, order); - }); - - this.userBots.set(chatId, instance); - - // Start monitoring - await monitor.start(); - - // Load persisted state (last seen trades) - const state = this.db.getUserState(chatId); - if (state.lastSeenTrades) { - for (const [addr, ts] of Object.entries(state.lastSeenTrades)) { - (monitor as any).lastSeenTrades?.set(addr, ts); - } - } - - await this.sendNotification(chatId, - `๐ŸŸข Trading Started!\n\n` + - `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n` + - `๐Ÿ‘ Following: ${trackedWallets.map(t => t.alias).join(', ')}\n` + - `๐Ÿ’ฐ Max position: $${settings.maxPositionSize}` - ); - } - - async stopUserBot(chatId: string): Promise { - const instance = this.userBots.get(chatId); - if (!instance) return; - - instance.isRunning = false; - - if (instance.monitor) { - instance.monitor.stop(); - } - - // Save state before stopping - this.saveUserState(chatId, instance); - - this.userBots.delete(chatId); - - await this.sendNotification(chatId, 'โน Trading Stopped'); - } - - async stopAll(): Promise { - for (const chatId of this.userBots.keys()) { - await this.stopUserBot(chatId); - } - } - - private async handleTradeSignal( - chatId: string, - instance: UserBotInstance, - signal: TradeSignal, - settings: UserSettings - ): Promise { - const trade = signal.trade; - - // Apply filters - const blacklist = settings.blacklistKeywords ? settings.blacklistKeywords.split(',').map(k => k.trim()).filter(Boolean) : []; - const whitelist = settings.whitelistKeywords ? settings.whitelistKeywords.split(',').map(k => k.trim()).filter(Boolean) : []; - - if (blacklist.length > 0 && isMarketBlacklisted(trade.title, blacklist)) { - console.log(`[User ${chatId}] Skipping blacklisted market: ${trade.title}`); - return; - } - - if (whitelist.length > 0 && !isMarketWhitelisted(trade.title, whitelist)) { - console.log(`[User ${chatId}] Skipping non-whitelisted market: ${trade.title}`); - return; - } - - // Probability filter - const price = parseFloat(trade.price); - if (price < settings.minProbability || price > settings.maxProbability) { - console.log(`[User ${chatId}] Skipping: price ${price} outside range [${settings.minProbability}, ${settings.maxProbability}]`); - return; - } - - // Skip sells if copySells is disabled - if (trade.side === 'SELL' && !settings.copySells) { - console.log(`[User ${chatId}] Skipping sell (copySells disabled)`); - return; - } - - // Check max open positions - const state = this.db.getUserState(chatId); - const positions = state.positions || {}; - if (settings.maxOpenPositions > 0 && Object.keys(positions).length >= settings.maxOpenPositions) { - console.log(`[User ${chatId}] Skipping: max open positions reached`); - return; - } - - // Calculate position size - const trackedWallets = this.db.getTrackedWallets(chatId); - const trackerWallet = trackedWallets.find(w => w.walletAddress === trade.proxyWallet); - const allocation = trackerWallet?.allocation || 100; - const tradeUsd = parseFloat(trade.size) * parseFloat(trade.price); - const scaledAmount = Math.min( - tradeUsd * (allocation / 100), - settings.maxPositionSize, - settings.userAccountSize * (settings.maxPercentagePerTrade / 100) - ); - - if (scaledAmount < settings.minTradeSize) { - console.log(`[User ${chatId}] Skipping: scaled amount $${scaledAmount.toFixed(2)} below minimum`); - return; - } - - // Enqueue order - instance.orderQueue.enqueue( - trade, - trackerWallet?.alias || 'Unknown', - trade.proxyWallet, - scaledAmount, - ); - } - - private async executeOrder(chatId: string, instance: UserBotInstance, order: QueuedOrder): Promise { - if (!instance.trader) return; - - const trade = order.trade; - const price = parseFloat(trade.price); - - try { - let result: TradeResult; - - if (trade.side === 'BUY') { - result = await instance.trader.copyTrade(trade, order.amount); - } else { - result = await instance.trader.sellPosition( - trade.asset, - parseFloat(trade.size), - price, - trade.title - ); - } - - // Update state - const state = this.db.getUserState(chatId); - if (!state.positions) state.positions = {}; - if (!state.stats) state.stats = { totalTrades: 0, successfulTrades: 0, failedTrades: 0, totalVolume: 0, startTime: Date.now(), lastUpdateTime: Date.now() }; - if (!state.orders) state.orders = []; - - state.stats.totalTrades++; - state.stats.lastUpdateTime = Date.now(); - - if (result.success) { - state.stats.successfulTrades++; - state.stats.totalVolume += order.amount; - - if (trade.side === 'BUY') { - state.positions[trade.asset] = { - asset: trade.asset, - size: order.amount / price, - avgPrice: price, - title: trade.title, - outcome: trade.outcome, - slug: trade.slug, - entryTime: Date.now(), - walletAlias: order.walletAlias, - conditionId: trade.conditionId, - }; - } else { - delete state.positions[trade.asset]; - } - - // Notify success - const emoji = trade.side === 'BUY' ? '๐ŸŸข' : '๐Ÿ”ด'; - await this.sendNotification(chatId, - `${emoji} Trade Executed\n\n` + - `๐Ÿ“Œ Side: ${trade.side}\n` + - `๐Ÿช Market: ${trade.title}\n` + - `๐ŸŽฒ Outcome: ${trade.outcome}\n` + - `๐Ÿ’ต Amount: $${order.amount.toFixed(2)}\n` + - `๐Ÿ’ฒ Price: ${(price * 100).toFixed(1)}%\n` + - `๐Ÿ‘ค Copied from: ${order.walletAlias}` - ); - } else { - state.stats.failedTrades++; - await this.sendNotification(chatId, - `โŒ Trade Failed\n\n` + - `๐Ÿ“Œ Side: ${trade.side}\n` + - `๐Ÿช Market: ${trade.title}\n` + - `โš ๏ธ Error: ${result.error}` - ); - } - - // Keep last 100 orders - state.orders.unshift({ - id: order.id, - asset: trade.asset, - side: trade.side, - amount: order.amount, - status: result.success ? 'filled' : 'failed', - createdAt: Date.now(), - walletAlias: order.walletAlias, - }); - if (state.orders.length > 100) state.orders = state.orders.slice(0, 100); - - this.db.saveUserState(chatId, state); - } catch (error: any) { - console.error(`[User ${chatId}] Order execution error:`, error.message); - await this.sendNotification(chatId, `โŒ โš ๏ธ Order error: ${error.message}`); - } - } - - private saveUserState(chatId: string, instance: UserBotInstance): void { - try { - const state = this.db.getUserState(chatId); - // Save last seen trade timestamps - if (instance.monitor) { - const lastSeen: Record = {}; - const monitorMap = (instance.monitor as any).lastSeenTrades; - if (monitorMap) { - for (const [addr, ts] of monitorMap.entries()) { - lastSeen[addr] = ts as number; - } - } - state.lastSeenTrades = lastSeen; - } - this.db.saveUserState(chatId, state); - } catch (error: any) { - console.error(`[User ${chatId}] Failed to save state:`, error.message); - } - } - - private async sendNotification(chatId: string, message: string): Promise { - try { - await this.telegramBot.sendMessage(chatId, message, { parse_mode: 'HTML' }); - } catch (error: any) { - console.error(`[User ${chatId}] Failed to send notification:`, error.message); - } - } -} diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts deleted file mode 100644 index d9f66d8..0000000 --- a/src/telegram/bot.ts +++ /dev/null @@ -1,65 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../db/userDb.js'; -import { UserBotManager } from '../services/userBotManager.js'; -import { registerStartHandler } from './handlers/start.js'; -import { registerWalletHandler } from './handlers/wallet.js'; -import { registerFollowHandler } from './handlers/follow.js'; -import { registerSettingsHandler } from './handlers/settings.js'; -import { registerPositionsHandler } from './handlers/positions.js'; -import { registerStatsHandler } from './handlers/stats.js'; -import { registerTradingHandler } from './handlers/trading.js'; -import { registerHelpHandler } from './handlers/help.js'; - -export class TelegramTradingBot { - private bot: TelegramBot; - private db: UserDb; - private botManager: UserBotManager; - - constructor(token: string) { - this.bot = new TelegramBot(token, { polling: true }); - this.db = new UserDb(); - this.botManager = new UserBotManager(this.db, this.bot); - - this.registerHandlers(); - this.setupErrorHandling(); - } - - private registerHandlers(): void { - const getBotManager = () => this.botManager; - - registerStartHandler(this.bot, this.db); - registerWalletHandler(this.bot, this.db); - registerFollowHandler(this.bot, this.db); - registerSettingsHandler(this.bot, this.db); - registerPositionsHandler(this.bot, this.db, getBotManager); - registerStatsHandler(this.bot, this.db); - registerTradingHandler(this.bot, this.db, getBotManager); - registerHelpHandler(this.bot); - } - - private setupErrorHandling(): void { - this.bot.on('polling_error', (error) => { - console.error('[TelegramBot] Polling error:', error.message); - }); - - this.bot.on('error', (error) => { - console.error('[TelegramBot] Error:', error.message); - }); - } - - async stop(): Promise { - console.log('[TelegramBot] Stopping...'); - await this.botManager.stopAll(); - await this.bot.stopPolling(); - this.db.close(); - console.log('[TelegramBot] Stopped'); - } - - getDb(): UserDb { - return this.db; - } - - getBotManager(): UserBotManager { - return this.botManager; - } -} diff --git a/src/telegram/handlers/follow.ts b/src/telegram/handlers/follow.ts deleted file mode 100644 index 319a6b7..0000000 --- a/src/telegram/handlers/follow.ts +++ /dev/null @@ -1,213 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { followMenu, backButton } from '../menus.js'; - -const followFlowState = new Map(); - -export function registerFollowHandler(bot: TelegramBot, db: UserDb): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:follow') { - const tracked = db.getTrackedWallets(chatId); - await bot.editMessageText( - '๐Ÿ‘ Follow Traders\n\n' + - 'Add Polymarket wallets to copy-trade from.\n' + - `Currently following: ${tracked.filter(t => t.enabled).length} trader(s)`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: followMenu(tracked.length > 0), - } - ); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'follow:add') { - followFlowState.set(chatId, { step: 'address' }); - await bot.editMessageText( - 'โž• Add Trader\n\n' + - 'Send the Polymarket wallet address to follow (0x...):\n\n' + - 'You can find trader addresses on polymarket.com/profile/[address]', - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: backButton('menu:follow'), - } - ); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'follow:list') { - const tracked = db.getTrackedWallets(chatId); - if (tracked.length === 0) { - await bot.editMessageText('๐Ÿ“ญ No traders being followed yet.', { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: followMenu(false), - }); - } else { - let text = '๐Ÿ“‹ Followed Traders\n\n'; - tracked.forEach((t, i) => { - const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; - const status = t.enabled ? 'โœ…' : 'โŒ'; - text += `${i + 1}. ${status} ${t.alias}\n`; - text += ` ๐Ÿ“ Address: ${short}\n`; - text += ` ๐Ÿ“Š Allocation: ${t.allocation}%\n\n`; - }); - - const buttons = tracked.map(t => [ - { - text: `${t.enabled ? 'โŒ Disable' : 'โœ… Enable'} ${t.alias}`, - callback_data: `follow:toggle:${t.id}`, - }, - ]); - buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); - - await bot.editMessageText(text, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - } - await bot.answerCallbackQuery(query.id); - } - - if (query.data?.startsWith('follow:toggle:')) { - const id = parseInt(query.data.split(':')[2]); - const enabled = db.toggleTrackedWallet(chatId, id); - await bot.answerCallbackQuery(query.id, { text: enabled ? 'โœ… Trader enabled' : 'โŒ Trader disabled' }); - // Refresh the list - const tracked = db.getTrackedWallets(chatId); - let text = '๐Ÿ“‹ Followed Traders\n\n'; - tracked.forEach((t, i) => { - const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; - const status = t.enabled ? 'โœ…' : 'โŒ'; - text += `${i + 1}. ${status} ${t.alias}\n`; - text += ` ๐Ÿ“ Address: ${short}\n`; - text += ` ๐Ÿ“Š Allocation: ${t.allocation}%\n\n`; - }); - const buttons = tracked.map(t => [ - { - text: `${t.enabled ? 'โŒ Disable' : 'โœ… Enable'} ${t.alias}`, - callback_data: `follow:toggle:${t.id}`, - }, - ]); - buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); - await bot.editMessageText(text, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - } - - if (query.data === 'follow:remove_list') { - const tracked = db.getTrackedWallets(chatId); - const buttons = tracked.map(t => { - const short = `${t.walletAddress.slice(0, 6)}...${t.walletAddress.slice(-4)}`; - return [{ text: `๐Ÿ—‘ ${t.alias} (${short})`, callback_data: `follow:remove:${t.id}` }]; - }); - buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:follow' }]); - await bot.editMessageText('๐Ÿ—‘ Remove Trader\n\nSelect trader to remove:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data?.startsWith('follow:remove:')) { - const id = parseInt(query.data.split(':')[2]); - db.removeTrackedWallet(chatId, id); - const tracked = db.getTrackedWallets(chatId); - await bot.editMessageText('โœ… Trader removed.', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: followMenu(tracked.length > 0), - }); - await bot.answerCallbackQuery(query.id); - } - }); - - // Handle text messages for follow flow - bot.on('message', async (msg) => { - if (!msg.text || msg.text.startsWith('/')) return; - const chatId = msg.chat.id.toString(); - const state = followFlowState.get(chatId); - if (!state) return; - - if (state.step === 'address') { - const addr = msg.text.trim(); - if (!addr.startsWith('0x') || addr.length !== 42) { - await bot.sendMessage(chatId, 'โŒ Invalid address. Must be 42 characters starting with 0x.\n\nTry again:', { - reply_markup: backButton('menu:follow'), - }); - return; - } - state.address = addr; - state.step = 'alias'; - await bot.sendMessage(chatId, - `โœ… Address: ${addr.slice(0, 6)}...${addr.slice(-4)}\n\n` + - `โœ๏ธ Send a name/alias for this trader (e.g., "Whale1", "TopTrader"):`, - { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } - ); - return; - } - - if (state.step === 'alias') { - const alias = msg.text.trim().slice(0, 20); - state.step = 'allocation'; - followFlowState.set(chatId, { ...state, step: 'allocation' }); - await bot.sendMessage(chatId, - `๐Ÿท Name: ${alias}\n\n` + - `๐Ÿ“Š Send allocation percentage (1-100):\n` + - `This controls what % of calculated position size to use for this trader.\n` + - `(100 = full size, 50 = half size)`, - { parse_mode: 'HTML', reply_markup: backButton('menu:follow') } - ); - // Store alias temporarily - (state as any).alias = alias; - return; - } - - if (state.step === 'allocation') { - const allocation = parseInt(msg.text.trim()); - if (isNaN(allocation) || allocation < 1 || allocation > 100) { - await bot.sendMessage(chatId, 'โŒ Invalid allocation. Enter a number between 1 and 100:', { - reply_markup: backButton('menu:follow'), - }); - return; - } - - try { - db.addTrackedWallet(chatId, state.address!, (state as any).alias, allocation); - followFlowState.delete(chatId); - - const tracked = db.getTrackedWallets(chatId); - await bot.sendMessage(chatId, - `โœ… Trader Added!\n\n` + - `๐Ÿท Name: ${(state as any).alias}\n` + - `๐Ÿ“ Address: ${state.address!.slice(0, 6)}...${state.address!.slice(-4)}\n` + - `๐Ÿ“Š Allocation: ${allocation}%`, - { - parse_mode: 'HTML', - reply_markup: followMenu(tracked.length > 0), - } - ); - } catch (err: any) { - await bot.sendMessage(chatId, `โŒ Error: ${err.message}`, { - reply_markup: backButton('menu:follow'), - }); - } - return; - } - }); -} diff --git a/src/telegram/handlers/help.ts b/src/telegram/handlers/help.ts deleted file mode 100644 index 7dbcc95..0000000 --- a/src/telegram/handlers/help.ts +++ /dev/null @@ -1,70 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { mainMenu } from '../menus.js'; - -export function registerHelpHandler(bot: TelegramBot): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:help') { - const helpText = ` -โ“ Polybot Help - -Getting Started: -1. ๐Ÿ”‘ Connect your Polymarket wallet -2. ๐Ÿ‘ Add traders to follow -3. โš™๏ธ Configure your settings -4. โ–ถ๏ธ Start trading! - -๐Ÿ”ง How it works: -โ€ข ๐Ÿ“ก Monitors followed traders in real-time -โ€ข ๐Ÿ”„ Copies their trades to your wallet -โ€ข โš–๏ธ Scales position sizes to your account -โ€ข ๐Ÿ›ก Manages exits via stop-loss, take-profit, trailing stops - -๐Ÿ’ผ Wallet Types: -โ€ข ๐ŸฆŠ Browser - MetaMask/Rabby with Polymarket proxy -โ€ข ๐Ÿ“ง Email - Magic Link exported key -โ€ข ๐Ÿ” EOA - Standalone wallet - -โš™๏ธ Key Settings: -โ€ข ๐ŸŽš Slippage - Max price deviation allowed -โ€ข ๐Ÿ”ป Stop Loss - Auto-sell at loss threshold -โ€ข ๐ŸŽฏ Take Profit - Auto-sell at profit target -โ€ข ๐Ÿ“‰ Trailing Stop - Lock in profits as price rises -โ€ข ๐Ÿงช Dry Run - Test without real trades - -๐Ÿ“‹ Commands: -/start - ๐Ÿ  Main menu -/menu - ๐Ÿ“ฑ Show menu -/help - โ“ This help message - -๐Ÿ’ก Tips: -โ€ข ๐Ÿงช Start with Dry Run mode ON -โ€ข ๐Ÿค Use small position sizes initially -โ€ข ๐ŸŒ Diversify across multiple traders -โ€ข ๐Ÿ›ก Set stop-loss to protect capital - `.trim(); - - await bot.editMessageText(helpText, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: mainMenu(), - }); - await bot.answerCallbackQuery(query.id); - } - - // Handle main menu callback - if (query.data === 'menu:main') { - await bot.editMessageText('๐Ÿค– Main Menu', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: mainMenu(), - }); - await bot.answerCallbackQuery(query.id); - } - }); -} diff --git a/src/telegram/handlers/positions.ts b/src/telegram/handlers/positions.ts deleted file mode 100644 index 5226797..0000000 --- a/src/telegram/handlers/positions.ts +++ /dev/null @@ -1,197 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { positionsMenu, confirmMenu, backButton } from '../menus.js'; -import type { UserBotManager } from '../../services/userBotManager.js'; - -export function registerPositionsHandler(bot: TelegramBot, db: UserDb, getBotManager: () => UserBotManager): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:positions') { - const state = db.getUserState(chatId); - const positions = state.positions ? Object.values(state.positions) : []; - const hasPositions = positions.length > 0; - - await bot.editMessageText( - `๐Ÿ“Š Positions\n\nOpen positions: ${positions.length}`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: positionsMenu(hasPositions), - } - ); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'positions:list') { - const state = db.getUserState(chatId); - const positions = state.positions ? Object.entries(state.positions) : []; - - if (positions.length === 0) { - await bot.editMessageText('๐Ÿ“Š No open positions', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: positionsMenu(false), - }); - } else { - let text = '๐Ÿ“Š Open Positions\n\n'; - (positions as [string, any][]).forEach(([asset, pos], i) => { - text += `${i + 1}. ${pos.outcome}\n`; - text += ` ๐Ÿช Market: ${pos.title?.slice(0, 40) || 'Unknown'}\n`; - text += ` ๐Ÿ“ฆ Size: ${pos.size?.toFixed(4) || '?'} shares @ $${pos.avgPrice?.toFixed(3) || '?'}\n`; - text += ` ๐Ÿ’ต Value: $${((pos.size || 0) * (pos.avgPrice || 0)).toFixed(2)}\n`; - if (pos.walletAlias) text += ` ๐Ÿ‘ค Copied from: ${pos.walletAlias}\n`; - text += '\n'; - }); - - await bot.editMessageText(text, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: positionsMenu(true), - }); - } - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'positions:sell_list') { - const state = db.getUserState(chatId); - const positions = state.positions ? Object.entries(state.positions) : []; - - if (positions.length === 0) { - await bot.answerCallbackQuery(query.id, { text: '๐Ÿ“ญ No positions to sell' }); - return; - } - - const buttons = (positions as [string, any][]).map(([asset, pos]) => [{ - text: `๐Ÿ”ด ${pos.outcome} (${pos.size?.toFixed(2) || '?'} shares)`, - callback_data: `positions:sell:${asset.slice(0, 40)}`, - }]); - buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:positions' }]); - - await bot.editMessageText('๐Ÿ”ด Sell Position\n\nSelect position to sell:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data?.startsWith('positions:sell:')) { - const assetPrefix = query.data.slice(14); - const botManager = getBotManager(); - const userBot = botManager.getUserBot(chatId); - - if (!userBot) { - await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Trading not active. Start trading first.' }); - return; - } - - await bot.editMessageText(`โณ Selling position...`, { - chat_id: chatId, - message_id: query.message.message_id, - }); - - try { - const state = db.getUserState(chatId); - const positions = state.positions || {}; - const matchingAsset = Object.keys(positions).find(a => a.startsWith(assetPrefix)); - - if (matchingAsset && userBot.trader) { - const pos = positions[matchingAsset]; - const result = await userBot.trader.sellPosition(matchingAsset, pos.size, pos.avgPrice || 0.5, pos.title || 'Unknown'); - if (result.success) { - delete positions[matchingAsset]; - state.positions = positions; - db.saveUserState(chatId, state); - await bot.editMessageText(`โœ… Position sold! Order: ${result.orderId || 'completed'}`, { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: positionsMenu(Object.keys(positions).length > 0), - }); - } else { - await bot.editMessageText(`โŒ Sell failed: ${result.error}`, { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: positionsMenu(true), - }); - } - } - } catch (err: any) { - await bot.editMessageText(`โŒ Error: ${err.message}`, { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: positionsMenu(true), - }); - } - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'positions:sell_all') { - await bot.editMessageText('โš ๏ธ Sell ALL positions?\n\nThis will sell all your open positions at market price.', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: confirmMenu('sell_all'), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'confirm:sell_all') { - const botManager = getBotManager(); - const userBot = botManager.getUserBot(chatId); - - if (!userBot || !userBot.trader) { - await bot.editMessageText('โŒ Trading not active.', { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: positionsMenu(false), - }); - await bot.answerCallbackQuery(query.id); - return; - } - - await bot.editMessageText('โณ Selling all positions...', { - chat_id: chatId, - message_id: query.message.message_id, - }); - - const state = db.getUserState(chatId); - const positions = state.positions || {}; - let sold = 0; - let failed = 0; - - for (const [asset, pos] of Object.entries(positions) as [string, any][]) { - try { - const result = await userBot.trader.sellPosition(asset, pos.size, pos.avgPrice || 0.5, pos.title || 'Unknown'); - if (result.success) { - delete positions[asset]; - sold++; - } else { - failed++; - } - } catch { - failed++; - } - } - - state.positions = positions; - db.saveUserState(chatId, state); - - await bot.editMessageText( - `โœ… Sold: ${sold}${failed > 0 ? `, โŒ Failed: ${failed}` : ''}`, - { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: positionsMenu(Object.keys(positions).length > 0), - } - ); - await bot.answerCallbackQuery(query.id); - } - }); -} diff --git a/src/telegram/handlers/settings.ts b/src/telegram/handlers/settings.ts deleted file mode 100644 index 2c4e834..0000000 --- a/src/telegram/handlers/settings.ts +++ /dev/null @@ -1,265 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { - settingsMenu, sizingMenu, riskMenu, filtersMenu, - executionMenu, timeExitsMenu, keywordsMenu, conflictStrategyMenu, - backButton, -} from '../menus.js'; - -// Track which setting is being edited per user -const pendingInput = new Map(); - -// Setting descriptions and validation -const settingInfo: Record number | null; returnMenu: string }> = { - userAccountSize: { label: 'Account Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, - maxPositionSize: { label: 'Max Position Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, - minTradeSize: { label: 'Min Trade Size', unit: '$', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:sizing' }, - maxPercentagePerTrade: { label: 'Max % Per Trade', unit: '%', parse: v => { const n = parseFloat(v); return n > 0 && n <= 100 ? n : null; }, returnMenu: 'settings:sizing' }, - maxPositionValue: { label: 'Max Position Value', unit: '$ (0=unlimited)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:sizing' }, - stopLossPercent: { label: 'Stop Loss', unit: '% (negative, e.g. -25)', parse: v => { const n = parseFloat(v); return n < 0 ? n : null; }, returnMenu: 'settings:risk' }, - takeProfitPercent: { label: 'Take Profit', unit: '%', parse: v => { const n = parseFloat(v); return n > 0 ? n : null; }, returnMenu: 'settings:risk' }, - trailingStopPercent: { label: 'Trailing Stop', unit: '% (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, - dailyLossLimit: { label: 'Daily Loss Limit', unit: '$ (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, - maxOpenPositions: { label: 'Max Open Positions', unit: '(0=unlimited)', parse: v => { const n = parseInt(v); return n >= 0 ? n : null; }, returnMenu: 'settings:risk' }, - minProbability: { label: 'Min Probability', unit: '% (e.g. 5 for 5%)', parse: v => { const n = parseFloat(v); return n >= 0 && n <= 100 ? n / 100 : null; }, returnMenu: 'settings:filters' }, - maxProbability: { label: 'Max Probability', unit: '% (e.g. 95 for 95%)', parse: v => { const n = parseFloat(v); return n > 0 && n <= 100 ? n / 100 : null; }, returnMenu: 'settings:filters' }, - orderSlippagePercent: { label: 'Slippage', unit: '%', parse: v => { const n = parseFloat(v); return n >= 0 && n <= 50 ? n : null; }, returnMenu: 'settings:execution' }, - maxHoldTimeHours: { label: 'Max Hold Time', unit: 'hours (0=disabled)', parse: v => { const n = parseFloat(v); return n >= 0 ? n : null; }, returnMenu: 'settings:time' }, - blacklistKeywords: { label: 'Blacklist Keywords', unit: 'comma-separated', parse: v => null, returnMenu: 'settings:keywords' }, - whitelistKeywords: { label: 'Whitelist Keywords', unit: 'comma-separated', parse: v => null, returnMenu: 'settings:keywords' }, -}; - -export function registerSettingsHandler(bot: TelegramBot, db: UserDb): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - const settings = db.getSettings(chatId); - - // Settings main menu - if (query.data === 'menu:settings') { - await bot.editMessageText('โš™๏ธ Settings\n\nConfigure your trading parameters:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: settingsMenu(), - }); - await bot.answerCallbackQuery(query.id); - } - - // Sub-menus - if (query.data === 'settings:sizing') { - await bot.editMessageText('๐Ÿ’ฐ Position Sizing\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: sizingMenu(settings), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'settings:risk') { - await bot.editMessageText('๐Ÿ›ก Risk Management\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: riskMenu(settings), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'settings:filters') { - await bot.editMessageText('๐Ÿ” Trade Filters\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: filtersMenu(settings), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'settings:execution') { - await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: executionMenu(settings), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'settings:time') { - await bot.editMessageText('โฐ Time-Based Exits\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: timeExitsMenu(settings), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'settings:keywords') { - await bot.editMessageText( - '๐Ÿ“ Keywords\n\n' + - `Blacklist: ${settings.blacklistKeywords || 'none'}\n` + - `Whitelist: ${settings.whitelistKeywords || 'none'}`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: keywordsMenu(settings), - } - ); - await bot.answerCallbackQuery(query.id); - } - - // Set a specific value - if (query.data?.startsWith('set:')) { - const key = query.data.slice(4); - const info = settingInfo[key]; - if (!info) return; - - pendingInput.set(chatId, { key, returnTo: info.returnMenu }); - - await bot.editMessageText( - `โœ๏ธ Set ${info.label}\n\n` + - `Current value: ${(settings as any)[key]}\n` + - `Enter new value (${info.unit}):`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: backButton(info.returnMenu), - } - ); - await bot.answerCallbackQuery(query.id); - } - - // Toggle boolean settings - if (query.data?.startsWith('toggle:')) { - const key = query.data.slice(7); - const currentValue = (settings as any)[key]; - const newValue = !currentValue; - db.updateSetting(chatId, key, newValue); - - await bot.answerCallbackQuery(query.id, { text: `${key} = ${newValue ? 'ON' : 'OFF'}` }); - - // Refresh the appropriate menu - const updatedSettings = db.getSettings(chatId); - if (key === 'copySells') { - await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: executionMenu(updatedSettings), - }); - } else if (key === 'dryRun') { - const { tradingMenu } = await import('../menus.js'); - await bot.editMessageText( - `โ–ถ๏ธ Trading Control\n\nDry Run: ${updatedSettings.dryRun ? 'โœ… Simulated' : 'โŒ LIVE'}`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: tradingMenu(updatedSettings.enableTrading, updatedSettings.dryRun), - } - ); - } - } - - // Conflict strategy selection - if (query.data === 'set:conflictStrategy') { - await bot.editMessageText( - 'โš”๏ธ Conflict Strategy\n\n' + - 'When followed traders make opposite trades:\n\n' + - '๐Ÿฅ‡ First - Follow the first signal\n' + - 'โญ Skip - Don\'t trade on conflicts\n' + - '๐Ÿ‘ฅ Majority - Follow the majority\n' + - '๐Ÿ’Ž Highest Allocation - Follow the highest-allocated trader', - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: conflictStrategyMenu(), - } - ); - await bot.answerCallbackQuery(query.id); - } - - if (query.data?.startsWith('conflict:')) { - const strategy = query.data.slice(9); - db.updateSetting(chatId, 'conflictStrategy', strategy); - await bot.answerCallbackQuery(query.id, { text: `โœ… Strategy: ${strategy}` }); - const updatedSettings = db.getSettings(chatId); - await bot.editMessageText('โšก Execution Settings\n\nTap a setting to change it:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: executionMenu(updatedSettings), - }); - } - }); - - // Handle text input for setting values - bot.on('message', async (msg) => { - if (!msg.text || msg.text.startsWith('/')) return; - const chatId = msg.chat.id.toString(); - const pending = pendingInput.get(chatId); - if (!pending) return; - - const info = settingInfo[pending.key]; - if (!info) { - pendingInput.delete(chatId); - return; - } - - // Handle string settings (keywords) - if (pending.key === 'blacklistKeywords' || pending.key === 'whitelistKeywords') { - const value = msg.text.trim(); - db.updateSetting(chatId, pending.key, value); - pendingInput.delete(chatId); - - const settings = db.getSettings(chatId); - await bot.sendMessage(chatId, - `โœ… ${info.label} updated to: ${value || 'none'}`, - { - parse_mode: 'HTML', - reply_markup: keywordsMenu(settings), - } - ); - return; - } - - // Handle numeric settings - const parsed = info.parse(msg.text.trim()); - if (parsed === null) { - await bot.sendMessage(chatId, `โŒ Invalid value. Expected: ${info.unit}\n\nTry again:`, { - reply_markup: backButton(pending.returnTo), - }); - return; - } - - db.updateSetting(chatId, pending.key, parsed); - pendingInput.delete(chatId); - - const settings = db.getSettings(chatId); - // Send confirmation and show the appropriate sub-menu - const menuFns: Record TelegramBot.InlineKeyboardMarkup> = { - 'settings:sizing': () => sizingMenu(settings), - 'settings:risk': () => riskMenu(settings), - 'settings:filters': () => filtersMenu(settings), - 'settings:execution': () => executionMenu(settings), - 'settings:time': () => timeExitsMenu(settings), - 'settings:keywords': () => keywordsMenu(settings), - }; - - const menuFn = menuFns[pending.returnTo]; - await bot.sendMessage(chatId, - `โœ… ${info.label} updated to: ${parsed}`, - { - parse_mode: 'HTML', - reply_markup: menuFn ? menuFn() : backButton('menu:settings'), - } - ); - }); -} diff --git a/src/telegram/handlers/start.ts b/src/telegram/handlers/start.ts deleted file mode 100644 index a84e1b2..0000000 --- a/src/telegram/handlers/start.ts +++ /dev/null @@ -1,40 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { mainMenu } from '../menus.js'; - -export function registerStartHandler(bot: TelegramBot, db: UserDb): void { - bot.onText(/\/start/, async (msg) => { - const chatId = msg.chat.id.toString(); - const username = msg.from?.username || msg.from?.first_name || null; - db.ensureUser(chatId, username || undefined); - - const wallets = db.getWallets(chatId); - const tracked = db.getTrackedWallets(chatId); - - let welcome = `๐Ÿค– Welcome to Polybot!\n\n`; - welcome += `๐Ÿค Multi-user Polymarket copy-trading bot.\n\n`; - welcome += `๐Ÿ“‹ Status:\n`; - welcome += `โ€ข ๐Ÿ’ผ Wallets: ${wallets.length} connected\n`; - welcome += `โ€ข ๐Ÿ‘ Following: ${tracked.filter(t => t.enabled).length} traders\n\n`; - - if (wallets.length === 0) { - welcome += `๐Ÿ‘‰ Start by connecting your wallet!\n`; - } - - await bot.sendMessage(chatId, welcome, { - parse_mode: 'HTML', - reply_markup: mainMenu(), - }); - }); - - // Also handle /menu to show main menu - bot.onText(/\/menu/, async (msg) => { - const chatId = msg.chat.id.toString(); - db.ensureUser(chatId, msg.from?.username || undefined); - - await bot.sendMessage(chatId, '๐Ÿค– Main Menu', { - parse_mode: 'HTML', - reply_markup: mainMenu(), - }); - }); -} diff --git a/src/telegram/handlers/stats.ts b/src/telegram/handlers/stats.ts deleted file mode 100644 index 8109705..0000000 --- a/src/telegram/handlers/stats.ts +++ /dev/null @@ -1,68 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { backButton } from '../menus.js'; - -export function registerStatsHandler(bot: TelegramBot, db: UserDb): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:stats') { - const state = db.getUserState(chatId); - const stats = state.stats || { - totalTrades: 0, - successfulTrades: 0, - failedTrades: 0, - totalVolume: 0, - }; - - const successRate = stats.totalTrades > 0 - ? ((stats.successfulTrades / stats.totalTrades) * 100).toFixed(1) - : '0.0'; - - const positions = state.positions ? Object.values(state.positions) : []; - const totalValue = (positions as any[]).reduce((sum: number, p: any) => sum + (p.size || 0) * (p.avgPrice || 0), 0); - - // Per-trader stats - let traderStatsText = ''; - if (state.traderStats) { - const entries = Object.entries(state.traderStats) as [string, any][]; - if (entries.length > 0) { - traderStatsText = '\n\n๐Ÿ‘ฅ Per-Trader Stats:\n'; - entries.forEach(([alias, ts]) => { - const winRate = ts.totalTrades > 0 ? ((ts.wins / ts.totalTrades) * 100).toFixed(0) : '0'; - traderStatsText += `โ€ข ${alias}: ${ts.totalTrades} trades, ${winRate}% win, P&L: $${ts.totalPnL?.toFixed(2) || '0.00'}\n`; - }); - } - } - - const message = ` -๐Ÿ“ˆ Trading Statistics - -๐Ÿ”ข Total Trades: ${stats.totalTrades} -โœ… Successful: ${stats.successfulTrades} -โŒ Failed: ${stats.failedTrades} -๐ŸŽฏ Success Rate: ${successRate}% -๐Ÿ’ฐ Total Volume: $${(stats.totalVolume || 0).toFixed(2)} - -๐Ÿ“ฆ Open Positions: ${positions.length} -๐Ÿ’ต Total Position Value: $${totalValue.toFixed(2)} -๐Ÿ“Š Daily P&L: $${(state.dailyPnL || 0).toFixed(2)}${traderStatsText} - `.trim(); - - const buttons = [ - [{ text: '๐Ÿ”„ Refresh', callback_data: 'menu:stats' }], - [{ text: 'โฌ…๏ธ Back', callback_data: 'menu:main' }], - ]; - - await bot.editMessageText(message, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - await bot.answerCallbackQuery(query.id); - } - }); -} diff --git a/src/telegram/handlers/trading.ts b/src/telegram/handlers/trading.ts deleted file mode 100644 index 46d0f9d..0000000 --- a/src/telegram/handlers/trading.ts +++ /dev/null @@ -1,96 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { tradingMenu, backButton } from '../menus.js'; -import type { UserBotManager } from '../../services/userBotManager.js'; - -export function registerTradingHandler(bot: TelegramBot, db: UserDb, getBotManager: () => UserBotManager): void { - - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:trading') { - const settings = db.getSettings(chatId); - const botManager = getBotManager(); - const isRunning = botManager.isUserRunning(chatId); - - const wallets = db.getWallets(chatId); - const tracked = db.getTrackedWallets(chatId).filter(t => t.enabled); - - let statusText = 'โ–ถ๏ธ Trading Control\n\n'; - statusText += `Status: ${isRunning ? '๐ŸŸข RUNNING' : 'โน STOPPED'}\n`; - statusText += `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run' : '๐ŸŸข LIVE'}\n`; - statusText += `๐Ÿ’ผ Wallets: ${wallets.length}\n`; - statusText += `๐Ÿ‘ Following: ${tracked.length} traders\n`; - - if (wallets.length === 0) { - statusText += '\nโš ๏ธ Connect a wallet first!'; - } - if (tracked.length === 0) { - statusText += '\nโš ๏ธ Follow at least one trader first!'; - } - - await bot.editMessageText(statusText, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: tradingMenu(isRunning, settings.dryRun), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'trading:start') { - const wallets = db.getWallets(chatId); - const tracked = db.getTrackedWallets(chatId).filter(t => t.enabled); - - if (wallets.length === 0) { - await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Connect a wallet first!', show_alert: true }); - return; - } - if (tracked.length === 0) { - await bot.answerCallbackQuery(query.id, { text: 'โš ๏ธ Follow at least one trader first!', show_alert: true }); - return; - } - - const botManager = getBotManager(); - try { - await botManager.startUserBot(chatId); - const settings = db.getSettings(chatId); - - await bot.editMessageText( - `๐ŸŸข Trading Started!\n\n` + - `Mode: ${settings.dryRun ? '๐Ÿ”ถ Dry Run (simulated)' : '๐ŸŸข LIVE'}\n` + - `๐Ÿ‘ Following: ${tracked.length} trader(s)\n` + - `๐Ÿ’ฐ Max position: $${settings.maxPositionSize}`, - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: tradingMenu(true, settings.dryRun), - } - ); - } catch (err: any) { - await bot.editMessageText(`โŒ Failed to start: ${err.message}`, { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: tradingMenu(false, db.getSettings(chatId).dryRun), - }); - } - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'trading:stop') { - const botManager = getBotManager(); - await botManager.stopUserBot(chatId); - const settings = db.getSettings(chatId); - - await bot.editMessageText('โน Trading Stopped', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: tradingMenu(false, settings.dryRun), - }); - await bot.answerCallbackQuery(query.id); - } - }); -} diff --git a/src/telegram/handlers/wallet.ts b/src/telegram/handlers/wallet.ts deleted file mode 100644 index f8ce228..0000000 --- a/src/telegram/handlers/wallet.ts +++ /dev/null @@ -1,238 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; -import { UserDb } from '../../db/userDb.js'; -import { walletsMenu, walletTypeMenu, backButton } from '../menus.js'; -import { privateKeyToAccount } from 'viem/accounts'; - -// Track conversation state for wallet connection flow -const walletFlowState = new Map(); - -export function registerWalletHandler(bot: TelegramBot, db: UserDb): void { - - // Handle menu:wallets callback - bot.on('callback_query', async (query) => { - if (!query.data || !query.message) return; - const chatId = query.message.chat.id.toString(); - - if (query.data === 'menu:wallets') { - const wallets = db.getWallets(chatId); - await bot.editMessageText('๐Ÿ”‘ Wallet Management\n\nConnect your Polymarket wallet to start trading.', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: walletsMenu(wallets.length > 0), - }); - await bot.answerCallbackQuery(query.id); - } - - if (query.data === 'wallet:connect') { - await bot.editMessageText( - '๐Ÿ”‘ Connect Wallet\n\n' + - 'Select your wallet type:\n\n' + - '๐ŸฆŠ Browser Wallet - MetaMask/Rabby with Polymarket proxy (most common)\n' + - '๐Ÿ“ง Email Wallet - Magic Link login, exported private key\n' + - '๐Ÿ” EOA - Standalone wallet, signer is funder', - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: walletTypeMenu(), - } - ); - await bot.answerCallbackQuery(query.id); - } - - // Wallet type selection - if (query.data?.startsWith('wallet:type:')) { - const signatureType = parseInt(query.data.split(':')[2]); - walletFlowState.set(chatId, { step: 'private_key', signatureType }); - - await bot.editMessageText( - '๐Ÿ” Step 1: Private Key\n\n' + - 'Send your private key (starts with 0x).\n\n' + - 'โš ๏ธ Your key will be encrypted and stored securely.\n' + - 'โš ๏ธ Delete your message after sending!', - { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: backButton('menu:wallets'), - } - ); - await bot.answerCallbackQuery(query.id); - } - - // List wallets - if (query.data === 'wallet:list') { - const wallets = db.getWallets(chatId); - if (wallets.length === 0) { - await bot.editMessageText('๐Ÿ“ญ No wallets connected.', { - chat_id: chatId, - message_id: query.message.message_id, - reply_markup: walletsMenu(false), - }); - } else { - let text = '๐Ÿ“‹ Your Wallets\n\n'; - wallets.forEach((w, i) => { - const addr = w.walletAddress; - const short = `${addr.slice(0, 6)}...${addr.slice(-4)}`; - const defaultTag = w.isDefault ? ' โญ Default' : ''; - const typeNames: Record = { 0: 'EOA', 1: 'Email', 2: 'Browser' }; - text += `${i + 1}. ${w.alias}${defaultTag}\n`; - text += ` ๐Ÿ“ Address: ${short}\n`; - text += ` ๐Ÿท Type: ${typeNames[w.signatureType] || 'Unknown'}\n\n`; - }); - await bot.editMessageText(text, { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: walletsMenu(true), - }); - } - await bot.answerCallbackQuery(query.id); - } - - // Remove wallet list - if (query.data === 'wallet:remove_list') { - const wallets = db.getWallets(chatId); - const buttons = wallets.map(w => { - const short = `${w.walletAddress.slice(0, 6)}...${w.walletAddress.slice(-4)}`; - return [{ text: `๐Ÿ—‘ ${w.alias} (${short})`, callback_data: `wallet:remove:${w.id}` }]; - }); - buttons.push([{ text: 'โฌ…๏ธ Back', callback_data: 'menu:wallets' }]); - await bot.editMessageText('๐Ÿ—‘ Remove Wallet\n\nSelect wallet to remove:', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: { inline_keyboard: buttons }, - }); - await bot.answerCallbackQuery(query.id); - } - - // Remove specific wallet - if (query.data?.startsWith('wallet:remove:')) { - const walletId = parseInt(query.data.split(':')[2]); - db.removeWallet(chatId, walletId); - const wallets = db.getWallets(chatId); - await bot.editMessageText('โœ… Wallet removed successfully.', { - chat_id: chatId, - message_id: query.message.message_id, - parse_mode: 'HTML', - reply_markup: walletsMenu(wallets.length > 0), - }); - await bot.answerCallbackQuery(query.id); - } - }); - - // Handle text messages for wallet connection flow - bot.on('message', async (msg) => { - if (!msg.text || msg.text.startsWith('/')) return; - const chatId = msg.chat.id.toString(); - const state = walletFlowState.get(chatId); - if (!state) return; - - if (state.step === 'private_key') { - const key = msg.text.trim(); - - // Try to delete the message containing the private key for security - try { await bot.deleteMessage(chatId, msg.message_id); } catch {} - - if (!key.startsWith('0x') || key.length < 64) { - await bot.sendMessage(chatId, 'โŒ Invalid private key. Must start with 0x and be 66 characters long.\n\nTry again or press Back.', { - parse_mode: 'HTML', - reply_markup: backButton('menu:wallets'), - }); - return; - } - - try { - // Derive address from private key - const account = privateKeyToAccount(key as `0x${string}`); - state.privateKey = key; - state.walletAddress = account.address; - - if (state.signatureType === 0) { - // EOA: signer is funder, no need for funder address - state.funderAddress = account.address; - state.step = 'alias'; - await bot.sendMessage(chatId, - `โœ… Key accepted!\n\n` + - `๐Ÿ“ Address: ${account.address}\n\n` + - `Step 2: โœ๏ธ Send a name/alias for this wallet (e.g., "Main", "Trading"):`, - { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } - ); - } else { - // Need funder/proxy address - state.step = 'funder_address'; - await bot.sendMessage(chatId, - `โœ… Key accepted!\n\n` + - `๐Ÿ“ Signer: ${account.address}\n\n` + - `Step 2: ๐Ÿ“ฌ Send your Polymarket proxy/funder wallet address:`, - { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } - ); - } - } catch (err: any) { - await bot.sendMessage(chatId, `โŒ Invalid private key: ${err.message}\n\nTry again:`, { - reply_markup: backButton('menu:wallets'), - }); - } - return; - } - - if (state.step === 'funder_address') { - const addr = msg.text.trim(); - if (!addr.startsWith('0x') || addr.length !== 42) { - await bot.sendMessage(chatId, 'โŒ Invalid address. Must be 42 characters starting with 0x.\n\nTry again:', { - reply_markup: backButton('menu:wallets'), - }); - return; - } - state.funderAddress = addr; - state.step = 'alias'; - await bot.sendMessage(chatId, - `โœ… Funder address set!\n\n` + - `Step 3: โœ๏ธ Send a name/alias for this wallet (e.g., "Main", "Trading"):`, - { parse_mode: 'HTML', reply_markup: backButton('menu:wallets') } - ); - return; - } - - if (state.step === 'alias') { - const alias = msg.text.trim().slice(0, 20); - - try { - db.addWallet( - chatId, - state.privateKey!, - state.walletAddress!, - state.funderAddress, - alias, - state.signatureType - ); - - walletFlowState.delete(chatId); - - const short = `${state.walletAddress!.slice(0, 6)}...${state.walletAddress!.slice(-4)}`; - await bot.sendMessage(chatId, - `โœ… Wallet Connected!\n\n` + - `๐Ÿท Name: ${alias}\n` + - `๐Ÿ“ Address: ${short}\n\n` + - `๐ŸŽ‰ You can now follow traders and start copy-trading!`, - { - parse_mode: 'HTML', - reply_markup: walletsMenu(true), - } - ); - } catch (err: any) { - await bot.sendMessage(chatId, `โŒ Error saving wallet: ${err.message}`, { - reply_markup: backButton('menu:wallets'), - }); - } - return; - } - }); -} - -// Export for cleanup -export function clearWalletFlowState(chatId: string): void { - walletFlowState.delete(chatId); -} diff --git a/src/telegram/menus.ts b/src/telegram/menus.ts deleted file mode 100644 index 3d2a8c0..0000000 --- a/src/telegram/menus.ts +++ /dev/null @@ -1,182 +0,0 @@ -import TelegramBot from 'node-telegram-bot-api'; - -type InlineButton = TelegramBot.InlineKeyboardButton; -type InlineMarkup = TelegramBot.InlineKeyboardMarkup; - -function btn(text: string, callbackData: string): InlineButton { - return { text, callback_data: callbackData }; -} - -export function mainMenu(): InlineMarkup { - return { - inline_keyboard: [ - [btn('๐Ÿ”‘ Wallets', 'menu:wallets'), btn('๐Ÿ‘ Follow Traders', 'menu:follow')], - [btn('โš™๏ธ Settings', 'menu:settings'), btn('๐Ÿ“Š Positions', 'menu:positions')], - [btn('๐Ÿ“ˆ Stats', 'menu:stats'), btn('โ–ถ๏ธ Trading', 'menu:trading')], - [btn('โ“ Help', 'menu:help')], - ], - }; -} - -export function walletsMenu(hasWallets: boolean): InlineMarkup { - const buttons: InlineButton[][] = [ - [btn('โž• Connect Wallet', 'wallet:connect')], - ]; - if (hasWallets) { - buttons.push([btn('๐Ÿ“‹ My Wallets', 'wallet:list'), btn('๐Ÿ—‘ Remove Wallet', 'wallet:remove_list')]); - } - buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); - return { inline_keyboard: buttons }; -} - -export function walletTypeMenu(): InlineMarkup { - return { - inline_keyboard: [ - [btn('๐ŸฆŠ Browser Wallet (MetaMask/Rabby)', 'wallet:type:2')], - [btn('๐Ÿ“ง Email Wallet (Magic Link)', 'wallet:type:1')], - [btn('๐Ÿ” EOA (Standalone)', 'wallet:type:0')], - [btn('โฌ…๏ธ Back', 'menu:wallets')], - ], - }; -} - -export function followMenu(hasTracked: boolean): InlineMarkup { - const buttons: InlineButton[][] = [ - [btn('โž• Add Trader', 'follow:add')], - ]; - if (hasTracked) { - buttons.push([btn('๐Ÿ“‹ List Traders', 'follow:list'), btn('๐Ÿ—‘ Remove Trader', 'follow:remove_list')]); - } - buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); - return { inline_keyboard: buttons }; -} - -export function settingsMenu(): InlineMarkup { - return { - inline_keyboard: [ - [btn('๐Ÿ’ฐ Position Sizing', 'settings:sizing'), btn('๐Ÿ›ก Risk Management', 'settings:risk')], - [btn('๐Ÿ” Trade Filters', 'settings:filters'), btn('โšก Execution', 'settings:execution')], - [btn('โฐ Time Exits', 'settings:time'), btn('๐Ÿ“ Keywords', 'settings:keywords')], - [btn('โฌ…๏ธ Back', 'menu:main')], - ], - }; -} - -export function sizingMenu(settings: any): InlineMarkup { - return { - inline_keyboard: [ - [btn(`๐Ÿ’ต Account Size: $${settings.userAccountSize}`, 'set:userAccountSize')], - [btn(`๐Ÿ“ Max Position: $${settings.maxPositionSize}`, 'set:maxPositionSize')], - [btn(`๐Ÿ”ป Min Trade: $${settings.minTradeSize}`, 'set:minTradeSize')], - [btn(`๐Ÿ“Š Max % Per Trade: ${settings.maxPercentagePerTrade}%`, 'set:maxPercentagePerTrade')], - [btn(`๐Ÿท Max Position Value: $${settings.maxPositionValue || 'โˆž'}`, 'set:maxPositionValue')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function riskMenu(settings: any): InlineMarkup { - return { - inline_keyboard: [ - [btn(`๐Ÿ”ป Stop Loss: ${settings.stopLossPercent}%`, 'set:stopLossPercent')], - [btn(`๐ŸŽฏ Take Profit: +${settings.takeProfitPercent}%`, 'set:takeProfitPercent')], - [btn(`๐Ÿ“‰ Trailing Stop: ${settings.trailingStopPercent}%`, 'set:trailingStopPercent')], - [btn(`๐Ÿšซ Daily Loss Limit: $${settings.dailyLossLimit || 'โˆž'}`, 'set:dailyLossLimit')], - [btn(`๐Ÿ“ฆ Max Open Positions: ${settings.maxOpenPositions || 'โˆž'}`, 'set:maxOpenPositions')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function filtersMenu(settings: any): InlineMarkup { - return { - inline_keyboard: [ - [btn(`โฌ‡๏ธ Min Probability: ${(settings.minProbability * 100).toFixed(0)}%`, 'set:minProbability')], - [btn(`โฌ†๏ธ Max Probability: ${(settings.maxProbability * 100).toFixed(0)}%`, 'set:maxProbability')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function executionMenu(settings: any): InlineMarkup { - return { - inline_keyboard: [ - [btn(`๐ŸŽš Slippage: ${settings.orderSlippagePercent}%`, 'set:orderSlippagePercent')], - [btn(`๐Ÿ“ค Copy Sells: ${settings.copySells ? 'โœ…' : 'โŒ'}`, 'toggle:copySells')], - [btn(`โš”๏ธ Conflict Strategy: ${settings.conflictStrategy}`, 'set:conflictStrategy')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function timeExitsMenu(settings: any): InlineMarkup { - return { - inline_keyboard: [ - [btn(`โฑ Max Hold Time: ${settings.maxHoldTimeHours || 'OFF'}h`, 'set:maxHoldTimeHours')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function keywordsMenu(settings: any): InlineMarkup { - const bl = settings.blacklistKeywords || 'none'; - const wl = settings.whitelistKeywords || 'none'; - return { - inline_keyboard: [ - [btn(`๐Ÿšซ Blacklist: ${bl.length > 20 ? bl.slice(0, 20) + '...' : bl}`, 'set:blacklistKeywords')], - [btn(`โœ… Whitelist: ${wl.length > 20 ? wl.slice(0, 20) + '...' : wl}`, 'set:whitelistKeywords')], - [btn('โฌ…๏ธ Back', 'menu:settings')], - ], - }; -} - -export function conflictStrategyMenu(): InlineMarkup { - return { - inline_keyboard: [ - [btn('๐Ÿฅ‡ First Signal Wins', 'conflict:first')], - [btn('โญ Skip Conflicts', 'conflict:skip')], - [btn('๐Ÿ‘ฅ Majority Rules', 'conflict:majority')], - [btn('๐Ÿ’Ž Highest Allocation', 'conflict:highest_allocation')], - [btn('โฌ…๏ธ Back', 'settings:execution')], - ], - }; -} - -export function tradingMenu(isRunning: boolean, isDryRun: boolean): InlineMarkup { - const buttons: InlineButton[][] = []; - - if (isRunning) { - buttons.push([btn('โน Stop Trading', 'trading:stop')]); - } else { - buttons.push([btn('โ–ถ๏ธ Start Trading', 'trading:start')]); - } - - buttons.push([btn(`๐Ÿงช Dry Run: ${isDryRun ? 'โœ… ON' : 'โŒ OFF'}`, 'toggle:dryRun')]); - buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); - - return { inline_keyboard: buttons }; -} - -export function positionsMenu(hasPositions: boolean): InlineMarkup { - const buttons: InlineButton[][] = []; - if (hasPositions) { - buttons.push([btn('๐Ÿ“‹ View All', 'positions:list')]); - buttons.push([btn('๐Ÿ”ด Sell Position', 'positions:sell_list'), btn('๐Ÿ”ด Sell All', 'positions:sell_all')]); - } - buttons.push([btn('โฌ…๏ธ Back', 'menu:main')]); - return { inline_keyboard: buttons }; -} - -export function confirmMenu(action: string): InlineMarkup { - return { - inline_keyboard: [ - [btn('โœ… Yes, confirm', `confirm:${action}`), btn('โŒ Cancel', 'menu:main')], - ], - }; -} - -export function backButton(target: string): InlineMarkup { - return { - inline_keyboard: [[btn('โฌ…๏ธ Back', target)]], - }; -}