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