├── 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 | --------------------------------------------------------------------------------