Write your own Telegram Wallet bot
Hey everyone š, I am Himanshu. I have been working as a frontend engineer at a Crypto Wallet extension in the Cosmos ecosystem for the last 1.7 years. Outside my work, I have done many freelance projects through a mutual friend. In freelance, mostly I have worked on web3 projects only, related to making a TG buy bot tracker, a coin flip web app game, and a TG wallet bot in the Ethereum ecosystem.
I have always found it difficult to find some good resources while working on those projects, most of the time I refer to Chat GPT, some random blogs, and YouTube videos. Thatās why I thought I should write a blog on an actual working TG wallet bot, and thatās why this blog.
Letās start.
Initialize the project
I will use a NodeJS environment to make this project. So, first, create a new folder tg-wallet-bot
, and run npm init -y
in that. After running this command, you must have got a package.json
file in your tg-wallet-bot
folder. Now, letās add a few dependencies that will help us to quickly test our bot. Create dependencies
, a new field in the package.json
file object, and add the following dependencies with the mentioned version.
"express": "4.18.2",
"telegraf": "4.12.2",
"dotenv": "16.3.1"
Why pinned dependencies and not a semver range? The reason for that is related to Security. Just to make sure that our project does not get compromised when an attacker tries to do some shady stuff in later versions.
Why are these versions only? I already had a project running with this configuration so I am referring to that only for the learning purpose of this blog. Thatās why.
Your package.json
file must be looking like this, so far
{
"name": "tg-wallet-bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "16.3.1",
"express": "4.18.2",
"telegraf": "4.12.2"
}
}
Now, letās make an index.js
file at the root level and add the following code
require("dotenv").config();
const { Telegraf } = require("telegraf");
const bot = new Telegraf(process.env.TG_WALLET_BOT_TOKEN);
bot.command("start", (ctx) => {
const message = `Welcome to the TG Wallet Bot!`;
ctx.reply(message);
});
bot.launch();
In the above code snippet, we are creating an instance of a Telegram bot and handling /start
command of the bot. You must have seen this command in a bot, this also gets run when you click on the āStartā button the very first time you start interacting with the bot. Then, we are just launching the bot.
Also, now itās time to create a bot on TG and link that to our code. To create a bot on TG, you will have to search for BotFather on Telegram. After searching that, click on the āStartā button, you must be getting a list of supported commands after that. To create a new bot, you will have to click on /newbot
and answer a few questions like TG bot name and username, after doing that you will get a token that is something that needs to be protected as this token gives access to your bot to anyone.
Now create a new .env
file on the root level of your project directory, and add the following code to that
TG_WALLET_BOT_TOKEN=<Bot Token that you got in BotFather>
We are almost ready to test our bot.
But before we do that, run the npm install
command in your terminal.
Go on the TG bot that you just created in BotFather (You must be seeing your TG bot link with the token on BotFather).
Now, run the node index.js
command in your terminal.
Click on the āStartā button you are seeing in your TG bot.
Woohoo! itās working
You can type /start
command in the message box, and that should work as well.
So far, we havenāt added any crypto-related code. Itās just a simple TG bot.
I wanted to show you how can you import a wallet in your bot, store it in the Telegram session, and show the imported wallet info (public address) to the user.
I will not show any transaction or anything else in this blog, as otherwise, the blog will become very long, and by importing a wallet only you will get an idea of how to get input from a user, how to handle the user action of clicking on any button, and how to show a button.
Import a wallet
Now, to let the user import a wallet, we will need to show a button that will give the user the option to do that. So, letās quickly add a button on the start command. To create a button, I will create a utility function that will return a button that we can use.
Create a utils
folder at the root level of the project directory, also create misc.js
and index.js
files in that.
In the utils/misc.js
file, add the following code
const { Markup } = require("telegraf");
function createCallBackBtn(btnLabel, cbActionCommand) {
return Markup.button.callback(btnLabel, cbActionCommand);
}
module.exports = {
createCallBackBtn,
};
The above code snippet creates a callback button that we can use to show a button to the user.
In the utils/index.js
file, add the following code
const misc = require("./misc");
module.exports = {
...misc,
};
In the above code snippet, we are just re-exporting the functions. This is something that will help us to keep our code organized.
Now, in the index.js
file, change the start command handler code with the following
const { Telegraf } = require("telegraf");
const { createCallBackBtn } = require("./utils");
...
bot.command("start", (ctx) => {
const message = `Welcome to the TG Wallet Bot!`;
const importWalletButton = createCallBackBtn(
"Import Wallet",
"import-wallet"
);
ctx.reply(message, {
reply_markup: {
inline_keyboard: [[importWalletButton]],
},
});
});
So, we are creating a button with the label āImport Walletā and the button has a callback action attached to it (soon you will see its use).
Now, when you run the node index.js
command in your terminal (stop the previous commands if anything is still running), you will see that āImport Walletā is coming in the start command message.
Try clicking on the button, nothing will happen.
Letās add a handler for the Import Wallet button.
In your index.js
file add the following code
...
bot.action("import-wallet", (ctx) => {
ctx.reply("Import wallet clicked");
});
bot.launch();
Now, when you run the node index.js
command in your terminal, and will click the Import Wallet button, you will see the āImport wallet clickedā message.
So, we can show a button and listen for a click event on that button, now we want to take the user input, in our case that input will be either the Seed Phrase or Private Key of a wallet.
To take user input in Telegram we need to create something called Scenes
, so basically when we want to take the userās input back and forth (if the user entered an incorrect value) we maintain a scene, and when want to stop doing that we leave the scene.
Letās create a scene for the import wallet.
Letās follow the same folder structure that we did for the utility function, first, create a folder scenes
at the root level of the project, and files importWalletScene.js
index.js
inside that.
In the scenes/importWalletScene.js
file, add the following code
const { Scenes } = require("telegraf");
const importWalletScene = "importWalletScene";
const importWalletStep = new Scenes.BaseScene(importWalletScene);
importWalletStep.enter((ctx) =>
ctx.reply(
"Please provide either the private key of the wallet you wish to import or a 12-word mnemonic phrase."
)
);
importWalletStep.on("text", (ctx) => {
const phrase = ctx.message.text;
ctx.reply("You entered" + phrase);
ctx.scene.leave();
});
module.exports = {
importWalletScene,
importWalletStep,
};
In the above code, we are just listening to the text
event and then replying with the text that the user entered.
In the scenes/index.js
file, add the following code
const importWalletScene = require("./importWalletScene");
module.exports = {
...importWalletScene,
};
Again, we are just re-exporting the scene variables.
Now, in the index.js
file, you will need to add the following code
const { Telegraf, Scenes, session } = require("telegraf");
const { createCallBackBtn } = require("./utils");
const { importWalletScene, importWalletStep } = require("./scenes");
const bot = new Telegraf(process.env.TG_WALLET_BOT_TOKEN);
const stage = new Scenes.Stage([importWalletStep]);
bot.use(session());
bot.use(stage.middleware());
...
bot.action("import-wallet", (ctx) => {
ctx.scene.enter(importWalletScene);
});
In the above code snippet, we are just importing the necessary variables, have put a few middleware for the bot to use, and then when the user clicks on āImport Walletā we will enter the scene.
Now, If you run the command node index.js
in the terminal again, you must be able to get the following
We can get the user input now.
Now, we should generate the account from the phrase that the user has entered.
To do that we will need to create a new utility function that will do that for us.
But, before we do that, we need a few more dependencies to add, so letās update the dependencies in package.json
to the following
{
...,
"dependencies": {
"crypto-js": "4.1.1",
"dotenv": "16.3.1",
"ethers": "5.7.2",
"express": "4.18.2",
"telegraf": "4.12.2"
}
}
Now, in the utils
folder, create an encryption.js
file and add the following code
const crypto = require("crypto-js");
function encrypt(text) {
return crypto.AES.encrypt(text, process.env.TG_WALLET_BOT_TOKEN).toString();
}
function decrypt(cipherText) {
const bytes = crypto.AES.decrypt(cipherText, process.env.TG_WALLET_BOT_TOKEN);
return bytes.toString(crypto.enc.Utf8);
}
module.exports = {
encrypt,
decrypt,
};
We are just creating some helper functions to encrypt and decrypt our info, as we canāt store sensitive information in plain text.
Why an Advanced Encryption System (AES) and not RSA encryption? You can RSA encryption as well here, as we are not dealing with a huge amount of data here. I used AES encryption so I donāt have to maintain two keys.
Now, update the utils/index.js
file with the following code
const misc = require("./misc");
const encryption = require("./encryption");
module.exports = {
...misc,
...encryption,
};
And, inside the utils/misc.js
file, letās add the helper function to generate the account.
const { encrypt } = require("./encryption");
const { Wallet } = require("ethers");
...
function generateAccount(phrase, index = 0) {
/**
* If the phrase does not contain spaces, it is likely a private key
*/
const wallet = phrase.includes(" ")
? Wallet.fromMnemonic(phrase, `m/44'/60'/0'/0/${index}`)
: new Wallet(phrase);
return {
address: wallet.address,
privateKey: encrypt(wallet.privateKey),
mnemonic: encrypt(phrase),
};
}
module.exports = {
...,
generateAccount,
};
In the above code snippet, we are checking if the passed phrase is a seed phrase then get the wallet at the given index using the derivation path. We are also encrypting the sensitive info before returning it.
Now, we will have to update the text
handler inside the scenes/importWalletScene.js
file with the following code
importWalletStep.on("text", (ctx) => {
const phrase = ctx.message.text;
try {
const wallet = generateAccount(phrase);
ctx.reply("Address: " + wallet.address);
} catch (error) {
ctx.reply(
"š This does not appear to be a valid private key / mnemonic phrase. Please try again."
);
}
ctx.scene.leave();
});
I think the code is self-explanatory here.
Now, we just need to check this code.
First, letās run the npm install
again.
After that, you can run node index.js
If you enter the wrong seed phrase/private key, you will get the following screen
But, if you enter the correct seed phrase/private key, you will get the Address in the reply. I am not doing that in this blog, but if you face any issue while doing that you can comment on the issue and I can try to debug that.
Ahh! Finally, the wallet is ready.
Show the wallet
What if we want to give the user an option to see the imported wallet later as well?
Well, to do that, you will need to store the imported wallet somewhere, and later when the user clicks on the āShow Walletā button (we will have a button for this) we will show the public address of the imported wallet.
We will store the complete object that we are returning from the generateAccount
function, so if later you want to use the private key or mnemonic phrase for some operation you can do that.
We will not use any persistent storage like DB here, so we will have something like a non-custodial wallet.
The telegram has something called session storage, so basically, that will persist the info in a session and whenever you will restart the server the session will get cleared.
Letās quickly add this feature.
First, add a new button in the /start
command message. To do that, add the following code in the index.js
file
bot.command("start", (ctx) => {
...
const showWalletButton = createCallBackBtn("Show Wallet", "show-wallet");
ctx.reply(message, {
reply_markup: {
inline_keyboard: [[importWalletButton], [showWalletButton]],
},
});
});
...
bot.action("show-wallet", (ctx) => {
if (ctx.session.wallet) {
ctx.reply(`Your wallet address is ${ctx.session.wallet.address}`);
} else {
ctx.reply("You have not imported any wallet yet.");
}
});
Now, in the scenes/importWalletScene.js
file, we will have to add the newly generated wallet in the session, and to do that add the following code
importWalletStep.on("text", (ctx) => {
try {
const wallet = generateAccount(phrase);
ctx.session.wallet = wallet;
ctx.reply(
`š Your wallet has been successfully imported. Your wallet address is ${wallet.address}.`
);
} catch (error) {
...
}
});
If you run the node index.js
command in your terminal now, import a wallet, then click on the show wallet button (you can type /start
command and check this), you will see something like the following
Finally, itās done š„±
You can find the complete code here ā https://github.com/hsnice16/tg-wallet-bot (Donāt forget to give a āļø to the repo š)
If you want to share your thoughts on the blog or anything, then you can mention that in the comments or message me on Twitter.
Also, you can follow me on Twitter