├── Procfile
├── README.md
├── app.yaml
├── index.js
└── package.json
/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ETHWatchBot
2 |
3 | Simple Telegram bot to monitor Ethereum addresses. The bot is available @ETHWatchBot.
4 |
5 | Commands:
6 | * /start - displays a greeting
7 | * /watch (address) - starts monitoring an address
8 | * /forget (address) - stops monitoring an address
9 | * /list - lists the currently monitored addresses
10 |
11 | The bot is using the [Etherscan API](https://etherscan.io/apis). In order to avoid service abuse it checks the addresses every 1 minute. It stops watching an address after 24 hours.
12 |
13 | ## To-do
14 |
15 | This current version is a very barebone MVP, and there's a lot more to do:
16 | - [x] ~~Porting it to Web3.js so the API limitations don't apply.~~ Turned out watching an address with Web3.js is a pain.
17 | - [x] Moved from Ethplorer to Etherscan, as it has more generous API limits.
18 | - [ ] Implementing a database, so the watchlist is not dropped every time the bot is restarted.
19 | - [x] Implementing a /forget command.
20 |
21 | # License
22 |
23 | Released under CC-BY.
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs8
2 | instance_class: F1
3 | automatic_scaling:
4 | max_instances: 1
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // *********************************
2 | // Defining variables
3 | // *********************************
4 |
5 | const TelegramBot = require('node-telegram-bot-api');
6 | const cron = require("node-cron");
7 | var etherscan = require('etherscan-api').init(process.env.ETHERSCAN_KEY);
8 |
9 | // Heroku deployment
10 | const url = process.env.NOW_URL;
11 |
12 | const options = {
13 | webHook: {
14 | port: process.env.PORT
15 | }
16 | };
17 | const url = process.env.APP_URL;
18 |
19 | const telegramBotToken = process.env.TOKEN;
20 | const bot = new TelegramBot(telegramBotToken, options);
21 | const botOwner = process.env.BOTOWNER;
22 |
23 | // Class to store addresses, previous balances and the Telegram chatID
24 | class WatchEntry {
25 | constructor(chatID, ETHaddress, currentBalance, timeAddedToWatchlist) {
26 | this.chatID = chatID;
27 | this.ETHaddress = ETHaddress;
28 | this.currentBalance = currentBalance;
29 | this.timeAddedToWatchlist = timeAddedToWatchlist;
30 | }
31 | }
32 |
33 | // Array to store WatchEntry objects
34 | var watchDB = [];
35 |
36 | // *********************************
37 | // Helper functions
38 | // *********************************
39 |
40 | // Function to check if an address is a valid ETH address
41 | var isAddress = function (address) {
42 | address = address.toLowerCase();
43 | if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
44 | return false;
45 | } else if (/^(0x)?[0-9a-f]{40}$/.test(address) || /^(0x)?[0-9A-F]{40}$/.test(address)) {
46 | return true;
47 | } else {
48 | return false;
49 | }
50 | };
51 |
52 | // *********************************
53 | // Telegram bot event listeners
54 | // *********************************
55 |
56 | // Telegram error handling
57 | bot.on('polling_error', (error) => {
58 | console.log(error.message); // => 'EFATAL'
59 | });
60 |
61 | // Telegram checking for commands w/o parameters
62 | bot.on('message', (msg) => {
63 | const chatId = msg.chat.id;
64 | if (msg.text === '/watch') {
65 | bot.sendMessage(chatId, 'You need to specify an address.\nType /watch followed by a valid ETH address like this:\n/watch 0xB91986a9854be250aC681f6737836945D7afF6Fa
' ,{parse_mode : "HTML"});
66 | }
67 | if (msg.text === "/forget") {
68 | bot.sendMessage(chatId, 'You need to specify an address.\nType /forget followed by an address you are watching currently, like this:\n/forget 0xB91986a9854be250aC681f6737836945D7afF6Fa
' ,{parse_mode : "HTML"});
69 | }
70 | });
71 |
72 | // Telegram /start command
73 | bot.onText(/\/start/, (msg) => {
74 | const chatId = msg.chat.id;
75 | bot.sendMessage(chatId, "***************\n\nHey there! I am a Telegram bot by @torsten1.\n\nI am here to watch Ethereum addresses. I will ping you if there's a change in balance. This is useful if you've just sent a transaction and want to be notified when it arrives. Due to API limitations, I can watch an address for no more than 24 hours.\n\nCommands\n\n* /watch (address)
- start watching an address.\n* /forget (address)
- stop watching an address.\n* /list
- list the addresses you are watching.\n\nHave fun :)" ,{parse_mode : "HTML"});
76 | });
77 |
78 | // Telegram /watch command
79 | bot.onText(/\/watch (.+)/, (msg, match) => {
80 | const chatId = msg.chat.id;
81 | const ETHaddress = match[1];
82 | if (isAddress(ETHaddress)) {
83 | var balance = etherscan.account.balance(ETHaddress);
84 | balance.then(function(balanceData){
85 | var date = new Date();
86 | var timestamp = date.getTime();
87 | const newEntry = new WatchEntry(chatId, ETHaddress, balanceData.result, timestamp);
88 | watchDB.push(newEntry);
89 | var balanceToDisplay = balanceData.result / 1000000000000000000;
90 | balanceToDisplay = balanceToDisplay.toFixed(4);
91 | bot.sendMessage(chatId, `Started watching the address ${ETHaddress}\nIt currently has ${balanceToDisplay} ETH.`);
92 | // Debug admin message for the bot owner
93 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nSomeone started watching the address\n${ETHaddress}\n`);
94 | });
95 | } else {
96 | bot.sendMessage(chatId, "This is not a valid ETH address.\nType /watch followed by a valid ETH address like this:\n/watch 0xB91986a9854be250aC681f6737836945D7afF6Fa
" ,{parse_mode : "HTML"});
97 | // Debug admin message for the bot owner
98 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nSomeone tried to watch an invalid address\n${ETHaddress}\n`);
99 |
100 | }
101 | });
102 |
103 | // Telegram /forget command
104 | bot.onText(/\/forget (.+)/, (msg, match) => {
105 | const chatId = msg.chat.id;
106 | const ETHaddress = match[1];
107 | var newWatchDB = [];
108 | var nothingToForget = true;
109 | watchDB.forEach(function(entry) {
110 | if ((entry.chatID === chatId) && (entry.ETHaddress === ETHaddress)) {
111 | bot.sendMessage(chatId, `I stopped monitoring the address ${entry.ETHaddress}.`);
112 | // Debug admin message for the bot owner
113 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nSomeone stopped watching the address\n${ETHaddress}\n`);
114 | nothingToForget = false;
115 | } else {
116 | newWatchDB.push(entry);
117 | }
118 | });
119 | if (nothingToForget) {
120 | bot.sendMessage(chatId, `I couldn't find the address ${ETHaddress} on the watchlist.`);
121 | // Debug admin message for the bot owner
122 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nSomeone tried to remove this non-existing address from watchlist:\n${ETHaddress}\n`);
123 | }
124 | watchDB = newWatchDB;
125 | });
126 |
127 | // Telegram /list command
128 | bot.onText(/\/list/, (msg) => {
129 | const chatId = msg.chat.id;
130 | var nothingToList = true;
131 | var listOfAddresses = '';
132 | watchDB.forEach(function(entry) {
133 | if (entry.chatID === chatId) {
134 | nothingToList = false;
135 | listOfAddresses = listOfAddresses + `* ${entry.ETHaddress}\n`;
136 | }
137 | });
138 | if (nothingToList) {
139 | bot.sendMessage(chatId, `There are no addresses on your watchlist. Maybe time to add some!`);
140 | } else {
141 | bot.sendMessage(chatId, 'You are currently monitoring\n' + listOfAddresses);
142 | }
143 | });
144 |
145 | // Telegram /check command (not public)
146 | bot.onText(/\/check/, (msg) => {
147 | // To manually trigger a check. For testing purposes.
148 | checkAllAddresses();
149 | // Debug admin message for the bot owner
150 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nSomeone called the /check function.`);
151 | });
152 |
153 | // *********************************
154 | // Main functions
155 | // *********************************
156 |
157 | async function checkAllAddresses() {
158 | var debugNumberOfAlertsDelivered = 0;
159 | var newWatchDB = [];
160 | // using the for i structure because it's async
161 | for (var i = 0; i < watchDB.length; i++) {
162 | var entry = watchDB[i];
163 | // we check if the balance has changed
164 | const balance = await etherscan.account.balance(entry.ETHaddress);
165 | if (balance.result === entry.currentBalance) {
166 | // no transfer
167 | } else {
168 | // there was a transfer
169 | var difference = (balance.result - entry.currentBalance) / 1000000000000000000;
170 | difference = difference.toFixed(4);
171 | var balanceToDisplay = balance.result / 1000000000000000000;
172 | balanceToDisplay = balanceToDisplay.toFixed(4);
173 | if (difference > 0) {
174 | //incoming transfer
175 | bot.sendMessage(entry.chatID, `I see incoming funds!\n\n${difference} ETH arrived to the address ${entry.ETHaddress} since I've last checked.\nCurrent balance is ${balanceToDisplay} ETH.`);
176 | } else {
177 | //outgoing transfer
178 | bot.sendMessage(entry.chatID, `Funds are flying out!\n\n${difference} ETH left the address ${entry.ETHaddress} since I've last checked.\nCurrent balance is ${balanceToDisplay} ETH.`);
179 | }
180 | // debug
181 | debugNumberOfAlertsDelivered = debugNumberOfAlertsDelivered + 1;
182 | }
183 | // if the entry is too old, we get rid of it
184 | var date = new Date();
185 | var now = date.getTime();
186 | if ((entry.timeAddedToWatchlist + (24*60000*60)) > now) {
187 | //has been added less than 24h ago
188 | const newEntry = new WatchEntry(entry.chatID, entry.ETHaddress, balance.result, entry.timeAddedToWatchlist);
189 | newWatchDB.push(newEntry);
190 | } else {
191 | bot.sendMessage(entry.chatID, `Due to API limitations, I can only watch an address for 24 hours.\n\nYou asked me to watch ${entry.ETHaddress} quite some time ago, so I dropped it from my list. Sorry about it!`);
192 | }
193 | }
194 | watchDB = newWatchDB;
195 | // Debug admin message for the bot owner
196 | if (debugNumberOfAlertsDelivered > 0) {
197 | bot.sendMessage(botOwner, `--> ADMIN MESSAGE\nNumber of notifications delivered: ${debugNumberOfAlertsDelivered}`);
198 | debugNumberOfAlertsDelivered = 0;
199 | }
200 | }
201 |
202 | function watch() {
203 | // do the scan every minute
204 | cron.schedule('*/1 * * * *', () => {
205 | checkAllAddresses();
206 | });
207 | }
208 |
209 | bot.setWebHook(`${url}/bot${telegramBotToken}`);
210 | // kick it off
211 | watch();
212 |
213 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ethwatch",
3 | "version": "1.0.0",
4 | "description": "ETH address watcher",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "dependencies": {
12 | "etherscan-api": "^10.0.0",
13 | "node-cron": "^2.0.3",
14 | "node-telegram-bot-api": "^0.30.0"
15 | },
16 | "engines": {
17 | "node": "8.13.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------