-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
384 lines (345 loc) · 11.6 KB
/
index.js
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
const { Bot, InlineKeyboard } = require("grammy");
const { Coinbase, Wallet } = require("@coinbase/coinbase-sdk");
const PouchDB = require('pouchdb');
const Decimal = require("decimal.js");
const Web3 = require("web3");
const crypto = require("crypto");
// Ensure environment variables are set.
require("dotenv").config();
const requiredEnvVars = [
"TELEGRAM_BOT_TOKEN",
"COINBASE_API_KEY_NAME",
"COINBASE_API_KEY_SECRET",
"ENCRYPTION_KEY",
];
requiredEnvVars.forEach((env) => {
if (!process.env[env]) {
throw new Error(`missing ${env} environment variable`);
}
});
// Create a bot object
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);
// In-memory storage for user states
const userStates = {};
// Local database for storing wallets.
const db = new PouchDB('myapp');
// Initialize Coinbase SDK
new Coinbase({
apiKeyName: process.env.COINBASE_API_KEY_NAME,
privateKey: process.env.COINBASE_API_KEY_SECRET,
});
// Helper functions
const updateUserState = (user, state) => {
userStates[user.id] = { ...userStates[user.id], ...state };
};
const clearUserState = (user) => {
delete userStates[user.id];
};
const sendReply = async (ctx, text, options = {}) => {
const message = await ctx.reply(text, options);
updateUserState(ctx.from, { messageId: message.message_id });
};
const handleUserState = async (ctx, handler) => {
const userState = userStates[ctx.from.id] || {};
if (
ctx.message.reply_to_message &&
ctx.message.reply_to_message.message_id === userState.messageId
) {
await handler(ctx);
} else {
await ctx.reply("Please select an option from the menu.");
}
};
// Bot command handlers
bot.command("start", async (ctx) => {
const { from: user } = ctx;
updateUserState(user, {});
userAddress = await getOrCreateAddress(user);
const keyboard = new InlineKeyboard()
.text("Check Balance", "check_balance")
.row()
.text("Deposit ETH", "deposit_eth")
.row()
.text("Withdraw ETH", "withdraw_eth")
.row()
.text("Buy", "buy")
.text("Sell", "sell")
.row()
.text("Export key", "export_key")
.text("Pin message", "pin_message");
const welcomeMessage = `
*Welcome to your Onchain Trading Bot!*
Your Base address is ${userAddress.getId()}.
Select an option below:`;
await sendReply(ctx, welcomeMessage, {
reply_markup: keyboard,
parse_mode: "Markdown",
});
});
// Callback query handlers
const callbackHandlers = {
check_balance: handleCheckBalance,
deposit_eth: handleDeposit,
withdraw_eth: handleInitialWithdrawal,
buy: handleInitialBuy,
sell: handleInitialSell,
pin_message: handlePinMessage,
export_key: handleExportKey,
};
bot.on("callback_query:data", async (ctx) => {
const handler = callbackHandlers[ctx.callbackQuery.data];
if (handler) {
await ctx.answerCallbackQuery();
await handler(ctx);
} else {
await ctx.reply("Unknown button clicked!");
}
console.log(
`User ID: ${ctx.from.id}, Username: ${ctx.from.username}, First Name: ${ctx.from.first_name}`,
);
});
// Handle user messages
bot.on("message:text", async (ctx) =>
handleUserState(ctx, async () => {
const userState = userStates[ctx.from.id] || {};
if (userState.withdrawalRequested) await handleWithdrawal(ctx);
else if (userState.buyRequested) await handleBuy(ctx);
else if (userState.sellRequested) await handleSell(ctx);
}),
);
// Get or create the user's address
async function getOrCreateAddress(user) {
if (userStates.address) {
return userStates.address;
}
let wallet;
try {
const result = await db.get(user.id.toString());
const { ivString, encryptedWalletData } = result;
const iv = Buffer.from(ivString, "hex");
const walletData = JSON.parse(decrypt(encryptedWalletData, iv));
wallet = await Wallet.import(walletData);
} catch (error) {
if (err.name === 'not_found' || err.status === 404) {
wallet = await Wallet.create({ networkId: "base-mainnet" });
const iv = crypto.randomBytes(16);
const encryptedWalletData = encrypt(JSON.stringify(wallet.export()), iv);
await db.put({
_id: user.id.toString(),
ivString: iv.toString("hex"),
encryptedWalletData,
});
} else {
console.log('Error fetching from local database: ', error);
}
}
updateUserState(user, { address: wallet.getDefaultAddress() });
return wallet.getDefaultAddress();
}
// Handle checking balance
async function handleCheckBalance(ctx) {
const userAddress = await getOrCreateAddress(ctx.from);
const balanceMap = await userAddress.listBalances();
const balancesString =
balanceMap.size > 0
? balanceMap.toString().slice(11, -1)
: "You have no balances.";
await sendReply(
ctx,
`Your current balances are as follows:\n${balancesString}`,
);
}
// Handle deposits
async function handleDeposit(ctx) {
const userAddress = await getOrCreateAddress(ctx.from);
await sendReply(
ctx,
"_Note: As this is a test app, make sure to deposit only small amounts of ETH!_",
{ parse_mode: "Markdown" },
);
await sendReply(
ctx,
"Please send your ETH to the following address on Base:",
);
await sendReply(ctx, `${userAddress.getId()}`, { parse_mode: "Markdown" });
}
// Handle initial withdrawal request
async function handleInitialWithdrawal(ctx) {
updateUserState(ctx.from, { withdrawalRequested: true });
await sendReply(
ctx,
"Please respond with the amount of ETH you want to withdraw.",
{ reply_markup: { force_reply: true } },
);
}
// Handle withdrawals
async function handleWithdrawal(ctx) {
const userState = userStates[ctx.from.id] || {};
if (!userState.withdrawalAmount) {
const withdrawalAmount = parseFloat(ctx.message.text);
if (isNaN(withdrawalAmount)) {
await ctx.reply("Invalid withdrawal amount. Please try again.");
clearUserState(ctx.from);
} else {
const userAddress = await getOrCreateAddress(ctx.from);
const currentBalance = await userAddress.getBalance(Coinbase.assets.Eth);
if (new Decimal(withdrawalAmount).greaterThan(currentBalance)) {
await ctx.reply("You do not have enough ETH to withdraw that amount.");
clearUserState(ctx.from);
} else {
await sendReply(
ctx,
"Please respond with the address, ENS name, or Base name at which you would like to receive the ETH.",
{ reply_markup: { force_reply: true } },
);
updateUserState(ctx.from, {
withdrawalAmount,
});
}
}
} else {
const destination = ctx.message.text;
if (!Web3.utils.isAddress(destination) && !destination.endsWith(".eth")) {
await ctx.reply("Invalid destination address. Please try again.");
clearUserState(ctx.from);
return;
}
const userAddress = await getOrCreateAddress(ctx.from);
try {
await sendReply(ctx, "Initiating withdrawal...");
const transfer = await userAddress.createTransfer({
amount: userState.withdrawalAmount,
assetId: Coinbase.assets.Eth,
destination: destination,
});
await transfer.wait();
await sendReply(
ctx,
`Successfully completed withdrawal: [Basescan Link](${transfer.getTransactionLink()})`,
{ parse_mode: "Markdown" },
);
clearUserState(ctx.from);
} catch (error) {
await ctx.reply("An error occurred while initiating the transfer.");
console.error(error);
clearUserState(ctx.from);
}
}
}
// Handle buy request
async function handleInitialBuy(ctx) {
await handleTradeInit(ctx, "buy");
}
// Handle buys
async function handleBuy(ctx) {
await executeTrade(ctx, "buy");
}
// Handle sell request
async function handleInitialSell(ctx) {
await handleTradeInit(ctx, "sell");
}
// Handle sells
async function handleSell(ctx) {
await executeTrade(ctx, "sell");
}
// Initialize trade (Buy/Sell)
async function handleTradeInit(ctx, type) {
const prompt =
type === "buy"
? "Please respond with the asset you would like to buy (ticker or contract address)."
: "Please respond with the asset you would like to sell (ticker or contract address).";
updateUserState(ctx.from, { [`${type}Requested`]: true });
await sendReply(ctx, prompt, { reply_markup: { force_reply: true } });
}
// Generalized function to execute trades
async function executeTrade(ctx, type) {
const userState = userStates[ctx.from.id] || {};
if (!userState.asset) {
// Prevent sale of ETH and log asset to user state
if (ctx.message.text.toLowerCase() === "eth" && type === "sell") {
await ctx.reply(
"You cannot sell ETH, as it is the quote currency. Please try again.",
);
clearUserState(ctx.from);
return;
}
updateUserState(ctx.from, { asset: ctx.message.text.toLowerCase() });
const prompt =
type === "buy"
? "Please respond with the amount of ETH you would like to spend."
: "Please respond with the amount of the asset you would like to sell.";
await sendReply(ctx, prompt, { reply_markup: { force_reply: true } });
} else {
const amount = new Decimal(parseFloat(ctx.message.text));
const userAddress = await getOrCreateAddress(ctx.from);
const currentBalance = await userAddress.getBalance(
type === "buy" ? Coinbase.assets.Eth : userState.asset,
);
if (amount.isNaN() || amount.greaterThan(currentBalance)) {
await ctx.reply(
"Invalid amount or insufficient balance. Please try again.",
);
clearUserState(ctx.from);
} else {
const tradeType =
type === "buy"
? { fromAssetId: Coinbase.assets.Eth, toAssetId: userState.asset }
: { fromAssetId: userState.asset, toAssetId: Coinbase.assets.Eth };
await sendReply(ctx, `Initiating ${type}...`);
try {
const userAddress = await getOrCreateAddress(ctx.from);
const trade = await userAddress.createTrade({ amount, ...tradeType });
await trade.wait();
await sendReply(
ctx,
`Successfully completed ${type}: [Basescan Link](${trade.getTransaction().getTransactionLink()})`,
{ parse_mode: "Markdown" },
);
clearUserState(ctx.from);
} catch (error) {
await ctx.reply(`An error occurred while initiating the ${type}.`);
console.error(error);
clearUserState(ctx.from);
}
}
}
}
// Handle pinning the start message
async function handlePinMessage(ctx) {
try {
await ctx.api.pinChatMessage(
ctx.chat.id,
userStates[ctx.from.id].messageId,
);
await ctx.reply("Message pinned successfully!");
} catch (error) {
console.error("Failed to pin the message:", error);
await ctx.reply(
"Failed to pin the message. Ensure the bot has the proper permissions.",
);
}
clearUserState(ctx.from);
}
// Handle exporting the key
async function handleExportKey(ctx) {
const userAddress = await getOrCreateAddress(ctx.from);
const privateKey = userAddress.export();
await sendReply(
ctx,
"Your private key will be in the next message. Do NOT share it with anyone, and make sure you store it in a safe place.",
);
await sendReply(ctx, privateKey);
}
// Encrypt and Decrypt functions
function encrypt(text, iv) {
const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
const cipher = crypto.createCipheriv("aes-256-cbc", encryptionKey, iv);
return cipher.update(text, "utf8", "hex") + cipher.final("hex");
}
function decrypt(encrypted, iv) {
const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
const decipher = crypto.createDecipheriv("aes-256-cbc", encryptionKey, iv);
return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
}
// Start the bot (using long polling)
bot.start();