Write your own Telegram Wallet bot

Himanshu Singh
10 min readFeb 11, 2024

--

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

--

--

Himanshu Singh

I write blogs around React JS, JavaScript, Web Dev, and Programming. Follow to read blogs around them.