├── bin ├── __init__.py ├── wallettostring.py ├── get_url.py ├── get_json_from_url.py ├── formatnumber.py ├── telegramprocessor.py ├── settings.py └── uniswapprocessor.py ├── api ├── coingecko │ ├── __init__.py │ └── tokeninformation.py ├── etherscan │ ├── __init__.py │ ├── lastblock.py │ ├── gettokenamount.py │ ├── uniswaptransactionbatch.py │ └── uniswaptransaction.py └── telegram │ ├── telegrambaseobject.py │ ├── telegramupdates.py │ └── telegramsendmessage.py ├── .gitignore ├── requirements.txt ├── .dockerignore ├── settings.default ├── Dockerfile ├── main.py └── README.md /bin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/coingecko/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/etherscan/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | #Visual Studio Code 7 | .vscode/ 8 | 9 | # Environments 10 | env/ 11 | 12 | # Settings file 13 | settings.config 14 | -------------------------------------------------------------------------------- /bin/wallettostring.py: -------------------------------------------------------------------------------- 1 | def wallettostring(wallet): 2 | ws = "{0:#0{1}x}".format(int(wallet,16),1) 3 | if len(ws) < 42: 4 | addzeros = 42 - len(ws) 5 | ws = ws.replace("0x","0x" + "0" * addzeros) 6 | return ws 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.4.2 2 | certifi==2020.11.8 3 | chardet==3.0.4 4 | colorama==0.4.4 5 | idna==2.10 6 | isort==5.6.4 7 | lazy-object-proxy==1.4.3 8 | mccabe==0.6.1 9 | pylint==2.6.0 10 | requests==2.25.0 11 | six==1.15.0 12 | toml==0.10.2 13 | typed-ast==1.4.1 14 | urllib3==1.26.4 15 | wrapt==1.12.1 16 | -------------------------------------------------------------------------------- /bin/get_url.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | # The function below retrieves the content from an URL which should be specified 4 | # as parameter. The outpuut is the raw data extracted from the URL. 5 | def get_url(url): 6 | '''URL to retrieve an URL''' 7 | response = requests.get(url) 8 | content = response.content.decode("utf8") 9 | return content -------------------------------------------------------------------------------- /bin/get_json_from_url.py: -------------------------------------------------------------------------------- 1 | import json 2 | from bin.get_url import get_url 3 | 4 | # The function below retrieves a JSON file from an URL. The function ask the 5 | # content of an URL via the get_URL function and convert to content to the JSON 6 | # format. 7 | def get_json_from_url(url): 8 | '''Function to retrieve a JSON file from an URL''' 9 | content = get_url(url) 10 | js = json.loads(content) 11 | return js -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | **/.classpath 3 | **/.dockerignore 4 | **/.env 5 | **/.git 6 | **/.gitignore 7 | **/.project 8 | **/.settings 9 | **/.toolstarget 10 | **/.vs 11 | **/.vscode 12 | **/*.*proj.user 13 | **/*.dbmdl 14 | **/*.jfm 15 | **/azds.yaml 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | README.md 25 | -------------------------------------------------------------------------------- /settings.default: -------------------------------------------------------------------------------- 1 | [PrimaryToken] 2 | primarytokenname = GET Protocol 3 | primarytokensymbol = GET 4 | primarytokencontractaddress = 0x8a854288a5976036a725879164ca3e91d30c6a1b 5 | 6 | [EtherScanAPI] 7 | etherscanapikey = ETHERSCANAPIKEYHERE 8 | 9 | [Uniswap] 10 | uniswapaddress = 0x2680a95fc9de215f1034f073185cc1f2a28b4107 11 | 12 | [Process] 13 | lastprocessedblocknumber = 0 14 | 15 | [Telegram] 16 | telegramapitoken = TELEGRAMAPITOKENHERE 17 | telegramactivatedchannels = None 18 | telegramlastprocessedupdateid = 0 19 | 20 | [Advanced] 21 | pairtokendecimals = 18 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # For more information, please refer to https://aka.ms/vscode-docker-python 2 | FROM python:3.8-slim-buster 3 | 4 | # Keeps Python from generating .pyc files in the container 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | 7 | # Turns off buffering for easier container logging 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | # Install pip requirements 11 | ADD requirements.txt . 12 | RUN python -m pip install -r requirements.txt 13 | 14 | WORKDIR /app 15 | ADD . /app 16 | 17 | # Switching to a non-root user, please refer to https://aka.ms/vscode-docker-python-user-rights 18 | RUN useradd appuser && chown -R appuser /app 19 | USER appuser 20 | 21 | # During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug 22 | CMD ["python", "main.py"] 23 | -------------------------------------------------------------------------------- /api/etherscan/lastblock.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | import sys 4 | from bin.get_json_from_url import get_json_from_url 5 | 6 | # Configure logging 7 | logger = logging.getLogger(__name__) 8 | 9 | def lastblock(): 10 | esurl = ("https://api.etherscan.io/api?module=proxy&action=eth_blockNumber&" 11 | "apikey={}".format(settings.config.etherscanapikey)) 12 | logger.info("Get last blocknumber from EtherScan url {}".format(esurl)) 13 | try: 14 | json = get_json_from_url(esurl) 15 | blocknumber = int(json["result"],16) 16 | except: 17 | logger.error("Can't retrieve blocknumber from URL") 18 | raise Exception("Can't retrieve blocknumber from url. Error: {}".format( 19 | sys.exc_info()[0])) 20 | 21 | return blocknumber -------------------------------------------------------------------------------- /bin/formatnumber.py: -------------------------------------------------------------------------------- 1 | def formatnumber(number,ndigits=2): 2 | # Format the number with 2 digits behind the comma 3 | number = round(number, ndigits) 4 | numberparts = str(number).split('.') 5 | 6 | # Format the number before the comma with a space as 1000 seperator 7 | l = len(numberparts[0]) - 1 8 | blocks = int(l / 3) 9 | offset = len(numberparts[0]) % 3 10 | 11 | if (offset == 0): 12 | output = numberparts[0][:3] 13 | else: 14 | output = numberparts[0][:offset] 15 | 16 | for r in range(blocks): 17 | output += ' ' 18 | output += numberparts[0][offset:offset + 3] 19 | offset += 3 20 | 21 | # If numbers has decimals 22 | if len(numberparts) == 2: 23 | output = output + "," + numberparts[1] 24 | 25 | return output 26 | 27 | 28 | print(formatnumber(1933.213)) -------------------------------------------------------------------------------- /api/telegram/telegrambaseobject.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | 4 | # Configure logging 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class TelegramBaseObject(): 9 | def apiurl(self,method,parameters={}): 10 | # Define api url 11 | apiurl = "https://api.telegram.org/bot{}/{}".format( 12 | settings.config.telegramapitoken,method) 13 | 14 | # Add parameters to the api url 15 | if len(parameters.keys()) != 0: 16 | apiurl = apiurl + "?" 17 | for parameter in parameters.keys(): 18 | apiurl = apiurl + parameter + "=" 19 | apiurl = apiurl + str(parameters[parameter]) + "&" 20 | 21 | # Remove last & 22 | apiurl = apiurl[:-1] 23 | 24 | logger.info("Telegram API url: {}".format(apiurl)) 25 | return apiurl -------------------------------------------------------------------------------- /api/telegram/telegramupdates.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | from bin.get_json_from_url import get_json_from_url 4 | from api.telegram.telegrambaseobject import TelegramBaseObject 5 | 6 | # Configure logging 7 | logger = logging.getLogger(__name__) 8 | 9 | class TelegramUpdates(TelegramBaseObject): 10 | def __init__(self, timeout=300): 11 | self.json = None 12 | self.timeout = timeout 13 | method = "getUpdates" 14 | # Offset = last update id + 1 (imported from settings file) 15 | offset = str(int(settings.config.telegramlastprocessedupdateid) + 1) 16 | parameters = { 17 | "timeout":timeout, 18 | "offset":offset 19 | } 20 | self.apiurl = super().apiurl( 21 | method = method, 22 | parameters = parameters 23 | ) 24 | logger.info("Get new updates for the bot via the Telegram API") 25 | self.json = get_json_from_url(self.apiurl) 26 | 27 | 28 | -------------------------------------------------------------------------------- /api/telegram/telegramsendmessage.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import urllib.parse 3 | from bin.get_json_from_url import get_json_from_url 4 | from api.telegram.telegrambaseobject import TelegramBaseObject 5 | 6 | # Configure logging 7 | logger = logging.getLogger(__name__) 8 | 9 | class TelegramSendMessage(TelegramBaseObject): 10 | def __init__(self,chat_id,text,formatstyle="HTML",disablewebpreview="True"): 11 | self.json = None 12 | method = "sendMessage" 13 | formattedtext = urllib.parse.quote_plus(text) 14 | parameters = { 15 | "chat_id":chat_id, 16 | "text":formattedtext, 17 | "parse_mode":formatstyle, 18 | "disable_web_page_preview":disablewebpreview 19 | } 20 | self.apiurl = super().apiurl( 21 | method = method, 22 | parameters = parameters 23 | ) 24 | 25 | logger.info("Sending message: {} to: {}".format(text,chat_id)) 26 | self.json = get_json_from_url(self.apiurl) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import bin.settings as settings 4 | from concurrent.futures import ThreadPoolExecutor 5 | from bin.uniswapprocessor import UniswapProcessor 6 | from bin.telegramprocessor import TelegramProcessor 7 | from api.etherscan.lastblock import lastblock 8 | 9 | # Configure the logger 10 | logging.basicConfig( 11 | format=('%(asctime)s - %(name)s - %(levelname)s - %(message)s'), 12 | datefmt="%a %d/%m/%Y %H:%M:%S", 13 | level=logging.INFO) 14 | 15 | # Initialize Settings 16 | settings.init() 17 | 18 | # Update settings file with last blocknumber, if the blocknumber is None 19 | if settings.config.lastprocessedblocknumber == "0": 20 | lastblock = lastblock() 21 | settings.config.updateblocknumber(lastblock) 22 | 23 | # Initialize Telegram Processor 24 | tgp = TelegramProcessor() 25 | 26 | # Initialize Uniswap Processor 27 | usp = UniswapProcessor() 28 | 29 | 30 | with ThreadPoolExecutor(max_workers=3) as executor: 31 | tgpprocessor = executor.submit(tgp.start) 32 | uspprocessor = executor.submit(usp.start) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /api/etherscan/gettokenamount.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | from decimal import Decimal 4 | from bin.get_json_from_url import get_json_from_url 5 | 6 | # Configure logging 7 | logger = logging.getLogger(__name__) 8 | 9 | def gettokenamount(walletaddress, contractaddress): 10 | logger.info(("Initializing object to easilty get the token amount for " 11 | "address: {} and contract {}").format(walletaddress,contractaddress)) 12 | esurl = ("https://api.etherscan.io/api" 13 | "?module=account" 14 | "&action=tokenbalance" 15 | "&contractaddress={}" 16 | "&address={}" 17 | "&apikey={}").format( 18 | contractaddress, 19 | walletaddress, 20 | settings.config.etherscanapikey 21 | ) 22 | 23 | logger.info("The Etherscan get balance URL is: {}".format(esurl)) 24 | try: 25 | json = get_json_from_url(esurl) 26 | logger.info("The JSON file is succesfully retrieved") 27 | amount = Decimal(json["result"]) / 1000000000000000000 28 | logger.info("The amount of tokens found is {}".format(amount)) 29 | 30 | except: 31 | logger.error("Can't retrieve the JSON file from the url") 32 | raise Exception("Can't retrieve the JSON file from the get balance url") 33 | 34 | return amount 35 | -------------------------------------------------------------------------------- /api/etherscan/uniswaptransactionbatch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | from bin.get_json_from_url import get_json_from_url 4 | 5 | # Configure logging 6 | logger = logging.getLogger(__name__) 7 | 8 | class UniswapTransactionBatch(): 9 | def __init__(self,startblock): 10 | self.startblock = startblock 11 | self.json = None 12 | self.transactionhashes = [] 13 | 14 | logger.info(("Initializing Uniswap transaction batch object. " 15 | "Startblock: {}").format(startblock)) 16 | 17 | self.get_uniswaptransactions() 18 | self.extracttransactionhashes() 19 | 20 | 21 | def get_uniswaptransactions(self): 22 | esurl = ("https://api.etherscan.io/api" 23 | "?module=account" 24 | "&action=tokentx" 25 | "&startblock={}" 26 | "&address={}" 27 | "&contractaddress={}" 28 | "&apikey={}".format( 29 | self.startblock, 30 | settings.config.uniswapaddress, 31 | settings.config.primarytokencontractaddress, 32 | settings.config.etherscanapikey 33 | )) 34 | 35 | logger.info("The Etherscan Uniswap transaction batch URL is: {}".format( 36 | esurl)) 37 | try: 38 | self.json = get_json_from_url(esurl) 39 | logger.info("The JSON file is succesfully retrieved") 40 | except: 41 | logger.error("Can't retrieve the JSON file from the url") 42 | 43 | def extracttransactionhashes(self): 44 | logger.info("Extracting transaction hashes") 45 | for transaction in self.json['result']: 46 | if transaction['hash'] not in self.transactionhashes: 47 | self.transactionhashes.append(transaction['hash']) 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniSwap Telegram bot 2 | This bot provides information when a swap or liquidity change for a specific Uniswap address occurred 3 | 4 | ## Installation 5 | * Copy settings.default to settings.config 6 | * Add the Telegram bot API and the etherscan API 7 | * Change the attributes in the [PrimaryToken] (By default these attributes are configured for the GET Protocol ) 8 | * Create a virtual environment 9 | ``` 10 | python3 -m venv env 11 | ``` 12 | * Activate the virtual environment 13 | ``` 14 | source env\bin\activate 15 | ``` 16 | * Install the depenendencies 17 | ``` 18 | pip install -r requirements.txt 19 | ``` 20 | * Join the corresponding Telegram bot a a Telegram channel and make sure it's allowed to post messages 21 | * Active the bot by typing the command "/start" in the channel 22 | * Start main.py and new tokens swaps and liquidity changes should appear in the channel 23 | 24 | 25 | ## Add the bot to systemd 26 | * Create a bash file (uniswaptelegrambot.sh) 27 | ``` 28 | #!/bin/bash 29 | 30 | HOME=/home/username 31 | VENVDIR=$HOME/Uniswap-Telegram-Bot/env 32 | BINDIR=$HOME/Uniswap-Telegram-Bot 33 | 34 | cd $BINDIR 35 | source $VENVDIR/bin/activate 36 | python $BINDIR/main.py 37 | 38 | ``` 39 | 40 | VENVDIR is the virtual environement dir 41 | The BINDIR is the directory where the program resides 42 | 43 | * Create a service file (/etc/systemd/system/uniswaptelegrambot.service) 44 | 45 | ``` 46 | [Unit] 47 | Description=Uniswap Telegram Bot 48 | After=network.target 49 | 50 | [Service] 51 | Type=simple 52 | User=username 53 | Group=users 54 | ExecStart=/home/username/uniswaptelegrambot.sh 55 | 56 | [Install] 57 | WantedBy=multi-user.target 58 | ``` 59 | 60 | * Reload, start the bot and enable it during startup 61 | ``` 62 | systemctl daemon-reload 63 | systemctl restart uniswaptelegrambot.service 64 | systemctl enable uniswaptelegrambot.service 65 | ``` 66 | 67 | * View the status of the bot 68 | ``` 69 | systemctl status uniswaptelegrambot.service 70 | ``` -------------------------------------------------------------------------------- /api/coingecko/tokeninformation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | from bin.get_json_from_url import get_json_from_url 4 | from decimal import Decimal 5 | 6 | 7 | # Configure logging 8 | logger = logging.getLogger(__name__) 9 | 10 | class TokenInformation(): 11 | """ 12 | Base class to collect TokenInformation from CoinGecko. 13 | The function needs a contract address to collect the correct information 14 | """ 15 | def __init__(self,contractaddress): 16 | logger.info( 17 | "Collecting token information for contract address: {}".format( 18 | contractaddress)) 19 | self.contractaddress = contractaddress 20 | self.json = None 21 | self.eurprice = None 22 | self.usdprice = None 23 | self.tokenname = None 24 | self.tokensymbol = None 25 | 26 | # Get the content from Coingecko (output in JSON format) 27 | self.get_content() 28 | # Extract the most import information from the JSON and store this 29 | # in the object. 30 | self.extract_json() 31 | 32 | def get_content(self): 33 | cgurl = ("https://api.coingecko.com/api/v3/coins/ethereum/contract/" 34 | "{}").format(self.contractaddress) 35 | logger.info("The Coingecko transaction URL is: {}".format( 36 | cgurl)) 37 | try: 38 | self.json = get_json_from_url(cgurl) 39 | logger.info("The JSON file is succesfully retrieved") 40 | except: 41 | logger.error("Can't retrieve the JSON file from the url") 42 | 43 | def extract_json(self): 44 | logger.info("Extracting import information from JSON file") 45 | self.tokenname = self.json['name'] 46 | logger.info("The following token name has been found: {}".format( 47 | self.tokenname)) 48 | self.tokensymbol = self.json['symbol'] 49 | logger.info("The following token symbol has been found: {}".format( 50 | self.tokensymbol)) 51 | self.eurprice = Decimal( 52 | self.json['market_data']['current_price']["eur"]) 53 | logger.info("The following Euro price has been found: €{}".format( 54 | self.eurprice)) 55 | self.usdprice = Decimal( 56 | self.json['market_data']['current_price']["usd"]) 57 | logger.info("The following Dollar price has been found: ${}".format( 58 | self.usdprice)) 59 | -------------------------------------------------------------------------------- /bin/telegramprocessor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import bin.settings as settings 4 | from api.telegram.telegramupdates import TelegramUpdates 5 | 6 | # Configure logging 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class TelegramProcessor(): 11 | def __init__(self): 12 | logger.info("Start TelegramProcessor Processor") 13 | 14 | def process_telegramupdatebatch(self): 15 | logger.info("Process telegrambatch") 16 | self.tgu = TelegramUpdates() 17 | 18 | for update in self.tgu.json['result']: 19 | # Get all the keys from the update 20 | updatekeys = update.keys() 21 | 22 | if 'channel_post' in updatekeys: 23 | self.process_channelpost(update) 24 | elif 'message' in updatekeys: 25 | self.process_message(update) 26 | 27 | self.update_updateid(update['update_id']) 28 | 29 | def process_channelpost(self, update): 30 | if update['channel_post']['text'].upper() == '/START': 31 | channelid = str(update['channel_post']['sender_chat']['id']) 32 | logger.info("Request to enable channel: {}".format(channelid)) 33 | if channelid in settings.config.telegramactivatedchannels: 34 | logger.info("Channel is already enabled") 35 | else: 36 | logger.info("Enabling channel") 37 | settings.config.update_telegramsettings( 38 | telegramchannel=channelid) 39 | else: 40 | logger.info("Someone is talking, but I can't understand it") 41 | 42 | def process_message(self, update): 43 | message = update['message'] 44 | messagekeys = message.keys() 45 | if 'text' in messagekeys: 46 | if message['text'].upper() == '/START': 47 | chatid = str(message['chat']['id']) 48 | logger.info("Request to enable chat: {}".format(chatid)) 49 | if chatid in settings.config.telegramactivatedchannels: 50 | logger.info("Chat is already enabled") 51 | else: 52 | logger.info("Enabling chat") 53 | settings.config.update_telegramsettings( 54 | telegramchannel=chatid) 55 | else: 56 | logger.info("Someone is talking, but I can't understand it") 57 | 58 | def update_updateid(self, updateid): 59 | currentupdateid = int(settings.config.telegramlastprocessedupdateid) 60 | if currentupdateid <= updateid: 61 | logger.info("Updating Telegram Update ID") 62 | settings.config.update_telegramsettings( 63 | updateid=(str(updateid))) 64 | 65 | def start(self, pollinterval=60): 66 | logger.info("Starting Telegram processor cycle") 67 | while True: 68 | try: 69 | self.process_telegramupdatebatch() 70 | logger.info("Telegram cycle keep alive message") 71 | except: 72 | logger.error("Telegram processor run failed") 73 | time.sleep(10) 74 | -------------------------------------------------------------------------------- /bin/settings.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import logging 3 | import sys 4 | 5 | # Configure logging 6 | logger = logging.getLogger(__name__) 7 | 8 | class Config(): 9 | def __init__(self,configfile="settings.config"): 10 | 11 | logger.info('Initializing config') 12 | self.config = configparser.ConfigParser() 13 | self.configfile = configfile 14 | self.etherscanapikey = None 15 | self.primarytokenname = None 16 | self.primarytokensymbol = None 17 | self.primarytokencontractaddress = None 18 | self.uniswapaddress = None 19 | self.lastprocessedblocknumber = None 20 | self.telegramapitoken = None 21 | self.telegramlastprocessedupdateid = None 22 | self.telegramactivatedchannels = [] 23 | 24 | # Read the config file and throw an error if this is failing 25 | try: 26 | self.readconfigfile() 27 | except: 28 | logger.error("Can't load the setting file correctly") 29 | raise Exception( 30 | "Can't load the setting file correctly. Error: {}".format( 31 | sys.exc_info()[0])) 32 | 33 | def readconfigfile(self): 34 | logger.info('Importing settings file: {}'.format(self.configfile)) 35 | self.config.read(self.configfile) 36 | # Import PrimaryToken info 37 | self.primarytokenname = \ 38 | self.config['PrimaryToken']['primarytokenname'] 39 | logger.info('Primary Token Name: {}'.format(self.primarytokenname)) 40 | self.primarytokensymbol = \ 41 | self.config['PrimaryToken']['primarytokensymbol'] 42 | logger.info('Primary Token Symbol: {}'.format(self.primarytokensymbol)) 43 | self.primarytokencontractaddress = \ 44 | self.config['PrimaryToken']['primarytokencontractaddress'] 45 | logger.info('Primary Token Address: {}'.format( 46 | self.primarytokencontractaddress)) 47 | # Import EtherScan info 48 | self.etherscanapikey = \ 49 | self.config['EtherScanAPI']['etherscanapikey'] 50 | logger.info('EtherScan API Key: {}'.format(self.etherscanapikey)) 51 | # Import Uniswap address 52 | self.uniswapaddress = \ 53 | self.config['Uniswap']['uniswapaddress'] 54 | logger.info('Uniswap Address: {}'.format(self.uniswapaddress)) 55 | # Import Telegram information 56 | self.telegramapitoken = \ 57 | self.config['Telegram']['telegramapitoken'] 58 | logger.info('Telegram Api Token: {}'.format(self.telegramapitoken)) 59 | 60 | self.telegramlastprocessedupdateid = \ 61 | self.config['Telegram']['telegramlastprocessedupdateid'] 62 | logger.info('Last processed Telegram update ID: {}'.format( 63 | self.telegramlastprocessedupdateid 64 | )) 65 | if self.config['Telegram']['telegramactivatedchannels'] != "None": 66 | self.telegramactivatedchannels = \ 67 | self.config['Telegram']['telegramactivatedchannels'].split(",") 68 | logger.info('Telegram activated channels found: {}'.format( 69 | self.config['Telegram']['telegramactivatedchannels'])) 70 | # Import processing information 71 | if self.config['Process']['lastprocessedblocknumber'] != "None": 72 | self.lastprocessedblocknumber = \ 73 | self.config['Process']['lastprocessedblocknumber'] 74 | logger.info('Last processed blocknumber: {}'.format( 75 | self.lastprocessedblocknumber)) 76 | else: 77 | logger.info('No last processed blocknumber found in config') 78 | # Import advanced info 79 | self.pairtokendecimals = \ 80 | self.config['Advanced']['pairtokendecimals'] 81 | logger.info('Pair token decimals: {}'.format( 82 | self.pairtokendecimals 83 | )) 84 | 85 | def writetofile(self): 86 | logger.info('Writing config file') 87 | 88 | # Write config file. 89 | with open(self.configfile, 'w') as configfile: 90 | self.config.write(configfile) 91 | 92 | def updateblocknumber(self,blocknumber): 93 | if int(self.lastprocessedblocknumber) <= int(blocknumber): 94 | self.config['Process'] = { 95 | "lastprocessedblocknumber" : str(blocknumber) 96 | } 97 | self.writetofile() 98 | self.lastprocessedblocknumber = str(blocknumber) 99 | 100 | def update_telegramsettings(self,telegramchannel=None,updateid=None): 101 | if telegramchannel != None: 102 | self.telegramactivatedchannels.append(telegramchannel) 103 | logger.info("Telegram channels updated with: {}".format( 104 | telegramchannel)) 105 | 106 | if updateid != None: 107 | self.telegramlastprocessedupdateid = updateid 108 | logger.info("Telegram update ID updated to: {}".format(updateid)) 109 | 110 | self.config['Telegram'] = { 111 | "telegramapitoken" : self.telegramapitoken , 112 | "telegramactivatedchannels" : ",".join( 113 | self.telegramactivatedchannels), 114 | "telegramlastprocessedupdateid" : self.telegramlastprocessedupdateid 115 | } 116 | self.writetofile() 117 | 118 | def init(): 119 | # Initilize config 120 | global config 121 | config = Config() -------------------------------------------------------------------------------- /api/etherscan/uniswaptransaction.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import bin.settings as settings 3 | from decimal import Decimal 4 | from bin.get_json_from_url import get_json_from_url 5 | from api.coingecko.tokeninformation import TokenInformation 6 | 7 | # Configure logging 8 | logger = logging.getLogger(__name__) 9 | 10 | class UniswapTransaction(): 11 | """ 12 | Base class of an UniSwap transactions. Need the tx hash to collect data. 13 | """ 14 | def __init__( 15 | self, 16 | txhash 17 | ): 18 | logger.info(("Initializing Uniswap transaction object for " 19 | "txhash: {}").format(txhash)) 20 | 21 | self.txhash = txhash 22 | self.json = None 23 | self.action = None 24 | self.blocknumber = None 25 | self.primarytokenamount = None 26 | self.pairtoken = None 27 | self.pairtokenamount = None 28 | self.eurpricetotal = None 29 | self.eurpricepertoken = None 30 | self.usdpricetotal = None 31 | self.usdpricepertoken = None 32 | self.wallet = None 33 | 34 | # Get content from EtherScan 35 | self.get_content() 36 | 37 | # Process the information found in Etherscan 38 | self.process_tx() 39 | 40 | # Calculate the price involved in the tranasaction 41 | self.calculate_price() 42 | 43 | def get_content(self): 44 | esurl = ("https://api.etherscan.io/api" 45 | "?module=proxy" 46 | "&action=eth_getTransactionReceipt" 47 | "&txhash={}" 48 | "&apikey={}").format(self.txhash,settings.config.etherscanapikey) 49 | logger.info("The Etherscan Uniswap transaction URL is: {}".format( 50 | esurl)) 51 | try: 52 | self.json = get_json_from_url(esurl) 53 | logger.info("The JSON file is succesfully retrieved") 54 | except: 55 | logger.error("Can't retrieve the JSON file from the url") 56 | 57 | 58 | def process_tx(self): 59 | # Transfer topic for ETH transactions = 60 | # 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef 61 | t = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 62 | # Mint topic for ETH transactions = 63 | # 0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f 64 | m = "0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f" 65 | # Butn topic for ETH transactions = 66 | # 0xdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496 67 | 68 | b = "0xdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496" 69 | logger.info("Processing logs for Uniswap Transaction: {}".format( 70 | self.txhash)) 71 | self.blocknumber = int(self.json['result']["blockNumber"],0) 72 | # Configure uniswap address with additional 0's 73 | uniswapaddress = "{0:#0{1}x}".format(int( 74 | settings.config.uniswapaddress,16),66) 75 | for logentry in self.json['result']['logs']: 76 | # If log entry is a transaction, process transfer 77 | # If log entry = mint. Set action on Liquidity Added. 78 | # If log entry = burn. Set action to Liquidity Removed 79 | if logentry['topics'][0] == t: 80 | if logentry['topics'][1] == uniswapaddress or \ 81 | logentry['topics'][2] == uniswapaddress: 82 | self.process_transfer(logentry) 83 | elif logentry['topics'][0] == m: 84 | self.action = 'Liquidity Added' 85 | elif logentry['topics'][0] == b: 86 | self.action = 'Liquidity Removed' 87 | 88 | def process_transfer(self,logentry): 89 | if logentry['address'] == settings.config.primarytokencontractaddress: 90 | logger.info("Primary token transactions found") 91 | 92 | # Configure uniswap address with additional 0's 93 | uniswapaddress = "{0:#0{1}x}".format(int( 94 | settings.config.uniswapaddress,16),66) 95 | 96 | if logentry['topics'][1] == uniswapaddress and self.action == None: 97 | logger.info( 98 | "The Primary token in this transaction has been bought") 99 | self.action = "Bought" 100 | self.wallet = logentry['topics'][2] 101 | logger.info( 102 | "The wallet which bought the primary token is: {}".format( 103 | self.wallet)) 104 | if logentry['topics'][2] == uniswapaddress and self.action == None: 105 | logger.info( 106 | "The Primary token in this transaction has been sold") 107 | self.action = "Sold" 108 | 109 | self.wallet = logentry['topics'][1] 110 | logger.info( 111 | "The wallet which sold the primary token is: {}".format( 112 | self.wallet)) 113 | 114 | # Processing the amount of tokens which has been send 115 | data = logentry["data"] 116 | self.primarytokenamount = Decimal( 117 | str(int(data,16))) / 1000000000000000000 118 | logger.info(("Amount of primary tokens involved in the " 119 | "transaction: {}").format(self.primarytokenamount)) 120 | elif logentry['address'] == settings.config.uniswapaddress: 121 | pass 122 | else: 123 | logger.info( 124 | "Secondary transaction contract address found: {}".format( 125 | logentry['address'])) 126 | # looking up information about the token which has been used 127 | self.pairtoken = TokenInformation(logentry['address']) 128 | 129 | # Processing the amount of tokens which has been send 130 | data = logentry["data"] 131 | self.pairtokenamount = Decimal( 132 | str(int(data,16))) / int(str( 133 | "1" + int(settings.config.pairtokendecimals) *"0")) 134 | logger.info("{} amount involved in transactions: {}".format( 135 | self.pairtoken.tokenname, self.pairtokenamount)) 136 | 137 | def calculate_price(self): 138 | self.eurpricetotal = self.pairtokenamount * self.pairtoken.eurprice 139 | logger.info("Calculated Euro price total transaction: €{}".format( 140 | self.eurpricetotal 141 | )) 142 | 143 | self.usdpricetotal = self.pairtokenamount * self.pairtoken.usdprice 144 | logger.info("Calculated Dollar price total transaction: ${}".format( 145 | self.usdpricetotal 146 | )) 147 | 148 | self.eurpricepertoken = self.eurpricetotal / self.primarytokenamount 149 | logger.info("Calculated Euro price per token: €{}".format( 150 | self.eurpricepertoken 151 | )) 152 | 153 | self.usdpricepertoken = self.usdpricetotal / self.primarytokenamount 154 | logger.info("Calculated Dollar price per token: ${}".format( 155 | self.usdpricepertoken 156 | )) 157 | 158 | 159 | # Calculte the price compared to the paired token 160 | self.pairtokenpricept = self.pairtokenamount / self.primarytokenamount 161 | logger.info("Calculated the paired price per token: {} {}".format( 162 | self.pairtokenpricept, self.pairtoken.tokenname 163 | )) 164 | 165 | 166 | def __str__(self): 167 | return "Uniswap transactionobject for txhash: {}".format(self.txhash) 168 | 169 | -------------------------------------------------------------------------------- /bin/uniswapprocessor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import bin.settings as settings 4 | from bin.formatnumber import formatnumber 5 | from bin.wallettostring import wallettostring 6 | from api.etherscan.uniswaptransactionbatch import UniswapTransactionBatch 7 | from api.etherscan.uniswaptransaction import UniswapTransaction 8 | from api.etherscan.gettokenamount import gettokenamount 9 | from api.telegram.telegramsendmessage import TelegramSendMessage 10 | 11 | # Configure logging 12 | logger = logging.getLogger(__name__) 13 | 14 | class UniswapProcessor(): 15 | def __init__(self): 16 | logger.info("Start Uniswap Processor") 17 | 18 | def process_uniswaptransactionbatch(self): 19 | # Get all transaction for the UniSwap address from the last processed 20 | # blocknumber 21 | logger.info("Start looking for a new Uniswap transaction batch") 22 | utb = UniswapTransactionBatch(settings.config.lastprocessedblocknumber) 23 | 24 | logger.info("Processing Uniswap transaction batch") 25 | # Foreach transaction has in the batch, get the information from the 26 | # transaction (amount, pair token, price etc. will be returned) 27 | for transactionhash in utb.transactionhashes: 28 | try: 29 | ut = UniswapTransaction(transactionhash) 30 | except: 31 | logger.warning("Transaction {} can't be processed".format( 32 | transactionhash)) 33 | continue 34 | 35 | # If wallet is None, skip to the next hash 36 | # Implemented for very rare transactions like (rare swap) 37 | # 0x02d3cd8e60ed3bac6bd32d75e32326d84ffdc38f1ab3d37c69127e050a07ac4c 38 | if ut.wallet == None: 39 | continue 40 | # Send message to active Telegram channels with the information 41 | # gathered earlier 42 | if ut.action == "Sold" or ut.action == "Bought": 43 | if ut.action == "Bought": 44 | actionicon = "\U0001f7e2" 45 | if ut.action == "Sold": 46 | actionicon = "\U0001f534" 47 | msg = ( 48 | "{primarytokenname} {action} {actionicon}\n" 49 | "Block: {blocknumber}\n\n" 50 | "{primarytokenamount} {primarytokensymbol} " 51 | "{laction} for {pairtokenamount} {pairtokenname}\n" 52 | "{primarytokensymbol} Value: " 53 | "{eurpricetotal} EUR / {usdpricetotal} USD\n" 54 | "(Price per {primarytokensymbol}: " 55 | "{eurpricepertoken} EUR / " 56 | "{usdpricepertoken} USD / " 57 | "{pairtokenpricept} {pairtokenname})\n\n" 58 | "1 {pairtokenname} = {pairtokeneurprice} EUR / " 59 | "{pairtokenusdprice} USD \n\n" 60 | "TX here: " 61 | "link\n" 62 | "Wallet: " 63 | "" 64 | "{wallet}\n" 65 | "Uniswap pair: " 66 | "" 67 | "link" 68 | ).format( 69 | action = ut.action, 70 | laction = ut.action.lower(), 71 | actionicon = actionicon, 72 | primarytokenamount = formatnumber(ut.primarytokenamount), 73 | primarytokenname = settings.config.primarytokenname, 74 | primarytokensymbol = settings.config.primarytokensymbol, 75 | pairtokenamount = formatnumber(ut.pairtokenamount), 76 | pairtokenname = ut.pairtoken.tokenname, 77 | pairtokeneurprice = formatnumber(ut.pairtoken.eurprice), 78 | pairtokenusdprice = formatnumber(ut.pairtoken.usdprice), 79 | eurpricetotal = formatnumber(ut.eurpricetotal), 80 | usdpricetotal = formatnumber(ut.usdpricetotal), 81 | txhash = ut.txhash, 82 | blocknumber = ut.blocknumber, 83 | eurpricepertoken= formatnumber(ut.eurpricepertoken), 84 | usdpricepertoken= formatnumber(ut.usdpricepertoken), 85 | pairtokenpricept = formatnumber(ut.pairtokenpricept,8), 86 | wallet = wallettostring(ut.wallet), 87 | uniswapaddress = settings.config.uniswapaddress 88 | ) 89 | elif ut.action == 'Liquidity Added' or \ 90 | ut.action == 'Liquidity Removed': 91 | 92 | if ut.action == 'Liquidity Added': 93 | actionicon = '\U0001f7e9' 94 | if ut.action == 'Liquidity Removed': 95 | actionicon = '\U0001f7e5' 96 | 97 | pairtokenatuniswap = gettokenamount( 98 | settings.config.uniswapaddress, 99 | ut.pairtoken.contractaddress 100 | ) 101 | primarytokenatuniswap = gettokenamount( 102 | settings.config.uniswapaddress, 103 | settings.config.primarytokencontractaddress 104 | ) 105 | msg = ( 106 | "{action} {actionicon}\n" 107 | "Block: {blocknumber}\n\n" 108 | "{pairtokenamount} {pairtokenname} and " 109 | "{primarytokenamount} {primarytokensymbol}\n" 110 | "Combined value: {eurpricetotal} EUR / " 111 | "{usdpricetotal} USD\n\n" 112 | "TX here: " 113 | "link\n" 114 | "Wallet: " 115 | "{wallet}\n" 116 | "Uniswap pair: " 117 | "" 118 | "link" 119 | "\n\nNew pooled token amounts:\n" 120 | "Pooled {pairtokenname}: {pairtokenatuniswap}\n" 121 | "Pooled {primarytokensymbol}: {primarytokenatuniswap}" 122 | ).format( 123 | action = ut.action, 124 | actionicon = actionicon, 125 | primarytokenamount = formatnumber(ut.primarytokenamount), 126 | primarytokensymbol = settings.config.primarytokensymbol, 127 | pairtokenamount = formatnumber(ut.pairtokenamount), 128 | pairtokenname = ut.pairtoken.tokenname, 129 | eurpricetotal = formatnumber(ut.eurpricetotal * 2), 130 | usdpricetotal = formatnumber(ut.usdpricetotal * 2), 131 | txhash = ut.txhash, 132 | blocknumber = ut.blocknumber, 133 | wallet = wallettostring(ut.wallet), 134 | pairtokenatuniswap = formatnumber(pairtokenatuniswap), 135 | primarytokenatuniswap = formatnumber(primarytokenatuniswap), 136 | uniswapaddress = settings.config.uniswapaddress 137 | ) 138 | 139 | for channel in settings.config.telegramactivatedchannels: 140 | TelegramSendMessage(channel,msg) 141 | 142 | # Change last blocknumber in settings file to last processed block 143 | # number + 1 144 | nextblocknumber = str((int(ut.blocknumber) + 1)) 145 | settings.config.updateblocknumber(nextblocknumber) 146 | 147 | def start(self,pollinterval=60): 148 | logger.info("Starting Uniswap processor cycle " 149 | " with a poll interval of: {} seconds".format(pollinterval)) 150 | while True: 151 | try: 152 | self.process_uniswaptransactionbatch() 153 | logger.info(("Uniswap processor cycle finished, waiting {} " 154 | "seconds").format( 155 | pollinterval)) 156 | time.sleep(pollinterval) 157 | except: 158 | logger.error("Uniswap Processor run failed") 159 | time.sleep(10) 160 | --------------------------------------------------------------------------------