-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbot.js
More file actions
executable file
·223 lines (197 loc) · 9.12 KB
/
bot.js
File metadata and controls
executable file
·223 lines (197 loc) · 9.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env node
/**
* bot.js - Single Bot Instance Launcher
*
* PM2-friendly entry point for single grid trading bot.
* Standalone launcher executed by PM2 for each configured bot.
* Handles bot initialization, authentication, and continuous trading loop.
*
* ===============================================================================
* STARTUP SEQUENCE
* ===============================================================================
*
* 1. BOT CONFIGURATION LOADING
* - Reads bot settings from profiles/bots.json by bot name (from argv)
* - Validates bot exists in configuration
* - Reports market pair and account being used
* - Loads trading parameters (grid size, spread, order count, etc.)
*
* 2. AUTHENTICATION
* - First attempts credential daemon (Unix socket) for pre-decrypted key
* - Falls back to interactive master password prompt if daemon unavailable
* - Master password never stored in environment variables
* - Private key loaded directly to bot memory
*
* 3. BOT INITIALIZATION
* - Waits for BitShares blockchain connection (30 second timeout)
* - Uses pre-decrypted private key for transaction signing
* - Resolves account ID from BitShares
* - Initializes OrderManager with bot configuration
* - Sets up event handlers for fills and blockchain updates
*
* 4. GRID INITIALIZATION OR RESUME
* - Loads persisted grid snapshot if it exists and matches on-chain orders
* - Validates persisted grid against current blockchain state (reconciliation)
* - Detects offline fills and updates fund accounting automatically
* - Creates fresh grid if no valid persisted state found
* - Synchronizes grid state with BitShares blockchain
* - Places initial orders to reach target count
* - Note: Grid uses Copy-on-Write pattern for safe rebalancing (isolated working copies)
*
* 5. TRADING LOOP
* - Continuously monitors for fill events via blockchain subscriptions
* - Updates order status from chain data
* - Processes fills and updates fund accounting
* - Regenerates/rebalances grid as needed
* - Runs indefinitely (PM2 manages restart/stop/monitoring)
*/
const fs = require('fs');
const path = require('path');
const accountBots = require('./modules/account_bots');
const { parseJsonWithComments } = accountBots;
const { createBotKey } = require('./modules/account_orders');
const DEXBot = require('./modules/dexbot_class');
const { normalizeBotEntry } = require('./modules/dexbot_class');
const { readBotsFileSync } = require('./modules/bots_file_lock');
const { setupGracefulShutdown, registerCleanup } = require('./modules/graceful_shutdown');
// Setup graceful shutdown handlers
setupGracefulShutdown();
const PROFILES_BOTS_FILE = path.join(__dirname, 'profiles', 'bots.json');
// Get bot name from args or environment
// Support both direct names (node bot.js botname) and flag format (node bot.js --botname)
// Flag format is used by PM2 for consistency with other CLI tools
let botNameArg = process.argv[2];
if (botNameArg && botNameArg.startsWith('--')) {
// Strip '--' prefix if present (e.g., --mybot becomes mybot)
botNameArg = botNameArg.substring(2);
}
const botNameEnv = process.env.BOT_NAME || process.env.PREFERRED_ACCOUNT;
const botName = botNameArg || botNameEnv;
if (!botName) {
console.error('[bot.js] No bot name provided. Usage: node bot.js <bot-name>');
console.error('[bot.js] Or set BOT_NAME or PREFERRED_ACCOUNT environment variable');
process.exit(1);
}
console.log(`[bot.js] Starting bot: ${botName}`);
/**
* Loads the configuration for a specific bot from profiles/bots.json.
* @param {string} name - The name of the bot to load.
* @returns {Object} The bot configuration entry.
* @throws {Error} If profiles/bots.json is missing or bot not found.
*/
function loadBotConfig(name) {
if (!fs.existsSync(PROFILES_BOTS_FILE)) {
console.error('[bot.js] profiles/bots.json not found. Run: dexbot bots');
process.exit(1);
}
try {
const { config } = readBotsFileSync(PROFILES_BOTS_FILE, parseJsonWithComments);
const bots = config.bots || [];
const botEntry = bots.find(b => b.name === name);
if (!botEntry) {
console.error(`[bot.js] Bot '${name}' not found in profiles/bots.json`);
console.error(`[bot.js] Available bots: ${bots.map(b => b.name).join(', ') || 'none'}`);
process.exit(1);
}
return botEntry;
} catch (err) {
console.error(`[bot.js] Error loading bot config:`, err.message);
process.exit(1);
}
}
/**
* Get private key for account from daemon or interactive prompt.
* Tries daemon first (if running), then falls back to interactive master password prompt.
* @param {string} accountName - The account name to retrieve key for.
* @returns {Promise<string>} The decrypted private key.
* @throws {Error} If both daemon and interactive authentication fail.
*/
async function getPrivateKeyForAccount(accountName) {
const chainKeys = require('./modules/chain_keys');
// Try daemon first
if (chainKeys.isDaemonReady()) {
console.log('[bot.js] Requesting private key from credential daemon...');
try {
const privateKey = await chainKeys.getPrivateKeyFromDaemon(accountName);
console.log('[bot.js] Private key loaded from daemon');
return privateKey;
} catch (err) {
console.warn('[bot.js] Daemon request failed:', err.message);
console.log('[bot.js] Falling back to interactive authentication...\n');
}
} else {
console.log('[bot.js] Credential daemon not available');
console.log('[bot.js] Falling back to interactive authentication...\n');
}
// Fallback to interactive master password prompt
const originalLog = console.log;
try {
console.log('[bot.js] Prompting for master password...');
// Suppress BitShares client logs during password prompt
console.log = (...args) => {
const msg = args.join(' ');
if (!msg.includes('bitshares_client') && !msg.includes('modules/')) {
originalLog(...args);
}
};
const masterPassword = await chainKeys.authenticate();
// Restore console before getting key
console.log = originalLog;
console.log('[bot.js] Master password authenticated');
// Get the private key using master password
const privateKey = chainKeys.getPrivateKey(accountName, masterPassword);
return privateKey;
} catch (err) {
console.log = originalLog;
if (err && err.message && err.message.includes('No master password set')) {
throw err;
}
throw err;
}
}
// Main entry point
(async () => {
try {
// Load bot configuration
const botConfig = loadBotConfig(botName);
console.log(`[bot.js] Loaded configuration for bot: ${botName}`);
console.log(`[bot.js] Market: ${botConfig.assetA}-${botConfig.assetB}, Account: ${botConfig.preferredAccount}`);
// Load all bots from configuration to prevent pruning other active bots
const allBotsConfig = readBotsFileSync(PROFILES_BOTS_FILE, parseJsonWithComments).config.bots || [];
// Normalize all active bots with their correct indices in the unfiltered array
// CRITICAL: Index must be based on position in allBotsConfig, not in filtered array.
// The index is embedded in botKey (e.g., "bot-0", "bot-1"), determining file names.
// If index changes, the bot loses access to persisted state files.
const allActiveBots = allBotsConfig
.map((b, idx) => b.active !== false ? normalizeBotEntry(b, idx) : null)
.filter(b => b !== null);
// Find the current bot's index in the unfiltered bots.json array
const botIndex = allBotsConfig.findIndex(b => b.name === botName);
if (botIndex === -1) {
throw new Error(`Bot "${botName}" not found in ${PROFILES_BOTS_FILE}`);
}
// Normalize config for current bot with correct index from unfiltered array
const normalizedConfig = normalizeBotEntry(botConfig, botIndex);
// Get private key from daemon or interactively
const preferredAccount = normalizedConfig.preferredAccount;
const privateKey = await getPrivateKeyForAccount(preferredAccount);
// Create and start bot with log prefix for [bot.js] context
const bot = new DEXBot(normalizedConfig, { logPrefix: '[bot.js]' });
try {
// Register bot cleanup on shutdown
registerCleanup(`Bot: ${botName}`, () => bot.shutdown());
await bot.startWithPrivateKey(privateKey);
} catch (err) {
// Attempt graceful cleanup before exiting
try {
await bot.shutdown();
} catch (shutdownErr) {
console.error('[bot.js] Error during cleanup:', shutdownErr.message);
}
throw err;
}
} catch (err) {
console.error('[bot.js] Failed to start bot:', err.message);
process.exit(1);
}
})();