From 572e09db06a173a805730b77d423f3a22571f453 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 19 Nov 2024 21:55:24 +0100 Subject: [PATCH 001/103] Adding pump.fun python class with new token listener, token listener and marketMaking function --- .gitignore | 166 ++++++++++++++++++++++- bot/__init__.py | 0 bot/config/__init__.py | 30 ++++ bot/main.py | 18 +++ bot/module/__init.__.py | 0 bot/module/pump.py | 286 +++++++++++++++++++++++++++++++++++++++ example/requirements.txt | 6 + 7 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 bot/__init__.py create mode 100644 bot/config/__init__.py create mode 100644 bot/main.py create mode 100644 bot/module/__init.__.py create mode 100644 bot/module/pump.py create mode 100644 example/requirements.txt diff --git a/.gitignore b/.gitignore index b3e7071..0b1c76b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,168 @@ dist/ .keys*/** .env -refs/ \ No newline at end of file +refs/ + +# Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/config/__init__.py b/bot/config/__init__.py new file mode 100644 index 0000000..b24570d --- /dev/null +++ b/bot/config/__init__.py @@ -0,0 +1,30 @@ +import os + + +class AuthConfig: + # our code environment + ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") + + # Stable and liquid pairs: 0.1% to 0.5%. + # Moderate trading volumes: 0.5% to 1%. + # High volatility or limited liquidity: 1% to 3% + # Very volatile or low-liquidity asset: up to 5% + SLIPPAGE = os.environ.get("SLIPPAGE", 10) + FEES = float(os.environ.get("FEES", 0.005)) + FEES_BPS = float(os.environ.get("FEES", 0.005)) * 10000 # Fees in BPS + PRIVKEY = os.environ.get( + "PRIVKEY", + "" + ) + SOL_BUY_AMOUNT = float(os.environ.get("SOL_BUY_AMOUNT", 0.001)) # here you can choose to increase/decrease the buy amount + MIN_SOL_TRADING_AMOUNT = float( + os.environ.get("MIN_SOL_TRADING_AMOUNT", 0.5) + ) # Minimum balance a token must have for being traded + RETRYING_SECONDS = 1 + RPC_URL = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" + MARKETMAKING_SOL_BUY_AMOUNT = 0.03 + MARKET_MAKING_RETRIES = 3 + + +appconfig: AuthConfig = AuthConfig() diff --git a/bot/main.py b/bot/main.py new file mode 100644 index 0000000..2dc1301 --- /dev/null +++ b/bot/main.py @@ -0,0 +1,18 @@ +import asyncio +import logging +from module.pump import Pump, TradeRoadmap + + +async def main(): + pump = Pump() + + tasks = [ + pump.subscribe(steps=TradeRoadmap.test) + ] + + await asyncio.gather(*tasks) + logging.log("Stoping program") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/bot/module/__init.__.py b/bot/module/__init.__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/module/pump.py b/bot/module/pump.py new file mode 100644 index 0000000..ab8d48f --- /dev/null +++ b/bot/module/pump.py @@ -0,0 +1,286 @@ +from config import appconfig +import json +import requests +import time +import websockets + +from enum import Enum +from solders.transaction import VersionedTransaction +from solders.keypair import Keypair +from solders.commitment_config import CommitmentLevel +from solders.rpc.requests import SendVersionedTransaction +from solders.rpc.config import RpcSendTransactionConfig + + +class Suscription(Enum): + subscribeNewToken = "subscribeNewToken" + subscribeAccountTrade = "subscribeAccountTrade" + subscribeTokenTrade = "subscribeTokenTrade" + + +class TxType(Enum): + buy = "buy" + sell = "sell" + + +class TradeRoadmap: + test = [ + {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, + {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, + ] + + +class Pump: + sniping_token_list = [] + + def __init__(self) -> None: + self.uri_data = "wss://pumpportal.fun/api/data" + self.accounts = [] + self.tokens = [] + self.traders = [] + self.min_initial_buy = 60000000 + self.token_target_amount = 1 + + def add_account(self, account: str): + self.accounts.append(account) + + def remove_account(self, account: str): + if account in self.accounts: + self.accounts.pop(self.accounts.index(account)) + + # Tokens to pay attention to + def add_token(self, token: str): + self.tokens.append(token) + + def clear_tokens(self): + self.tokens = [] + + def remove_token(self, token: str): + if token in self.tokens: + self.tokens.pop( + self.tokens.index(token) + ) + + # Traders to follow for starting and stoping pumps + def add_trader(self, trader: str): + self.traders.append(trader) + + def remove_trader(self, trader: str): + if trader in self.traders: + self.traders.pop(self.traders.index(trader)) + + async def subscribe(self, steps: list): + step_index = 0 + + async with websockets.connect(self.uri_data) as websocket: + # Subscribing to token creation events + while True: + suscription = steps[step_index]["suscription"] + + payload = { + "method": suscription.value, + } + + if suscription.value == Suscription.subscribeAccountTrade.value: + payload["keys"] = self.accounts + + if suscription.value == Suscription.subscribeTokenTrade.value: + payload["keys"] = self.tokens + + await websocket.send(json.dumps(payload)) + + async for message in websocket: + msg = json.loads(message) + move_to_next_step = False + + # This is the first message we get when we connect + if "message" in msg: + print(msg["message"]) + continue + + if suscription.value == Suscription.subscribeNewToken.value: + move_to_next_step = self.newTokenSuscription(msg=msg) + + if suscription.value == Suscription.subscribeTokenTrade.value: + move_to_next_step = self.tokenTradeSuscription(msg=msg) + + # Moving to the next step or going back to the first one + if move_to_next_step: + step_index += 1 + move_to_next_step = False + step_index = 0 if step_index >= len(steps) else step_index + roadmap_name = steps[step_index]["name"] + if step_index == 0: + print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) + + break + + # TODO: add exiting way of ending the program + + + def newTokenSuscription(self, msg: str) -> bool: + if "initialBuy" in msg and int(msg["initialBuy"]) > self.min_initial_buy: + # Listening to a predefined amount of tokens + if len(self.tokens) < self.token_target_amount: + print( + "Symbol: {}. mint: {}. tarder: {}. Initial buy: {}".format( + msg["symbol"], + msg["mint"], + msg["traderPublicKey"][:5], + int(msg["initialBuy"]) + ) + ) + self.clear_tokens() # TODO: Change this when more than one token will be checked + self.add_token(token=msg["mint"]) + # testing + self.marketMaking(token=msg["mint"]) + + return len(self.tokens) == self.token_target_amount + + def tokenTradeSuscription(self, msg: str) -> bool: + # TODO: remove this temporal validation for testing + if not self.traders and "txType" in msg: + if TxType.buy.value == msg["txType"]: + self.add_trader(trader=msg["traderPublicKey"]) + + if "traderPublicKey" in msg and msg["traderPublicKey"] in self.traders: + if TxType.buy.value == msg["txType"]: + print("Starting pumping bot. Listening to trader: {} - Buy: {}. MarketCap: {}".format( + msg["traderPublicKey"][:5], + msg["tokenAmount"], + msg["marketCapSol"] + ) + ) + + if TxType.sell.value == msg["txType"]: + print("Stoping pumping bot. Sell: {}. MarketCap: {}".format( + msg["tokenAmount"], + msg["marketCapSol"] + ) + ) + self.traders = [] + return True + + return False + + # def start_suscription(self, suscription=Suscription.subscribeTokenTrade): + # # Run the subscribe function + # asyncio.get_event_loop().run_until_complete( + # self.subscribe(suscription=suscription) + # ) + def marketMaking( + self, + token: str, + amount: int = appconfig.MARKETMAKING_SOL_BUY_AMOUNT + ): + """marketMaking will buy an amount of tokens and inmediatelly sell them + + Args: + token (str): token mint address + amount (int): amount of Solana + """ + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + + # BUYING tokens with an amount of Solana + data = { + "publicKey": str(keypair.pubkey()), + "action": TxType.buy.value, + "mint": token, + "amount": amount, # amount of SOL or tokens to trade + "denominatedInSol": "true", # "true" if amount is amount of SOL, "false" if amount is number of tokens + "slippage": appconfig.SLIPPAGE, # percent slippage allowed + "priorityFee": appconfig.FEES, # amount to use as priority fee + "pool": "pump" # exchange to trade on. "pump" or "raydium" + } + + response = None + retries = 0 + while True: + response = requests.post( + url=appconfig.PUMPFUN_TRANSACTION_URL, + data=data + ) + if response.status_code != 200: + time.sleep(appconfig.RETRYING_SECONDS) + continue + + tx = VersionedTransaction( + VersionedTransaction.from_bytes(response.content).message, + [keypair] + ) + + config = RpcSendTransactionConfig( + preflight_commitment=CommitmentLevel.Confirmed + ) + txPayload = SendVersionedTransaction(tx, config) + + response = requests.post( + url=appconfig.RPC_URL, + headers={"Content-Type": "application/json"}, + data=txPayload.to_json() + ) + if response.status_code == 200: + try: + txSignature = response.json()['result'] + print(f'Transaction: https://solscan.io/tx/{txSignature}') + break + + except Exception as e: + print("marketMaking->Buy. Transaction failed. retrying again: {}".format( + response.json()["error"]["message"] + )) + retries += 1 + time.sleep(appconfig.RETRYING_SECONDS) + if retries == appconfig.MARKET_MAKING_RETRIES: + print("marketMaking.error-> Buy: Max retries reached. Exiting") + break + continue + + # SELLING 100% of tokens + data = { + "publicKey": keypair.pubkey(), + "action": TxType.sell.value, + "mint": token, + "amount": "100%", + "denominatedInSol": "false", + "slippage": appconfig.SLIPPAGE, + "priorityFee": appconfig.FEES, + "pool": "pump" + } + + response = None + while True: + response = requests.post( + url=appconfig.PUMPFUN_TRANSACTION_URL, + data=data + ) + if response.status_code != 200: + time.sleep(appconfig.RETRYING_SECONDS) + continue + + tx = VersionedTransaction( + VersionedTransaction.from_bytes(response.content).message, + [keypair] + ) + + config = RpcSendTransactionConfig( + preflight_commitment=CommitmentLevel.Confirmed + ) + txPayload = SendVersionedTransaction(tx, config) + + response = requests.post( + url=appconfig.RPC_URL, + headers={"Content-Type": "application/json"}, + data=txPayload.to_json() + ) + if response.status_code == 200: + try: + txSignature = response.json()['result'] + print(f'Transaction: https://solscan.io/tx/{txSignature}') + break + except Exception as e: + print("marketMaking->Buy. Transaction failed. retrying again: {}".format( + response.json()["error"]["message"] + )) + time.sleep(appconfig.RETRYING_SECONDS) + continue diff --git a/example/requirements.txt b/example/requirements.txt new file mode 100644 index 0000000..d50f011 --- /dev/null +++ b/example/requirements.txt @@ -0,0 +1,6 @@ +solana==0.35.0 +solders==0.21.0 +cachetools==5.5.0 +urllib3==2.2.3 +websockets==11.0.3 +environ==1.0 From 9891299c25b1059ae40fc2eb9e47a342f5a2078a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 19 Nov 2024 22:44:47 +0100 Subject: [PATCH 002/103] Adding basic testtings to pump class --- .vscode/settings.json | 11 ++++++++++ bot/module/pump.py | 2 +- bot/requirements.txt | 10 +++++++++ bot/tests/__init__.py | 0 bot/tests/module/__init__.py | 0 bot/tests/module/fixture_pump.py | 21 +++++++++++++++++++ bot/tests/module/test_pump.py | 36 ++++++++++++++++++++++++++++++++ 7 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 bot/requirements.txt create mode 100644 bot/tests/__init__.py create mode 100644 bot/tests/module/__init__.py create mode 100644 bot/tests/module/fixture_pump.py create mode 100644 bot/tests/module/test_pump.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4d1eae2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./bot", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/bot/module/pump.py b/bot/module/pump.py index ab8d48f..3b5635c 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1,4 +1,4 @@ -from config import appconfig +from bot.config import appconfig import json import requests import time diff --git a/bot/requirements.txt b/bot/requirements.txt new file mode 100644 index 0000000..ef79ab9 --- /dev/null +++ b/bot/requirements.txt @@ -0,0 +1,10 @@ +solana==0.35.0 +solders==0.21.0 +cachetools==5.5.0 +urllib3==2.2.3 +websockets==11.0.3 +environ==1.0 +pytest==8.3.3 +requests==2.32.3 +coverage==7.6.7 +pytest-cov==6.0.0 diff --git a/bot/tests/__init__.py b/bot/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/tests/module/__init__.py b/bot/tests/module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/tests/module/fixture_pump.py b/bot/tests/module/fixture_pump.py new file mode 100644 index 0000000..3517a28 --- /dev/null +++ b/bot/tests/module/fixture_pump.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture +def get_account(): + yield "Some_account" + + +@pytest.fixture +def get_token(): + yield "Some_token_pump" + + +@pytest.fixture +def get_token2(): + yield "Some_token2_pump" + + +@pytest.fixture +def get_trader(): + yield "Some_trader_address" diff --git a/bot/tests/module/test_pump.py b/bot/tests/module/test_pump.py new file mode 100644 index 0000000..ece169b --- /dev/null +++ b/bot/tests/module/test_pump.py @@ -0,0 +1,36 @@ +from bot.module.pump import Pump +from bot.config import appconfig +from bot.tests.module.fixture_pump import * + + +def test_attribute_handling( + get_account, + get_token, + get_token2, + get_trader +): + pump = Pump() + + pump.add_account(account=get_account) + assert pump.accounts == [get_account] + + pump.remove_account(account=get_account) + assert pump.accounts == [] + + pump.add_token(token=get_token) + assert pump.tokens == [get_token] + + pump.remove_token(token=get_token) + assert pump.tokens == [] + + pump.add_token(token=get_token2) + pump.clear_tokens() + assert pump.tokens == [] + + pump.add_trader(trader=get_trader) + assert pump.traders == [get_trader] + + pump.remove_trader(trader=get_trader) + assert pump.traders == [] + + \ No newline at end of file From 5a48313b0d6a7a03ff5fda33fe7dbff0bc6f540d Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 20 Nov 2024 17:38:09 +0100 Subject: [PATCH 003/103] add comments, new libraries and a list of bypasses --- bot/bug_and_bypasses.txt | 9 +++++++++ bot/module/pump.py | 19 +++++++++++++++---- bot/requirements.txt | 2 ++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 bot/bug_and_bypasses.txt diff --git a/bot/bug_and_bypasses.txt b/bot/bug_and_bypasses.txt new file mode 100644 index 0000000..f5a4ed0 --- /dev/null +++ b/bot/bug_and_bypasses.txt @@ -0,0 +1,9 @@ +Error: + Exception has occurred: SSLCertVerificationError + [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1007) + +Solution + Run in Mac: /Applications/Python\ 3.10/Install\ Certificates.command + pip install certifi + python3 -m ensurepip --upgrade + python3 -m pip install --upgrade pip setuptools \ No newline at end of file diff --git a/bot/module/pump.py b/bot/module/pump.py index 3b5635c..8c34e96 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1,8 +1,11 @@ -from bot.config import appconfig +from config import appconfig import json import requests import time import websockets +import ssl +import certifi + from enum import Enum from solders.transaction import VersionedTransaction @@ -28,6 +31,7 @@ class TradeRoadmap: {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, ] + class Pump: @@ -41,6 +45,9 @@ def __init__(self) -> None: self.min_initial_buy = 60000000 self.token_target_amount = 1 + self.ssl_context = ssl.create_default_context(cafile=certifi.where()) + + def add_account(self, account: str): self.accounts.append(account) @@ -72,7 +79,8 @@ def remove_trader(self, trader: str): async def subscribe(self, steps: list): step_index = 0 - async with websockets.connect(self.uri_data) as websocket: + # SSL context if required in Mac and not on windows + async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: # Subscribing to token creation events while True: suscription = steps[step_index]["suscription"] @@ -89,6 +97,7 @@ async def subscribe(self, steps: list): await websocket.send(json.dumps(payload)) + # MAIN WEBSOCKET THREAT async for message in websocket: msg = json.loads(message) move_to_next_step = False @@ -133,10 +142,12 @@ def newTokenSuscription(self, msg: str) -> bool: self.clear_tokens() # TODO: Change this when more than one token will be checked self.add_token(token=msg["mint"]) # testing - self.marketMaking(token=msg["mint"]) + # self.marketMaking(token=msg["mint"]) + # TODO: relase tokens to snipers with redis recods. At this moment we're listening to one token only return len(self.tokens) == self.token_target_amount + def tokenTradeSuscription(self, msg: str) -> bool: # TODO: remove this temporal validation for testing if not self.traders and "txType" in msg: @@ -279,7 +290,7 @@ def marketMaking( print(f'Transaction: https://solscan.io/tx/{txSignature}') break except Exception as e: - print("marketMaking->Buy. Transaction failed. retrying again: {}".format( + print("marketMaking->Sell. Transaction failed. retrying again: {}".format( response.json()["error"]["message"] )) time.sleep(appconfig.RETRYING_SECONDS) diff --git a/bot/requirements.txt b/bot/requirements.txt index ef79ab9..619da70 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -8,3 +8,5 @@ pytest==8.3.3 requests==2.32.3 coverage==7.6.7 pytest-cov==6.0.0 +certifi==2024.8.30 + From 97b9bc2b8985548b4d0bd29ceae933b50af39660 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 20 Nov 2024 22:03:40 +0100 Subject: [PATCH 004/103] Refactoring: having buy and sell in a single trade function --- bot/module/pump.py | 197 +++++++++++++++++++++++---------------------- 1 file changed, 100 insertions(+), 97 deletions(-) diff --git a/bot/module/pump.py b/bot/module/pump.py index 8c34e96..d904f01 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -31,7 +31,7 @@ class TradeRoadmap: {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, ] - + class Pump: @@ -108,10 +108,10 @@ async def subscribe(self, steps: list): continue if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.newTokenSuscription(msg=msg) + move_to_next_step = self.new_token_suscription(msg=msg) if suscription.value == Suscription.subscribeTokenTrade.value: - move_to_next_step = self.tokenTradeSuscription(msg=msg) + move_to_next_step = self.token_trade_suscription(msg=msg) # Moving to the next step or going back to the first one if move_to_next_step: @@ -127,12 +127,12 @@ async def subscribe(self, steps: list): # TODO: add exiting way of ending the program - def newTokenSuscription(self, msg: str) -> bool: + def new_token_suscription(self, msg: str) -> bool: if "initialBuy" in msg and int(msg["initialBuy"]) > self.min_initial_buy: # Listening to a predefined amount of tokens if len(self.tokens) < self.token_target_amount: print( - "Symbol: {}. mint: {}. tarder: {}. Initial buy: {}".format( + "New token subscription. Symbol: {}. mint: {}. tarder: {}. Initial buy: {}".format( msg["symbol"], msg["mint"], msg["traderPublicKey"][:5], @@ -148,7 +148,7 @@ def newTokenSuscription(self, msg: str) -> bool: return len(self.tokens) == self.token_target_amount - def tokenTradeSuscription(self, msg: str) -> bool: + def token_trade_suscription(self, msg: str) -> bool: # TODO: remove this temporal validation for testing if not self.traders and "txType" in msg: if TxType.buy.value == msg["txType"]: @@ -174,46 +174,72 @@ def tokenTradeSuscription(self, msg: str) -> bool: return False - # def start_suscription(self, suscription=Suscription.subscribeTokenTrade): - # # Run the subscribe function - # asyncio.get_event_loop().run_until_complete( - # self.subscribe(suscription=suscription) - # ) - def marketMaking( - self, - token: str, - amount: int = appconfig.MARKETMAKING_SOL_BUY_AMOUNT - ): - """marketMaking will buy an amount of tokens and inmediatelly sell them - Args: - token (str): token mint address - amount (int): amount of Solana + def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> str: """ - keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + This function allows to BUY or SELL tokens. + When BUYING + - Amount of SOLs must be specified. + When selling, we sell tokens and not solanas. + For this MVP version, we're selling 100% of tokens. + + Parameters: + tx_type (TxType): The type of transaction (e.g., buy, sell, transfer). + token (str): The token symbol or identifier. + amount (int | None): The amount to trade (can be an integer when BUYING or None when SELLING). + keypair (Keypair): The user's Solana keypair for signing the transaction. + + Returns: + str: The transaction signature or identifier. + """ + txSignature = None + + denominated_in_sol = "true" if txtype.value == TxType.buy.value else "false" + amount = int(amount) if txtype.value == TxType.buy.value else "100%" - # BUYING tokens with an amount of Solana + # BUYING/SELLING tokens with an amount of Solana data = { "publicKey": str(keypair.pubkey()), - "action": TxType.buy.value, + "action": txtype.value, "mint": token, - "amount": amount, # amount of SOL or tokens to trade - "denominatedInSol": "true", # "true" if amount is amount of SOL, "false" if amount is number of tokens - "slippage": appconfig.SLIPPAGE, # percent slippage allowed - "priorityFee": appconfig.FEES, # amount to use as priority fee - "pool": "pump" # exchange to trade on. "pump" or "raydium" + "amount": amount, # amount of SOL or tokens to trade. Can be "100%" when selling + "denominatedInSol": denominated_in_sol, # "true" if amount is amount of SOL, "false" if amount is number of tokens + "slippage": appconfig.SLIPPAGE, # percent slippage allowed + "priorityFee": appconfig.FEES, # amount to use as priority fee + "pool": "pump" # exchange to trade on. "pump" or "raydium" } response = None retries = 0 while True: - response = requests.post( - url=appconfig.PUMPFUN_TRANSACTION_URL, - data=data - ) - if response.status_code != 200: + try: + response = requests.post( + url=appconfig.PUMPFUN_TRANSACTION_URL, + data=data + ) + if response.status_code != 200: + retries += 1 + + print("Trade->{} Error: {} returned a status code {}. Retrying again {} times. Response: {}".format( + txtype.value, + appconfig.PUMPFUN_TRANSACTION_URL, + response.status_code, + retries, + response + )) + time.sleep(appconfig.RETRYING_SECONDS) + continue + except Exception as e: + retries += 1 + print("Trade->{} Exception: {}. Retrying again {} times. Message: {}".format( + txtype.value, + appconfig.PUMPFUN_TRANSACTION_URL, + retries, + e + )) time.sleep(appconfig.RETRYING_SECONDS) continue + tx = VersionedTransaction( VersionedTransaction.from_bytes(response.content).message, @@ -225,73 +251,50 @@ def marketMaking( ) txPayload = SendVersionedTransaction(tx, config) - response = requests.post( - url=appconfig.RPC_URL, - headers={"Content-Type": "application/json"}, - data=txPayload.to_json() - ) - if response.status_code == 200: - try: - txSignature = response.json()['result'] - print(f'Transaction: https://solscan.io/tx/{txSignature}') - break - - except Exception as e: - print("marketMaking->Buy. Transaction failed. retrying again: {}".format( - response.json()["error"]["message"] - )) - retries += 1 - time.sleep(appconfig.RETRYING_SECONDS) - if retries == appconfig.MARKET_MAKING_RETRIES: - print("marketMaking.error-> Buy: Max retries reached. Exiting") - break - continue - - # SELLING 100% of tokens - data = { - "publicKey": keypair.pubkey(), - "action": TxType.sell.value, - "mint": token, - "amount": "100%", - "denominatedInSol": "false", - "slippage": appconfig.SLIPPAGE, - "priorityFee": appconfig.FEES, - "pool": "pump" - } + try: + response = requests.post( + url=appconfig.RPC_URL, + headers={"Content-Type": "application/json"}, + data=txPayload.to_json() + ) - response = None - while True: - response = requests.post( - url=appconfig.PUMPFUN_TRANSACTION_URL, - data=data - ) - if response.status_code != 200: + txSignature = response.json()['result'] + print("Trade->{} Transaction: https://solscan.io/tx/{}".format( + txtype.value, + txSignature + )) + return txSignature + + except Exception as e: + retries += 1 + print("Trade->{} Transaction failed. Retrying again {} times: {}".format( + txtype.value, + retries, + response.json()["error"]["message"] + )) + time.sleep(appconfig.RETRYING_SECONDS) + if retries == appconfig.MARKET_MAKING_RETRIES: + # TODO: send a telegram message notifying that a manual sell must be done + print("Trade->{} Error: Max retries reached. Exiting".format(txtype.value)) + return txSignature + continue - tx = VersionedTransaction( - VersionedTransaction.from_bytes(response.content).message, - [keypair] - ) - config = RpcSendTransactionConfig( - preflight_commitment=CommitmentLevel.Confirmed - ) - txPayload = SendVersionedTransaction(tx, config) + def market_making( + self, + token: str, + amount: int = appconfig.MARKETMAKING_SOL_BUY_AMOUNT + ): + """marketMaking will buy an amount of tokens and inmediatelly sell them - response = requests.post( - url=appconfig.RPC_URL, - headers={"Content-Type": "application/json"}, - data=txPayload.to_json() - ) - if response.status_code == 200: - try: - txSignature = response.json()['result'] - print(f'Transaction: https://solscan.io/tx/{txSignature}') - break - except Exception as e: - print("marketMaking->Sell. Transaction failed. retrying again: {}".format( - response.json()["error"]["message"] - )) - time.sleep(appconfig.RETRYING_SECONDS) - continue + Args: + token (str): token mint address + amount (int): amount of Solana + """ + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + + txn = self.buy(token=token, amount=amount, keypair=keypair) + + \ No newline at end of file From 5db35f48034e4604bad542644230cf7a90f4b4dc Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 23 Nov 2024 03:02:58 +0100 Subject: [PATCH 005/103] Adding redis domain object with basic functions --- .vscode/launch.json | 18 +++++ bot/bug_and_bypasses.txt | 24 +++++- bot/config/__init__.py | 11 ++- bot/domain/__init.__.py | 0 bot/domain/redis_db.py | 134 ++++++++++++++++++++++++++++++++ bot/module/pump.py | 161 ++++++++++++++++++++++++++++----------- bot/requirements.txt | 2 + 7 files changed, 300 insertions(+), 50 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 bot/domain/__init.__.py create mode 100644 bot/domain/redis_db.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1f635a9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ +"version": "0.2.0", +"configurations": [ + { + "name": "Debug redis_db.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/domain/redis_db.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + }, + "justMyCode": false + } +] +} + \ No newline at end of file diff --git a/bot/bug_and_bypasses.txt b/bot/bug_and_bypasses.txt index f5a4ed0..7e9442c 100644 --- a/bot/bug_and_bypasses.txt +++ b/bot/bug_and_bypasses.txt @@ -6,4 +6,26 @@ Solution Run in Mac: /Applications/Python\ 3.10/Install\ Certificates.command pip install certifi python3 -m ensurepip --upgrade - python3 -m pip install --upgrade pip setuptools \ No newline at end of file + python3 -m pip install --upgrade pip setuptools + +# Redis installation +vim ~/.zshrc + export PATH="/opt/homebrew/bin:$PATH" +source ~/.zshrc +brew update +brew install redis +# Verify installation +redis-stack-server --version +# Start redis stack +brew services start redis + + +# Redis Stack on docker +docker pull redis/redis-stack:latest +docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest +docker stop redis-stack +docker start redis-stack + + +JSON.SET token:12345 $ '[{"amount":500,"timestamp":1690228321,"is_traded":0}]' +FT.CREATE token_idx ON JSON PREFIX 1 "token:" SCHEMA $.amount NUMERIC $.timestamp NUMERIC $.is_traded BOOL \ No newline at end of file diff --git a/bot/config/__init__.py b/bot/config/__init__.py index b24570d..fe82af9 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -10,8 +10,8 @@ class AuthConfig: # High volatility or limited liquidity: 1% to 3% # Very volatile or low-liquidity asset: up to 5% SLIPPAGE = os.environ.get("SLIPPAGE", 10) - FEES = float(os.environ.get("FEES", 0.005)) - FEES_BPS = float(os.environ.get("FEES", 0.005)) * 10000 # Fees in BPS + FEES = float(os.environ.get("FEES", 0.0005)) + FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", "" @@ -23,8 +23,13 @@ class AuthConfig: RETRYING_SECONDS = 1 RPC_URL = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" + PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 - MARKET_MAKING_RETRIES = 3 + TRADING_DEFAULT_AMOUNT = 0.50 + TRADING_RETRIES = 3 + #DB + REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") + REDIS_PORT = os.environ.get("REDIS_PORT", "6379") appconfig: AuthConfig = AuthConfig() diff --git a/bot/domain/__init.__.py b/bot/domain/__init.__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py new file mode 100644 index 0000000..a5af086 --- /dev/null +++ b/bot/domain/redis_db.py @@ -0,0 +1,134 @@ +import redis +from redisearch import Client, NumericField +from datetime import datetime +from bot.config import appconfig +from bot.lib.utils import TxType +from typing import List, Dict + + +class Path: + @staticmethod + def rootPath(): + return "$" + + +# Connect to Redis +client = redis.StrictRedis( + host=appconfig.REDIS_HOST, + port=appconfig.REDIS_PORT, + decode_responses=True +) + +# Use SCAN to find keys with "Key*" prefix + +class Redis: + key_prefix = "token" + index_name = "token_idx" + + def __init__(self) -> None: + self.client = redis.StrictRedis( + host=appconfig.REDIS_HOST, + port=appconfig.REDIS_PORT, + db=0, + decode_responses=True + ) + self.search_client = Client("token_idx", conn=self.client) + self.create_index() + + def index_exists(self, index_name): + indexes = self.client.execute_command("FT._LIST") + return index_name in indexes + + def create_index(self): + if not self.index_exists(Redis.index_name): + # Create the index on JSON fields + self.search_client.create_index([ + NumericField("amount"), + NumericField("timestamp"), + NumericField("is_traded") + ]) + else: + print("Index '{}' already exists".format(Redis.index_name)) + + + def set_token(self, token: str, token_data: Dict)->bool: + response = self.client.json().set( + name="{}:{}".format(Redis.key_prefix, token), + path=Path.rootPath(), + obj=token_data + ) + return response + + def get_token_keys(self, pattern="token:*"): + keys = self.client.scan_iter(match=pattern) + return keys + + def get_fresh_tokens(self) -> List[Dict]: + tokens = [] + # Search for all tokens where is_traded = False + token_keys = self.get_token_keys() + + # query = Query('@is_traded:[0 0]').paging(0, 1000) + # results = self.search_client.search(query) + for key in token_keys: + data = client.json().get(key) + if data and "is_traded" in data and data["is_traded"] == 0: + token = { + "key": key, + "mint": key.replace("token:", ""), + "amount": float(data["amount"]), + "is_traded": True if int(data["is_traded"]) == 1 else False, + "timestamp": datetime.fromtimestamp(int(data["timestamp"])), + "name": data["name"], + "ticker": data["ticker"] + } + tokens.append(token) + + return tokens + + def update_token(self, token: Dict, txn: str, action: TxType, amount: float) -> Dict: + + key = token["key"] + data = token.copy() + data.pop("key") + data.pop("mint") + data["timestamp"] = data["timestamp"].timestamp() + + # Prepare the data to update + trading_time = datetime.now().timestamp() + new_data = { + "{}_time".format(action.value): trading_time, # New trading time as timestamp + "{}_txn".format(action.value): txn, # New transaction string + "is_traded": 1, + "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action + } + + data.update(new_data) + + # Add or update the fields in the existing token + client.json().set(key, Path.rootPath(), data) + + # Returning original token data with new data + token.update(new_data) + return token + + +if __name__ == "__main__": + redis_object = Redis() + is_traded = False + token_data = { + "name": "Some Day", + "ticker": "SMD", + "amount": 15.500, + "is_traded": 1 if is_traded else 0, + "timestamp": datetime.now().timestamp() + } + redis_object.set_token(token="asdf4", token_data=token_data) + tokens = redis_object.get_fresh_tokens() + + print(tokens) + + for token in tokens: + data = redis_object.update_token(token=token, txn="txn1", action=TxType.buy, amount=15.500) + data = redis_object.update_token(token=data, txn="txn2", action=TxType.sell, amount=45.500) + print(data) diff --git a/bot/module/pump.py b/bot/module/pump.py index d904f01..52f1203 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -6,6 +6,8 @@ import ssl import certifi +from bot.lib.utils import TxType + from enum import Enum from solders.transaction import VersionedTransaction @@ -19,11 +21,13 @@ class Suscription(Enum): subscribeNewToken = "subscribeNewToken" subscribeAccountTrade = "subscribeAccountTrade" subscribeTokenTrade = "subscribeTokenTrade" + unsubscribeTokenTrade = "unsubscribeTokenTrade" -class TxType(Enum): - buy = "buy" - sell = "sell" +class Redis(Enum): + readToken = "readToken" + selectToken = "selectToken" + claseToken = "closeToken" class TradeRoadmap: @@ -31,20 +35,44 @@ class TradeRoadmap: {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, ] + sniper_1 = [ + {"step": 0, "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken}, + {"step": 1, "name": "TRADE_TOKEN", "action": TxType.buy}, + {"step": 2, "name": "TOKEN_SUBSCRIPTION", "suscription": Suscription.subscribeTokenTrade}, + { + "step": 3, + "name": "TRADE_TOKEN", + "trade": TxType.sell, + "criteria": { + "max_seconds_between_buys": 3, + "trader_has_sold": True, + "same_balance": True, + "max_seconds_in_market": 30 + } + }, + {"step": 4, "name": "test", "suscription": Suscription.unsubscribeTokenTrade}, + {"step": 5, "name": "MARK_TOKEN", "redis": Redis.claseToken}, + ] class Pump: sniping_token_list = [] - def __init__(self) -> None: - self.uri_data = "wss://pumpportal.fun/api/data" + def __init__( + self, + sniper_name: str, + amount: float = appconfig.TRADING_DEFAULT_AMOUNT + ) -> None: + self.uri_data = appconfig.PUMPFUN_WEBSOCKET self.accounts = [] self.tokens = [] self.traders = [] self.min_initial_buy = 60000000 self.token_target_amount = 1 - + self.trading_amount = amount + self.sniper_name = sniper_name + self.keypair = Keypair.from_base58_string(appconfig.PRIVKEY) self.ssl_context = ssl.create_default_context(cafile=certifi.where()) @@ -83,47 +111,85 @@ async def subscribe(self, steps: list): async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: # Subscribing to token creation events while True: - suscription = steps[step_index]["suscription"] + step_index = 0 if step_index >= len(steps) else step_index + roadmap_name = steps[step_index]["name"] + if step_index == 0: + print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) - payload = { - "method": suscription.value, - } + step = steps[step_index] - if suscription.value == Suscription.subscribeAccountTrade.value: - payload["keys"] = self.accounts + + if "redis" in step: + # Listen to redis change + if step["redis"] == Redis.readToken: + # TODO: subscribe to redis websocket as a separate function + print("Reading redis and waiting for a new token") + # TODO: Mark that token with the sniper name + print("Token {} assigned to sniper {} to trade {}".format( + self.tokens[0], + self.sniper_name, + self.trading_amount + )) + step_index += 1 - if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = self.tokens + continue + if step["redis"] == Redis.claseToken: + # Save in redis the amount of buy and sell with timestamp. Calculate P&L + step_index += 1 + continue + + if "action" in step: + if step["action"] == TxType.buy: + txn = self.trade( + txtype=TxType.buy, + token=self.add_token[0], + keypair=self.keypair, + amount=self.trading_amount + ) + step_index += 1 + continue - await websocket.send(json.dumps(payload)) - # MAIN WEBSOCKET THREAT - async for message in websocket: - msg = json.loads(message) - move_to_next_step = False + if "subscription" in step: + suscription = steps[step_index]["suscription"] - # This is the first message we get when we connect - if "message" in msg: - print(msg["message"]) - continue + payload = { + "method": suscription.value, + } - if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.new_token_suscription(msg=msg) + if suscription.value == Suscription.subscribeAccountTrade.value: + payload["keys"] = self.accounts if suscription.value == Suscription.subscribeTokenTrade.value: - move_to_next_step = self.token_trade_suscription(msg=msg) + payload["keys"] = self.tokens - # Moving to the next step or going back to the first one - if move_to_next_step: - step_index += 1 + if suscription.value == Suscription.unsubscribeTokenTrade.value: + payload["keys"] = self.tokens + + await websocket.send(json.dumps(payload)) + + # MAIN WEBSOCKET THREAT + async for message in websocket: + msg = json.loads(message) move_to_next_step = False - step_index = 0 if step_index >= len(steps) else step_index - roadmap_name = steps[step_index]["name"] - if step_index == 0: - print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) - - break - + + # This is the first message we get when we connect + if "message" in msg: + print(msg["message"]) + continue + + if suscription.value == Suscription.subscribeNewToken.value: + move_to_next_step = self.new_token_suscription(msg=msg) + + if suscription.value == Suscription.subscribeTokenTrade.value: + move_to_next_step = self.token_trade_suscription(msg=msg) + + # Moving to the next step or going back to the first one + if move_to_next_step: + step_index += 1 + move_to_next_step = False + break + # TODO: add exiting way of ending the program @@ -175,7 +241,7 @@ def token_trade_suscription(self, msg: str) -> bool: return False - def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> str: + def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = None) -> str: """ This function allows to BUY or SELL tokens. When BUYING @@ -186,7 +252,7 @@ def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> s Parameters: tx_type (TxType): The type of transaction (e.g., buy, sell, transfer). token (str): The token symbol or identifier. - amount (int | None): The amount to trade (can be an integer when BUYING or None when SELLING). + amount (float | None): The amount to trade (can be an float when BUYING or None when SELLING). keypair (Keypair): The user's Solana keypair for signing the transaction. Returns: @@ -212,6 +278,11 @@ def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> s response = None retries = 0 while True: + if retries == appconfig.TRADING_RETRIES: + # TODO: send a telegram message notifying that a manual sell must be done + print("Trade->{} Error: Max retries reached. Exiting".format(txtype.value)) + break + try: response = requests.post( url=appconfig.PUMPFUN_TRANSACTION_URL, @@ -240,9 +311,10 @@ def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> s time.sleep(appconfig.RETRYING_SECONDS) continue - + vst = VersionedTransaction.from_bytes(response.content) + msg = vst.message tx = VersionedTransaction( - VersionedTransaction.from_bytes(response.content).message, + msg, [keypair] ) @@ -263,7 +335,8 @@ def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> s txtype.value, txSignature )) - return txSignature + break + except Exception as e: retries += 1 @@ -274,12 +347,8 @@ def trade(txtype: TxType, token: str, keypair: Keypair, amount: int = None) -> s )) time.sleep(appconfig.RETRYING_SECONDS) - if retries == appconfig.MARKET_MAKING_RETRIES: - # TODO: send a telegram message notifying that a manual sell must be done - print("Trade->{} Error: Max retries reached. Exiting".format(txtype.value)) - return txSignature - continue + return txSignature def market_making( diff --git a/bot/requirements.txt b/bot/requirements.txt index 619da70..1453c86 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -9,4 +9,6 @@ requests==2.32.3 coverage==7.6.7 pytest-cov==6.0.0 certifi==2024.8.30 +redis=4.2.0 +redisearch==2.0.0 From 96b491c7103fff1c9407dd7c44156e5aaff6c187 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 24 Nov 2024 20:19:37 +0100 Subject: [PATCH 006/103] Basic Redis implementation. Sniper and copytrade Trade Roadmaps defined. debug main enabled. Redis docker image setup defined. --- .vscode/launch.json | 30 +++++++++++----- bot/bug_and_bypasses.txt | 31 +++++++++++++++-- bot/domain/redis_db.py | 75 ++++++++++++++++++++++++++++------------ bot/main.py | 4 +-- bot/module/pump.py | 71 ++++++++++++++++++++++++++++--------- 5 files changed, 158 insertions(+), 53 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f635a9..8870f7a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,16 +2,28 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug redis_db.py", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/bot/domain/redis_db.py", - "console": "integratedTerminal", - "cwd": "${workspaceFolder}", - "env": { - "PYTHONPATH": "${workspaceFolder}" + "name": "Debug redis_db.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/domain/redis_db.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + }, + "justMyCode": false }, - "justMyCode": false + { + "name": "Debug main.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/main.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + }, + "justMyCode": false } ] } diff --git a/bot/bug_and_bypasses.txt b/bot/bug_and_bypasses.txt index 7e9442c..2630b8e 100644 --- a/bot/bug_and_bypasses.txt +++ b/bot/bug_and_bypasses.txt @@ -19,12 +19,39 @@ redis-stack-server --version # Start redis stack brew services start redis - +######################################################### # Redis Stack on docker docker pull redis/redis-stack:latest -docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest +# Docker image comes without redis.config file +# OPTION 1: start container and pass variable +docker run -d --name redis-stack \ + -p 6379:6379 \ + -e "REDIS_ARGS=--notify-keyspace-events KA$" \ + redis/redis-stack:latest + +# OPTION 2: start container with a config file +### In my local machine: downloan latest redis.conf file +wget https://raw.githubusercontent.com/redis/redis/unstable/redis.conf -O redis.conf +vim redis.config +### add this +notify-keyspace-events KA$ +docker run -d --name redis-stack \ + -p 6379:6379 \ + -v $(pwd)/redis.conf:/usr/local/etc/redis/redis.conf \ + redis/redis-stack:latest \ + redis-server /usr/local/etc/redis/redis.con + +# Validate the configuration is enabled ##### +docker exec -it redis-stack redis-cli +CONFIG GET notify-keyspace-events +### You should see a response like this: +1) "notify-keyspace-events" +2) "AK" +## If required docker stop redis-stack docker start redis-stack +######################################################### + JSON.SET token:12345 $ '[{"amount":500,"timestamp":1690228321,"is_traded":0}]' diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index a5af086..0acbdbe 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -2,16 +2,10 @@ from redisearch import Client, NumericField from datetime import datetime from bot.config import appconfig -from bot.lib.utils import TxType +from bot.lib.utils import TxType, Path, Trader from typing import List, Dict -class Path: - @staticmethod - def rootPath(): - return "$" - - # Connect to Redis client = redis.StrictRedis( host=appconfig.REDIS_HOST, @@ -21,7 +15,7 @@ def rootPath(): # Use SCAN to find keys with "Key*" prefix -class Redis: +class RedisDB: key_prefix = "token" index_name = "token_idx" @@ -29,18 +23,21 @@ def __init__(self) -> None: self.client = redis.StrictRedis( host=appconfig.REDIS_HOST, port=appconfig.REDIS_PORT, - db=0, + db=0, # DB index number decode_responses=True ) self.search_client = Client("token_idx", conn=self.client) self.create_index() + # Event listener + self.pubsub = self.client.pubsub() + self.pubsub.psubscribe({"__keyspace@0__:*"}) def index_exists(self, index_name): indexes = self.client.execute_command("FT._LIST") return index_name in indexes def create_index(self): - if not self.index_exists(Redis.index_name): + if not self.index_exists(RedisDB.index_name): # Create the index on JSON fields self.search_client.create_index([ NumericField("amount"), @@ -48,12 +45,11 @@ def create_index(self): NumericField("is_traded") ]) else: - print("Index '{}' already exists".format(Redis.index_name)) - + print("Index '{}' already exists".format(RedisDB.index_name)) def set_token(self, token: str, token_data: Dict)->bool: response = self.client.json().set( - name="{}:{}".format(Redis.key_prefix, token), + name="{}:{}".format(RedisDB.key_prefix, token), path=Path.rootPath(), obj=token_data ) @@ -63,7 +59,7 @@ def get_token_keys(self, pattern="token:*"): keys = self.client.scan_iter(match=pattern) return keys - def get_fresh_tokens(self) -> List[Dict]: + def get_fresh_tokens(self, trader=Trader) -> List[Dict]: tokens = [] # Search for all tokens where is_traded = False token_keys = self.get_token_keys() @@ -72,11 +68,19 @@ def get_fresh_tokens(self) -> List[Dict]: # results = self.search_client.search(query) for key in token_keys: data = client.json().get(key) - if data and "is_traded" in data and data["is_traded"] == 0: + if not data: + continue + + # Extract and validate fields + is_traded = data.get("is_traded") == 0 + trader_match = data.get("trader", "unknown") == trader.value + + if is_traded and trader_match: token = { "key": key, "mint": key.replace("token:", ""), "amount": float(data["amount"]), + "trader": data["trader"], "is_traded": True if int(data["is_traded"]) == 1 else False, "timestamp": datetime.fromtimestamp(int(data["timestamp"])), "name": data["name"], @@ -86,7 +90,14 @@ def get_fresh_tokens(self) -> List[Dict]: return tokens - def update_token(self, token: Dict, txn: str, action: TxType, amount: float) -> Dict: + def update_token( + self, + token: Dict, + txn: str, + action: TxType, + amount: float, + trader: Trader + ) -> Dict: key = token["key"] data = token.copy() @@ -101,6 +112,7 @@ def update_token(self, token: Dict, txn: str, action: TxType, amount: float) -> "{}_txn".format(action.value): txn, # New transaction string "is_traded": 1, "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action + "trader": trader.value } data.update(new_data) @@ -113,22 +125,39 @@ def update_token(self, token: Dict, txn: str, action: TxType, amount: float) -> return token -if __name__ == "__main__": - redis_object = Redis() +def test_create_record(mint_address: str): + redis_object = RedisDB() is_traded = False token_data = { "name": "Some Day", "ticker": "SMD", "amount": 15.500, "is_traded": 1 if is_traded else 0, - "timestamp": datetime.now().timestamp() + "timestamp": datetime.now().timestamp(), + "trader": Trader.sniper.value } - redis_object.set_token(token="asdf4", token_data=token_data) - tokens = redis_object.get_fresh_tokens() + redis_object.set_token(token=mint_address, token_data=token_data) + + +def test_update_record(): + redis_object = RedisDB() + tokens = redis_object.get_fresh_tokens(trader=Trader.sniper) print(tokens) for token in tokens: - data = redis_object.update_token(token=token, txn="txn1", action=TxType.buy, amount=15.500) - data = redis_object.update_token(token=data, txn="txn2", action=TxType.sell, amount=45.500) + data = redis_object.update_token( + token=token, + txn="txn1", + action=TxType.buy, + amount=15.500, + trader=Trader.sniper + ) + data = redis_object.update_token( + token=data, + txn="txn2", + action=TxType.sell, + amount=45.500, + trader=Trader.sniper + ) print(data) diff --git a/bot/main.py b/bot/main.py index 2dc1301..5054c98 100644 --- a/bot/main.py +++ b/bot/main.py @@ -4,10 +4,10 @@ async def main(): - pump = Pump() + pump = Pump(sniper_name="sniper1") tasks = [ - pump.subscribe(steps=TradeRoadmap.test) + pump.subscribe(steps=TradeRoadmap.sniper_1) ] await asyncio.gather(*tasks) diff --git a/bot/module/pump.py b/bot/module/pump.py index 52f1203..dd5a0c1 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1,4 +1,4 @@ -from config import appconfig +import asyncio import json import requests import time @@ -6,7 +6,9 @@ import ssl import certifi -from bot.lib.utils import TxType +from bot.lib.utils import TxType, Trader +from config import appconfig +from domain.redis_db import RedisDB from enum import Enum @@ -28,6 +30,7 @@ class Redis(Enum): readToken = "readToken" selectToken = "selectToken" claseToken = "closeToken" + readTraders = "readTraders" class TradeRoadmap: @@ -35,22 +38,42 @@ class TradeRoadmap: {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, ] + sniper_copytrade = [ # TODO: to be developed + {"step": 0, "name": "WAIT_FOR_TRADERS", "redis": Redis.readTraders}, + {"step": 1, "name": "TRADER_SUBSCRIPTION", "suscription": Suscription.subscribeNewToken}, + {"step": 2, "name": "TRADE_TOKEN", "action": TxType.buy}, + { + "step": 3, + "name": "TOKEN_SUBSCRIPTION", + "suscription": Suscription.subscribeTokenTrade, + "criteria_out": { + "copytrade_sell": True, + "same_balance": True, + "market_inactivity": 30 + } + }, + {"step": 4, "name": "TRADE_TOKEN", "trade": TxType.sell}, + {"step": 5, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, + {"step": 6, "name": "MARK_TOKEN", "redis": Redis.claseToken}, + + ] sniper_1 = [ {"step": 0, "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken}, {"step": 1, "name": "TRADE_TOKEN", "action": TxType.buy}, - {"step": 2, "name": "TOKEN_SUBSCRIPTION", "suscription": Suscription.subscribeTokenTrade}, { - "step": 3, - "name": "TRADE_TOKEN", - "trade": TxType.sell, - "criteria": { + "step": 2, + "name": "TOKEN_SUBSCRIPTION", + "suscription": Suscription.subscribeTokenTrade, + "criteria_out": { "max_seconds_between_buys": 3, - "trader_has_sold": True, + "developer_has_sold": True, "same_balance": True, + "market_inactivity": 10, "max_seconds_in_market": 30 } }, - {"step": 4, "name": "test", "suscription": Suscription.unsubscribeTokenTrade}, + {"step": 3, "name": "TRADE_TOKEN", "trade": TxType.sell}, + {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, {"step": 5, "name": "MARK_TOKEN", "redis": Redis.claseToken}, ] @@ -106,11 +129,14 @@ def remove_trader(self, trader: str): async def subscribe(self, steps: list): step_index = 0 + redisdb = RedisDB() + await asyncio.sleep(2) # Wait 2 seconds for the redis connection to start - # SSL context if required in Mac and not on windows + # SSL context is required in Mac and not on windows async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: # Subscribing to token creation events while True: + # Step index reset if necessary step_index = 0 if step_index >= len(steps) else step_index roadmap_name = steps[step_index]["name"] if step_index == 0: @@ -118,13 +144,24 @@ async def subscribe(self, steps: list): step = steps[step_index] - + # REDIS DB HANDLING if "redis" in step: # Listen to redis change if step["redis"] == Redis.readToken: - # TODO: subscribe to redis websocket as a separate function - print("Reading redis and waiting for a new token") - # TODO: Mark that token with the sniper name + for message in redisdb.pubsub.listen(): + if message["type"] == "psubscribe": + print("Subscribed to redis") + if message["type"] == "pmessage": + # Parse the message + key = message["channel"].split(":")[-1] + event = message["data"] + print(f"Event '{event}' detected for key '{key}'") + # TODO: + # - Take key and retrieve token from redis + # - move to next step and update the token as being traded + # - ignore token that are not fresh + # - safely exit this listening + print("Token {} assigned to sniper {} to trade {}".format( self.tokens[0], self.sniper_name, @@ -216,9 +253,9 @@ def new_token_suscription(self, msg: str) -> bool: def token_trade_suscription(self, msg: str) -> bool: # TODO: remove this temporal validation for testing - if not self.traders and "txType" in msg: - if TxType.buy.value == msg["txType"]: - self.add_trader(trader=msg["traderPublicKey"]) + # if not self.traders and "txType" in msg: + # if TxType.buy.value == msg["txType"]: + # self.add_trader(trader=msg["traderPublicKey"]) if "traderPublicKey" in msg and msg["traderPublicKey"] in self.traders: if TxType.buy.value == msg["txType"]: From 00cf1d839871d0353eb109dad097e6ef9322034a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 25 Nov 2024 00:34:21 +0100 Subject: [PATCH 007/103] redisdb: close connection implementation , and both get_fresh_tokens and update_token function improvement. Pump: WAIT_FOR_TOKEN step implementation. Config: new variables added --- bot/config/__init__.py | 2 + bot/domain/redis_db.py | 49 +++++++++----- bot/main.py | 7 +- bot/module/pump.py | 144 ++++++++++++++++++++++++++++++++--------- 4 files changed, 153 insertions(+), 49 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index fe82af9..3772ac0 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -26,6 +26,8 @@ class AuthConfig: PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 TRADING_DEFAULT_AMOUNT = 0.50 + TRADING_TOKENS_AT_THE_SAME_TIME = 1 + TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 TRADING_RETRIES = 3 #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 0acbdbe..746b9c4 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -32,6 +32,11 @@ def __init__(self) -> None: self.pubsub = self.client.pubsub() self.pubsub.psubscribe({"__keyspace@0__:*"}) + def stop(self): + print("Unsubscribing and cleaning up...") + self.pubsub.punsubscribe() + self.pubsub.close() + def index_exists(self, index_name): indexes = self.client.execute_command("FT._LIST") return index_name in indexes @@ -59,10 +64,14 @@ def get_token_keys(self, pattern="token:*"): keys = self.client.scan_iter(match=pattern) return keys - def get_fresh_tokens(self, trader=Trader) -> List[Dict]: + def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict]: tokens = [] + token_keys = [] # Search for all tokens where is_traded = False - token_keys = self.get_token_keys() + if mint_address: + token_keys = [mint_address] + else: + token_keys = self.get_token_keys() # query = Query('@is_traded:[0 0]').paging(0, 1000) # results = self.search_client.search(query) @@ -72,10 +81,10 @@ def get_fresh_tokens(self, trader=Trader) -> List[Dict]: continue # Extract and validate fields - is_traded = data.get("is_traded") == 0 + is_traded = data.get("is_traded", False) trader_match = data.get("trader", "unknown") == trader.value - if is_traded and trader_match: + if not is_traded and trader_match: token = { "key": key, "mint": key.replace("token:", ""), @@ -93,10 +102,12 @@ def get_fresh_tokens(self, trader=Trader) -> List[Dict]: def update_token( self, token: Dict, - txn: str, - action: TxType, - amount: float, - trader: Trader + txn: str = None, + action: TxType = None, + amount: float = None, + trader: Trader = None, + checked_as_being_traded: bool = False, + balance: int = None ) -> Dict: key = token["key"] @@ -107,13 +118,21 @@ def update_token( # Prepare the data to update trading_time = datetime.now().timestamp() - new_data = { - "{}_time".format(action.value): trading_time, # New trading time as timestamp - "{}_txn".format(action.value): txn, # New transaction string - "is_traded": 1, - "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action - "trader": trader.value - } + new_data = {} + if checked_as_being_traded: + new_data = { + "checked_time": trading_time, + "is_checked": True, + "initial_balance": balance + } + else: + new_data = { + "{}_time".format(action.value): trading_time, # New trading time as timestamp + "{}_txn".format(action.value): txn, # New transaction string + "is_traded": True, + "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action + "trader": trader.value + } data.update(new_data) diff --git a/bot/main.py b/bot/main.py index 5054c98..2c0e971 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,10 +1,13 @@ import asyncio import logging from module.pump import Pump, TradeRoadmap - +from lib.utils import Trader async def main(): - pump = Pump(sniper_name="sniper1") + pump = Pump( + executor_name="sniper1", + trader_type=Trader.sniper + ) tasks = [ pump.subscribe(steps=TradeRoadmap.sniper_1) diff --git a/bot/module/pump.py b/bot/module/pump.py index dd5a0c1..94ac574 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -10,7 +10,6 @@ from config import appconfig from domain.redis_db import RedisDB - from enum import Enum from solders.transaction import VersionedTransaction from solders.keypair import Keypair @@ -18,6 +17,8 @@ from solders.rpc.requests import SendVersionedTransaction from solders.rpc.config import RpcSendTransactionConfig +from typing import Dict + class Suscription(Enum): subscribeNewToken = "subscribeNewToken" @@ -29,7 +30,7 @@ class Suscription(Enum): class Redis(Enum): readToken = "readToken" selectToken = "selectToken" - claseToken = "closeToken" + closeToken = "closeToken" readTraders = "readTraders" @@ -46,15 +47,15 @@ class TradeRoadmap: "step": 3, "name": "TOKEN_SUBSCRIPTION", "suscription": Suscription.subscribeTokenTrade, - "criteria_out": { + "criteria": { "copytrade_sell": True, "same_balance": True, "market_inactivity": 30 } }, - {"step": 4, "name": "TRADE_TOKEN", "trade": TxType.sell}, + {"step": 4, "name": "TRADE_TOKEN", "action": TxType.sell}, {"step": 5, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, - {"step": 6, "name": "MARK_TOKEN", "redis": Redis.claseToken}, + {"step": 6, "name": "MARK_TOKEN", "redis": Redis.closeToken}, ] sniper_1 = [ @@ -64,7 +65,7 @@ class TradeRoadmap: "step": 2, "name": "TOKEN_SUBSCRIPTION", "suscription": Suscription.subscribeTokenTrade, - "criteria_out": { + "criteria": { "max_seconds_between_buys": 3, "developer_has_sold": True, "same_balance": True, @@ -72,9 +73,9 @@ class TradeRoadmap: "max_seconds_in_market": 30 } }, - {"step": 3, "name": "TRADE_TOKEN", "trade": TxType.sell}, + {"step": 3, "name": "TRADE_TOKEN", "action": TxType.sell}, {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, - {"step": 5, "name": "MARK_TOKEN", "redis": Redis.claseToken}, + {"step": 5, "name": "CLOSE_TOKEN", "grooming": Redis.closeToken}, ] @@ -84,21 +85,31 @@ class Pump: def __init__( self, - sniper_name: str, - amount: float = appconfig.TRADING_DEFAULT_AMOUNT + executor_name: str, + trader_type: Trader, + amount: float = appconfig.TRADING_DEFAULT_AMOUNT, + target: float = appconfig.TRADING_EXPECTED_GAIN_IN_PERCENTAGE ) -> None: self.uri_data = appconfig.PUMPFUN_WEBSOCKET self.accounts = [] self.tokens = [] self.traders = [] + self.executor_name = executor_name + self.trader_type = trader_type self.min_initial_buy = 60000000 - self.token_target_amount = 1 + self.tokens_to_be_traded = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME self.trading_amount = amount - self.sniper_name = sniper_name + self.trading_sell_amount = amount * (1 + target) + self.keypair = Keypair.from_base58_string(appconfig.PRIVKEY) self.ssl_context = ssl.create_default_context(cafile=certifi.where()) + self.balance = self.get_balance() + # TODO: get balance from wallet + def get_balance(self): + return 10000 + def add_account(self, account: str): self.accounts.append(account) @@ -107,8 +118,12 @@ def remove_account(self, account: str): self.accounts.pop(self.accounts.index(account)) # Tokens to pay attention to - def add_token(self, token: str): + def add_token(self, token: Dict): self.tokens.append(token) + + def remove_token(self, token: str): + if token in self.tokens: + self.tokens.pop(self.accounts.index(token)) def clear_tokens(self): self.tokens = [] @@ -141,6 +156,13 @@ async def subscribe(self, steps: list): roadmap_name = steps[step_index]["name"] if step_index == 0: print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) + + + + print("Step {}: {}".format( + step_index, + steps[step_index]["name"] + )) step = steps[step_index] @@ -153,24 +175,54 @@ async def subscribe(self, steps: list): print("Subscribed to redis") if message["type"] == "pmessage": # Parse the message - key = message["channel"].split(":")[-1] - event = message["data"] - print(f"Event '{event}' detected for key '{key}'") - # TODO: + key = ":".join(message["channel"].split(":")[1:]) # - Take key and retrieve token from redis + tokens = redisdb.get_fresh_tokens( + trader=Trader.sniper, + mint_address=key + ) + # - Ignore token that are not fresh or for not the current trader type + if not tokens: + continue + # - Get token information + # TODO: snipe againt to more than one token at the same time + token = tokens[0] + + mint = token["mint"] + amount = token["amount"] + + if self.balance >= amount: + self.trading_amount = amount + else: + print("{}: Not enough balance for Token {} and amount {}. Balance is {}".format( + self.executor_name, + mint, + amount, + self.balance + )) + continue + # - move to next step and update the token as being traded - # - ignore token that are not fresh + token = redisdb.update_token( + token=token, + checked_as_being_traded=True, + balance=self.balance + ) + self.add_token(token=token) + # - safely exit this listening + redisdb.pubsub.close() + break - print("Token {} assigned to sniper {} to trade {}".format( - self.tokens[0], - self.sniper_name, + print("Token {} assigned to {} to trade {}".format( + self.tokens[0]["mint"], + self.executor_name, self.trading_amount )) step_index += 1 continue - if step["redis"] == Redis.claseToken: + if step["redis"] == Redis.closeToken: # Save in redis the amount of buy and sell with timestamp. Calculate P&L step_index += 1 continue @@ -179,10 +231,24 @@ async def subscribe(self, steps: list): if step["action"] == TxType.buy: txn = self.trade( txtype=TxType.buy, - token=self.add_token[0], + token=self.add_token[0]["mint"], keypair=self.keypair, amount=self.trading_amount ) + # TODO: update token record in redis + + step_index += 1 + continue + + if step["action"] == TxType.sell: + txn = self.trade( + txtype=TxType.buy, + token=self.add_token[0]["mint"], + keypair=self.keypair, + amount=None + ) + # TODO: update token record in redis + step_index += 1 continue @@ -219,21 +285,31 @@ async def subscribe(self, steps: list): move_to_next_step = self.new_token_suscription(msg=msg) if suscription.value == Suscription.subscribeTokenTrade.value: - move_to_next_step = self.token_trade_suscription(msg=msg) + move_to_next_step = self.validate_criteria( + msg=msg, + criteria=steps[step_index]["criteria"] + ) - # Moving to the next step or going back to the first one + # Moving to the next step if move_to_next_step: step_index += 1 move_to_next_step = False break - - # TODO: add exiting way of ending the program + + + if "grooming" in step: + grooming = step[step_index]["grooming"] + if grooming.value == Redis.closeToken.value: + self.remove_token(token=self.tokens[0]) + # TODO: copy redis data to postgres and delete data from redis + + # TODO: add exiting way of safely ending the program def new_token_suscription(self, msg: str) -> bool: if "initialBuy" in msg and int(msg["initialBuy"]) > self.min_initial_buy: # Listening to a predefined amount of tokens - if len(self.tokens) < self.token_target_amount: + if len(self.tokens) < self.tokens_to_be_traded: print( "New token subscription. Symbol: {}. mint: {}. tarder: {}. Initial buy: {}".format( msg["symbol"], @@ -248,10 +324,10 @@ def new_token_suscription(self, msg: str) -> bool: # self.marketMaking(token=msg["mint"]) # TODO: relase tokens to snipers with redis recods. At this moment we're listening to one token only - return len(self.tokens) == self.token_target_amount + return len(self.tokens) == self.tokens_to_be_traded - def token_trade_suscription(self, msg: str) -> bool: + def token_copytrade_suscription(self, msg: str) -> bool: # TODO: remove this temporal validation for testing # if not self.traders and "txType" in msg: # if TxType.buy.value == msg["txType"]: @@ -260,7 +336,7 @@ def token_trade_suscription(self, msg: str) -> bool: if "traderPublicKey" in msg and msg["traderPublicKey"] in self.traders: if TxType.buy.value == msg["txType"]: print("Starting pumping bot. Listening to trader: {} - Buy: {}. MarketCap: {}".format( - msg["traderPublicKey"][:5], + msg["traderPublicKey"][:6], msg["tokenAmount"], msg["marketCapSol"] ) @@ -277,6 +353,10 @@ def token_trade_suscription(self, msg: str) -> bool: return False + # TODO: implement criteria validation an required functions from lib.utils + def validate_criteria(msg: Dict, criteria: Dict) -> bool: + pass + def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = None) -> str: """ @@ -315,7 +395,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response = None retries = 0 while True: - if retries == appconfig.TRADING_RETRIES: + if retries == appconfig.TRADING_RETRIES and txtype.value == TxType.sell.value: # TODO: send a telegram message notifying that a manual sell must be done print("Trade->{} Error: Max retries reached. Exiting".format(txtype.value)) break From 79ad6559b585d1f4c0eb44ad98ac625b3a519a79 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 26 Nov 2024 11:07:13 +0100 Subject: [PATCH 008/103] Getting token balance after buying and selling. redis_db: token is checked the first time and then is mark as traded when buying and selling. Adding nest_asyncio lib to requirements --- bot/domain/redis_db.py | 24 ++++---- bot/module/pump.py | 125 +++++++++++++++++++++++++++++++---------- bot/requirements.txt | 2 +- 3 files changed, 110 insertions(+), 41 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 746b9c4..f9e5713 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -81,16 +81,18 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict continue # Extract and validate fields - is_traded = data.get("is_traded", False) + is_checked = data.get("is_checked", False) + #is_traded = data.get("is_traded", False) trader_match = data.get("trader", "unknown") == trader.value - if not is_traded and trader_match: + if not is_checked and trader_match: token = { "key": key, "mint": key.replace("token:", ""), "amount": float(data["amount"]), "trader": data["trader"], - "is_traded": True if int(data["is_traded"]) == 1 else False, + # "is_traded": True if int(data["is_traded"]) == 1 else False, + "is_checked": is_checked, "timestamp": datetime.fromtimestamp(int(data["timestamp"])), "name": data["name"], "ticker": data["ticker"] @@ -107,7 +109,8 @@ def update_token( amount: float = None, trader: Trader = None, checked_as_being_traded: bool = False, - balance: int = None + balance: float = None, + token_balance: float = None, ) -> Dict: key = token["key"] @@ -123,15 +126,16 @@ def update_token( new_data = { "checked_time": trading_time, "is_checked": True, - "initial_balance": balance } else: new_data = { - "{}_time".format(action.value): trading_time, # New trading time as timestamp - "{}_txn".format(action.value): txn, # New transaction string + "{}_time".format(action.value): trading_time, # New trading time as timestamp + "{}_txn".format(action.value): txn, # New transaction string "is_traded": True, - "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action - "trader": trader.value + "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action + "trader": trader.value, + "{}_balance".format(action.value): balance, # Wallet balance before buy / after sell + "{}_token_balance".format(action.value): token_balance # Token balance after buy } data.update(new_data) @@ -151,7 +155,7 @@ def test_create_record(mint_address: str): "name": "Some Day", "ticker": "SMD", "amount": 15.500, - "is_traded": 1 if is_traded else 0, + "is_traded": is_traded, "timestamp": datetime.now().timestamp(), "trader": Trader.sniper.value } diff --git a/bot/module/pump.py b/bot/module/pump.py index 94ac574..a833943 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1,4 +1,5 @@ import asyncio +import nest_asyncio import json import requests import time @@ -6,7 +7,12 @@ import ssl import certifi -from bot.lib.utils import TxType, Trader +from bot.lib.utils import ( + get_solana_balance, + get_own_token_balance, + Trader, + TxType +) from config import appconfig from domain.redis_db import RedisDB @@ -36,17 +42,17 @@ class Redis(Enum): class TradeRoadmap: test = [ - {"step": 0, "name": "test", "suscription": Suscription.subscribeNewToken}, - {"step": 1, "name": "test", "suscription": Suscription.subscribeTokenTrade}, + {"step": 0, "name": "test", "subscription": Suscription.subscribeNewToken}, + {"step": 1, "name": "test", "subscription": Suscription.subscribeTokenTrade}, ] sniper_copytrade = [ # TODO: to be developed {"step": 0, "name": "WAIT_FOR_TRADERS", "redis": Redis.readTraders}, - {"step": 1, "name": "TRADER_SUBSCRIPTION", "suscription": Suscription.subscribeNewToken}, + {"step": 1, "name": "TRADER_SUBSCRIPTION", "subscription": Suscription.subscribeNewToken}, {"step": 2, "name": "TRADE_TOKEN", "action": TxType.buy}, { "step": 3, "name": "TOKEN_SUBSCRIPTION", - "suscription": Suscription.subscribeTokenTrade, + "subscription": Suscription.subscribeTokenTrade, "criteria": { "copytrade_sell": True, "same_balance": True, @@ -54,7 +60,7 @@ class TradeRoadmap: } }, {"step": 4, "name": "TRADE_TOKEN", "action": TxType.sell}, - {"step": 5, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, + {"step": 5, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, {"step": 6, "name": "MARK_TOKEN", "redis": Redis.closeToken}, ] @@ -64,7 +70,7 @@ class TradeRoadmap: { "step": 2, "name": "TOKEN_SUBSCRIPTION", - "suscription": Suscription.subscribeTokenTrade, + "subscription": Suscription.subscribeTokenTrade, "criteria": { "max_seconds_between_buys": 3, "developer_has_sold": True, @@ -74,11 +80,13 @@ class TradeRoadmap: } }, {"step": 3, "name": "TRADE_TOKEN", "action": TxType.sell}, - {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "suscription": Suscription.unsubscribeTokenTrade}, + {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, {"step": 5, "name": "CLOSE_TOKEN", "grooming": Redis.closeToken}, ] +# Patch the running event loop. Required to get wallet's balance +nest_asyncio.apply() class Pump: sniping_token_list = [] @@ -99,16 +107,25 @@ def __init__( self.min_initial_buy = 60000000 self.tokens_to_be_traded = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME self.trading_amount = amount - self.trading_sell_amount = amount * (1 + target) + self.trading_sell_amount = amount * (1 + target) # TODO: apply this when we have the Bounding Courve self.keypair = Keypair.from_base58_string(appconfig.PRIVKEY) self.ssl_context = ssl.create_default_context(cafile=certifi.where()) - self.balance = self.get_balance() + self.balance = self.get_balance(public_key=self.keypair.pubkey()) + self.msgs = [] - # TODO: get balance from wallet - def get_balance(self): - return 10000 + def get_balance(self, public_key): + balance = asyncio.run(get_solana_balance(public_key=public_key)) + return balance + + def get_tkn_balance(self, wallet_pubkey, token_account): + token_balance = asyncio.run(get_own_token_balance( + wallet_pubkey=wallet_pubkey, + token_mint_addres=token_account + ) + ) + return token_balance def add_account(self, account: str): self.accounts.append(account) @@ -120,7 +137,7 @@ def remove_account(self, account: str): # Tokens to pay attention to def add_token(self, token: Dict): self.tokens.append(token) - + def remove_token(self, token: str): if token in self.tokens: self.tokens.pop(self.accounts.index(token)) @@ -157,8 +174,6 @@ async def subscribe(self, steps: list): if step_index == 0: print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) - - print("Step {}: {}".format( step_index, steps[step_index]["name"] @@ -202,11 +217,10 @@ async def subscribe(self, steps: list): )) continue - # - move to next step and update the token as being traded + # - move to next step and update the token as being checked token = redisdb.update_token( token=token, - checked_as_being_traded=True, - balance=self.balance + checked_as_being_traded=True ) self.add_token(token=token) @@ -229,32 +243,76 @@ async def subscribe(self, steps: list): if "action" in step: if step["action"] == TxType.buy: + token = self.tokens[0] txn = self.trade( txtype=TxType.buy, - token=self.add_token[0]["mint"], + token=token["mint"], keypair=self.keypair, amount=self.trading_amount ) - # TODO: update token record in redis + # Get the token balance in wallet + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=token["mint"] + ) + + # Update token record in redis + token_updated = redisdb.update_token( + token=token, + txn=txn, + action=TxType.buy, + amount=self.trading_amount, + trader=self.trader_type, + checked_as_being_traded=False, + balance=self.balance, + token_balance=token_balance + ) + # Update token + self.remove_token(token=token) + self.add_token(token=token_updated) step_index += 1 continue if step["action"] == TxType.sell: + token = self.tokens[0] txn = self.trade( txtype=TxType.buy, - token=self.add_token[0]["mint"], + token=token["mint"], keypair=self.keypair, - amount=None + amount=None # Amount will be handled buy trade function + ) + # Update wallet balance after selling + self.get_balance(public_key=self.keypair.pubkey()) + + # Get the token balance in wallet + # Note: although we're selling 100% of tokens we might sell a % of tokens + # in the future and that's way we get the token balance when selling + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=token["mint"] + ) + # Update token record in redis + token_updated = redisdb.update_token( + token=token, + txn=txn, + action=TxType.sell, + amount=self.trading_amount, + trader=self.trader_type, + checked_as_being_traded=False, + balance=self.balance, + token_balance=token_balance ) - # TODO: update token record in redis + + # Update token + self.remove_token(token=token) + self.add_token(token=token_updated) step_index += 1 continue - if "subscription" in step: - suscription = steps[step_index]["suscription"] + suscription = steps[step_index]["subscription"] payload = { "method": suscription.value, @@ -264,10 +322,10 @@ async def subscribe(self, steps: list): payload["keys"] = self.accounts if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = self.tokens + payload["keys"] = [token["mint"] for token in self.tokens] if suscription.value == Suscription.unsubscribeTokenTrade.value: - payload["keys"] = self.tokens + payload["keys"] = [token["mint"] for token in self.tokens] await websocket.send(json.dumps(payload)) @@ -285,6 +343,7 @@ async def subscribe(self, steps: list): move_to_next_step = self.new_token_suscription(msg=msg) if suscription.value == Suscription.subscribeTokenTrade.value: + self.msgs.append(msg) move_to_next_step = self.validate_criteria( msg=msg, criteria=steps[step_index]["criteria"] @@ -354,7 +413,7 @@ def token_copytrade_suscription(self, msg: str) -> bool: return False # TODO: implement criteria validation an required functions from lib.utils - def validate_criteria(msg: Dict, criteria: Dict) -> bool: + def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: pass @@ -378,7 +437,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No txSignature = None denominated_in_sol = "true" if txtype.value == TxType.buy.value else "false" - amount = int(amount) if txtype.value == TxType.buy.value else "100%" + amount = amount if txtype.value == TxType.buy.value else "100%" # BUYING/SELLING tokens with an amount of Solana data = { @@ -395,6 +454,12 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response = None retries = 0 while True: + + print("trade -> TEST MODE: returning dummy transaction") + txSignature = "txn_dummy_{}".format(txtype.value) + break + + if retries == appconfig.TRADING_RETRIES and txtype.value == TxType.sell.value: # TODO: send a telegram message notifying that a manual sell must be done print("Trade->{} Error: Max retries reached. Exiting".format(txtype.value)) diff --git a/bot/requirements.txt b/bot/requirements.txt index 1453c86..4ff5fa9 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -11,4 +11,4 @@ pytest-cov==6.0.0 certifi==2024.8.30 redis=4.2.0 redisearch==2.0.0 - +nest_asyncio==1.6.0 From bae783f7d612f27eb0d6f1da86f517a43cebdae9 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 28 Nov 2024 05:13:23 +0100 Subject: [PATCH 009/103] Adding libraries for data analytics and utils. Pump: working with many tokens at the same time (under development) and using data_analytics for exiting market using creitrias (under developement). Redis: closing token when selling. --- .vscode/launch.json | 16 ++- MODULE | 0 bot/domain/redis_db.py | 11 +- bot/libs/__init.__.py | 0 bot/libs/criterias.py | 277 +++++++++++++++++++++++++++++++++++++++++ bot/libs/utils.py | 99 +++++++++++++++ bot/module/pump.py | 239 ++++++++++++++++++++--------------- 7 files changed, 539 insertions(+), 103 deletions(-) create mode 100644 MODULE create mode 100644 bot/libs/__init.__.py create mode 100644 bot/libs/criterias.py create mode 100644 bot/libs/utils.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 8870f7a..16c4e88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,21 @@ "console": "integratedTerminal", "cwd": "${workspaceFolder}", "env": { - "PYTHONPATH": "${workspaceFolder}" + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, + { + "name": "Debug creiterias.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/lib/criterias.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" }, "justMyCode": false } diff --git a/MODULE b/MODULE new file mode 100644 index 0000000..e69de29 diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index f9e5713..5c20a42 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -108,7 +108,8 @@ def update_token( action: TxType = None, amount: float = None, trader: Trader = None, - checked_as_being_traded: bool = False, + is_checked: bool = True, + is_closed: bool = False, balance: float = None, token_balance: float = None, ) -> Dict: @@ -122,10 +123,13 @@ def update_token( # Prepare the data to update trading_time = datetime.now().timestamp() new_data = {} - if checked_as_being_traded: + if not is_checked: new_data = { "checked_time": trading_time, "is_checked": True, + "is_traded": False, + "is_closed": False, + "trades": [] # We'll keep track of every buy/sell during our trade } else: new_data = { @@ -135,7 +139,8 @@ def update_token( "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action "trader": trader.value, "{}_balance".format(action.value): balance, # Wallet balance before buy / after sell - "{}_token_balance".format(action.value): token_balance # Token balance after buy + "{}_token_balance".format(action.value): token_balance, # Token balance after buy + "is_closed": is_closed } data.update(new_data) diff --git a/bot/libs/__init.__.py b/bot/libs/__init.__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py new file mode 100644 index 0000000..abb516d --- /dev/null +++ b/bot/libs/criterias.py @@ -0,0 +1,277 @@ +from datetime import datetime +from typing import Dict, List +from bot.libs.utils import TxType + +from solders.pubkey import Pubkey + + +def same_balance(amount: float, trades: List[Dict]) -> bool: + criteria_met = False + + return criteria_met + +def exit_on_first_sale(msg: Dict) -> bool: + return msg["txType"].lower() == TxType.sell.value + +def trading_analytics( + msg: Dict, + previous_trades: List, + amount_traded: float, + pubkey: Pubkey, + traders: List[str] = [] + ) -> Dict: + """ + This function compare incomming messages with previous trades and include in the msg + statistics new attributes that will help on deciding exit trading criteria + :param msg: Incomming message when listening to Pump.fun tokens. + :param previous_trades: history of incomming messages + :return: msg with more attributes + """ + + new_msg = msg.copy() + new_msg["timestamp"] = datetime.now() # Including timestamp in incomming message + + # Default values that might change + new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 + new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 + + if not previous_trades: + aprox = 0 + # Math calculation for Solana in Bonding Courve just before we bought + if new_msg["traderPublicKey"] == str(pubkey): + # We're the first recorded trade: we discount the amount to the current bonding curve + new_msg["vSolInBondingCurve_Base"] = new_msg["vSolInBondingCurve"] - amount_traded + else: + # The first recorded trade is not from us. So we can make an aproximation + aprox = new_msg["tokenAmount"] * new_msg["vSolInBondingCurve"] + # This is an aproximation of the Sols traded by the current trader + aprox = aprox / new_msg["vTokensInBondingCurve"] + # We discount the Sols traded by the current trader to the current Sols in Bonding Curve + new_msg["vSolInBondingCurve_Base"] = new_msg["vSolInBondingCurve"] - aprox + + new_msg["seconds_between_buys"] = 0 + new_msg["seconds_between_sells"] = 0 + new_msg["market_inactivity"] = 0 + new_msg["max_seconds_in_market"] = 0 + new_msg["max_consecutive_buys"] = [ + { + "quantity": new_msg["consecutive_buys"], + "sols": aprox or amount_traded | amount_traded + } + ] + + else: + last_msg = previous_trades[-1] + # We always keep track of the consecutive buys + new_msg["max_consecutive_buys"] = last_msg["max_consecutive_buys"] + + if msg["txType"].lower() == TxType.buy.value: + new_msg["consecutive_sells"] = 0 + new_msg["seconds_between_sells"] = 0 + + if last_msg["txType"].lower() == TxType.buy.value: + new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] + new_msg["seconds_between_buys"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + # Updating the last record + new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] + new_msg["max_consecutive_buys"][-1]["sols"] += new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + else: + new_msg["consecutive_buys"] = 1 + new_msg["seconds_between_buys"] = 0 + # Start again with a new record + new_msg["max_consecutive_buys"].append( + { + "quantity": 1, + "sols": new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + } + ) + + if msg["txType"].lower() == TxType.sell.value: + new_msg["consecutive_buys"] = 0 + new_msg["seconds_between_buys"] = 0 + + if last_msg["txType"].lower() == TxType.sell.value: + new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] + new_msg["seconds_between_sells"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + else: + new_msg["consecutive_sells"] = 1 + new_msg["seconds_between_sells"] = 0 + + new_msg["market_inactivity"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + new_msg["max_seconds_in_market"] = (datetime.now() - previous_trades[0]["timestamp"]).total_seconds() + + if msg["txType"].lower() == TxType.buy.value: + new_msg["seconds_between_buys"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + + # We'll keep track of the Solanas in Bounding courve since the first recorded trade + new_msg["vSolInBondingCurve_Base"] = last_msg["vSolInBondingCurve_Base"] + + + # Checking if the current traders is in the trader's list + new_msg["developer_has_sold"] = new_msg["traderPublicKey"] in traders + + # Amount of Solana the token is holding after our buy + new_msg["sols_in_token_after_buying"] = new_msg["vSolInBondingCurve"] - new_msg["vSolInBondingCurve_Base"] + + return new_msg + +def test(): + messages = [ + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"7d7iapfxQoMi5jM46h5vm8hHxrjsSVV2twYVSrYaCJdz", + "txType":"buy", + "tokenAmount":30582206.734745, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":977513247.598136, + "vSolInBondingCurve":32.93049999996889, + "marketCapSol":33.68803449046135 + }, + { + "signature":"5eZ88gyECt27NR47Rpe7yUd8t2FBxanVV5FUFLjUQjax3z4WzvWdbxAxJmMHkAHR6zVsYw9DRXuGFPBKxTVvFFY5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":14605679.543704, + "newTokenBalance":14605679.543704, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":962907568.054432, + "vSolInBondingCurve":33.42999999993804, + "marketCapSol":34.717766386948036 + }, + { + "signature":"5DcDFCF1L3A5m86GDSgCtM1vVFqXh9cQh1bf8K2Z8WobiStLUXbyE87wNFdLsEa2Az8xxY5ziC5bwEA9X2j47pkL", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"buy", + "tokenAmount":7147473.040359, + "newTokenBalance":7147473.040359, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":955760095.014073, + "vSolInBondingCurve":33.67999999992259, + "marketCapSol":35.238968623634236 + }, + { + "signature":"21KFHt6dgEVuk4qsetLiad9mnPwyt4Z2U6s3Sy8iefopBYffUxMd41XaYjrGjg1FWpBnn4DsAxuuZi4d9vvkSv3p", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"AuKQzaXcZwWH77sJmwheexwVAyVg9oGfrdmKpgPuj7at", + "txType":"buy", + "tokenAmount":8429777.204002, + "newTokenBalance":8429777.204002, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":947330317.810071, + "vSolInBondingCurve":33.97969999990408, + "marketCapSol":35.868903761524734 + }, + { + "signature":"3ZNma6hgtYk5GqfjAsH2EzEXn2WfciTxuq9vWsBFdxpsk8HECu22YnY5qcWmGkGhh1Mav1Ma8M1CwntBr6ara4ux", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"sell", + "tokenAmount":14605679.543704, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":961935997.353775, + "vSolInBondingCurve":33.46376483316213, + "marketCapSol":34.78793279929104 + }, + { + "signature":"4yvsGZZoT16C1Qjs6sEzBLbj7yfiA8wFHYfGbfk2p3coofKYYVwgEtJC2SxfxTqgontRKprmEfYXaHmF9f6Dkhs2", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"AuKQzaXcZwWH77sJmwheexwVAyVg9oGfrdmKpgPuj7at", + "txType":"sell", + "tokenAmount":8429777.204002, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":970365774.557777, + "vSolInBondingCurve":33.173057875696294, + "marketCapSol":34.18613758385511 + }, + { + "signature":"55nSrxZQiCdFSxt1RGoMcDRHRJefkZFx5MZff6h3i86GswPa7yw4PJpGQfjzfzzCixuRAfs8R3zUxkKH3Wj55EuF", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"7d7iapfxQoMi5jM46h5vm8hHxrjsSVV2twYVSrYaCJdz", + "txType":"sell", + "tokenAmount":30582206.734745, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1000947981.292522, + "vSolInBondingCurve":32.15951338293637, + "marketCapSol":32.12905563924397 + }, + { + "signature":"4jSL1sa5nXqbvqzJ2nSL3kZwZqNTaksA7KaPDkfG6YwtN8RumMWD3UXLkaNRm3i2PUd6Wj2RGZX9huQKsFcGywFq", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"buy", + "tokenAmount":584679.9521120004, + "newTokenBalance":7732152.992471, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1000363301.34041, + "vSolInBondingCurve":32.17830957699855, + "marketCapSol":32.16662339960101 + }, + { + "signature":"3nWLjZYrrbPvYGNshApaiHaQS9jidMvzYLgsqywNRNebJsNdhA4kYZhxXh6R3DRR7snYUMM5pTmCx6aQJHxPxc26", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"GazCsmGe5RkzZmaTtPrfYKnHqQ2RQZjq2uoW8nRUgYri", + "txType":"sell", + "tokenAmount":34612903.225806, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1034976204.566216, + "vSolInBondingCurve":31.102164337673464, + "marketCapSol":30.051091223598853 + }, + { + "signature":"22xyhmt35AutSph1ZdMcbrUADpdXPwiwe9G2VomMKuMHDUhNSeqEYN2XJXYSswHManeJnH6QJA8Z1xjfxiuwH94K", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8", + "txType":"sell", + "tokenAmount":30291642.440098997, + "newTokenBalance":0.001214, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1065267847.006315, + "vSolInBondingCurve":30.217752361964582, + "marketCapSol":28.366342274278225 + }, + { + "signature":"osUSpasfsvdPkGXZGoLa68X7b1PRSpgUimM8MUNtw9nwMW9em3L4UKzDUDTDuF7FMAw1XGNtNDtPRCBnE7jRdr5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"sell", + "tokenAmount":7732152.992471, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1072999999.998786, + "vSolInBondingCurve":30.000000000033943, + "marketCapSol":27.958993476298126 + } + ] + + tokens = {"mint_address": {"trades": []}} + mint = "mint_address" + trading_amount = 0.45 + from solders.keypair import Keypair + from bot.config import appconfig + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + + if "trades" not in tokens[mint]: + tokens[mint]["trades"] = [] + + for msg in messages: + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( + msg=msg, + previous_trades=tokens[mint]["trades"], + amount_traded=trading_amount, + pubkey=keypair.pubkey() + ) + # Including last message with new metadata into trades list + tokens[mint]["trades"].append(new_msg) + + print(tokens[mint]["trades"]) + +test() \ No newline at end of file diff --git a/bot/libs/utils.py b/bot/libs/utils.py new file mode 100644 index 0000000..0184e03 --- /dev/null +++ b/bot/libs/utils.py @@ -0,0 +1,99 @@ +import base58 + +from enum import Enum +from solana.rpc.async_api import AsyncClient +from solana.rpc.types import TokenAccountOpts +from solders.pubkey import Pubkey + +from bot.config import appconfig + + +class Path: + @staticmethod + def rootPath(): + return "$" + +class TxType(Enum): + buy = "buy" + sell = "sell" + +class Trader(Enum): + sniper = "sniper" + volume_bot = "volume_bot" + token_creator = "token_creator" + +async def get_solana_balance(public_key: Pubkey) -> float: + """ + Fetches the SOL balance of a given Solana wallet address. + + :param wallet_address: The public address of the Solana wallet. + :return: Balance in SOL as a float. + """ + async with AsyncClient(appconfig.RPC_URL) as client: + try: + # Fetch the balance (in lamports) + balance_response = await client.get_balance(public_key) + # Balance is returned in lamports (1 SOL = 10^9 lamports) + lamports = balance_response.value + sol_balance = lamports / 1e9 + return sol_balance + except Exception as e: + print(f"Error fetching balance: {e}") + return 0.0 + +async def get_own_token_balance(wallet_pubkey: Pubkey, token_mint_addres: str) -> float: + """ + Fetches the balance of a specific token in a given Solana wallet. + + :param wallet_pubkey: The public address of the Solana wallet. + :param token_mint_pubkey: The mint address of the token to query. + :return: Token balance as a float. + """ + async with AsyncClient(appconfig.RPC_URL) as client: + try: + # Fetch token mint public key owned by the wallet + token_mint_pubkey = decode_pump_fun_token(token=token_mint_addres) + + opts = TokenAccountOpts( + mint=token_mint_pubkey, # The mint address of the token + program_id=None, # (Optional) SPL token program ID + encoding="base64" # (Optional) Response encoding + ) + response = await client.get_token_accounts_by_owner( + owner=wallet_pubkey, + opts=opts, + ) + + if len(response.value) == 0: + raise Exception("No token accounts found for this mint address.") + + # Assume the first token account holds the balance (adjust for multisig if needed) + token_pubkey = response.value[0].pubkey + # Fetch the token balance + balance_response = await client.get_token_account_balance( + token_pubkey + ) + balance_data = balance_response.value + raw_balance = int(balance_data.amount) + decimals = int(balance_data.decimals) + + # Convert to human-readable balance + readable_balance = raw_balance / (10 ** decimals) + return readable_balance + + except Exception as e: + print(f"Error fetching token '{token_mint_addres}' balance: {e}") + return 0.0 + +def decode_pump_fun_token(token: str) -> Pubkey: + """ + Decodes a 44-character token from pump.fun to retrieve its mint address. + + :param token: 44-character token. + :return: Token mint address (base58 encoded). + """ + try: + decoded_bytes = base58.b58decode(token) + return Pubkey(decoded_bytes) + except Exception as e: + raise ValueError(f"Failed to decode token: {e}") diff --git a/bot/module/pump.py b/bot/module/pump.py index a833943..600056b 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -7,7 +7,8 @@ import ssl import certifi -from bot.lib.utils import ( +from bot.libs.criterias import trading_analytics +from bot.libs.utils import ( get_solana_balance, get_own_token_balance, Trader, @@ -16,6 +17,7 @@ from config import appconfig from domain.redis_db import RedisDB +from datetime import datetime from enum import Enum from solders.transaction import VersionedTransaction from solders.keypair import Keypair @@ -23,7 +25,7 @@ from solders.rpc.requests import SendVersionedTransaction from solders.rpc.config import RpcSendTransactionConfig -from typing import Dict +from typing import Dict, List class Suscription(Enum): @@ -72,11 +74,13 @@ class TradeRoadmap: "name": "TOKEN_SUBSCRIPTION", "subscription": Suscription.subscribeTokenTrade, "criteria": { + "max_consecutive_buys": 2, + "max_consecutive_sells": 1, "max_seconds_between_buys": 3, "developer_has_sold": True, - "same_balance": True, + "max_sols_in_token_after_buying_in_percentage": 100, "market_inactivity": 10, - "max_seconds_in_market": 30 + "max_seconds_in_market": 60 } }, {"step": 3, "name": "TRADE_TOKEN", "action": TxType.sell}, @@ -100,7 +104,7 @@ def __init__( ) -> None: self.uri_data = appconfig.PUMPFUN_WEBSOCKET self.accounts = [] - self.tokens = [] + self.tokens = {} self.traders = [] self.executor_name = executor_name self.trader_type = trader_type @@ -112,7 +116,6 @@ def __init__( self.keypair = Keypair.from_base58_string(appconfig.PRIVKEY) self.ssl_context = ssl.create_default_context(cafile=certifi.where()) self.balance = self.get_balance(public_key=self.keypair.pubkey()) - self.msgs = [] def get_balance(self, public_key): @@ -136,21 +139,15 @@ def remove_account(self, account: str): # Tokens to pay attention to def add_token(self, token: Dict): - self.tokens.append(token) + self.tokens[token["mint"]] = token def remove_token(self, token: str): - if token in self.tokens: - self.tokens.pop(self.accounts.index(token)) + if token["mint"] in self.tokens: + del self.tokens["miunt"] def clear_tokens(self): self.tokens = [] - def remove_token(self, token: str): - if token in self.tokens: - self.tokens.pop( - self.tokens.index(token) - ) - # Traders to follow for starting and stoping pumps def add_trader(self, trader: str): self.traders.append(trader) @@ -220,19 +217,20 @@ async def subscribe(self, steps: list): # - move to next step and update the token as being checked token = redisdb.update_token( token=token, - checked_as_being_traded=True + is_checked=False ) self.add_token(token=token) # - safely exit this listening redisdb.pubsub.close() + + print("Token {} assigned to {} to trade {}".format( + token["mint"], + self.executor_name, + self.trading_amount + )) break - - print("Token {} assigned to {} to trade {}".format( - self.tokens[0]["mint"], - self.executor_name, - self.trading_amount - )) + step_index += 1 continue @@ -243,76 +241,77 @@ async def subscribe(self, steps: list): if "action" in step: if step["action"] == TxType.buy: - token = self.tokens[0] - txn = self.trade( - txtype=TxType.buy, - token=token["mint"], - keypair=self.keypair, - amount=self.trading_amount - ) - # Get the token balance in wallet - token_balance = self.get_tkn_balance( - wallet_pubkey=self.keypair.pubkey(), - token_account=token["mint"] - ) - - # Update token record in redis - token_updated = redisdb.update_token( - token=token, - txn=txn, - action=TxType.buy, - amount=self.trading_amount, - trader=self.trader_type, - checked_as_being_traded=False, - balance=self.balance, - token_balance=token_balance - ) - # Update token - self.remove_token(token=token) - self.add_token(token=token_updated) + for mint_address, token_data in self.tokens.items(): + txn = self.trade( + txtype=TxType.buy, + token=mint_address, + keypair=self.keypair, + amount=self.trading_amount + ) + # Get the token balance in wallet + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=mint_address + ) - step_index += 1 + self.tokens[mint_address]["token_balance"] = token_balance + + # Update token record in redis + token_updated = redisdb.update_token( + token=token_data, + txn=txn, + action=TxType.buy, + amount=self.trading_amount, + trader=self.trader_type, + balance=self.balance, + token_balance=token_balance + ) + # Update token + self.remove_token(token=token) + self.add_token(token=token_updated) + + step_index += 1 continue if step["action"] == TxType.sell: - token = self.tokens[0] - txn = self.trade( - txtype=TxType.buy, - token=token["mint"], - keypair=self.keypair, - amount=None # Amount will be handled buy trade function - ) - # Update wallet balance after selling - self.get_balance(public_key=self.keypair.pubkey()) - - # Get the token balance in wallet - # Note: although we're selling 100% of tokens we might sell a % of tokens - # in the future and that's way we get the token balance when selling - token_balance = self.get_tkn_balance( - wallet_pubkey=self.keypair.pubkey(), - token_account=token["mint"] - ) - # Update token record in redis - token_updated = redisdb.update_token( - token=token, - txn=txn, - action=TxType.sell, - amount=self.trading_amount, - trader=self.trader_type, - checked_as_being_traded=False, - balance=self.balance, - token_balance=token_balance - ) - - # Update token - self.remove_token(token=token) - self.add_token(token=token_updated) + for mint_address, token_data in self.tokens.items(): + txn = self.trade( + txtype=TxType.buy, + token=mint_address, + keypair=self.keypair, + amount=None # Amount will be handled buy trade function + ) + # Update wallet balance after selling + self.get_balance(public_key=self.keypair.pubkey()) + + # Get the token balance in wallet + # Note: although we're selling 100% of tokens we might sell a % of tokens + # in the future and that's way we get the token balance when selling + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=mint_address + ) + # Update token record in redis + token_updated = redisdb.update_token( + token=token_data, + txn=txn, + action=TxType.sell, + amount=self.trading_amount, + trader=self.trader_type, + is_closed=True, + balance=self.balance, + token_balance=token_balance + ) - step_index += 1 + # Update token + self.remove_token(token=token) + self.add_token(token=token_updated) + + step_index += 1 continue if "subscription" in step: - suscription = steps[step_index]["subscription"] + suscription = step["subscription"] payload = { "method": suscription.value, @@ -322,14 +321,24 @@ async def subscribe(self, steps: list): payload["keys"] = self.accounts if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = [token["mint"] for token in self.tokens] + payload["keys"] = [mint for mint, _ in self.tokens.items() if not self.tokens[mint]["is_traded"]] if suscription.value == Suscription.unsubscribeTokenTrade.value: - payload["keys"] = [token["mint"] for token in self.tokens] + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] await websocket.send(json.dumps(payload)) - # MAIN WEBSOCKET THREAT + ##################### + # ADD HERE THE BUYING TRADEs in separate threads and fix the step so Buy and Sell are inside subscription + # NOTE: test buy and sell in real before having them in threads + ##################### + for mint, token_data in self.tokens.items(): + if not self.tokens[mint]["is_traded"] and \ + self.tokens[mint]["is_checked"] and \ + not self.tokens[mint]["is_closed"]: + pass + + # MAIN WEBSOCKET THREAD async for message in websocket: msg = json.loads(message) move_to_next_step = False @@ -343,11 +352,32 @@ async def subscribe(self, steps: list): move_to_next_step = self.new_token_suscription(msg=msg) if suscription.value == Suscription.subscribeTokenTrade.value: - self.msgs.append(msg) - move_to_next_step = self.validate_criteria( + # Getting the mint address we'll work with + mint = msg["mint"] + + # We'll not pay attention to closed token trades + if self.tokens[mint]["is_closed"]: + continue + + if "trades" not in self.tokens[mint]: + self.tokens[mint]["trades"] = [] + + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( msg=msg, - criteria=steps[step_index]["criteria"] + previous_trades=self.tokens[mint]["trades"], + amount_traded=self.trading_amount, + pubkey=self.keypair.pubkey() ) + # Including last message with new metadata into trades list + self.tokens[mint]["trades"].append(new_msg) + + move_to_next_step, criteria = self.validate_criteria( + trades=self.tokens[mint]["trades"], + criteria=step["criteria"] + ) + # Including exit criteria in token for further analytics + self.tokens[0]["exit_criteria"] = criteria # Moving to the next step if move_to_next_step: @@ -357,12 +387,12 @@ async def subscribe(self, steps: list): if "grooming" in step: - grooming = step[step_index]["grooming"] + grooming = step["grooming"] if grooming.value == Redis.closeToken.value: self.remove_token(token=self.tokens[0]) # TODO: copy redis data to postgres and delete data from redis - # TODO: add exiting way of safely ending the program + # TODO: add a saftley exit way of ending the program def new_token_suscription(self, msg: str) -> bool: @@ -413,8 +443,20 @@ def token_copytrade_suscription(self, msg: str) -> bool: return False # TODO: implement criteria validation an required functions from lib.utils - def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: - pass + def validate_criteria(self, trades: List[Dict], criteria: Dict) -> bool: + is_valid = False + exit_criteria = None + for key, value in criteria.items(): + { + "max_seconds_between_buys": 3, + "developer_has_sold": True, + "same_balance": True, + "market_inactivity": 10, + "max_seconds_in_market": 30 + } + + return is_valid, exit_criteria + def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = None) -> str: @@ -454,11 +496,10 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response = None retries = 0 while True: - - print("trade -> TEST MODE: returning dummy transaction") - txSignature = "txn_dummy_{}".format(txtype.value) - break - + if appconfig.ENVIRONMENT == "DUMM": + print("trade -> DUMMY MODE: returning dummy transaction") + txSignature = "txn_dummy_{}".format(txtype.value) + break if retries == appconfig.TRADING_RETRIES and txtype.value == TxType.sell.value: # TODO: send a telegram message notifying that a manual sell must be done From fb9c60415d62b86a8756c3e5c45d501a99f54fdc Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 28 Nov 2024 12:19:15 +0100 Subject: [PATCH 010/103] test for pump and bug fixing --- bot/domain/redis_db.py | 2 +- bot/libs/criterias.py | 105 +++++++++++++++++++- bot/module/pump.py | 220 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 306 insertions(+), 21 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 5c20a42..e21624b 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -2,7 +2,7 @@ from redisearch import Client, NumericField from datetime import datetime from bot.config import appconfig -from bot.lib.utils import TxType, Path, Trader +from bot.libs.utils import TxType, Path, Trader from typing import List, Dict diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index abb516d..24a1381 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -56,7 +56,7 @@ def trading_analytics( new_msg["max_consecutive_buys"] = [ { "quantity": new_msg["consecutive_buys"], - "sols": aprox or amount_traded | amount_traded + "sols": amount_traded if aprox == 0 else aprox } ] @@ -115,12 +115,111 @@ def trading_analytics( return new_msg + +def max_consecutive_buys(buys: int, msg: dict) -> bool: + """ + Checks if the maximum consecutive buys condition is met. + + Args: + buys (int): The maximum number of consecutive buys allowed. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + return buys >= msg["max_consecutive_buys"] + + +def max_consecutive_sells(sells: int, msg: dict) -> bool: + """ + Checks if the maximum consecutive sells condition is met. + + Args: + sells (int): The maximum number of consecutive sells allowed. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + return sells >= msg["max_consecutive_sells"] + + +def max_seconds_between_buys(seconds: int, msg: dict) -> bool: + """ + Checks if the maximum allowed seconds between buys condition is met. + + Args: + seconds (int): The maximum number of seconds allowed between buys. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + pass + + +def developer_has_sold(sold: bool, msg: dict) -> bool: + """ + Checks if the developer has sold their tokens. + + Args: + sold (bool): A flag indicating whether the developer has sold tokens. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + pass + + +def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict) -> bool: + """ + Checks if the maximum percentage of SOLs in the token after buying condition is met. + + Args: + percentage (int): The maximum percentage of SOLs allowed in the token. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + pass + + +def market_inactivity(seconds: int, msg: dict) -> bool: + """ + Checks if the market inactivity condition is met. + + Args: + seconds (int): The maximum number of seconds of inactivity allowed. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + pass + + +def max_seconds_in_market(seconds: int, msg: dict) -> bool: + """ + Checks if the maximum seconds in the market condition is met. + + Args: + seconds (int): The maximum number of seconds allowed in the market. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + pass + + def test(): messages = [ { "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", - "traderPublicKey":"7d7iapfxQoMi5jM46h5vm8hHxrjsSVV2twYVSrYaCJdz", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", "txType":"buy", "tokenAmount":30582206.734745, "newTokenBalance":30582206.734745, @@ -273,5 +372,3 @@ def test(): tokens[mint]["trades"].append(new_msg) print(tokens[mint]["trades"]) - -test() \ No newline at end of file diff --git a/bot/module/pump.py b/bot/module/pump.py index 600056b..e810577 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -7,17 +7,26 @@ import ssl import certifi -from bot.libs.criterias import trading_analytics +from bot.libs.criterias import ( + trading_analytics, + max_consecutive_buys, + max_consecutive_sells, + max_seconds_between_buys, + max_seconds_in_market, + max_sols_in_token_after_buying_in_percentage, + developer_has_sold, + market_inactivity +) +from bot.libs import criterias as criteria_functions from bot.libs.utils import ( get_solana_balance, get_own_token_balance, Trader, TxType ) -from config import appconfig -from domain.redis_db import RedisDB +from bot.config import appconfig +from bot.domain.redis_db import RedisDB -from datetime import datetime from enum import Enum from solders.transaction import VersionedTransaction from solders.keypair import Keypair @@ -373,7 +382,7 @@ async def subscribe(self, steps: list): self.tokens[mint]["trades"].append(new_msg) move_to_next_step, criteria = self.validate_criteria( - trades=self.tokens[mint]["trades"], + msg=new_msg, criteria=step["criteria"] ) # Including exit criteria in token for further analytics @@ -442,18 +451,31 @@ def token_copytrade_suscription(self, msg: str) -> bool: return False - # TODO: implement criteria validation an required functions from lib.utils - def validate_criteria(self, trades: List[Dict], criteria: Dict) -> bool: + + def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: + """ + This function takes the incomming trading message from Pump.fun previouly + trated by trading_analytics function and apply all criteria functions for the + current step. + :param msg: message from Pump.fun when listening to trading tokens and modified by trading_analytics funciton. + :param criteria: list of functions and values for evaluation on exiting or not the trading position. + :return: is_valid[True|False] and the function name as an exit_criteria . + """ is_valid = False exit_criteria = None - for key, value in criteria.items(): - { - "max_seconds_between_buys": 3, - "developer_has_sold": True, - "same_balance": True, - "market_inactivity": 10, - "max_seconds_in_market": 30 - } + for function_name, parameter in criteria.items(): + # Dynamically get the function reference + function = getattr(criteria_functions, function_name, None) + if callable(function): + # Call the function with the parameter and msg + is_valid = function(parameter, msg) + print(f"Function {function_name} returned: {is_valid}") + else: + print(f"validate_criteria-> Error: {function_name} not found.") + + if is_valid: + exit_criteria = function_name + break return is_valid, exit_criteria @@ -589,4 +611,170 @@ def market_making( txn = self.buy(token=token, amount=amount, keypair=keypair) - \ No newline at end of file + +def test(): + messages = [ + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":30582206.734745, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":977513247.598136, + "vSolInBondingCurve":32.93049999996889, + "marketCapSol":33.68803449046135 + }, + { + "signature":"5eZ88gyECt27NR47Rpe7yUd8t2FBxanVV5FUFLjUQjax3z4WzvWdbxAxJmMHkAHR6zVsYw9DRXuGFPBKxTVvFFY5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":14605679.543704, + "newTokenBalance":14605679.543704, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":962907568.054432, + "vSolInBondingCurve":33.42999999993804, + "marketCapSol":34.717766386948036 + }, + { + "signature":"5DcDFCF1L3A5m86GDSgCtM1vVFqXh9cQh1bf8K2Z8WobiStLUXbyE87wNFdLsEa2Az8xxY5ziC5bwEA9X2j47pkL", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"buy", + "tokenAmount":7147473.040359, + "newTokenBalance":7147473.040359, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":955760095.014073, + "vSolInBondingCurve":33.67999999992259, + "marketCapSol":35.238968623634236 + }, + { + "signature":"21KFHt6dgEVuk4qsetLiad9mnPwyt4Z2U6s3Sy8iefopBYffUxMd41XaYjrGjg1FWpBnn4DsAxuuZi4d9vvkSv3p", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"AuKQzaXcZwWH77sJmwheexwVAyVg9oGfrdmKpgPuj7at", + "txType":"buy", + "tokenAmount":8429777.204002, + "newTokenBalance":8429777.204002, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":947330317.810071, + "vSolInBondingCurve":33.97969999990408, + "marketCapSol":35.868903761524734 + }, + { + "signature":"3ZNma6hgtYk5GqfjAsH2EzEXn2WfciTxuq9vWsBFdxpsk8HECu22YnY5qcWmGkGhh1Mav1Ma8M1CwntBr6ara4ux", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"sell", + "tokenAmount":14605679.543704, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":961935997.353775, + "vSolInBondingCurve":33.46376483316213, + "marketCapSol":34.78793279929104 + }, + { + "signature":"4yvsGZZoT16C1Qjs6sEzBLbj7yfiA8wFHYfGbfk2p3coofKYYVwgEtJC2SxfxTqgontRKprmEfYXaHmF9f6Dkhs2", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"AuKQzaXcZwWH77sJmwheexwVAyVg9oGfrdmKpgPuj7at", + "txType":"sell", + "tokenAmount":8429777.204002, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":970365774.557777, + "vSolInBondingCurve":33.173057875696294, + "marketCapSol":34.18613758385511 + }, + { + "signature":"55nSrxZQiCdFSxt1RGoMcDRHRJefkZFx5MZff6h3i86GswPa7yw4PJpGQfjzfzzCixuRAfs8R3zUxkKH3Wj55EuF", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"7d7iapfxQoMi5jM46h5vm8hHxrjsSVV2twYVSrYaCJdz", + "txType":"sell", + "tokenAmount":30582206.734745, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1000947981.292522, + "vSolInBondingCurve":32.15951338293637, + "marketCapSol":32.12905563924397 + }, + { + "signature":"4jSL1sa5nXqbvqzJ2nSL3kZwZqNTaksA7KaPDkfG6YwtN8RumMWD3UXLkaNRm3i2PUd6Wj2RGZX9huQKsFcGywFq", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"buy", + "tokenAmount":584679.9521120004, + "newTokenBalance":7732152.992471, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1000363301.34041, + "vSolInBondingCurve":32.17830957699855, + "marketCapSol":32.16662339960101 + }, + { + "signature":"3nWLjZYrrbPvYGNshApaiHaQS9jidMvzYLgsqywNRNebJsNdhA4kYZhxXh6R3DRR7snYUMM5pTmCx6aQJHxPxc26", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"GazCsmGe5RkzZmaTtPrfYKnHqQ2RQZjq2uoW8nRUgYri", + "txType":"sell", + "tokenAmount":34612903.225806, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1034976204.566216, + "vSolInBondingCurve":31.102164337673464, + "marketCapSol":30.051091223598853 + }, + { + "signature":"22xyhmt35AutSph1ZdMcbrUADpdXPwiwe9G2VomMKuMHDUhNSeqEYN2XJXYSswHManeJnH6QJA8Z1xjfxiuwH94K", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"orcACRJYTFjTeo2pV8TfYRTpmqfoYgbVi9GeANXTCc8", + "txType":"sell", + "tokenAmount":30291642.440098997, + "newTokenBalance":0.001214, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1065267847.006315, + "vSolInBondingCurve":30.217752361964582, + "marketCapSol":28.366342274278225 + }, + { + "signature":"osUSpasfsvdPkGXZGoLa68X7b1PRSpgUimM8MUNtw9nwMW9em3L4UKzDUDTDuF7FMAw1XGNtNDtPRCBnE7jRdr5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"sell", + "tokenAmount":7732152.992471, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1072999999.998786, + "vSolInBondingCurve":30.000000000033943, + "marketCapSol":27.958993476298126 + } + ] + + tokens = {"mint_address": {"trades": []}} + mint = "mint_address" + trading_amount = 0.45 + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + pump = Pump( + executor_name="sniper_test", + trader_type=Trader.sniper + ) + + if "trades" not in tokens[mint]: + tokens[mint]["trades"] = [] + + for msg in messages: + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( + msg=msg, + previous_trades=tokens[mint]["trades"], + amount_traded=trading_amount, + pubkey=keypair.pubkey() + ) + # Including last message with new metadata into trades list + tokens[mint]["trades"].append(new_msg) + exit_trade, exis_criteria = pump.validate_criteria(msg=new_msg, criteria=TradeRoadmap.sniper_1[2]["criteria"]) + if exit_trade: + print("Pump.test -> Criteria out: {}".format(exis_criteria)) + + + print(tokens[mint]["trades"]) + +#test() From c3304b0b844478760ae2bc6ece4502f66cc72a9c Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 28 Nov 2024 23:07:30 +0100 Subject: [PATCH 011/103] Sniper trade map almost done: Need to mark the token as closed in redis. Criterias and redis_db: we'll save and read dates as timestamp and transform them as datetime when required (doing calculations, etc). Redis: whe're not closing the connection but only unsubscribing to a redis open channel --- bot/domain/redis_db.py | 65 ++++++++++++++++++++++++++++++---- bot/libs/criterias.py | 22 +++++++----- bot/main.py | 2 +- bot/module/pump.py | 80 ++++++++++++++++++++++++++---------------- 4 files changed, 123 insertions(+), 46 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index e21624b..3a8e30e 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -1,3 +1,4 @@ +import json import redis from redisearch import Client, NumericField from datetime import datetime @@ -30,7 +31,12 @@ def __init__(self) -> None: self.create_index() # Event listener self.pubsub = self.client.pubsub() + + def subscribe(self): self.pubsub.psubscribe({"__keyspace@0__:*"}) + + def unsubscribe(self): + self.pubsub.unsubscribe({"__keyspace@0__:*"}) def stop(self): print("Unsubscribing and cleaning up...") @@ -93,7 +99,8 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict "trader": data["trader"], # "is_traded": True if int(data["is_traded"]) == 1 else False, "is_checked": is_checked, - "timestamp": datetime.fromtimestamp(int(data["timestamp"])), + #### "timestamp": datetime.fromtimestamp(float(data["timestamp"])), + "timestamp": data["timestamp"], "name": data["name"], "ticker": data["ticker"] } @@ -112,13 +119,14 @@ def update_token( is_closed: bool = False, balance: float = None, token_balance: float = None, + trades: List[Dict] = [] ) -> Dict: key = token["key"] data = token.copy() data.pop("key") data.pop("mint") - data["timestamp"] = data["timestamp"].timestamp() + #data["timestamp"] = data["timestamp"].isoformat() # Prepare the data to update trading_time = datetime.now().timestamp() @@ -132,6 +140,9 @@ def update_token( "trades": [] # We'll keep track of every buy/sell during our trade } else: + for trade in trades: + trade["timestamp"] = trade["timestamp"] + new_data = { "{}_time".format(action.value): trading_time, # New trading time as timestamp "{}_txn".format(action.value): txn, # New transaction string @@ -140,7 +151,8 @@ def update_token( "trader": trader.value, "{}_balance".format(action.value): balance, # Wallet balance before buy / after sell "{}_token_balance".format(action.value): token_balance, # Token balance after buy - "is_closed": is_closed + "is_closed": is_closed, + "trades": trades } data.update(new_data) @@ -171,7 +183,40 @@ def test_update_record(): redis_object = RedisDB() tokens = redis_object.get_fresh_tokens(trader=Trader.sniper) - print(tokens) + trades = [ + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":30582206.734745, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":977513247.598136, + "vSolInBondingCurve":32.93049999996889, + "marketCapSol":33.68803449046135, + "timestamp":1732830811.317805, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":32.480499999968885, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":4, + "sols":1.4991999999351904 + }, + { + "quantity":1, + "sols":0.018796194062183247 + } + ], + "developer_has_sold":False, + "sols_in_token_after_buying":0.45000000000000284 + } + ] for token in tokens: data = redis_object.update_token( @@ -179,13 +224,21 @@ def test_update_record(): txn="txn1", action=TxType.buy, amount=15.500, - trader=Trader.sniper + trader=Trader.sniper, + is_checked=True ) data = redis_object.update_token( token=data, txn="txn2", action=TxType.sell, amount=45.500, - trader=Trader.sniper + trader=Trader.sniper, + + is_closed = False, + balance = 21.500, + token_balance = 1234567890, + trades = trades ) print(data) + +#test_update_record() diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 24a1381..f381c1e 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -29,7 +29,7 @@ def trading_analytics( """ new_msg = msg.copy() - new_msg["timestamp"] = datetime.now() # Including timestamp in incomming message + new_msg["timestamp"] = datetime.now().timestamp() # Including timestamp in incomming message # Default values that might change new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 @@ -62,6 +62,10 @@ def trading_analytics( else: last_msg = previous_trades[-1] + + last_msg_timestamp = datetime.fromtimestamp(last_msg["timestamp"]) + first_trade_timestamp = datetime.fromtimestamp(previous_trades[0]["timestamp"]) + # We always keep track of the consecutive buys new_msg["max_consecutive_buys"] = last_msg["max_consecutive_buys"] @@ -71,7 +75,7 @@ def trading_analytics( if last_msg["txType"].lower() == TxType.buy.value: new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] - new_msg["seconds_between_buys"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() # Updating the last record new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] new_msg["max_consecutive_buys"][-1]["sols"] += new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] @@ -92,16 +96,16 @@ def trading_analytics( if last_msg["txType"].lower() == TxType.sell.value: new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] - new_msg["seconds_between_sells"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + new_msg["seconds_between_sells"] = (datetime.now() - last_msg_timestamp).total_seconds() else: new_msg["consecutive_sells"] = 1 new_msg["seconds_between_sells"] = 0 - new_msg["market_inactivity"] = (datetime.now() - last_msg["timestamp"]).total_seconds() - new_msg["max_seconds_in_market"] = (datetime.now() - previous_trades[0]["timestamp"]).total_seconds() + new_msg["market_inactivity"] = (datetime.now() - last_msg_timestamp).total_seconds() + new_msg["max_seconds_in_market"] = (datetime.now() - first_trade_timestamp).total_seconds() if msg["txType"].lower() == TxType.buy.value: - new_msg["seconds_between_buys"] = (datetime.now() - last_msg["timestamp"]).total_seconds() + new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() # We'll keep track of the Solanas in Bounding courve since the first recorded trade new_msg["vSolInBondingCurve_Base"] = last_msg["vSolInBondingCurve_Base"] @@ -127,7 +131,7 @@ def max_consecutive_buys(buys: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return buys >= msg["max_consecutive_buys"] + return buys >= msg["consecutive_buys"] def max_consecutive_sells(sells: int, msg: dict) -> bool: @@ -141,7 +145,7 @@ def max_consecutive_sells(sells: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return sells >= msg["max_consecutive_sells"] + return sells >= msg["consecutive_sells"] def max_seconds_between_buys(seconds: int, msg: dict) -> bool: @@ -372,3 +376,5 @@ def test(): tokens[mint]["trades"].append(new_msg) print(tokens[mint]["trades"]) + +#test() diff --git a/bot/main.py b/bot/main.py index 2c0e971..b02cad0 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,7 +1,7 @@ import asyncio import logging from module.pump import Pump, TradeRoadmap -from lib.utils import Trader +from bot.libs.utils import Trader async def main(): pump = Pump( diff --git a/bot/module/pump.py b/bot/module/pump.py index e810577..5d2cd2c 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -58,7 +58,14 @@ class TradeRoadmap: ] sniper_copytrade = [ # TODO: to be developed {"step": 0, "name": "WAIT_FOR_TRADERS", "redis": Redis.readTraders}, - {"step": 1, "name": "TRADER_SUBSCRIPTION", "subscription": Suscription.subscribeNewToken}, + { + "step": 1, + "name": "TRADER_SUBSCRIPTION", + "subscription": Suscription.subscribeNewToken, + "criteria": { + "discard_lower_than_sols": 1.00, + } + }, {"step": 2, "name": "TRADE_TOKEN", "action": TxType.buy}, { "step": 3, @@ -77,7 +84,7 @@ class TradeRoadmap: ] sniper_1 = [ {"step": 0, "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken}, - {"step": 1, "name": "TRADE_TOKEN", "action": TxType.buy}, + {"step": 1, "name": "TRADE_TOKEN_BUY", "action": TxType.buy}, { "step": 2, "name": "TOKEN_SUBSCRIPTION", @@ -88,13 +95,13 @@ class TradeRoadmap: "max_seconds_between_buys": 3, "developer_has_sold": True, "max_sols_in_token_after_buying_in_percentage": 100, - "market_inactivity": 10, - "max_seconds_in_market": 60 + "market_inactivity": 5, + "max_seconds_in_market": 10 } }, - {"step": 3, "name": "TRADE_TOKEN", "action": TxType.sell}, + {"step": 3, "name": "TRADE_TOKEN_SELL", "action": TxType.sell}, {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, - {"step": 5, "name": "CLOSE_TOKEN", "grooming": Redis.closeToken}, + {"step": 5, "name": "CLOSE_TOKEN", "redis": Redis.closeToken}, ] @@ -147,12 +154,24 @@ def remove_account(self, account: str): self.accounts.pop(self.accounts.index(account)) # Tokens to pay attention to - def add_token(self, token: Dict): - self.tokens[token["mint"]] = token + def add_update_token(self, token: Dict): + """ + Updates the token in the dictionary based on the provided mint key. + If the mint key does not exist, it will add a new token. - def remove_token(self, token: str): + Args: + mint (str): The mint attribute of the token to be updated. + updated_data (Dict): The data to update the token with. + """ if token["mint"] in self.tokens: - del self.tokens["miunt"] + # Update only the provided fields + self.tokens[token["mint"]].update(token) + else: + # If the mint doesn't exist, treat it as a new token + self.tokens[token["mint"]] = token + + def remove_token(self, token: Dict): + del self.tokens[token["mint"]] def clear_tokens(self): self.tokens = [] @@ -190,7 +209,9 @@ async def subscribe(self, steps: list): # REDIS DB HANDLING if "redis" in step: # Listen to redis change + redisdb.subscribe() if step["redis"] == Redis.readToken: + # TODO: Solve BUG: redis listener is not working at the second time for message in redisdb.pubsub.listen(): if message["type"] == "psubscribe": print("Subscribed to redis") @@ -228,10 +249,10 @@ async def subscribe(self, steps: list): token=token, is_checked=False ) - self.add_token(token=token) + self.add_update_token(token=token) - # - safely exit this listening - redisdb.pubsub.close() + # Safely unsubscribing current channellistening + redisdb.unsubscribe() print("Token {} assigned to {} to trade {}".format( token["mint"], @@ -244,7 +265,8 @@ async def subscribe(self, steps: list): continue if step["redis"] == Redis.closeToken: - # Save in redis the amount of buy and sell with timestamp. Calculate P&L + # TODO: mark token in redis as closed + self.remove_token(token=self.tokens[mint]) step_index += 1 continue @@ -276,8 +298,7 @@ async def subscribe(self, steps: list): token_balance=token_balance ) # Update token - self.remove_token(token=token) - self.add_token(token=token_updated) + self.add_update_token(token=token_updated) step_index += 1 continue @@ -309,12 +330,12 @@ async def subscribe(self, steps: list): trader=self.trader_type, is_closed=True, balance=self.balance, - token_balance=token_balance + token_balance=token_balance, + trades=self.tokens[mint]["trades"] ) # Update token - self.remove_token(token=token) - self.add_token(token=token_updated) + self.add_update_token(token=token_updated) step_index += 1 continue @@ -330,7 +351,7 @@ async def subscribe(self, steps: list): payload["keys"] = self.accounts if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = [mint for mint, _ in self.tokens.items() if not self.tokens[mint]["is_traded"]] + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_checked"] and self.tokens[mint]["is_traded"]] if suscription.value == Suscription.unsubscribeTokenTrade.value: payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] @@ -355,7 +376,10 @@ async def subscribe(self, steps: list): # This is the first message we get when we connect if "message" in msg: print(msg["message"]) - continue + if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): + move_to_next_step = True + else: + continue if suscription.value == Suscription.subscribeNewToken.value: move_to_next_step = self.new_token_suscription(msg=msg) @@ -386,7 +410,7 @@ async def subscribe(self, steps: list): criteria=step["criteria"] ) # Including exit criteria in token for further analytics - self.tokens[0]["exit_criteria"] = criteria + self.tokens[mint]["exit_criteria"] = criteria # Moving to the next step if move_to_next_step: @@ -395,13 +419,7 @@ async def subscribe(self, steps: list): break - if "grooming" in step: - grooming = step["grooming"] - if grooming.value == Redis.closeToken.value: - self.remove_token(token=self.tokens[0]) - # TODO: copy redis data to postgres and delete data from redis - - # TODO: add a saftley exit way of ending the program + # TODO: add a safely exit way of ending the program def new_token_suscription(self, msg: str) -> bool: @@ -417,7 +435,7 @@ def new_token_suscription(self, msg: str) -> bool: ) ) self.clear_tokens() # TODO: Change this when more than one token will be checked - self.add_token(token=msg["mint"]) + self.add_update_token(token=msg["mint"]) # testing # self.marketMaking(token=msg["mint"]) @@ -518,7 +536,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response = None retries = 0 while True: - if appconfig.ENVIRONMENT == "DUMM": + if appconfig.ENVIRONMENT == "DUMMY": print("trade -> DUMMY MODE: returning dummy transaction") txSignature = "txn_dummy_{}".format(txtype.value) break From fdf33a5a06f957ea6418e5fa7c3c1d6d33158395 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 30 Nov 2024 13:37:07 +0100 Subject: [PATCH 012/103] Adding criterias tests (under development). Wizard image. Pump: discarting old pumps when reading from redis. Config: adding AppMoode enum class. --- .vscode/launch.json | 17 +- .vscode/settings.json | 7 +- bot/config/__init__.py | 25 ++- bot/libs/criterias.py | 72 ++++++-- bot/module/pump.py | 84 ++++++---- bot/tests/libs/__init__.py | 0 bot/tests/libs/fixtures_criterias.py | 20 +++ bot/tests/libs/test_criterias.py | 242 +++++++++++++++++++++++++++ images/wizard.png | Bin 0 -> 369128 bytes 9 files changed, 415 insertions(+), 52 deletions(-) create mode 100644 bot/tests/libs/__init__.py create mode 100644 bot/tests/libs/fixtures_criterias.py create mode 100644 bot/tests/libs/test_criterias.py create mode 100644 images/wizard.png diff --git a/.vscode/launch.json b/.vscode/launch.json index 16c4e88..943489d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,10 +27,23 @@ "justMyCode": false }, { - "name": "Debug creiterias.py", + "name": "Debug criterias.py", "type": "python", "request": "launch", - "program": "${workspaceFolder}/bot/lib/criterias.py", + "program": "${workspaceFolder}/bot/libs/criterias.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, + { + "name": "Debug pump.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/module/pump.py", "console": "integratedTerminal", "cwd": "${workspaceFolder}", "env": { diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d1eae2..ef02222 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,9 @@ "-p", "test_*.py" ], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": [ + "bot" + ] } \ No newline at end of file diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 3772ac0..90ef873 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -1,9 +1,18 @@ import os +from enum import Enum + + +class AppMode(Enum): + dummy = "DUMMY" + simulation = "SIMULATION" + real = "REAL" + analytics = "ANALYTICS" class AuthConfig: # our code environment ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") + APPMODE = os.environ.get("APPMODE", AppMode.dummy.value) # Stable and liquid pairs: 0.1% to 0.5%. # Moderate trading volumes: 0.5% to 1%. @@ -16,7 +25,6 @@ class AuthConfig: "PRIVKEY", "" ) - SOL_BUY_AMOUNT = float(os.environ.get("SOL_BUY_AMOUNT", 0.001)) # here you can choose to increase/decrease the buy amount MIN_SOL_TRADING_AMOUNT = float( os.environ.get("MIN_SOL_TRADING_AMOUNT", 0.5) ) # Minimum balance a token must have for being traded @@ -25,10 +33,23 @@ class AuthConfig: PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 - TRADING_DEFAULT_AMOUNT = 0.50 + TRADING_DEFAULT_AMOUNT = float(os.environ.get("TRADING_DEFAULT_AMOUNT", 0.05)) + TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT = float( + os.environ.get( + "TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT", + 0.05 + ) + ) # Min amount of a trade to be considered relevat enough for criteria decision making. + TRADING_CRITERIA_CONSECUTIVES_NON_RELEVANT_TRADES_TOLERANCE = float( + os.environ.get( + "TRADING_CRITERIA_CONSECUTIVES_NON_RELEVANT_TRADES_TOLERANCE", + 3 + ) + ) TRADING_TOKENS_AT_THE_SAME_TIME = 1 TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 TRADING_RETRIES = 3 + TRADING_TOKEN_TOO_OLD_SECONDS = 15 #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index f381c1e..059244a 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -1,5 +1,6 @@ from datetime import datetime from typing import Dict, List +from bot.config import appconfig from bot.libs.utils import TxType from solders.pubkey import Pubkey @@ -31,6 +32,9 @@ def trading_analytics( new_msg = msg.copy() new_msg["timestamp"] = datetime.now().timestamp() # Including timestamp in incomming message + # Checking if the trade position is relevant enough to be considering for criteria + new_msg["is_relevant_trade"] = True + # Default values that might change new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 @@ -41,6 +45,10 @@ def trading_analytics( if new_msg["traderPublicKey"] == str(pubkey): # We're the first recorded trade: we discount the amount to the current bonding curve new_msg["vSolInBondingCurve_Base"] = new_msg["vSolInBondingCurve"] - amount_traded + + # Volume bot case: small trades might not be relevant. Starting counter + new_msg["is_relevant_trade"] = amount_traded >= appconfig.TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT + new_msg["is_non_relevant_trade_count"] = 0 if new_msg["is_relevant_trade"] else 1 else: # The first recorded trade is not from us. So we can make an aproximation aprox = new_msg["tokenAmount"] * new_msg["vSolInBondingCurve"] @@ -48,6 +56,9 @@ def trading_analytics( aprox = aprox / new_msg["vTokensInBondingCurve"] # We discount the Sols traded by the current trader to the current Sols in Bonding Curve new_msg["vSolInBondingCurve_Base"] = new_msg["vSolInBondingCurve"] - aprox + # Checking if this trade is relavant or not and starting counter for consecutives buys/sells + new_msg["is_relevant_trade"] = aprox >= appconfig.TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT + new_msg["is_non_relevant_trade_count"] = 0 if new_msg["is_relevant_trade"] else 1 new_msg["seconds_between_buys"] = 0 new_msg["seconds_between_sells"] = 0 @@ -66,6 +77,13 @@ def trading_analytics( last_msg_timestamp = datetime.fromtimestamp(last_msg["timestamp"]) first_trade_timestamp = datetime.fromtimestamp(previous_trades[0]["timestamp"]) + # Current solanas traded + sols = new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + # Checking if this trade is relavant or not. Keeping counter for consecutives buys/sells + new_msg["is_relevant_trade"] = sols >= appconfig.TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT + # We just count non relevant trades with consecutives buys/sells + new_msg["is_non_relevant_trade_count"] = 0 if new_msg["is_relevant_trade"] else 1 + # We always keep track of the consecutive buys new_msg["max_consecutive_buys"] = last_msg["max_consecutive_buys"] @@ -74,11 +92,24 @@ def trading_analytics( new_msg["seconds_between_sells"] = 0 if last_msg["txType"].lower() == TxType.buy.value: - new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] - new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() - # Updating the last record - new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] - new_msg["max_consecutive_buys"][-1]["sols"] += new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + # Starting counter for non relevant trades with consecutives buys/sells + if not new_msg["is_relevant_trade"] and not last_msg["is_relevant_trade"]: + new_msg["is_non_relevant_trade_count"] += last_msg["is_non_relevant_trade_count"] + + # Considering consecutives buys for relevan trades or for consecutives non relevant + # trades that have reached the accepted tolerance + tolerance = appconfig.TRADING_CRITERIA_CONSECUTIVES_NON_RELEVANT_TRADES_TOLERANCE + if new_msg["is_relevant_trade"] or \ + not new_msg["is_relevant_trade"] and new_msg["is_non_relevant_trade_count"] >= tolerance: + + new_msg["is_non_relevant_trade_count"] = 0 + + new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] + new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() + # Updating the last record + new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] + new_msg["max_consecutive_buys"][-1]["sols"] += sols + else: new_msg["consecutive_buys"] = 1 new_msg["seconds_between_buys"] = 0 @@ -86,7 +117,7 @@ def trading_analytics( new_msg["max_consecutive_buys"].append( { "quantity": 1, - "sols": new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + "sols": sols } ) @@ -95,8 +126,21 @@ def trading_analytics( new_msg["seconds_between_buys"] = 0 if last_msg["txType"].lower() == TxType.sell.value: - new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] - new_msg["seconds_between_sells"] = (datetime.now() - last_msg_timestamp).total_seconds() + # Starting counter for non relevant trades with consecutives buys/sells + if not new_msg["is_relevant_trade"] and not last_msg["is_relevant_trade"]: + new_msg["is_non_relevant_trade_count"] += last_msg["is_non_relevant_trade_count"] + + # Considering consecutives buys for relevan trades or for consecutives non relevant + # trades that have reached the accepted tolerance + tolerance = appconfig.TRADING_CRITERIA_CONSECUTIVES_NON_RELEVANT_TRADES_TOLERANCE + if new_msg["is_relevant_trade"] or \ + not new_msg["is_relevant_trade"] and new_msg["is_non_relevant_trade_count"] >= tolerance: + + new_msg["is_non_relevant_trade_count"] = 0 + + new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] + new_msg["seconds_between_sells"] = (datetime.now() - last_msg_timestamp).total_seconds() + else: new_msg["consecutive_sells"] = 1 new_msg["seconds_between_sells"] = 0 @@ -122,7 +166,7 @@ def trading_analytics( def max_consecutive_buys(buys: int, msg: dict) -> bool: """ - Checks if the maximum consecutive buys condition is met. + Checks if the maximum consecutive buys has been reached. Args: buys (int): The maximum number of consecutive buys allowed. @@ -131,7 +175,7 @@ def max_consecutive_buys(buys: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return buys >= msg["consecutive_buys"] + return msg["consecutive_buys"] >= buys def max_consecutive_sells(sells: int, msg: dict) -> bool: @@ -145,12 +189,12 @@ def max_consecutive_sells(sells: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return sells >= msg["consecutive_sells"] + return msg["consecutive_sells"] >= sells def max_seconds_between_buys(seconds: int, msg: dict) -> bool: """ - Checks if the maximum allowed seconds between buys condition is met. + Checks if the maximum allowed seconds between buys has been reached. Args: seconds (int): The maximum number of seconds allowed between buys. @@ -159,7 +203,7 @@ def max_seconds_between_buys(seconds: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - pass + return msg["seconds_between_buys"] >= seconds def developer_has_sold(sold: bool, msg: dict) -> bool: @@ -219,6 +263,8 @@ def max_seconds_in_market(seconds: int, msg: dict) -> bool: def test(): + print("#######################") + print(" TEST NON RELEVANT TRADES COUNT AND MODIFY MESSAGES IN THIS FUNCTION") messages = [ { "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", diff --git a/bot/module/pump.py b/bot/module/pump.py index 5d2cd2c..2749c4d 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -24,9 +24,10 @@ Trader, TxType ) -from bot.config import appconfig +from bot.config import appconfig, AppMode from bot.domain.redis_db import RedisDB +from datetime import datetime from enum import Enum from solders.transaction import VersionedTransaction from solders.keypair import Keypair @@ -215,10 +216,11 @@ async def subscribe(self, steps: list): for message in redisdb.pubsub.listen(): if message["type"] == "psubscribe": print("Subscribed to redis") + if message["type"] == "pmessage": # Parse the message key = ":".join(message["channel"].split(":")[1:]) - # - Take key and retrieve token from redis + # Get token information: Take key and retrieve token from redis tokens = redisdb.get_fresh_tokens( trader=Trader.sniper, mint_address=key @@ -226,35 +228,50 @@ async def subscribe(self, steps: list): # - Ignore token that are not fresh or for not the current trader type if not tokens: continue - # - Get token information - # TODO: snipe againt to more than one token at the same time - token = tokens[0] - - mint = token["mint"] - amount = token["amount"] - - if self.balance >= amount: - self.trading_amount = amount - else: - print("{}: Not enough balance for Token {} and amount {}. Balance is {}".format( - self.executor_name, - mint, - amount, - self.balance - )) - continue - - # - move to next step and update the token as being checked - token = redisdb.update_token( - token=token, - is_checked=False - ) - self.add_update_token(token=token) - - # Safely unsubscribing current channellistening + # Snipe against many tokens + how_many = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME + if len(tokens) < appconfig.TRADING_TOKENS_AT_THE_SAME_TIME: + how_many = len(tokens) + + for token in tokens[0: how_many - 1]: + mint = token["mint"] + amount = token["amount"] + + # FILTERING BY TOKEN'S CREATION TIME + token_creation_time = datetime.fromtimestamp(token["timestamp"]) + token_age = (datetime.now() - token_creation_time).total_seconds() + if token_age >= appconfig.TRADING_TOKEN_TOO_OLD_SECONDS and \ + appconfig.APPMODE not in [AppMode.dummy.value, AppMode.simulation.value]: + print("Warning: susbscribe -> Token {} is too old for trading.".format( + token["name"] + )) + continue + # Checking wallet's balance before trading + if self.balance >= amount: + self.trading_amount = amount + else: + print("{}: Warning: susbscribe -> Not enough balance for Token {} and amount {}. Balance is {}".format( + self.executor_name, + mint, + amount, + self.balance + )) + if appconfig.APPMODE in [AppMode.dummy.value, AppMode.simulation.value]: + continue + else: + break + + # - move to next step and update the token as being checked + token = redisdb.update_token( + token=token, + is_checked=False + ) + self.add_update_token(token=token) + + # Safely unsubscribing from current channel listening redisdb.unsubscribe() - print("Token {} assigned to {} to trade {}".format( + print("Token {} assigned to {} to trade {}Sols".format( token["mint"], self.executor_name, self.trading_amount @@ -262,10 +279,9 @@ async def subscribe(self, steps: list): break step_index += 1 - continue + if step["redis"] == Redis.closeToken: - # TODO: mark token in redis as closed self.remove_token(token=self.tokens[mint]) step_index += 1 continue @@ -376,6 +392,7 @@ async def subscribe(self, steps: list): # This is the first message we get when we connect if "message" in msg: print(msg["message"]) + # If the message is about unsibscribing, then we move on to the next step if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): move_to_next_step = True else: @@ -536,8 +553,9 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response = None retries = 0 while True: - if appconfig.ENVIRONMENT == "DUMMY": - print("trade -> DUMMY MODE: returning dummy transaction") + # Faking transaction for none real modes + if appconfig.APPMODE not in [AppMode.real.value]: + print("trade -> {} MODE: returning dummy transaction".format(appconfig.APPMODE)) txSignature = "txn_dummy_{}".format(txtype.value) break diff --git a/bot/tests/libs/__init__.py b/bot/tests/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/tests/libs/fixtures_criterias.py b/bot/tests/libs/fixtures_criterias.py new file mode 100644 index 0000000..f1155cd --- /dev/null +++ b/bot/tests/libs/fixtures_criterias.py @@ -0,0 +1,20 @@ +import pytest +from solders.pubkey import Pubkey + + +@pytest.fixture +def get_account(): + yield "Some_account" + +@pytest.fixture +def get_pubkey(): + yield Pubkey.from_string("4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb") + + +@pytest.fixture +def get_max_consecutive_buys(): + yield 2 + +@pytest.fixture +def get_max_seconds_between_buys(): + yield 3 diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py new file mode 100644 index 0000000..9889e46 --- /dev/null +++ b/bot/tests/libs/test_criterias.py @@ -0,0 +1,242 @@ +from typing import Any +from unittest.mock import patch, Mock +import pytest +from bot.tests.libs.fixtures_criterias import * +from bot.libs.criterias import ( + exit_on_first_sale, + trading_analytics, + exit_on_first_sale, + max_consecutive_buys, + max_seconds_between_buys +) + + +@pytest.mark.parametrize( + "expected_result, msg, description", [ + ( + True, + { + "signature":"osUSpasfsvdPkGXZGoLa68X7b1PRSpgUimM8MUNtw9nwMW9em3L4UKzDUDTDuF7FMAw1XGNtNDtPRCBnE7jRdr5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"sell", + "tokenAmount":7732152.992471, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1072999999.998786, + "vSolInBondingCurve":30.000000000033943, + "marketCapSol":27.958993476298126 + }, + "Sell test" + ), + ( + False, + { + "signature":"osUSpasfsvdPkGXZGoLa68X7b1PRSpgUimM8MUNtw9nwMW9em3L4UKzDUDTDuF7FMAw1XGNtNDtPRCBnE7jRdr5", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"5gaewKWutRmK5J7iAFLFeEz8aeuhLMGsYwmXnZw8ib9L", + "txType":"buy", + "tokenAmount":7732152.992471, + "newTokenBalance":0, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":1072999999.998786, + "vSolInBondingCurve":30.000000000033943, + "marketCapSol":27.958993476298126 + }, + "Buy test" + ) + ] +) +def test_exit_on_first_sale(expected_result, msg, description): + assert exit_on_first_sale(msg=msg) == expected_result, description + +@pytest.mark.parametrize( + """ + description, + msg, + previous_trades, + amount, + traders, + relevant_trade, + is_non_relevant_trade_count, + max_consecutive_buys_result, + max_seconds_between_buys_result, + """, + [ + ( + "Sniper is the first to do a trade", + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":30582206.734745, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":977513247.598136, + "vSolInBondingCurve":32.000, + "marketCapSol":33.000 + }, + [], + 0.5, + [], + True, + 0, + False, + False + ), + ( + "Sniper: first non relevant trade", + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":30582206.734745, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":977513247.598136, + "vSolInBondingCurve":32.000, + "marketCapSol":32.001 + }, + [], + 0.001, + [], + False, + 1, + False, + False + ), + ( + "Other trader: first relevant trade", + { + "signature":"XGAnLe4EKCx4NNHv7soDimYZifwRndEGq1myrMDZR6DSRa6FgvcsMbpEz7XJrvRHxH6GcwPTr1oKt9jNWj5T4Uh", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.000, + "marketCapSol":33.000 + }, + [], + 0.5, + [], + True, + 0, + False, + False + ), + ( + "Other trader: first NON relevant trade", + { + "signature":"xxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.000, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + False, + 1, + False, + False + ), + ( + "Second trade: relevant Buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ] + } + ], + 0.5, + [], + True, + 0, + True, + True + ) + ] +) +def test_trading_analytics_buys( + get_pubkey, + get_max_consecutive_buys, + get_max_seconds_between_buys, + msg, + previous_trades, + amount, + traders, + description, + relevant_trade, + is_non_relevant_trade_count, + max_consecutive_buys_result, + max_seconds_between_buys_result +): + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=amount, + pubkey=get_pubkey, + traders=traders + ) + if str(get_pubkey) == new_msg["traderPublicKey"]: + assert new_msg["vSolInBondingCurve_Base"] == new_msg["vSolInBondingCurve"] - amount, description + else: + new_amount = round(float(new_msg["sols_in_token_after_buying"]), 4) + assert new_msg["vSolInBondingCurve_Base"] == new_msg["vSolInBondingCurve"] - new_amount, description + assert new_msg["is_relevant_trade"] == relevant_trade, description + assert new_msg["is_non_relevant_trade_count"] == is_non_relevant_trade_count, description + + assert max_consecutive_buys(buys=get_max_consecutive_buys, msg=new_msg) == max_consecutive_buys_result + if not max_seconds_between_buys_result: + assert max_seconds_between_buys( + seconds=get_max_seconds_between_buys, + msg=new_msg + ) == max_seconds_between_buys_result + diff --git a/images/wizard.png b/images/wizard.png new file mode 100644 index 0000000000000000000000000000000000000000..69b732768b0506c202e6f855f9f6fa5126c2e0c4 GIT binary patch literal 369128 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
  • 9$5-z9vmPz5_X((kUn@#UT^S=t@xF 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

    p@@K!7kcE_C9&X%C$s_~r#e{)H|#bKfY^up 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

    XARtAuY@Cy25MkZ$TUsfY{$`+qSK$Xu&#h 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 aWuR9*`n8}Dn*#v+la*AG_$g)_@_ztG7lJte literal 0 HcmV?d00001 From 9a3a3c001d29f8e376111de55b80e52967038135 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 30 Nov 2024 20:36:50 +0100 Subject: [PATCH 013/103] Test criterias: adding test_max_seconds_between_buys --- bot/tests/libs/fixtures_criterias.py | 4 - bot/tests/libs/test_criterias.py | 148 ++++++++++++++++++++++++--- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/bot/tests/libs/fixtures_criterias.py b/bot/tests/libs/fixtures_criterias.py index f1155cd..bcc5d13 100644 --- a/bot/tests/libs/fixtures_criterias.py +++ b/bot/tests/libs/fixtures_criterias.py @@ -2,10 +2,6 @@ from solders.pubkey import Pubkey -@pytest.fixture -def get_account(): - yield "Some_account" - @pytest.fixture def get_pubkey(): yield Pubkey.from_string("4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb") diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 9889e46..696e8cb 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from typing import Any from unittest.mock import patch, Mock import pytest @@ -59,8 +60,7 @@ def test_exit_on_first_sale(expected_result, msg, description): traders, relevant_trade, is_non_relevant_trade_count, - max_consecutive_buys_result, - max_seconds_between_buys_result, + max_consecutive_buys_result """, [ ( @@ -82,7 +82,6 @@ def test_exit_on_first_sale(expected_result, msg, description): [], True, 0, - False, False ), ( @@ -104,7 +103,6 @@ def test_exit_on_first_sale(expected_result, msg, description): [], False, 1, - False, False ), ( @@ -126,7 +124,6 @@ def test_exit_on_first_sale(expected_result, msg, description): [], True, 0, - False, False ), ( @@ -148,7 +145,6 @@ def test_exit_on_first_sale(expected_result, msg, description): [], False, 1, - False, False ), ( @@ -199,7 +195,6 @@ def test_exit_on_first_sale(expected_result, msg, description): [], True, 0, - True, True ) ] @@ -207,7 +202,6 @@ def test_exit_on_first_sale(expected_result, msg, description): def test_trading_analytics_buys( get_pubkey, get_max_consecutive_buys, - get_max_seconds_between_buys, msg, previous_trades, amount, @@ -215,8 +209,7 @@ def test_trading_analytics_buys( description, relevant_trade, is_non_relevant_trade_count, - max_consecutive_buys_result, - max_seconds_between_buys_result + max_consecutive_buys_result ): new_msg = trading_analytics( msg=msg, @@ -234,9 +227,136 @@ def test_trading_analytics_buys( assert new_msg["is_non_relevant_trade_count"] == is_non_relevant_trade_count, description assert max_consecutive_buys(buys=get_max_consecutive_buys, msg=new_msg) == max_consecutive_buys_result - if not max_seconds_between_buys_result: - assert max_seconds_between_buys( + + +@pytest.mark.parametrize( + """ + description, + msg, + previous_trades, + amount, + traders, + max_seconds_between_buys_result, + """, + [ + ( + "Max Seconds between buys: not exceeded", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":(datetime.now() - timedelta(seconds=2)).timestamp(), + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ] + } + ], + 0.5, + [], + False + ), + ( + "Max Seconds between buys: exceeded", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":(datetime.now() - timedelta(seconds=100)).timestamp(), + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ] + } + ], + 0.5, + [], + True + ) + ] +) +def test_max_seconds_between_buys( + get_pubkey, + get_max_seconds_between_buys, + msg, + previous_trades, + amount, + traders, + description, + max_seconds_between_buys_result +): + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=amount, + pubkey=get_pubkey, + traders=traders + ) + + seconds_exceeded = max_seconds_between_buys( seconds=get_max_seconds_between_buys, msg=new_msg - ) == max_seconds_between_buys_result - + ) + assert seconds_exceeded == max_seconds_between_buys_result, description From 0c500a855659409b03b5860fd9623e53fb275e0d Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 30 Nov 2024 20:53:35 +0100 Subject: [PATCH 014/103] Test criterias: adding test_trader_has_sold --- bot/libs/criterias.py | 10 +-- bot/module/pump.py | 4 +- bot/tests/libs/test_criterias.py | 136 ++++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 8 deletions(-) diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 059244a..857309c 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -155,8 +155,8 @@ def trading_analytics( new_msg["vSolInBondingCurve_Base"] = last_msg["vSolInBondingCurve_Base"] - # Checking if the current traders is in the trader's list - new_msg["developer_has_sold"] = new_msg["traderPublicKey"] in traders + # Checking if the current trader is in the trader's list + new_msg["trader_has_sold"] = new_msg["traderPublicKey"] in traders # Amount of Solana the token is holding after our buy new_msg["sols_in_token_after_buying"] = new_msg["vSolInBondingCurve"] - new_msg["vSolInBondingCurve_Base"] @@ -206,9 +206,9 @@ def max_seconds_between_buys(seconds: int, msg: dict) -> bool: return msg["seconds_between_buys"] >= seconds -def developer_has_sold(sold: bool, msg: dict) -> bool: +def trader_has_sold(msg: dict) -> bool: """ - Checks if the developer has sold their tokens. + Checks if the trader or developer has sold their tokens. Args: sold (bool): A flag indicating whether the developer has sold tokens. @@ -217,7 +217,7 @@ def developer_has_sold(sold: bool, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - pass + return msg["trader_has_sold"] def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict) -> bool: diff --git a/bot/module/pump.py b/bot/module/pump.py index 2749c4d..f4b5abf 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -14,7 +14,7 @@ max_seconds_between_buys, max_seconds_in_market, max_sols_in_token_after_buying_in_percentage, - developer_has_sold, + trader_has_sold, market_inactivity ) from bot.libs import criterias as criteria_functions @@ -94,7 +94,7 @@ class TradeRoadmap: "max_consecutive_buys": 2, "max_consecutive_sells": 1, "max_seconds_between_buys": 3, - "developer_has_sold": True, + "trader_has_sold": True, "max_sols_in_token_after_buying_in_percentage": 100, "market_inactivity": 5, "max_seconds_in_market": 10 diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 696e8cb..a466898 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -8,7 +8,8 @@ trading_analytics, exit_on_first_sale, max_consecutive_buys, - max_seconds_between_buys + max_seconds_between_buys, + trader_has_sold ) @@ -360,3 +361,136 @@ def test_max_seconds_between_buys( msg=new_msg ) assert seconds_exceeded == max_seconds_between_buys_result, description + + +@pytest.mark.parametrize( + """ + description, + msg, + previous_trades, + amount, + traders, + trader_has_sold_result, + """, + [ + ( + "Trader has sold: False", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":(datetime.now() - timedelta(seconds=2)).timestamp(), + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ] + } + ], + 0.5, + [ + "4XXnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43xxX" + ], + False + ), + ( + "Trader has sold: True", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":(datetime.now() - timedelta(seconds=2)).timestamp(), + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ] + } + ], + 0.5, + [ + "4XXnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43xxX", + "4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC" + ], + True + ), + ] +) +def test_trader_has_sold( + get_pubkey, + description, + msg, + previous_trades, + amount, + traders, + trader_has_sold_result +): + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=amount, + pubkey=get_pubkey, + traders=traders + ) + has_sold = trader_has_sold(msg=new_msg) + assert has_sold == trader_has_sold_result, description From 8c3638cb2541e53a1aef26d5f7ab34f84a27b4a6 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 30 Nov 2024 23:13:26 +0100 Subject: [PATCH 015/103] Adding track_traders both in redis reading nd in trading_analytics functions. Pump: bug fixed when reading many toknes from redis and printing in console the token being red. --- bot/domain/redis_db.py | 18 +++++++++--------- bot/module/pump.py | 19 +++++++++++-------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 3a8e30e..18721aa 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -92,17 +92,17 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict trader_match = data.get("trader", "unknown") == trader.value if not is_checked and trader_match: + track_traders = data["track_traders"] if "track_traders" in data else [] token = { "key": key, - "mint": key.replace("token:", ""), - "amount": float(data["amount"]), - "trader": data["trader"], - # "is_traded": True if int(data["is_traded"]) == 1 else False, - "is_checked": is_checked, - #### "timestamp": datetime.fromtimestamp(float(data["timestamp"])), - "timestamp": data["timestamp"], - "name": data["name"], - "ticker": data["ticker"] + "mint": key.replace("token:", ""), # Mint address from key value + "amount": float(data["amount"]), # Amount of Sols to be traded + "trader": data["trader"], # Trader bot that will execute this trade + "is_checked": is_checked, # By default is_checked is False. + "timestamp": data["timestamp"], # Getting timestamp from redis + "name": data["name"], # Token name + "ticker": data["ticker"], # Token's Ticker + "track_traders": track_traders # Tracking other traders activity } tokens.append(token) diff --git a/bot/module/pump.py b/bot/module/pump.py index f4b5abf..a8f5fe7 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -233,7 +233,7 @@ async def subscribe(self, steps: list): if len(tokens) < appconfig.TRADING_TOKENS_AT_THE_SAME_TIME: how_many = len(tokens) - for token in tokens[0: how_many - 1]: + for token in tokens[0: how_many]: mint = token["mint"] amount = token["amount"] @@ -268,14 +268,16 @@ async def subscribe(self, steps: list): ) self.add_update_token(token=token) + print("Token {}:{} assigned to {} to trade {}Sols".format( + token["name"], + token["mint"], + self.executor_name, + self.trading_amount + )) + # Safely unsubscribing from current channel listening redisdb.unsubscribe() - print("Token {} assigned to {} to trade {}Sols".format( - token["mint"], - self.executor_name, - self.trading_amount - )) break step_index += 1 @@ -417,7 +419,8 @@ async def subscribe(self, steps: list): msg=msg, previous_trades=self.tokens[mint]["trades"], amount_traded=self.trading_amount, - pubkey=self.keypair.pubkey() + pubkey=self.keypair.pubkey(), + traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] ) # Including last message with new metadata into trades list self.tokens[mint]["trades"].append(new_msg) @@ -504,7 +507,7 @@ def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: if callable(function): # Call the function with the parameter and msg is_valid = function(parameter, msg) - print(f"Function {function_name} returned: {is_valid}") + print(f"validate_criteria-> Function {function_name} returned: {is_valid}") else: print(f"validate_criteria-> Error: {function_name} not found.") From fb2dd83305622751686d1b4b07a8eaf238f6c2f4 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 30 Nov 2024 23:33:00 +0100 Subject: [PATCH 016/103] Pump: Bug fixed when token is too old to deal with and we're not ready to exit the redis listening loop --- bot/module/pump.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/module/pump.py b/bot/module/pump.py index a8f5fe7..3523f9e 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -187,6 +187,8 @@ def remove_trader(self, trader: str): async def subscribe(self, steps: list): step_index = 0 + print("Subscribe is working in {} mode.".format(appconfig.APPMODE)) + redisdb = RedisDB() await asyncio.sleep(2) # Wait 2 seconds for the redis connection to start @@ -274,11 +276,13 @@ async def subscribe(self, steps: list): self.executor_name, self.trading_amount )) + + tokens_to_trade = any(token for token in tokens if token["is_checked"]) # Safely unsubscribing from current channel listening - redisdb.unsubscribe() - - break + if tokens_to_trade: + redisdb.unsubscribe() + break step_index += 1 continue From 9331d6345c903d3d7a7c8ba6e493fbe1b5cdc6b4 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 1 Dec 2024 00:14:44 +0100 Subject: [PATCH 017/103] Adding market inactivity for trading timeout in config. Adding timeout to websoclet listening and catching if timeout error --- bot/config/__init__.py | 1 + bot/libs/criterias.py | 20 +---- bot/module/pump.py | 168 +++++++++++++++++++++++++++++------------ 3 files changed, 122 insertions(+), 67 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 90ef873..bda0889 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -50,6 +50,7 @@ class AuthConfig: TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 TRADING_RETRIES = 3 TRADING_TOKEN_TOO_OLD_SECONDS = 15 + TRADING_MARKETING_INACTIVITY_TIMEOUT = 60 #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 857309c..212cea6 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -234,9 +234,9 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict) -> pass -def market_inactivity(seconds: int, msg: dict) -> bool: +def market_inactivity(seconds: int, msg: dict = None) -> bool: """ - Checks if the market inactivity condition is met. + Dummy fuction as it retrieves seconds received Args: seconds (int): The maximum number of seconds of inactivity allowed. @@ -245,21 +245,7 @@ def market_inactivity(seconds: int, msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - pass - - -def max_seconds_in_market(seconds: int, msg: dict) -> bool: - """ - Checks if the maximum seconds in the market condition is met. - - Args: - seconds (int): The maximum number of seconds allowed in the market. - msg (dict): The message dictionary containing context. - - Returns: - bool: True if the condition is met, otherwise False. - """ - pass + return seconds def test(): diff --git a/bot/module/pump.py b/bot/module/pump.py index 3523f9e..78feb6a 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -12,7 +12,6 @@ max_consecutive_buys, max_consecutive_sells, max_seconds_between_buys, - max_seconds_in_market, max_sols_in_token_after_buying_in_percentage, trader_has_sold, market_inactivity @@ -96,8 +95,7 @@ class TradeRoadmap: "max_seconds_between_buys": 3, "trader_has_sold": True, "max_sols_in_token_after_buying_in_percentage": 100, - "market_inactivity": 5, - "max_seconds_in_market": 10 + "market_inactivity": 3 } }, {"step": 3, "name": "TRADE_TOKEN_SELL", "action": TxType.sell}, @@ -369,6 +367,11 @@ async def subscribe(self, steps: list): "method": suscription.value, } + websocket_timeout = appconfig.TRADING_MARKETING_INACTIVITY_TIMEOUT + if "criteria" in step: + if "market_inactivity" in step["criteria"]: + websocket_timeout = step["criteria"]["market_inactivity"] + if suscription.value == Suscription.subscribeAccountTrade.value: payload["keys"] = self.accounts @@ -389,59 +392,124 @@ async def subscribe(self, steps: list): self.tokens[mint]["is_checked"] and \ not self.tokens[mint]["is_closed"]: pass - - # MAIN WEBSOCKET THREAD - async for message in websocket: - msg = json.loads(message) - move_to_next_step = False - - # This is the first message we get when we connect - if "message" in msg: - print(msg["message"]) - # If the message is about unsibscribing, then we move on to the next step - if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): - move_to_next_step = True - else: - continue - - if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.new_token_suscription(msg=msg) - - if suscription.value == Suscription.subscribeTokenTrade.value: - # Getting the mint address we'll work with - mint = msg["mint"] - - # We'll not pay attention to closed token trades - if self.tokens[mint]["is_closed"]: - continue - - if "trades" not in self.tokens[mint]: - self.tokens[mint]["trades"] = [] - - # Doing some analytics like how many continuous buys have happend, etc - new_msg = trading_analytics( - msg=msg, - previous_trades=self.tokens[mint]["trades"], - amount_traded=self.trading_amount, - pubkey=self.keypair.pubkey(), - traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] - ) - # Including last message with new metadata into trades list - self.tokens[mint]["trades"].append(new_msg) - - move_to_next_step, criteria = self.validate_criteria( - msg=new_msg, - criteria=step["criteria"] + + while True: + try: + message = await asyncio.wait_for( + websocket.recv(), + timeout=websocket_timeout ) - # Including exit criteria in token for further analytics - self.tokens[mint]["exit_criteria"] = criteria + msg = json.loads(message) + move_to_next_step = False + + # This is the first message we get when we connect + if "message" in msg: + print(msg["message"]) + # If the message is about unsibscribing, then we move on to the next step + if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): + move_to_next_step = True + else: + continue + + if suscription.value == Suscription.subscribeNewToken.value: + move_to_next_step = self.new_token_suscription(msg=msg) - # Moving to the next step - if move_to_next_step: + if suscription.value == Suscription.subscribeTokenTrade.value: + # Getting the mint address we'll work with + mint = msg["mint"] + + # We'll not pay attention to closed token trades + if self.tokens[mint]["is_closed"]: + continue + + if "trades" not in self.tokens[mint]: + self.tokens[mint]["trades"] = [] + + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( + msg=msg, + previous_trades=self.tokens[mint]["trades"], + amount_traded=self.trading_amount, + pubkey=self.keypair.pubkey(), + traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] + ) + # Including last message with new metadata into trades list + self.tokens[mint]["trades"].append(new_msg) + + move_to_next_step, criteria = self.validate_criteria( + msg=new_msg, + criteria=step["criteria"] + ) + # Including exit criteria in token for further analytics + self.tokens[mint]["exit_criteria"] = criteria + + # Moving to the next step + if move_to_next_step: + step_index += 1 + move_to_next_step = False + break + + except asyncio.TimeoutError: + print("No trades detected during {} seconds. Moving to next step.".format( + websocket_timeout + )) + self.tokens[mint]["exit_criteria"] = "market_inactivity" step_index += 1 move_to_next_step = False break + # # MAIN WEBSOCKET THREAD + # async for message in websocket: + # msg = json.loads(message) + # move_to_next_step = False + + # # This is the first message we get when we connect + # if "message" in msg: + # print(msg["message"]) + # # If the message is about unsibscribing, then we move on to the next step + # if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): + # move_to_next_step = True + # else: + # continue + + # if suscription.value == Suscription.subscribeNewToken.value: + # move_to_next_step = self.new_token_suscription(msg=msg) + + # if suscription.value == Suscription.subscribeTokenTrade.value: + # # Getting the mint address we'll work with + # mint = msg["mint"] + + # # We'll not pay attention to closed token trades + # if self.tokens[mint]["is_closed"]: + # continue + + # if "trades" not in self.tokens[mint]: + # self.tokens[mint]["trades"] = [] + + # # Doing some analytics like how many continuous buys have happend, etc + # new_msg = trading_analytics( + # msg=msg, + # previous_trades=self.tokens[mint]["trades"], + # amount_traded=self.trading_amount, + # pubkey=self.keypair.pubkey(), + # traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] + # ) + # # Including last message with new metadata into trades list + # self.tokens[mint]["trades"].append(new_msg) + + # move_to_next_step, criteria = self.validate_criteria( + # msg=new_msg, + # criteria=step["criteria"] + # ) + # # Including exit criteria in token for further analytics + # self.tokens[mint]["exit_criteria"] = criteria + + # # Moving to the next step + # if move_to_next_step: + # step_index += 1 + # move_to_next_step = False + # break + # TODO: add a safely exit way of ending the program From d0d813fa3e435642a9a22a95ff62a62c02f881f3 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 1 Dec 2024 15:48:03 +0100 Subject: [PATCH 018/103] Including scanner functionality --- bot/config/__init__.py | 5 ++ bot/domain/redis_db.py | 19 ++--- bot/libs/utils.py | 12 ++- bot/main.py | 10 ++- bot/module/pump.py | 176 ++++++++++++++++++++++++----------------- 5 files changed, 131 insertions(+), 91 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index bda0889..08b60da 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -51,6 +51,11 @@ class AuthConfig: TRADING_RETRIES = 3 TRADING_TOKEN_TOO_OLD_SECONDS = 15 TRADING_MARKETING_INACTIVITY_TIMEOUT = 60 + SCANNER_MIN_TRADING_AMOUNT = 1.00 # Min Sols a token must have as first buy to be considered for trading + SCANNER_WRITTING_CAPACITY = 1 # How many tokens to scanned will write at the same time in Redis + SCANNER_TRADING_AMOUNT = 1.00 # Sols the sniper will trade + SCANNER_WORKING_TIME = 60 # Seconds the scanner will be working + SCANNER_PUMPDONTFUN_INITIAL_FUND = 30 # Sols placed by pump.fun to launch a token #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 18721aa..3b34fcf 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -7,15 +7,6 @@ from typing import List, Dict -# Connect to Redis -client = redis.StrictRedis( - host=appconfig.REDIS_HOST, - port=appconfig.REDIS_PORT, - decode_responses=True -) - -# Use SCAN to find keys with "Key*" prefix - class RedisDB: key_prefix = "token" index_name = "token_idx" @@ -82,13 +73,13 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict # query = Query('@is_traded:[0 0]').paging(0, 1000) # results = self.search_client.search(query) for key in token_keys: - data = client.json().get(key) + data = self.client.json().get(key) if not data: continue # Extract and validate fields is_checked = data.get("is_checked", False) - #is_traded = data.get("is_traded", False) + trader_match = data.get("trader", "unknown") == trader.value if not is_checked and trader_match: @@ -101,7 +92,7 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict "is_checked": is_checked, # By default is_checked is False. "timestamp": data["timestamp"], # Getting timestamp from redis "name": data["name"], # Token name - "ticker": data["ticker"], # Token's Ticker + "symbol": data["symbol"], # Token's symbol "track_traders": track_traders # Tracking other traders activity } tokens.append(token) @@ -158,7 +149,7 @@ def update_token( data.update(new_data) # Add or update the fields in the existing token - client.json().set(key, Path.rootPath(), data) + self.client.json().set(key, Path.rootPath(), data) # Returning original token data with new data token.update(new_data) @@ -170,7 +161,7 @@ def test_create_record(mint_address: str): is_traded = False token_data = { "name": "Some Day", - "ticker": "SMD", + "symbol": "SMD", "amount": 15.500, "is_traded": is_traded, "timestamp": datetime.now().timestamp(), diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 0184e03..968b79e 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -21,6 +21,11 @@ class Trader(Enum): sniper = "sniper" volume_bot = "volume_bot" token_creator = "token_creator" + analytics = "analytics" + scanner = "scanner" + +class Celebrimborg(Enum): + exit = "exit" async def get_solana_balance(public_key: Pubkey) -> float: """ @@ -38,7 +43,7 @@ async def get_solana_balance(public_key: Pubkey) -> float: sol_balance = lamports / 1e9 return sol_balance except Exception as e: - print(f"Error fetching balance: {e}") + print(f"Error fetching balance: {e.error_msg}") return 0.0 async def get_own_token_balance(wallet_pubkey: Pubkey, token_mint_addres: str) -> float: @@ -97,3 +102,8 @@ def decode_pump_fun_token(token: str) -> Pubkey: return Pubkey(decoded_bytes) except Exception as e: raise ValueError(f"Failed to decode token: {e}") + +def initial_buy_calculator(sol_in_bonding_curve: float): + pump_dont_fun_initial_fund = appconfig.SCANNER_PUMPDONTFUN_INITIAL_FUND + sols = round(sol_in_bonding_curve - pump_dont_fun_initial_fund, 3) + return sols diff --git a/bot/main.py b/bot/main.py index b02cad0..55e59d3 100644 --- a/bot/main.py +++ b/bot/main.py @@ -9,12 +9,18 @@ async def main(): trader_type=Trader.sniper ) + scanner = Pump( + executor_name="scanner", + trader_type=Trader.scanner + ) + tasks = [ - pump.subscribe(steps=TradeRoadmap.sniper_1) + #pump.subscribe(steps=TradeRoadmap.sniper_1), + scanner.subscribe(steps=TradeRoadmap.scanner) ] await asyncio.gather(*tasks) - logging.log("Stoping program") + print("Stoping program") if __name__ == "__main__": diff --git a/bot/module/pump.py b/bot/module/pump.py index 78feb6a..98fa1ff 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -21,7 +21,9 @@ get_solana_balance, get_own_token_balance, Trader, - TxType + TxType, + Celebrimborg, + initial_buy_calculator ) from bot.config import appconfig, AppMode from bot.domain.redis_db import RedisDB @@ -49,6 +51,7 @@ class Redis(Enum): selectToken = "selectToken" closeToken = "closeToken" readTraders = "readTraders" + saveToken = "saveToken" class TradeRoadmap: @@ -63,7 +66,7 @@ class TradeRoadmap: "name": "TRADER_SUBSCRIPTION", "subscription": Suscription.subscribeNewToken, "criteria": { - "discard_lower_than_sols": 1.00, + "min_initial_buy": 1.00, } }, {"step": 2, "name": "TRADE_TOKEN", "action": TxType.buy}, @@ -82,6 +85,27 @@ class TradeRoadmap: {"step": 6, "name": "MARK_TOKEN", "redis": Redis.closeToken}, ] + scanner = [ + { + "step": 0, + "name": "SCANNER", + "subscription": Suscription.subscribeNewToken, + "criteria": { + "min_initial_buy": 1.50, # Filter: we'll trade tokens with this initial buying amount at least. + "scanner_activity_time": 60, # How much time the scanner will be working. -1 is always + "trade_amount": 1.00, # Amount to be traded by snipers + "trader": Trader.sniper.value # Who will trade the tokens + }, + "action":{ + "name": "SAVE_TOKEN_IN_REDIS", + "redis": Redis.saveToken, + "criteria": { + "capacity": 1 # How many tokens will be written to redis at the same time + } + } + }, + {"step": 1, "name": "exit", "system_action": Celebrimborg.exit}, + ] sniper_1 = [ {"step": 0, "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken}, {"step": 1, "name": "TRADE_TOKEN_BUY", "action": TxType.buy}, @@ -123,7 +147,7 @@ def __init__( self.traders = [] self.executor_name = executor_name self.trader_type = trader_type - self.min_initial_buy = 60000000 + self.min_initial_buy = appconfig.SCANNER_MIN_TRADING_AMOUNT self.tokens_to_be_traded = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME self.trading_amount = amount self.trading_sell_amount = amount * (1 + target) # TODO: apply this when we have the Bounding Courve @@ -132,6 +156,10 @@ def __init__( self.ssl_context = ssl.create_default_context(cafile=certifi.where()) self.balance = self.get_balance(public_key=self.keypair.pubkey()) + self.scanner_start_time = None + + def start_scanner(self): + self.scanner_start_time = datetime.now() def get_balance(self, public_key): balance = asyncio.run(get_solana_balance(public_key=public_key)) @@ -207,6 +235,11 @@ async def subscribe(self, steps: list): step = steps[step_index] + if "system_action" in step: + if step["system_action"] == Celebrimborg.exit: + print("Exiting Celebrimborg as expected.") + break + # REDIS DB HANDLING if "redis" in step: # Listen to redis change @@ -412,7 +445,7 @@ async def subscribe(self, steps: list): continue if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.new_token_suscription(msg=msg) + move_to_next_step = self.new_token_suscription(msg=msg, step=step, redisdb=redisdb) if suscription.value == Suscription.subscribeTokenTrade.value: # Getting the mint address we'll work with @@ -458,81 +491,77 @@ async def subscribe(self, steps: list): move_to_next_step = False break - # # MAIN WEBSOCKET THREAD - # async for message in websocket: - # msg = json.loads(message) - # move_to_next_step = False - - # # This is the first message we get when we connect - # if "message" in msg: - # print(msg["message"]) - # # If the message is about unsibscribing, then we move on to the next step - # if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): - # move_to_next_step = True - # else: - # continue - - # if suscription.value == Suscription.subscribeNewToken.value: - # move_to_next_step = self.new_token_suscription(msg=msg) - - # if suscription.value == Suscription.subscribeTokenTrade.value: - # # Getting the mint address we'll work with - # mint = msg["mint"] - - # # We'll not pay attention to closed token trades - # if self.tokens[mint]["is_closed"]: - # continue - - # if "trades" not in self.tokens[mint]: - # self.tokens[mint]["trades"] = [] - - # # Doing some analytics like how many continuous buys have happend, etc - # new_msg = trading_analytics( - # msg=msg, - # previous_trades=self.tokens[mint]["trades"], - # amount_traded=self.trading_amount, - # pubkey=self.keypair.pubkey(), - # traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] - # ) - # # Including last message with new metadata into trades list - # self.tokens[mint]["trades"].append(new_msg) - - # move_to_next_step, criteria = self.validate_criteria( - # msg=new_msg, - # criteria=step["criteria"] - # ) - # # Including exit criteria in token for further analytics - # self.tokens[mint]["exit_criteria"] = criteria - - # # Moving to the next step - # if move_to_next_step: - # step_index += 1 - # move_to_next_step = False - # break + # TODO: add a safely exit way of ending the program - # TODO: add a safely exit way of ending the program + def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: + move_to_next_step = False + + if self.scanner_start_time is None: + self.start_scanner() - def new_token_suscription(self, msg: str) -> bool: - if "initialBuy" in msg and int(msg["initialBuy"]) > self.min_initial_buy: - # Listening to a predefined amount of tokens - if len(self.tokens) < self.tokens_to_be_traded: - print( - "New token subscription. Symbol: {}. mint: {}. tarder: {}. Initial buy: {}".format( - msg["symbol"], - msg["mint"], - msg["traderPublicKey"][:5], - int(msg["initialBuy"]) - ) - ) - self.clear_tokens() # TODO: Change this when more than one token will be checked - self.add_update_token(token=msg["mint"]) - # testing - # self.marketMaking(token=msg["mint"]) + scanner_activity_time = appconfig.SCANNER_WORKING_TIME + if "scanner_activity_time" in step["criteria"]: + scanner_activity_time = step["criteria"]["scanner_activity_time"] + + # Check if scanner needs to be torned off. + if (datetime.now() - self.scanner_start_time).total_seconds() >= scanner_activity_time: + move_to_next_step = True + return move_to_next_step + + min_initial_buy = self.min_initial_buy + if "min_initial_buy" in step["criteria"]: + min_initial_buy = step["criteria"]["min_initial_buy"] + + capacity = appconfig.SCANNER_WRITTING_CAPACITY + if "action" in step and "criteria" in step["action"]: + if "capacity" in step["action"]["criteria"]: + capacity = step["action"]["criteria"]["capacity"] + + trading_amount = appconfig.SCANNER_TRADING_AMOUNT + if "trading_amount" in step["criteria"]: + trading_amount = step["criteria"]["trading_amount"] + + trader = Trader.sniper.value # Sniper as default trader. We can scann for Analytics + if "trader" in step["criteria"]: + trader = step["criteria"]["trader"] + + initial_buy_sols = initial_buy_calculator(sol_in_bonding_curve=msg["vSolInBondingCurve"]) + + if initial_buy_sols >= min_initial_buy: + if "action" in step and "redis" in step["action"]: + if step["action"]["redis"] == Redis.saveToken: + # Read if there's any unchecked token based on reading capacity + tokens = redisdb.get_fresh_tokens( + trader=Trader.sniper + ) + + unchecked_tokens = [token for token in tokens if not token["is_checked"]] + # capacity reached: no more tokens will be added to redis + if len(unchecked_tokens) >= capacity: + return move_to_next_step + # Scanner has capacity to ad more tokens to readis + for i in range(capacity - len(unchecked_tokens)): + token_data = { + "amount": trading_amount, + "is_checked": False, + "is_traded": False, + "is_closed": False, + "timestamp": datetime.now().timestamp(), + "trader": trader + } + token_data.update(msg) + print("Saving in redis-> Token '{}', mint {} and initial buy of {}".format( + token_data["name"], + token_data["mint"], + initial_buy_sols + )) + redisdb.set_token(token=msg["mint"], token_data=token_data) + # TODO: relase tokens to snipers with redis recods. At this moment we're listening to one token only - return len(self.tokens) == self.tokens_to_be_traded + return move_to_next_step def token_copytrade_suscription(self, msg: str) -> bool: @@ -590,7 +619,6 @@ def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: return is_valid, exit_criteria - def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = None) -> str: """ This function allows to BUY or SELL tokens. From d8bfc82c6585cef0c56ff6ac3bb65fbb5dfee496 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 2 Dec 2024 10:13:40 +0100 Subject: [PATCH 019/103] Adding first draft of dockerfile. Adding base58 to requirements.txt. Pump: alowing scanner to ron forever and preventind trades on closed_token because of not having enough balance. Config: adding TIME_FORMAT to match pump.fun time format --- bot/Dockerfile | 46 ++++++++++++++++++++++++++++++++++++ bot/bug_and_bypasses.txt | 12 ++++++---- bot/config/__init__.py | 4 +++- bot/domain/redis_db.py | 2 +- bot/main.py | 6 ++--- bot/module/pump.py | 50 +++++++++++++++++++++++++++++----------- bot/requirements.txt | 3 ++- 7 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 bot/Dockerfile diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..c15adf2 --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,46 @@ +# Use an official Python 3.10 image as the base +FROM python:3.10-slim + +# Set the working directory inside the container +WORKDIR /bot + +# Update system packages and install necessary tools +RUN apt-get update && apt-get -y upgrade && apt-get install -y \ + build-essential \ + procps \ + bash \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update && apt-get install -y redis-tools + +# Download and install websocat +RUN apt-get update && apt-get install -y \ + curl \ + && curl -sSL https://github.com/vi/websocat/releases/download/v1.7.1/websocat_amd64-linux -o /usr/local/bin/websocat \ + && chmod +x /usr/local/bin/websocat + +# Install pip and venv +RUN python3 -m pip install --upgrade pip setuptools wheel + +# Copy requirements.txt into the container +COPY requirements.txt . + +# Create a virtual environment and install dependencies +RUN python3 -m venv .venv && \ + /bot/.venv/bin/pip install --no-cache-dir -r requirements.txt + +# Copy all files from the local project to the container +COPY . . + +# Set the virtual environment as the default for all commands +ENV PATH="/bot/.venv/bin:$PATH" + +# Expose the port your application listens on (e.g., 5000) and Redis (6379) +EXPOSE 5000 6379 + +# Set the default command to run the Python script +CMD ["bash"] +#CMD ["python", "bot/main.py"] +#CMD ["python", "-m", "bot.main"] +CMD ["PYTHONPATH=/", "python", "main.py"] diff --git a/bot/bug_and_bypasses.txt b/bot/bug_and_bypasses.txt index 2630b8e..223e00f 100644 --- a/bot/bug_and_bypasses.txt +++ b/bot/bug_and_bypasses.txt @@ -51,8 +51,12 @@ CONFIG GET notify-keyspace-events docker stop redis-stack docker start redis-stack ######################################################### - - - +REDIS JSON.SET token:12345 $ '[{"amount":500,"timestamp":1690228321,"is_traded":0}]' -FT.CREATE token_idx ON JSON PREFIX 1 "token:" SCHEMA $.amount NUMERIC $.timestamp NUMERIC $.is_traded BOOL \ No newline at end of file +FT.CREATE token_idx ON JSON PREFIX 1 "token:" SCHEMA $.amount NUMERIC $.timestamp NUMERIC $.is_traded BOOL + +# RUN LOCALLY +PYTHONPATH=/Users/saulmaldonado/Documents/projects/pumpdotfun-sdk python3 main.py +PYTHONPATH=/ python3 main.py +docker build -t celebrinborg-scanner . +docker run -it --name python-bot-container --network host celebrinborg-scanner /bin/bash diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 08b60da..d67dc29 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -14,6 +14,8 @@ class AuthConfig: ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") APPMODE = os.environ.get("APPMODE", AppMode.dummy.value) + TIME_FORMAT = "%b %-d %-I:%M:%S %p" + # Stable and liquid pairs: 0.1% to 0.5%. # Moderate trading volumes: 0.5% to 1%. # High volatility or limited liquidity: 1% to 3% @@ -54,7 +56,7 @@ class AuthConfig: SCANNER_MIN_TRADING_AMOUNT = 1.00 # Min Sols a token must have as first buy to be considered for trading SCANNER_WRITTING_CAPACITY = 1 # How many tokens to scanned will write at the same time in Redis SCANNER_TRADING_AMOUNT = 1.00 # Sols the sniper will trade - SCANNER_WORKING_TIME = 60 # Seconds the scanner will be working + SCANNER_WORKING_TIME = 600 # Seconds the scanner will be working SCANNER_PUMPDONTFUN_INITIAL_FUND = 30 # Sols placed by pump.fun to launch a token #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 3b34fcf..49a6e4b 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -127,7 +127,7 @@ def update_token( "checked_time": trading_time, "is_checked": True, "is_traded": False, - "is_closed": False, + "is_closed": is_closed, "trades": [] # We'll keep track of every buy/sell during our trade } else: diff --git a/bot/main.py b/bot/main.py index 55e59d3..7de4d78 100644 --- a/bot/main.py +++ b/bot/main.py @@ -15,12 +15,12 @@ async def main(): ) tasks = [ - #pump.subscribe(steps=TradeRoadmap.sniper_1), - scanner.subscribe(steps=TradeRoadmap.scanner) + pump.subscribe(steps=TradeRoadmap.sniper_1), + #scanner.subscribe(steps=TradeRoadmap.scanner) ] await asyncio.gather(*tasks) - print("Stoping program") + print("Stoping {} program".format(Trader.scanner.value)) if __name__ == "__main__": diff --git a/bot/module/pump.py b/bot/module/pump.py index 98fa1ff..b73d3d1 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -92,7 +92,7 @@ class TradeRoadmap: "subscription": Suscription.subscribeNewToken, "criteria": { "min_initial_buy": 1.50, # Filter: we'll trade tokens with this initial buying amount at least. - "scanner_activity_time": 60, # How much time the scanner will be working. -1 is always + "scanner_activity_time": -1, # How much time the scanner will be working. -1 is always "trade_amount": 1.00, # Amount to be traded by snipers "trader": Trader.sniper.value # Who will trade the tokens }, @@ -237,7 +237,7 @@ async def subscribe(self, steps: list): if "system_action" in step: if step["system_action"] == Celebrimborg.exit: - print("Exiting Celebrimborg as expected.") + print("Exiting Celebrimborg as expected after reaching timeout.") break # REDIS DB HANDLING @@ -279,8 +279,10 @@ async def subscribe(self, steps: list): token["name"] )) continue + + enough_balance = self.balance >= amount # Checking wallet's balance before trading - if self.balance >= amount: + if enough_balance: self.trading_amount = amount else: print("{}: Warning: susbscribe -> Not enough balance for Token {} and amount {}. Balance is {}".format( @@ -290,14 +292,19 @@ async def subscribe(self, steps: list): self.balance )) if appconfig.APPMODE in [AppMode.dummy.value, AppMode.simulation.value]: - continue + self.trading_amount = amount else: - break - + # TODO: send an alert message to redis notifying having not enough balance + print("{}: Sending message notifying that there's not enough balance for trading".format( + self.executor_name + )) + # - move to next step and update the token as being checked + # Also closing the token if there's not enough balance for trading token = redisdb.update_token( token=token, - is_checked=False + is_checked=False, + is_closed=not enough_balance ) self.add_update_token(token=token) @@ -326,6 +333,14 @@ async def subscribe(self, steps: list): if "action" in step: if step["action"] == TxType.buy: for mint_address, token_data in self.tokens.items(): + buy_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + url = "https://pump.fun/coin/{}".format(mint_address) + print("Buy {} at {}".format(url, buy_time)) + # We can trade closed tokens. This might happen if there's not enough balabnce + if token_data["is_closed"]: + print(" Can't buy {} as this token is closed".format(url)) + continue + txn = self.trade( txtype=TxType.buy, token=mint_address, @@ -353,11 +368,15 @@ async def subscribe(self, steps: list): # Update token self.add_update_token(token=token_updated) - step_index += 1 - continue + step_index += 1 if step["action"] == TxType.sell: for mint_address, token_data in self.tokens.items(): + sell_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Sell {} at {}".format(url, sell_time)) + if token_data["is_closed"]: + continue + txn = self.trade( txtype=TxType.buy, token=mint_address, @@ -390,8 +409,8 @@ async def subscribe(self, steps: list): # Update token self.add_update_token(token=token_updated) - step_index += 1 - continue + step_index += 1 + if "subscription" in step: suscription = step["subscription"] @@ -505,8 +524,9 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: if "scanner_activity_time" in step["criteria"]: scanner_activity_time = step["criteria"]["scanner_activity_time"] - # Check if scanner needs to be torned off. - if (datetime.now() - self.scanner_start_time).total_seconds() >= scanner_activity_time: + # Check if scanner needs to be torned off. scanner_activity_time == -1 -> runs forever. + if (datetime.now() - self.scanner_start_time).total_seconds() >= scanner_activity_time and \ + scanner_activity_time != -1: move_to_next_step = True return move_to_next_step @@ -552,7 +572,9 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: "trader": trader } token_data.update(msg) - print("Saving in redis-> Token '{}', mint {} and initial buy of {}".format( + save_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Redis {}:-> Token '{}', mint {} and initial buy of {}".format( + save_time, token_data["name"], token_data["mint"], initial_buy_sols diff --git a/bot/requirements.txt b/bot/requirements.txt index 4ff5fa9..46c4701 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -9,6 +9,7 @@ requests==2.32.3 coverage==7.6.7 pytest-cov==6.0.0 certifi==2024.8.30 -redis=4.2.0 +redis==4.2.0 redisearch==2.0.0 nest_asyncio==1.6.0 +base58==2.1.1 From 609dbc3e0f9fc335b52587db66564a7a6f327942 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 2 Dec 2024 14:09:11 +0100 Subject: [PATCH 020/103] Pump: adding keppalive loop above main websocket main thread. Scanner: start step added and logged. Bypass: get token balance has been disabled. Sniper: allow to trade qie all balance if it's lower than expected trading amount. Trading: never trade higher that initial buy form token. --- bot/libs/utils.py | 1 + bot/module/pump.py | 668 +++++++++++++++++++++++++-------------------- 2 files changed, 370 insertions(+), 299 deletions(-) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 968b79e..8aa137a 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -26,6 +26,7 @@ class Trader(Enum): class Celebrimborg(Enum): exit = "exit" + start = "start" async def get_solana_balance(public_key: Pubkey) -> float: """ diff --git a/bot/module/pump.py b/bot/module/pump.py index b73d3d1..420f85c 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -6,6 +6,7 @@ import websockets import ssl import certifi +import websockets.exceptions from bot.libs.criterias import ( trading_analytics, @@ -28,7 +29,7 @@ from bot.config import appconfig, AppMode from bot.domain.redis_db import RedisDB -from datetime import datetime +from datetime import datetime, timedelta from enum import Enum from solders.transaction import VersionedTransaction from solders.keypair import Keypair @@ -88,12 +89,20 @@ class TradeRoadmap: scanner = [ { "step": 0, + "name": "START_SCANNER", + "system_action": Celebrimborg.start, + "criteria": { + "scanner_activity_time": 60, # How much time the scanner will be working. -1 is always + }, + }, + { + "step": 1, "name": "SCANNER", "subscription": Suscription.subscribeNewToken, "criteria": { "min_initial_buy": 1.50, # Filter: we'll trade tokens with this initial buying amount at least. "scanner_activity_time": -1, # How much time the scanner will be working. -1 is always - "trade_amount": 1.00, # Amount to be traded by snipers + "trading_amount": 1.00, # Amount to be traded by snipers "trader": Trader.sniper.value # Who will trade the tokens }, "action":{ @@ -104,11 +113,22 @@ class TradeRoadmap: } } }, - {"step": 1, "name": "exit", "system_action": Celebrimborg.exit}, + {"step": 2, "name": "exit", "system_action": Celebrimborg.exit}, ] sniper_1 = [ - {"step": 0, "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken}, - {"step": 1, "name": "TRADE_TOKEN_BUY", "action": TxType.buy}, + { + "step": 0, + "name": "WAIT_FOR_TOKEN", + "redis": Redis.readToken, + "criteria": { + "use_all_balance": True # If True, all balance will be used if balance < trading amount + } + }, + { + "step": 1, + "name": "TRADE_TOKEN_BUY", + "action": TxType.buy + }, { "step": 2, "name": "TOKEN_SUBSCRIPTION", @@ -157,6 +177,8 @@ def __init__( self.balance = self.get_balance(public_key=self.keypair.pubkey()) self.scanner_start_time = None + self.scanner_activity_time = appconfig.SCANNER_WORKING_TIME + self.stop_app = False def start_scanner(self): self.scanner_start_time = datetime.now() @@ -166,11 +188,14 @@ def get_balance(self, public_key): return balance def get_tkn_balance(self, wallet_pubkey, token_account): - token_balance = asyncio.run(get_own_token_balance( - wallet_pubkey=wallet_pubkey, - token_mint_addres=token_account - ) - ) + # token_balance = asyncio.run(get_own_token_balance( + # wallet_pubkey=wallet_pubkey, + # token_mint_addres=token_account + # ) + # ) + # TODO: get balance from redis + # BYPASS + token_balance = 0 return token_balance def add_account(self, account: str): @@ -218,300 +243,338 @@ async def subscribe(self, steps: list): redisdb = RedisDB() await asyncio.sleep(2) # Wait 2 seconds for the redis connection to start - # SSL context is required in Mac and not on windows - async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: - # Subscribing to token creation events - while True: - # Step index reset if necessary - step_index = 0 if step_index >= len(steps) else step_index - roadmap_name = steps[step_index]["name"] - if step_index == 0: - print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) - - print("Step {}: {}".format( - step_index, - steps[step_index]["name"] - )) + # Keepalive main loop + while not self.stop_app: + try: + # SSL context is required in Mac and not on windows + async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: + # Handle WebSocket communication + while True: + # Step index reset if necessary + step_index = 0 if step_index >= len(steps) else step_index + roadmap_name = steps[step_index]["name"] + if step_index == 0: + print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) + + print("Step {}: {}".format( + step_index, + steps[step_index]["name"] + )) - step = steps[step_index] - - if "system_action" in step: - if step["system_action"] == Celebrimborg.exit: - print("Exiting Celebrimborg as expected after reaching timeout.") - break - - # REDIS DB HANDLING - if "redis" in step: - # Listen to redis change - redisdb.subscribe() - if step["redis"] == Redis.readToken: - # TODO: Solve BUG: redis listener is not working at the second time - for message in redisdb.pubsub.listen(): - if message["type"] == "psubscribe": - print("Subscribed to redis") - - if message["type"] == "pmessage": - # Parse the message - key = ":".join(message["channel"].split(":")[1:]) - # Get token information: Take key and retrieve token from redis - tokens = redisdb.get_fresh_tokens( - trader=Trader.sniper, - mint_address=key - ) - # - Ignore token that are not fresh or for not the current trader type - if not tokens: - continue - # Snipe against many tokens - how_many = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME - if len(tokens) < appconfig.TRADING_TOKENS_AT_THE_SAME_TIME: - how_many = len(tokens) - - for token in tokens[0: how_many]: - mint = token["mint"] - amount = token["amount"] - - # FILTERING BY TOKEN'S CREATION TIME - token_creation_time = datetime.fromtimestamp(token["timestamp"]) - token_age = (datetime.now() - token_creation_time).total_seconds() - if token_age >= appconfig.TRADING_TOKEN_TOO_OLD_SECONDS and \ - appconfig.APPMODE not in [AppMode.dummy.value, AppMode.simulation.value]: - print("Warning: susbscribe -> Token {} is too old for trading.".format( - token["name"] - )) - continue + step = steps[step_index] - enough_balance = self.balance >= amount - # Checking wallet's balance before trading - if enough_balance: - self.trading_amount = amount - else: - print("{}: Warning: susbscribe -> Not enough balance for Token {} and amount {}. Balance is {}".format( - self.executor_name, - mint, - amount, - self.balance - )) - if appconfig.APPMODE in [AppMode.dummy.value, AppMode.simulation.value]: - self.trading_amount = amount - else: - # TODO: send an alert message to redis notifying having not enough balance - print("{}: Sending message notifying that there's not enough balance for trading".format( - self.executor_name + if "system_action" in step: + if step["system_action"] == Celebrimborg.start: + # Applying starting criterias + if "scanner_activity_time" in step["criteria"]: + self.scanner_activity_time = step["criteria"]["scanner_activity_time"] + + start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Start at {} ".format( + start_time + )) + stop_time = datetime.now() + timedelta(seconds=self.scanner_activity_time) + stop_time = stop_time.strftime(appconfig.TIME_FORMAT).lower() + stop_time = "Never" if self.scanner_activity_time == -1 else stop_time + print("Expected stop at {} ".format( + stop_time + )) + step_index += 1 + continue + + if step["system_action"] == Celebrimborg.exit: + self.stop_app = True + print("Exiting Celebrimborg as expected after reaching timeout.") + break + + # REDIS DB HANDLING + if "redis" in step: + # Listen to redis change + redisdb.subscribe() + if step["redis"] == Redis.readToken: + for message in redisdb.pubsub.listen(): + if message["type"] == "psubscribe": + print("Subscribed to redis") + + if message["type"] == "pmessage": + # Parse the message + key = ":".join(message["channel"].split(":")[1:]) + # Get token information: Take key and retrieve token from redis + tokens = redisdb.get_fresh_tokens( + trader=Trader.sniper, + mint_address=key + ) + # - Ignore token that are not fresh or for not the current trader type + if not tokens: + continue + # Snipe against many tokens + how_many = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME + if len(tokens) < appconfig.TRADING_TOKENS_AT_THE_SAME_TIME: + how_many = len(tokens) + + for token in tokens[0: how_many]: + mint = token["mint"] + amount = token["amount"] + + # FILTERING BY TOKEN'S CREATION TIME + token_creation_time = datetime.fromtimestamp(token["timestamp"]) + token_age = (datetime.now() - token_creation_time).total_seconds() + if token_age >= appconfig.TRADING_TOKEN_TOO_OLD_SECONDS and \ + appconfig.APPMODE not in [AppMode.dummy.value, AppMode.simulation.value]: + print("Warning: susbscribe -> Token {} is too old for trading.".format( + token["name"] + )) + continue + + enough_balance = self.balance >= amount + # Checking wallet's balance before trading + if enough_balance: + self.trading_amount = amount + else: + # Checking if we are allowed to use all balance + if "criteria" in step and "use_all_balance" in step["criteria"]: + if step["criteria"]["use_all_balance"]: + self.trading_amount = self.balance + enough_balance = True + + print("{}: Warning: susbscribe -> Not enough balance for Token {} and amount {}. Balance is {}".format( + self.executor_name, + mint, + amount, + self.balance + )) + if appconfig.APPMODE in [AppMode.dummy.value, AppMode.simulation.value]: + self.trading_amount = amount + else: + # TODO: send an alert message to redis notifying having not enough balance + print("{}: Sending message notifying that there's not enough balance for trading".format( + self.executor_name + )) + + # - move to next step and update the token as being checked + # Also closing the token if there's not enough balance for trading + token = redisdb.update_token( + token=token, + is_checked=False, + is_closed=not enough_balance + ) + self.add_update_token(token=token) + + print("Token {}:{} assigned to {} to trade {}Sols".format( + token["name"], + token["mint"], + self.executor_name, + self.trading_amount )) + + tokens_to_trade = any(token for token in tokens if token["is_checked"]) + + # Safely unsubscribing from current channel listening + if tokens_to_trade: + redisdb.unsubscribe() + break - # - move to next step and update the token as being checked - # Also closing the token if there's not enough balance for trading - token = redisdb.update_token( - token=token, - is_checked=False, - is_closed=not enough_balance + step_index += 1 + continue + + if step["redis"] == Redis.closeToken: + self.remove_token(token=self.tokens[mint]) + step_index += 1 + continue + + if "action" in step: + if step["action"] == TxType.buy: + for mint_address, token_data in self.tokens.items(): + buy_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + url = "https://pump.fun/coin/{}".format(mint_address) + print("Buy at {} on {}".format(buy_time, url)) + # We can trade closed tokens. This might happen if there's not enough balabnce + if token_data["is_closed"]: + print(" Can't buy {} as this token is closed".format(url)) + continue + + txn = self.trade( + txtype=TxType.buy, + token=mint_address, + keypair=self.keypair, + amount=self.trading_amount + ) + # Get the token balance in wallet + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=mint_address ) - self.add_update_token(token=token) - print("Token {}:{} assigned to {} to trade {}Sols".format( - token["name"], - token["mint"], - self.executor_name, - self.trading_amount - )) - - tokens_to_trade = any(token for token in tokens if token["is_checked"]) + self.tokens[mint_address]["token_balance"] = token_balance + + # Update token record in redis + token_updated = redisdb.update_token( + token=token_data, + txn=txn, + action=TxType.buy, + amount=self.trading_amount, + trader=self.trader_type, + balance=self.balance, + token_balance=token_balance + ) + # Update token + self.add_update_token(token=token_updated) - # Safely unsubscribing from current channel listening - if tokens_to_trade: - redisdb.unsubscribe() - break + step_index += 1 - step_index += 1 - continue + if step["action"] == TxType.sell: + for mint_address, token_data in self.tokens.items(): + sell_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Sell {} at {}".format(url, sell_time)) + if token_data["is_closed"]: + continue - if step["redis"] == Redis.closeToken: - self.remove_token(token=self.tokens[mint]) - step_index += 1 - continue - - if "action" in step: - if step["action"] == TxType.buy: - for mint_address, token_data in self.tokens.items(): - buy_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() - url = "https://pump.fun/coin/{}".format(mint_address) - print("Buy {} at {}".format(url, buy_time)) - # We can trade closed tokens. This might happen if there's not enough balabnce - if token_data["is_closed"]: - print(" Can't buy {} as this token is closed".format(url)) - continue - - txn = self.trade( - txtype=TxType.buy, - token=mint_address, - keypair=self.keypair, - amount=self.trading_amount - ) - # Get the token balance in wallet - token_balance = self.get_tkn_balance( - wallet_pubkey=self.keypair.pubkey(), - token_account=mint_address - ) - - self.tokens[mint_address]["token_balance"] = token_balance - - # Update token record in redis - token_updated = redisdb.update_token( - token=token_data, - txn=txn, - action=TxType.buy, - amount=self.trading_amount, - trader=self.trader_type, - balance=self.balance, - token_balance=token_balance - ) - # Update token - self.add_update_token(token=token_updated) - - step_index += 1 - - if step["action"] == TxType.sell: - for mint_address, token_data in self.tokens.items(): - sell_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() - print("Sell {} at {}".format(url, sell_time)) - if token_data["is_closed"]: - continue + txn = self.trade( + txtype=TxType.buy, + token=mint_address, + keypair=self.keypair, + amount=None # Amount will be handled buy trade function + ) + # Update wallet balance after selling + self.get_balance(public_key=self.keypair.pubkey()) + + # Get the token balance in wallet + # Note: although we're selling 100% of tokens we might sell a % of tokens + # in the future and that's way we get the token balance when selling + token_balance = self.get_tkn_balance( + wallet_pubkey=self.keypair.pubkey(), + token_account=mint_address + ) + # Update token record in redis + token_updated = redisdb.update_token( + token=token_data, + txn=txn, + action=TxType.sell, + amount=self.trading_amount, + trader=self.trader_type, + is_closed=True, + balance=self.balance, + token_balance=token_balance, + trades=self.tokens[mint]["trades"] + ) - txn = self.trade( - txtype=TxType.buy, - token=mint_address, - keypair=self.keypair, - amount=None # Amount will be handled buy trade function - ) - # Update wallet balance after selling - self.get_balance(public_key=self.keypair.pubkey()) - - # Get the token balance in wallet - # Note: although we're selling 100% of tokens we might sell a % of tokens - # in the future and that's way we get the token balance when selling - token_balance = self.get_tkn_balance( - wallet_pubkey=self.keypair.pubkey(), - token_account=mint_address - ) - # Update token record in redis - token_updated = redisdb.update_token( - token=token_data, - txn=txn, - action=TxType.sell, - amount=self.trading_amount, - trader=self.trader_type, - is_closed=True, - balance=self.balance, - token_balance=token_balance, - trades=self.tokens[mint]["trades"] - ) - - # Update token - self.add_update_token(token=token_updated) - - step_index += 1 - - - if "subscription" in step: - suscription = step["subscription"] - - payload = { - "method": suscription.value, - } - - websocket_timeout = appconfig.TRADING_MARKETING_INACTIVITY_TIMEOUT - if "criteria" in step: - if "market_inactivity" in step["criteria"]: - websocket_timeout = step["criteria"]["market_inactivity"] - - if suscription.value == Suscription.subscribeAccountTrade.value: - payload["keys"] = self.accounts - - if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_checked"] and self.tokens[mint]["is_traded"]] - - if suscription.value == Suscription.unsubscribeTokenTrade.value: - payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] - - await websocket.send(json.dumps(payload)) - - ##################### - # ADD HERE THE BUYING TRADEs in separate threads and fix the step so Buy and Sell are inside subscription - # NOTE: test buy and sell in real before having them in threads - ##################### - for mint, token_data in self.tokens.items(): - if not self.tokens[mint]["is_traded"] and \ - self.tokens[mint]["is_checked"] and \ - not self.tokens[mint]["is_closed"]: - pass - - while True: - try: - message = await asyncio.wait_for( - websocket.recv(), - timeout=websocket_timeout - ) - msg = json.loads(message) - move_to_next_step = False - - # This is the first message we get when we connect - if "message" in msg: - print(msg["message"]) - # If the message is about unsibscribing, then we move on to the next step - if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): - move_to_next_step = True - else: - continue - - if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.new_token_suscription(msg=msg, step=step, redisdb=redisdb) + # Update token + self.add_update_token(token=token_updated) - if suscription.value == Suscription.subscribeTokenTrade.value: - # Getting the mint address we'll work with - mint = msg["mint"] - - # We'll not pay attention to closed token trades - if self.tokens[mint]["is_closed"]: - continue - - if "trades" not in self.tokens[mint]: - self.tokens[mint]["trades"] = [] - - # Doing some analytics like how many continuous buys have happend, etc - new_msg = trading_analytics( - msg=msg, - previous_trades=self.tokens[mint]["trades"], - amount_traded=self.trading_amount, - pubkey=self.keypair.pubkey(), - traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] - ) - # Including last message with new metadata into trades list - self.tokens[mint]["trades"].append(new_msg) - - move_to_next_step, criteria = self.validate_criteria( - msg=new_msg, - criteria=step["criteria"] - ) - # Including exit criteria in token for further analytics - self.tokens[mint]["exit_criteria"] = criteria - - # Moving to the next step - if move_to_next_step: step_index += 1 - move_to_next_step = False - break - except asyncio.TimeoutError: - print("No trades detected during {} seconds. Moving to next step.".format( - websocket_timeout - )) - self.tokens[mint]["exit_criteria"] = "market_inactivity" - step_index += 1 - move_to_next_step = False - break - # TODO: add a safely exit way of ending the program + if "subscription" in step: + suscription = step["subscription"] + + payload = { + "method": suscription.value, + } + websocket_timeout = appconfig.TRADING_MARKETING_INACTIVITY_TIMEOUT + if "criteria" in step: + if "market_inactivity" in step["criteria"]: + websocket_timeout = step["criteria"]["market_inactivity"] + + if suscription.value == Suscription.subscribeAccountTrade.value: + payload["keys"] = self.accounts + + if suscription.value == Suscription.subscribeTokenTrade.value: + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_checked"] and self.tokens[mint]["is_traded"]] + + if suscription.value == Suscription.unsubscribeTokenTrade.value: + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] + + await websocket.send(json.dumps(payload)) + + ##################### + # ADD HERE THE BUYING TRADEs in separate threads and fix the step so Buy and Sell are inside subscription + # NOTE: test buy and sell in real before having them in threads + ##################### + for mint, token_data in self.tokens.items(): + if not self.tokens[mint]["is_traded"] and \ + self.tokens[mint]["is_checked"] and \ + not self.tokens[mint]["is_closed"]: + pass + + while True: + try: + message = await asyncio.wait_for( + websocket.recv(), + timeout=websocket_timeout + ) + msg = json.loads(message) + move_to_next_step = False + + # This is the first message we get when we connect + if "message" in msg: + print(msg["message"]) + # If the message is about unsibscribing, then we move on to the next step + if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): + move_to_next_step = True + else: + continue + + if suscription.value == Suscription.subscribeNewToken.value: + move_to_next_step = self.new_token_suscription(msg=msg, step=step, redisdb=redisdb) + + if suscription.value == Suscription.subscribeTokenTrade.value: + # Getting the mint address we'll work with + mint = msg["mint"] + + # We'll not pay attention to closed token trades + if self.tokens[mint]["is_closed"]: + continue + + if "trades" not in self.tokens[mint]: + self.tokens[mint]["trades"] = [] + + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( + msg=msg, + previous_trades=self.tokens[mint]["trades"], + amount_traded=self.trading_amount, + pubkey=self.keypair.pubkey(), + traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] + ) + # Including last message with new metadata into trades list + self.tokens[mint]["trades"].append(new_msg) + + move_to_next_step, criteria = self.validate_criteria( + msg=new_msg, + criteria=step["criteria"] + ) + # Including exit criteria in token for further analytics + self.tokens[mint]["exit_criteria"] = criteria + + # Moving to the next step + if move_to_next_step: + step_index += 1 + move_to_next_step = False + break + + except asyncio.TimeoutError: + print("No trades detected during {} seconds. Moving to next step.".format( + websocket_timeout + )) + self.tokens[mint]["exit_criteria"] = "market_inactivity" + step_index += 1 + move_to_next_step = False + break + except websockets.exceptions.ConnectionClosedError: + print("Connection lost, reconnecting...") + + # TODO: add a safely exit way of ending the program + + + + except websockets.exceptions.ConnectionClosedError: + print("Connection lost, reconnecting...") + await asyncio.sleep(5) # Wait before reconnecting + except Exception as e: + print(f"Unexpected error: {e}. Exiting program abruptly.") + break # Exit on non-recoverable errors + def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: @@ -520,13 +583,9 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: if self.scanner_start_time is None: self.start_scanner() - scanner_activity_time = appconfig.SCANNER_WORKING_TIME - if "scanner_activity_time" in step["criteria"]: - scanner_activity_time = step["criteria"]["scanner_activity_time"] - # Check if scanner needs to be torned off. scanner_activity_time == -1 -> runs forever. - if (datetime.now() - self.scanner_start_time).total_seconds() >= scanner_activity_time and \ - scanner_activity_time != -1: + if (datetime.now() - self.scanner_start_time).total_seconds() >= self.scanner_activity_time and \ + self.scanner_activity_time != -1: move_to_next_step = True return move_to_next_step @@ -555,21 +614,32 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: # Read if there's any unchecked token based on reading capacity tokens = redisdb.get_fresh_tokens( trader=Trader.sniper - ) + ) unchecked_tokens = [token for token in tokens if not token["is_checked"]] # capacity reached: no more tokens will be added to redis if len(unchecked_tokens) >= capacity: return move_to_next_step - # Scanner has capacity to ad more tokens to readis - for i in range(capacity - len(unchecked_tokens)): + # Scanner has capacity to add more tokens to readis + for _ in range(capacity - len(unchecked_tokens)): + + # Rule of thumb: never buy more than the token's initial buy + if trading_amount > initial_buy_sols: + print("INFO: redis -> Amount {} was reduced to {}. Token {} initial buy is lower than expected".format( + trading_amount, + initial_buy_sols, + "{}: {}".format(msg["name"], msg["mint"]) + )) + trading_amount = initial_buy_sols + token_data = { "amount": trading_amount, "is_checked": False, "is_traded": False, "is_closed": False, "timestamp": datetime.now().timestamp(), - "trader": trader + "trader": trader, + "initial_buy_sols": initial_buy_sols } token_data.update(msg) save_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() From b4328ad72b5aafa510904a427d8c28f3283e3470 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 3 Dec 2024 01:27:24 +0100 Subject: [PATCH 021/103] Bug solved related to how trades are stored in token's lists in memory. Adding exit step to buy and sell steps --- bot/domain/redis_db.py | 2 +- bot/libs/criterias.py | 28 ++++++++-------- bot/main.py | 9 ++++- bot/module/pump.py | 76 +++++++++++++++++++++++++++++------------- 4 files changed, 77 insertions(+), 38 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 49a6e4b..2020db6 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -64,7 +64,7 @@ def get_token_keys(self, pattern="token:*"): def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict]: tokens = [] token_keys = [] - # Search for all tokens where is_traded = False + # Search for all tokens where is_checked = False if mint_address: token_keys = [mint_address] else: diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 212cea6..670a5f2 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -72,7 +72,7 @@ def trading_analytics( ] else: - last_msg = previous_trades[-1] + last_msg = previous_trades.copy()[-1] last_msg_timestamp = datetime.fromtimestamp(last_msg["timestamp"]) first_trade_timestamp = datetime.fromtimestamp(previous_trades[0]["timestamp"]) @@ -87,7 +87,7 @@ def trading_analytics( # We always keep track of the consecutive buys new_msg["max_consecutive_buys"] = last_msg["max_consecutive_buys"] - if msg["txType"].lower() == TxType.buy.value: + if new_msg["txType"].lower() == TxType.buy.value: new_msg["consecutive_sells"] = 0 new_msg["seconds_between_sells"] = 0 @@ -121,7 +121,7 @@ def trading_analytics( } ) - if msg["txType"].lower() == TxType.sell.value: + if new_msg["txType"].lower() == TxType.sell.value: new_msg["consecutive_buys"] = 0 new_msg["seconds_between_buys"] = 0 @@ -164,7 +164,7 @@ def trading_analytics( return new_msg -def max_consecutive_buys(buys: int, msg: dict) -> bool: +def max_consecutive_buys(buys: int, msg: dict, amount_traded: float = None) -> bool: """ Checks if the maximum consecutive buys has been reached. @@ -178,7 +178,7 @@ def max_consecutive_buys(buys: int, msg: dict) -> bool: return msg["consecutive_buys"] >= buys -def max_consecutive_sells(sells: int, msg: dict) -> bool: +def max_consecutive_sells(sells: int, msg: dict, amount_traded: float = None) -> bool: """ Checks if the maximum consecutive sells condition is met. @@ -192,7 +192,7 @@ def max_consecutive_sells(sells: int, msg: dict) -> bool: return msg["consecutive_sells"] >= sells -def max_seconds_between_buys(seconds: int, msg: dict) -> bool: +def max_seconds_between_buys(seconds: float, msg: dict, amount_traded: float = None) -> bool: """ Checks if the maximum allowed seconds between buys has been reached. @@ -206,7 +206,7 @@ def max_seconds_between_buys(seconds: int, msg: dict) -> bool: return msg["seconds_between_buys"] >= seconds -def trader_has_sold(msg: dict) -> bool: +def trader_has_sold(expected: bool, msg: dict, amount_traded: float = None) -> bool: """ Checks if the trader or developer has sold their tokens. @@ -217,10 +217,10 @@ def trader_has_sold(msg: dict) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return msg["trader_has_sold"] + return expected == msg["trader_has_sold"] -def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict) -> bool: +def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amount_traded: float) -> bool: """ Checks if the maximum percentage of SOLs in the token after buying condition is met. @@ -231,12 +231,14 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict) -> Returns: bool: True if the condition is met, otherwise False. """ - pass + sols = msg["max_consecutive_buys"][-1]["sols"] + max_sols = amount_traded * percentage / 100 + return sols <= max_sols -def market_inactivity(seconds: int, msg: dict = None) -> bool: +def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: """ - Dummy fuction as it retrieves seconds received + Dummy fuction as it retrieves False Args: seconds (int): The maximum number of seconds of inactivity allowed. @@ -245,7 +247,7 @@ def market_inactivity(seconds: int, msg: dict = None) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return seconds + return False def test(): diff --git a/bot/main.py b/bot/main.py index 7de4d78..c2e2a74 100644 --- a/bot/main.py +++ b/bot/main.py @@ -1,7 +1,9 @@ import asyncio import logging +from datetime import datetime, timedelta from module.pump import Pump, TradeRoadmap from bot.libs.utils import Trader +from config import appconfig async def main(): pump = Pump( @@ -18,9 +20,14 @@ async def main(): pump.subscribe(steps=TradeRoadmap.sniper_1), #scanner.subscribe(steps=TradeRoadmap.scanner) ] + start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + # TODO: implement trader execution from input parameters + print("Starting {} program at {}".format(Trader.sniper.value, start_time)) + await asyncio.gather(*tasks) - print("Stoping {} program".format(Trader.scanner.value)) + stop_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Stoping {} program at {}".format(Trader.scanner.value, stop_time)) if __name__ == "__main__": diff --git a/bot/module/pump.py b/bot/module/pump.py index 420f85c..b4dd00b 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -92,7 +92,7 @@ class TradeRoadmap: "name": "START_SCANNER", "system_action": Celebrimborg.start, "criteria": { - "scanner_activity_time": 60, # How much time the scanner will be working. -1 is always + "scanner_activity_time": 10, # How much time the scanner will be working. -1 is always }, }, { @@ -101,8 +101,7 @@ class TradeRoadmap: "subscription": Suscription.subscribeNewToken, "criteria": { "min_initial_buy": 1.50, # Filter: we'll trade tokens with this initial buying amount at least. - "scanner_activity_time": -1, # How much time the scanner will be working. -1 is always - "trading_amount": 1.00, # Amount to be traded by snipers + "trading_amount": 0.5, # Amount to be traded by snipers "trader": Trader.sniper.value # Who will trade the tokens }, "action":{ @@ -127,7 +126,8 @@ class TradeRoadmap: { "step": 1, "name": "TRADE_TOKEN_BUY", - "action": TxType.buy + "action": TxType.buy, + "on_error_go_to_step": 4 }, { "step": 2, @@ -136,13 +136,18 @@ class TradeRoadmap: "criteria": { "max_consecutive_buys": 2, "max_consecutive_sells": 1, - "max_seconds_between_buys": 3, + "max_seconds_between_buys": 2.5, "trader_has_sold": True, "max_sols_in_token_after_buying_in_percentage": 100, "market_inactivity": 3 } }, - {"step": 3, "name": "TRADE_TOKEN_SELL", "action": TxType.sell}, + { + "step": 3, + "name": "TRADE_TOKEN_SELL", + "action": TxType.sell, + "on_error_go_to_step": 4 + }, {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, {"step": 5, "name": "CLOSE_TOKEN", "redis": Redis.closeToken}, ] @@ -226,7 +231,7 @@ def remove_token(self, token: Dict): del self.tokens[token["mint"]] def clear_tokens(self): - self.tokens = [] + self.tokens = {} # Traders to follow for starting and stoping pumps def add_trader(self, trader: str): @@ -254,7 +259,7 @@ async def subscribe(self, steps: list): step_index = 0 if step_index >= len(steps) else step_index roadmap_name = steps[step_index]["name"] if step_index == 0: - print("subscribe -> restarting roadmap '{}'".format(roadmap_name)) + print("subscribe -> restarting roadmap '{}'\n".format(roadmap_name)) print("Step {}: {}".format( step_index, @@ -378,7 +383,8 @@ async def subscribe(self, steps: list): continue if step["redis"] == Redis.closeToken: - self.remove_token(token=self.tokens[mint]) + for mint in list(self.tokens.keys()): + self.remove_token(token=self.tokens[mint]) step_index += 1 continue @@ -387,7 +393,7 @@ async def subscribe(self, steps: list): for mint_address, token_data in self.tokens.items(): buy_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() url = "https://pump.fun/coin/{}".format(mint_address) - print("Buy at {} on {}".format(buy_time, url)) + print("Buy {} at {}".format(url, buy_time)) # We can trade closed tokens. This might happen if there's not enough balabnce if token_data["is_closed"]: print(" Can't buy {} as this token is closed".format(url)) @@ -399,6 +405,13 @@ async def subscribe(self, steps: list): keypair=self.keypair, amount=self.trading_amount ) + + if txn is None: + if "on_error_go_to_step" in step: + # Need to point to previous step + step_index = step["on_error_go_to_step"] - 1 + break + # Get the token balance in wallet token_balance = self.get_tkn_balance( wallet_pubkey=self.keypair.pubkey(), @@ -430,7 +443,7 @@ async def subscribe(self, steps: list): continue txn = self.trade( - txtype=TxType.buy, + txtype=TxType.sell, token=mint_address, keypair=self.keypair, amount=None # Amount will be handled buy trade function @@ -455,7 +468,7 @@ async def subscribe(self, steps: list): is_closed=True, balance=self.balance, token_balance=token_balance, - trades=self.tokens[mint]["trades"] + trades=self.tokens[mint_address]["trades"] ) # Update token @@ -520,28 +533,31 @@ async def subscribe(self, steps: list): if suscription.value == Suscription.subscribeTokenTrade.value: # Getting the mint address we'll work with + # CHeck mint value for each tokeb being processed mint = msg["mint"] - # We'll not pay attention to closed token trades if self.tokens[mint]["is_closed"]: continue - if "trades" not in self.tokens[mint]: - self.tokens[mint]["trades"] = [] - # Doing some analytics like how many continuous buys have happend, etc new_msg = trading_analytics( msg=msg, - previous_trades=self.tokens[mint]["trades"], + previous_trades=token["trades"], amount_traded=self.trading_amount, pubkey=self.keypair.pubkey(), traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] ) # Including last message with new metadata into trades list - self.tokens[mint]["trades"].append(new_msg) - + token = self.tokens[mint].copy() # Key point: need to copy the token + if not token["trades"]: + token["trades"] = [new_msg] + else: + token["trades"].append(new_msg) + self.add_update_token(token=token) + move_to_next_step, criteria = self.validate_criteria( msg=new_msg, + amount_traded=self.trading_amount, criteria=step["criteria"] ) # Including exit criteria in token for further analytics @@ -551,6 +567,7 @@ async def subscribe(self, steps: list): if move_to_next_step: step_index += 1 move_to_next_step = False + print("Exiting subscription") break except asyncio.TimeoutError: @@ -561,8 +578,6 @@ async def subscribe(self, steps: list): step_index += 1 move_to_next_step = False break - except websockets.exceptions.ConnectionClosedError: - print("Connection lost, reconnecting...") # TODO: add a safely exit way of ending the program @@ -683,7 +698,7 @@ def token_copytrade_suscription(self, msg: str) -> bool: return False - def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: + def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> bool: """ This function takes the incomming trading message from Pump.fun previouly trated by trading_analytics function and apply all criteria functions for the @@ -699,7 +714,7 @@ def validate_criteria(self, msg: Dict, criteria: Dict) -> bool: function = getattr(criteria_functions, function_name, None) if callable(function): # Call the function with the parameter and msg - is_valid = function(parameter, msg) + is_valid = function(parameter, msg, amount_traded) print(f"validate_criteria-> Function {function_name} returned: {is_valid}") else: print(f"validate_criteria-> Error: {function_name} not found.") @@ -765,6 +780,15 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No data=data ) if response.status_code != 200: + if txtype.value == TxType.buy.value: + print("Trade->{} Failed get quote. Exiting trade function. Error: {} returned a status code {}. Response: {}".format( + txtype.value, + appconfig.PUMPFUN_TRANSACTION_URL, + response.status_code, + response + )) + break + retries += 1 print("Trade->{} Error: {} returned a status code {}. Retrying again {} times. Response: {}".format( @@ -816,6 +840,12 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No except Exception as e: retries += 1 + if txtype.value == TxType.buy.value: + print("Trade->{} Transaction failed. Exiting trade function. Error: {}".format( + txtype.value, + response.json()["error"]["message"] + )) + break print("Trade->{} Transaction failed. Retrying again {} times: {}".format( txtype.value, retries, From 6111a5788972106e0aae7bd2705548f798f19df7 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 3 Dec 2024 19:48:18 +0100 Subject: [PATCH 022/103] trading_analytics minor refactoring to deal with own trades processing. Also including copy library to do deepcopy of dicts --- bot/libs/criterias.py | 17 +++++-- bot/tests/libs/test_criterias.py | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 670a5f2..98f67e1 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -1,3 +1,4 @@ +import copy from datetime import datetime from typing import Dict, List from bot.config import appconfig @@ -29,7 +30,7 @@ def trading_analytics( :return: msg with more attributes """ - new_msg = msg.copy() + new_msg = copy.deepcopy(msg) new_msg["timestamp"] = datetime.now().timestamp() # Including timestamp in incomming message # Checking if the trade position is relevant enough to be considering for criteria @@ -39,10 +40,12 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 + is_this_my_trade = new_msg["traderPublicKey"] == str(pubkey) + if not previous_trades: aprox = 0 # Math calculation for Solana in Bonding Courve just before we bought - if new_msg["traderPublicKey"] == str(pubkey): + if is_this_my_trade: # We're the first recorded trade: we discount the amount to the current bonding curve new_msg["vSolInBondingCurve_Base"] = new_msg["vSolInBondingCurve"] - amount_traded @@ -72,15 +75,21 @@ def trading_analytics( ] else: - last_msg = previous_trades.copy()[-1] + last_msg = copy.deepcopy(previous_trades[-1]) last_msg_timestamp = datetime.fromtimestamp(last_msg["timestamp"]) first_trade_timestamp = datetime.fromtimestamp(previous_trades[0]["timestamp"]) # Current solanas traded sols = new_msg["vSolInBondingCurve"] - last_msg["vSolInBondingCurve"] + sols = sols if not is_this_my_trade else amount_traded + # Checking if this trade is relavant or not. Keeping counter for consecutives buys/sells - new_msg["is_relevant_trade"] = sols >= appconfig.TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT + is_relevant_trade = sols >= appconfig.TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT + + # Key: we're not considering our owns tradeS TO BE RELAVENT for criteria decision + new_msg["is_relevant_trade"] = False if is_this_my_trade else is_relevant_trade + # We just count non relevant trades with consecutives buys/sells new_msg["is_non_relevant_trade_count"] = 0 if new_msg["is_relevant_trade"] else 1 diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index a466898..479a946 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -494,3 +494,81 @@ def test_trader_has_sold( ) has_sold = trader_has_sold(msg=new_msg) assert has_sold == trader_has_sold_result, description + +@pytest.mark.parametrize( + """ + description, + msg, + previous_trades, + amount, + traders, + """, + [ + ( + "Receiving our own trade: no relevant trades tolerance reached", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4RBnqw6CB9ANn9e16WWamqZNBZDHXwuFVWSjosk43ptC", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":(datetime.now() - timedelta(seconds=2)).timestamp(), + "is_relevant_trade":False, + "consecutive_buys":2, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":2, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ] + } + ], + 0.5, + [], + ), + ] +) +def test_detect_own_trade( + get_pubkey, + description, + msg, + previous_trades, + amount, + traders +): + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades.copy(), + amount_traded=amount, + pubkey=get_pubkey, + traders=traders + ) + + last_max_consecutive_buys = previous_trades[0]["max_consecutive_buys"][0]["quantity"] + max_consecutive_buys = new_msg["max_consecutive_buys"][0]["quantity"] + assert max_consecutive_buys == last_max_consecutive_buys + 1, description From b497ce6a58d26e5707f04896fe97d603b9709aec Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 4 Dec 2024 11:10:13 +0100 Subject: [PATCH 023/103] adding Ump.log_trade_token_timestamp and adjust timestamp printing after receiving txn signature --- bot/module/pump.py | 40 +++++++++-- bot/tests/module/fixture_pump.py | 10 ++- bot/tests/module/test_pump.py | 110 ++++++++++++++++++++++++++++--- 3 files changed, 145 insertions(+), 15 deletions(-) diff --git a/bot/module/pump.py b/bot/module/pump.py index b4dd00b..4d18aff 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -233,6 +233,12 @@ def remove_token(self, token: Dict): def clear_tokens(self): self.tokens = {} + def log_trade_token_timestamp(self, mint: str, txtype: TxType, trade_timestamp: int): + token = self.tokens[mint] + key = "{}_timestamp".format(txtype.value) + token[key] = trade_timestamp + self.add_update_token(token=token) + # Traders to follow for starting and stoping pumps def add_trader(self, trader: str): self.traders.append(trader) @@ -391,13 +397,13 @@ async def subscribe(self, steps: list): if "action" in step: if step["action"] == TxType.buy: for mint_address, token_data in self.tokens.items(): - buy_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() - url = "https://pump.fun/coin/{}".format(mint_address) - print("Buy {} at {}".format(url, buy_time)) + # We can trade closed tokens. This might happen if there's not enough balabnce if token_data["is_closed"]: print(" Can't buy {} as this token is closed".format(url)) continue + + print("Attempting to buy token {}".format(mint_address)) txn = self.trade( txtype=TxType.buy, @@ -411,6 +417,19 @@ async def subscribe(self, steps: list): # Need to point to previous step step_index = step["on_error_go_to_step"] - 1 break + + buy_time = datetime.now() + url = "https://pump.fun/coin/{}".format(mint_address) + print("Buy {} at {}".format( + url, + buy_time.strftime(appconfig.TIME_FORMAT).lower()) + ) + + self.log_trade_token_timestamp( + token=mint_address, + txtype=TxType.buy, + timestamp=buy_time.timestamp() + ) # Get the token balance in wallet token_balance = self.get_tkn_balance( @@ -437,8 +456,6 @@ async def subscribe(self, steps: list): if step["action"] == TxType.sell: for mint_address, token_data in self.tokens.items(): - sell_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() - print("Sell {} at {}".format(url, sell_time)) if token_data["is_closed"]: continue @@ -448,6 +465,19 @@ async def subscribe(self, steps: list): keypair=self.keypair, amount=None # Amount will be handled buy trade function ) + + sell_time = datetime.now() + print("Sell {} at {}".format( + url, + sell_time.strftime(appconfig.TIME_FORMAT).lower() + ) + ) + + self.log_trade_token_timestamp( + token=mint_address, + txtype=TxType.sell, + timestamp=buy_time.timestamp() + ) # Update wallet balance after selling self.get_balance(public_key=self.keypair.pubkey()) diff --git a/bot/tests/module/fixture_pump.py b/bot/tests/module/fixture_pump.py index 3517a28..cfb4475 100644 --- a/bot/tests/module/fixture_pump.py +++ b/bot/tests/module/fixture_pump.py @@ -1,4 +1,5 @@ import pytest +from solders.keypair import Keypair @pytest.fixture @@ -8,14 +9,19 @@ def get_account(): @pytest.fixture def get_token(): - yield "Some_token_pump" + yield {"mint": "Some_token_pump"} @pytest.fixture def get_token2(): - yield "Some_token2_pump" + yield {"mint": "Some_token2_pump"} @pytest.fixture def get_trader(): yield "Some_trader_address" + + +@pytest.fixture +def get_pubkey(): + yield "1ajMNhqWCeDVJtddbNhD1ss1N1CFZ11nV1Mg1StvBHdb" diff --git a/bot/tests/module/test_pump.py b/bot/tests/module/test_pump.py index ece169b..ebc1d0b 100644 --- a/bot/tests/module/test_pump.py +++ b/bot/tests/module/test_pump.py @@ -1,15 +1,25 @@ -from bot.module.pump import Pump +from bot.module.pump import Pump, TxType from bot.config import appconfig +from bot.libs.utils import Trader, get_solana_balance from bot.tests.module.fixture_pump import * +from datetime import datetime +from typing import Any +from unittest.mock import patch, Mock +from solders.message import Message + +@patch.object(Pump, "get_balance") def test_attribute_handling( + get_balance_mocked, get_account, get_token, get_token2, get_trader ): - pump = Pump() + get_balance_mocked.return_value = 5.00 + + pump = Pump(executor_name="test", trader_type=Trader.sniper) pump.add_account(account=get_account) assert pump.accounts == [get_account] @@ -17,15 +27,15 @@ def test_attribute_handling( pump.remove_account(account=get_account) assert pump.accounts == [] - pump.add_token(token=get_token) - assert pump.tokens == [get_token] + pump.add_update_token(token=get_token) + assert get_token == pump.tokens[get_token["mint"]] pump.remove_token(token=get_token) - assert pump.tokens == [] + assert pump.tokens == {} - pump.add_token(token=get_token2) + pump.add_update_token(token=get_token2) pump.clear_tokens() - assert pump.tokens == [] + assert pump.tokens == {} pump.add_trader(trader=get_trader) assert pump.traders == [get_trader] @@ -33,4 +43,88 @@ def test_attribute_handling( pump.remove_trader(trader=get_trader) assert pump.traders == [] - \ No newline at end of file + +@pytest.mark.parametrize( + "txtype, token, amount, expected_value, retries, status_code, content, description", [ + ( + TxType.buy, + "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump", + 1.0, + None, + 1, + 404, + "fake content", + "PUMPFUN_TRANSACTION_URL returns a 404 status code error" + ), + ( + TxType.buy, + "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump", + 1.0, + None, + 1, + 200, + "fake content", + "RPC_URL returns a 500 status code error" + ) + ] +) +@patch.object(Keypair, "pubkey") +@patch("requests.post") +@patch("solders.transaction.VersionedTransaction.from_bytes") +@patch("solders.transaction.VersionedTransaction.message") +@patch.object(Pump, "get_balance") +def test_trade( + pubkey_mocked, + post_mocked, + from_bytes_mocked, + message_mocked, + get_balance_mocked, + txtype, + token, + amount, + expected_value, + retries, + status_code, + content, + description, + get_pubkey +): + pubkey_mocked.return_value = Mock(get_pubkey) + + mock_response = Mock() + mock_response.status_code = status_code + mock_response.content = content + post_mocked.return_value = mock_response + + appconfig.TRADING_RETRIES = retries + + from_bytes_mocked.return_value = Mock(message=Mock()) + message_mocked.return_value = Mock() + + get_balance_mocked.return_value = 5.00 + + pump = Pump(executor_name="test", trader_type=Trader.sniper) + txn = pump.trade(txtype=txtype, token=token, keypair=pubkey_mocked, amount=amount) + assert txn == expected_value, description + post_mocked.assert_called_once + +@pytest.mark.parametrize( + "description, trade_action, mint_address", [ + ( + "Timestamp a buy", TxType.buy, "mint_key_address" + ), + ( + "Timestamp a sell", TxType.sell, "mint_key_address" + ) + ] +) +def test_log_trade_token_timestamp(description, trade_action, mint_address): + pump = Pump(executor_name="test", trader_type=Trader.sniper) + token = {"mint": mint_address} + pump.add_update_token(token=token) + trade_timestamp = datetime.now().timestamp() + pump.log_trade_token_timestamp(mint=mint_address, txtype=trade_action, trade_timestamp=trade_timestamp) + key = "{}_timestamp".format(trade_action.value) + assert key in pump.tokens[mint_address], description + assert pump.tokens[mint_address][key] == trade_timestamp + From b28d351c5f660a1b222359bab4634cba9ffc2272 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 4 Dec 2024 16:08:35 +0100 Subject: [PATCH 024/103] pump: adding trade fees, increase and decrease fees, also test fees done. criteria: test trade timedelta. --- bot/config/__init__.py | 4 ++- bot/libs/criterias.py | 20 ++++++++---- bot/module/pump.py | 22 +++++++++++++- bot/tests/libs/test_criterias.py | 52 +++++++++++++++++++++++++++++++- bot/tests/module/test_pump.py | 15 +++++++++ 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index d67dc29..d5b43d5 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -21,7 +21,9 @@ class AuthConfig: # High volatility or limited liquidity: 1% to 3% # Very volatile or low-liquidity asset: up to 5% SLIPPAGE = os.environ.get("SLIPPAGE", 10) - FEES = float(os.environ.get("FEES", 0.0005)) + FEES = float(os.environ.get("FEES", 0.00805)) + FEES_INCREASMENT = 0.002 + FEES_TIMEDELTA_IN_SECONDS = 0.5 FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 98f67e1..02ac5d0 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -20,7 +20,8 @@ def trading_analytics( previous_trades: List, amount_traded: float, pubkey: Pubkey, - traders: List[str] = [] + token_timestamps: Dict, + traders: List[str] = [], ) -> Dict: """ This function compare incomming messages with previous trades and include in the msg @@ -29,9 +30,11 @@ def trading_analytics( :param previous_trades: history of incomming messages :return: msg with more attributes """ - new_msg = copy.deepcopy(msg) - new_msg["timestamp"] = datetime.now().timestamp() # Including timestamp in incomming message + + # Including timestamp in incomming message + new_msg["timestamp"] = datetime.now().timestamp() + new_msg["trade_time_delta"] = 0 # Checking if the trade position is relevant enough to be considering for criteria new_msg["is_relevant_trade"] = True @@ -118,6 +121,12 @@ def trading_analytics( # Updating the last record new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] new_msg["max_consecutive_buys"][-1]["sols"] += sols + + # Calculating timedelta between buy and received message + if is_this_my_trade and "buy_timestamp" in token_timestamps: + token_time = datetime.fromtimestamp(token_timestamps["buy_timestamp"]) + message_time = datetime.fromtimestamp(new_msg["timestamp"]) + new_msg["trade_time_delta"] = (message_time - token_time).total_seconds() else: new_msg["consecutive_buys"] = 1 @@ -215,18 +224,17 @@ def max_seconds_between_buys(seconds: float, msg: dict, amount_traded: float = N return msg["seconds_between_buys"] >= seconds -def trader_has_sold(expected: bool, msg: dict, amount_traded: float = None) -> bool: +def trader_has_sold(msg: dict, amount_traded: float = None) -> bool: """ Checks if the trader or developer has sold their tokens. Args: - sold (bool): A flag indicating whether the developer has sold tokens. msg (dict): The message dictionary containing context. Returns: bool: True if the condition is met, otherwise False. """ - return expected == msg["trader_has_sold"] + return msg["trader_has_sold"] def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amount_traded: float) -> bool: diff --git a/bot/module/pump.py b/bot/module/pump.py index 4d18aff..4935a03 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -185,6 +185,8 @@ def __init__( self.scanner_activity_time = appconfig.SCANNER_WORKING_TIME self.stop_app = False + self.trade_fees = appconfig.FEES + def start_scanner(self): self.scanner_start_time = datetime.now() @@ -203,6 +205,16 @@ def get_tkn_balance(self, wallet_pubkey, token_account): token_balance = 0 return token_balance + def increase_fees(self): + self.trade_fees += appconfig.FEES_INCREASMENT + + def decrease_fees(self): + lower_fees = round(self.trade_fees - appconfig.FEES_INCREASMENT, 5) + self.trade_fees = lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES + + def reset_fees(self): + self.trade_fees = appconfig.FEES + def add_account(self, account: str): self.accounts.append(account) @@ -569,13 +581,21 @@ async def subscribe(self, steps: list): if self.tokens[mint]["is_closed"]: continue + time_stamps = {"buy_timestamp": None, "sell_timestamp": None} + if "buy_timestamp" in token: + time_stamps["buy_timestamp"] = token["buy_timestamp"] + + if "sell_timestamp" in token: + time_stamps["sell_timestamp"] = token["sell_timestamp"] + # Doing some analytics like how many continuous buys have happend, etc new_msg = trading_analytics( msg=msg, previous_trades=token["trades"], amount_traded=self.trading_amount, pubkey=self.keypair.pubkey(), - traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [] + traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [], + token_timestamps=time_stamps ) # Including last message with new metadata into trades list token = self.tokens[mint].copy() # Key point: need to copy the token diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 479a946..c159bf5 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -217,7 +217,8 @@ def test_trading_analytics_buys( previous_trades=previous_trades, amount_traded=amount, pubkey=get_pubkey, - traders=traders + traders=traders, + token_timestamps={} ) if str(get_pubkey) == new_msg["traderPublicKey"]: assert new_msg["vSolInBondingCurve_Base"] == new_msg["vSolInBondingCurve"] - amount, description @@ -353,6 +354,7 @@ def test_max_seconds_between_buys( previous_trades=previous_trades, amount_traded=amount, pubkey=get_pubkey, + token_timestamps={}, traders=traders ) @@ -490,6 +492,7 @@ def test_trader_has_sold( previous_trades=previous_trades, amount_traded=amount, pubkey=get_pubkey, + token_timestamps={}, traders=traders ) has_sold = trader_has_sold(msg=new_msg) @@ -572,3 +575,50 @@ def test_detect_own_trade( last_max_consecutive_buys = previous_trades[0]["max_consecutive_buys"][0]["quantity"] max_consecutive_buys = new_msg["max_consecutive_buys"][0]["quantity"] assert max_consecutive_buys == last_max_consecutive_buys + 1, description + + +@pytest.mark.parametrize( + """ + description, + msg, + previous_trades, + amount, + traders, + """, + [ + ( + "Receiving our own trade ", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + ), + ] +) +def test_trade_timedelta( + get_pubkey, + description, + msg, + previous_trades, + amount, + traders +): + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades.copy(), + amount_traded=amount, + pubkey=get_pubkey, + traders=traders, + token_timestamps={"buy_timestamp":(datetime.now() - timedelta(seconds=20)).timestamp()} + ) diff --git a/bot/tests/module/test_pump.py b/bot/tests/module/test_pump.py index ebc1d0b..358f445 100644 --- a/bot/tests/module/test_pump.py +++ b/bot/tests/module/test_pump.py @@ -128,3 +128,18 @@ def test_log_trade_token_timestamp(description, trade_action, mint_address): assert key in pump.tokens[mint_address], description assert pump.tokens[mint_address][key] == trade_timestamp + +def test_fees(): + pump = Pump(executor_name="test", trader_type=Trader.sniper) + assert pump.trade_fees == appconfig.FEES + + pump.increase_fees() + assert pump.trade_fees >= appconfig.FEES + + pump.decrease_fees() + assert pump.trade_fees == appconfig.FEES + + pump.decrease_fees() + pump.decrease_fees() + pump.decrease_fees() + assert pump.trade_fees == appconfig.FEES \ No newline at end of file From 5d2d4f50b5740f533272dbb24fc5aff53607949d Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 5 Dec 2024 06:30:18 +0100 Subject: [PATCH 025/103] Implementing exit criteria for timedelta exceeded when trading --- bot/libs/criterias.py | 29 ++++--- bot/module/pump.py | 3 +- bot/tests/libs/test_criterias.py | 131 +++++++++++++++++++++++++++++-- 3 files changed, 146 insertions(+), 17 deletions(-) diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 02ac5d0..f8a5c51 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -32,9 +32,18 @@ def trading_analytics( """ new_msg = copy.deepcopy(msg) + is_this_my_trade = new_msg["traderPublicKey"] == str(pubkey) + # Including timestamp in incomming message new_msg["timestamp"] = datetime.now().timestamp() new_msg["trade_time_delta"] = 0 + # Calculating timedelta between buy/sell and received message + if is_this_my_trade: + timestamp_key = "{}_timestamp".format(new_msg["txType"].lower()) + if timestamp_key in token_timestamps: + token_time = datetime.fromtimestamp(token_timestamps[timestamp_key]) + message_time = datetime.fromtimestamp(new_msg["timestamp"]) + new_msg["trade_time_delta"] = (message_time - token_time).total_seconds() # Checking if the trade position is relevant enough to be considering for criteria new_msg["is_relevant_trade"] = True @@ -43,8 +52,6 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 - is_this_my_trade = new_msg["traderPublicKey"] == str(pubkey) - if not previous_trades: aprox = 0 # Math calculation for Solana in Bonding Courve just before we bought @@ -122,12 +129,6 @@ def trading_analytics( new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] new_msg["max_consecutive_buys"][-1]["sols"] += sols - # Calculating timedelta between buy and received message - if is_this_my_trade and "buy_timestamp" in token_timestamps: - token_time = datetime.fromtimestamp(token_timestamps["buy_timestamp"]) - message_time = datetime.fromtimestamp(new_msg["timestamp"]) - new_msg["trade_time_delta"] = (message_time - token_time).total_seconds() - else: new_msg["consecutive_buys"] = 1 new_msg["seconds_between_buys"] = 0 @@ -252,6 +253,16 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amo max_sols = amount_traded * percentage / 100 return sols <= max_sols +def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: float = None) -> bool: + """ + Checks if the timedelta in seconds between the buy/sell and the message received is acceptable or not + Args: + msg (dict): The message dictionary containing context. + expected (bool): The expected bool value outcome + Returns: + bool: True if the condition is met, otherwise False. + """ + return msg["trade_time_delta"] >= appconfig.FEES_TIMEDELTA_IN_SECONDS and expected def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: """ @@ -427,5 +438,3 @@ def test(): tokens[mint]["trades"].append(new_msg) print(tokens[mint]["trades"]) - -#test() diff --git a/bot/module/pump.py b/bot/module/pump.py index 4935a03..a1dc14a 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -139,7 +139,8 @@ class TradeRoadmap: "max_seconds_between_buys": 2.5, "trader_has_sold": True, "max_sols_in_token_after_buying_in_percentage": 100, - "market_inactivity": 3 + "market_inactivity": 3, + "validate_trade_timedelta_exceeded": True } }, { diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index c159bf5..35640eb 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -2,6 +2,7 @@ from typing import Any from unittest.mock import patch, Mock import pytest +from bot.config import appconfig from bot.tests.libs.fixtures_criterias import * from bot.libs.criterias import ( exit_on_first_sale, @@ -9,7 +10,8 @@ exit_on_first_sale, max_consecutive_buys, max_seconds_between_buys, - trader_has_sold + trader_has_sold, + validate_trade_timedelta_exceeded ) @@ -569,7 +571,8 @@ def test_detect_own_trade( previous_trades=previous_trades.copy(), amount_traded=amount, pubkey=get_pubkey, - traders=traders + traders=traders, + token_timestamps={"buy_timestamp":(datetime.now() - timedelta(seconds=0)).timestamp()} ) last_max_consecutive_buys = previous_trades[0]["max_consecutive_buys"][0]["quantity"] @@ -580,14 +583,18 @@ def test_detect_own_trade( @pytest.mark.parametrize( """ description, + txType, msg, previous_trades, amount, traders, + tdelta, + expected_result """, [ ( - "Receiving our own trade ", + "Buy: Receiving our own trade with delay", + "buy", { "signature":"xxxx", "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", @@ -603,22 +610,134 @@ def test_detect_own_trade( [], 0.5, [], + 0.5, + True + ), + ( + "Buy: Receiving our own trade with NO delay", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + 0.4999, + False + ), + ( + "Sell: Receiving our own trade with delay", + "sell", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"sell", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + 0.5, + True + ), + ( + "Sell: Receiving our own trade with NO delay", + "sell", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"sell", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + 0.4999, + False + ), + ( + "Create: Receiving our own trade with delay", + "create", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"create", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + 0.5, + True + ), + ( + "Create: Receiving our own trade with NO delay", + "create", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"create", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [], + 0.5, + [], + 0.4999, + False ), ] ) def test_trade_timedelta( get_pubkey, + txType, description, msg, previous_trades, amount, - traders + traders, + tdelta, + expected_result ): + tkey = "{}_timestamp".format(txType) new_msg = trading_analytics( msg=msg, - previous_trades=previous_trades.copy(), + previous_trades=previous_trades, amount_traded=amount, pubkey=get_pubkey, traders=traders, - token_timestamps={"buy_timestamp":(datetime.now() - timedelta(seconds=20)).timestamp()} + token_timestamps={tkey:(datetime.now() - timedelta(seconds=tdelta)).timestamp()} ) + assert validate_trade_timedelta_exceeded(expected=expected_result, msg=new_msg) == expected_result, description From b43a7d5cb4cf05e98f6756af28f98015bedcc294 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 5 Dec 2024 06:49:29 +0100 Subject: [PATCH 026/103] Implementing timedelta exceeded in trade function to increase fees. Adding trade function exit when catching BUY exception when requestion quote --- bot/module/pump.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/bot/module/pump.py b/bot/module/pump.py index a1dc14a..326b4f3 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -207,11 +207,20 @@ def get_tkn_balance(self, wallet_pubkey, token_account): return token_balance def increase_fees(self): + print("### INCREASING FEES: from {} to {}".format( + self.trade_fees, + self.trade_fees + appconfig.FEES_INCREASMENT + )) self.trade_fees += appconfig.FEES_INCREASMENT def decrease_fees(self): lower_fees = round(self.trade_fees - appconfig.FEES_INCREASMENT, 5) + print("### DECREASING FEES: from {} to {}".format( + self.trade_fees, + lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES + )) self.trade_fees = lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES + def reset_fees(self): self.trade_fees = appconfig.FEES @@ -796,6 +805,11 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No """ txSignature = None + # Increase fees validation + if "exit_criteria" in self.tokens[token]: + if self.tokens[token]["exit_criteria"] == "validate_trade_timedelta_exceeded": + self.increase_fees() + denominated_in_sol = "true" if txtype.value == TxType.buy.value else "false" amount = amount if txtype.value == TxType.buy.value else "100%" @@ -832,7 +846,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No ) if response.status_code != 200: if txtype.value == TxType.buy.value: - print("Trade->{} Failed get quote. Exiting trade function. Error: {} returned a status code {}. Response: {}".format( + print("Trade->{} Failed getting quote. Exiting trade function. Error: {} returned a status code {}. Response: {}".format( txtype.value, appconfig.PUMPFUN_TRANSACTION_URL, response.status_code, @@ -852,6 +866,14 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No time.sleep(appconfig.RETRYING_SECONDS) continue except Exception as e: + if txtype.value == TxType.buy.value: + print("Trade->{} Exception: {}. Exiting trade function. Message: {}".format( + txtype.value, + appconfig.PUMPFUN_TRANSACTION_URL, + e + )) + break + retries += 1 print("Trade->{} Exception: {}. Retrying again {} times. Message: {}".format( txtype.value, From 7d78bbed3564c870377ae6b785a8a899388fec0a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 6 Dec 2024 15:58:44 +0100 Subject: [PATCH 027/103] New criteria: seller_is_a_known_trader applied to sniper_1 and tested. Utils: add get_instructions_from_message (under developement) to retrieve impacPriocePct from quote. criterias.trading_analytics: seller_is_a_known_trader attribute added to message. Bitquery: adding first draft to query solana directly using payed servies called Bitquery --- bot/domain/bitquery.py | 85 +++++++++++++++++++ bot/libs/criterias.py | 8 ++ bot/libs/utils.py | 58 +++++++++++++ bot/module/pump.py | 12 +-- bot/tests/libs/test_criterias.py | 138 ++++++++++++++++++++++++++++++- 5 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 bot/domain/bitquery.py diff --git a/bot/domain/bitquery.py b/bot/domain/bitquery.py new file mode 100644 index 0000000..a351976 --- /dev/null +++ b/bot/domain/bitquery.py @@ -0,0 +1,85 @@ +import asyncio +import websockets +import json + +# Define the GraphQL query +graphql_query = """ +subscription MyQuery { + Solana { + DEXTradeByTokens( + where: { + Trade: { + PriceInUSD: {gt: 0.000015, lt: 0.000016} + Dex: {ProgramAddress: {is: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"}} + Side: { + Currency: {MintAddress: {is: "11111111111111111111111111111111"}} + AmountInUSD: {ge: "0.007200000"} + } + } + Transaction: {Result: {Success: true}} + Block: {Time: {since: "2024-12-05T23:30:00Z"}} + any: {Trade: {}} + } + limit: {count: 1} + ) { + Block { + Time + } + Trade { + Currency { + Name + Symbol + Decimals + MintAddress + } + Price + PriceInUSD + Dex { + ProtocolName + ProtocolFamily + ProgramAddress + } + Side { + Currency { + MintAddress + Name + Symbol + } + } + } + } + } +} +""" + +# Define the WebSocket server URL +GRAPHQL_ENDPOINT = "wss://graphql.bitquery.io" # Replace with the actual endpoint + +async def run_query(): + """ TO BE TESTED - UNDER DEVELOPMENT """ + async with websockets.connect( + GRAPHQL_ENDPOINT, + extra_headers={"Authorization": "Bearer BQYVFhPCtWWwZVxA6QB8PuvKj0dc5HR5"} + ) as websocket: + # Send the subscription request + payload = { + "type": "start", + "id": "1", # Unique identifier for the query + "payload": { + "query": graphql_query + } + } + await websocket.send(json.dumps(payload)) + print("Subscription initiated...") + + # Listen for responses + try: + while True: + response = await websocket.recv() + data = json.loads(response) + print("Received response:", json.dumps(data, indent=2)) + except websockets.ConnectionClosed as e: + print(f"WebSocket closed: {e}") + +# Run the async function +asyncio.run(run_query()) diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index f8a5c51..f5e81ca 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -52,6 +52,10 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 + new_msg["seller_is_a_known_trader"] = any( + trade for trade in previous_trades if new_msg["traderPublicKey"] == trade["traderPublicKey"] + ) + if not previous_trades: aprox = 0 # Math calculation for Solana in Bonding Courve just before we bought @@ -264,6 +268,10 @@ def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: """ return msg["trade_time_delta"] >= appconfig.FEES_TIMEDELTA_IN_SECONDS and expected + +def seller_is_a_known_trader(expected: bool, msg: dict, amount_traded: float = None) -> bool: + return msg["seller_is_a_known_trader"] and expected + def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: """ Dummy fuction as it retrieves False diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 8aa137a..a795d3e 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -1,8 +1,12 @@ import base58 +import base64 +import json +import struct from enum import Enum from solana.rpc.async_api import AsyncClient from solana.rpc.types import TokenAccountOpts +from solders.message import Message from solders.pubkey import Pubkey from bot.config import appconfig @@ -108,3 +112,57 @@ def initial_buy_calculator(sol_in_bonding_curve: float): pump_dont_fun_initial_fund = appconfig.SCANNER_PUMPDONTFUN_INITIAL_FUND sols = round(sol_in_bonding_curve - pump_dont_fun_initial_fund, 3) return sols + +def flatten_data(data): + if isinstance(data, list): + return b"".join(flatten_data(item) for item in data) + elif isinstance(data, int): + return data.to_bytes(1, 'little') # Convert integer to a single byte + return b"" + +def decode_instruction(raw_data): + if len(raw_data) >= 5: # Ensure at least enough data for a float + # Assume the first byte is the Field ID + field_id = raw_data[0] + # Interpret the next 4 bytes as a float (little-endian) + try: + price_impact = struct.unpack(' Date: Fri, 6 Dec 2024 16:05:48 +0100 Subject: [PATCH 028/103] refactor: seller_is_a_known_trader -> seller_is_an_unknown_traderdef --- bot/config/__init__.py | 2 +- bot/libs/criterias.py | 6 +++--- bot/module/pump.py | 5 +++-- bot/tests/libs/test_criterias.py | 14 +++++++------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index d5b43d5..e63c0d8 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -23,7 +23,7 @@ class AuthConfig: SLIPPAGE = os.environ.get("SLIPPAGE", 10) FEES = float(os.environ.get("FEES", 0.00805)) FEES_INCREASMENT = 0.002 - FEES_TIMEDELTA_IN_SECONDS = 0.5 + FEES_TIMEDELTA_IN_SECONDS = 0.5 # Tolerance to delays between buying and entering in the trade FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index f5e81ca..905a959 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -52,7 +52,7 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 - new_msg["seller_is_a_known_trader"] = any( + new_msg["seller_is_an_unknown_trader"] = not any( trade for trade in previous_trades if new_msg["traderPublicKey"] == trade["traderPublicKey"] ) @@ -269,8 +269,8 @@ def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: return msg["trade_time_delta"] >= appconfig.FEES_TIMEDELTA_IN_SECONDS and expected -def seller_is_a_known_trader(expected: bool, msg: dict, amount_traded: float = None) -> bool: - return msg["seller_is_a_known_trader"] and expected +def seller_is_an_unknown_trader(expected: bool, msg: dict, amount_traded: float = None) -> bool: + return msg["seller_is_an_unknown_trader"] and expected def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: """ diff --git a/bot/module/pump.py b/bot/module/pump.py index c397133..b3d8a9f 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -15,7 +15,7 @@ max_seconds_between_buys, max_sols_in_token_after_buying_in_percentage, trader_has_sold, - seller_is_a_known_trader, + seller_is_an_unknown_trader, market_inactivity ) from bot.libs import criterias as criteria_functions @@ -134,6 +134,7 @@ class TradeRoadmap: "step": 2, "name": "TOKEN_SUBSCRIPTION", "subscription": Suscription.subscribeTokenTrade, + # Exit criteria to move on to the next step "criteria": { "max_consecutive_buys": 4, "max_consecutive_sells": 2, @@ -142,7 +143,7 @@ class TradeRoadmap: "max_sols_in_token_after_buying_in_percentage": 100, "market_inactivity": 3, "validate_trade_timedelta_exceeded": True, - "seller_is_a_known_trader": True + "seller_is_an_unknown_trader": True } }, { diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index b605357..4442b84 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -12,7 +12,7 @@ max_seconds_between_buys, trader_has_sold, validate_trade_timedelta_exceeded, - seller_is_a_known_trader + seller_is_an_unknown_trader ) @@ -798,12 +798,12 @@ def test_trade_timedelta( "sols":1.6 } ], - "seller_is_a_known_trader": False, + "seller_is_an_unknown_trader": False, } ], 0.5, [], - True + False ), ( "Sell: unknown seller that might had buyed before we did. ", @@ -848,16 +848,16 @@ def test_trade_timedelta( "sols":1.6 } ], - "seller_is_a_known_trader": False, + "seller_is_an_unknown_trader": False, } ], 0.5, [], - False + True ), ] ) -def test_seller_is_a_known_trader( +def test_seller_is_an_unknown_trader( get_pubkey, txType, description, @@ -876,4 +876,4 @@ def test_seller_is_a_known_trader( traders=traders, token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} ) - assert seller_is_a_known_trader(expected=expected_result, msg=new_msg) == expected_result, description + assert seller_is_an_unknown_trader(expected=expected_result, msg=new_msg) == expected_result, description From 577114456bac1ab76f771f6e9835fedba2885658 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 6 Dec 2024 17:00:16 +0100 Subject: [PATCH 029/103] Scanner step: delete_unchecked_tokens function included before starting the scanner. --- bot/domain/redis_db.py | 23 +++++++++++++++++++++++ bot/module/pump.py | 24 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 2020db6..31f4507 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -155,6 +155,29 @@ def update_token( token.update(new_data) return token + def delete_unchecked_tokens(self)-> int: + deleted_tokens = 0 + # Search for keys where is_checked is false + try: + # Scan and filter keys + cursor = 0 + while True: + cursor, keys = self.client.scan(cursor=cursor, match='token:*', count=1000) + for key in keys: + data = self.client.json().get(key) + if data: + try: + if data.get('is_checked') is False: # Check the condition + self.client.delete(key) + deleted_tokens += 1 + except json.JSONDecodeError: + continue # Skip keys with invalid JSON + if cursor == 0: + break + except Exception as e: + print("Redis.delete_unchecked_tokens Error: {}".format(e)) + + return deleted_tokens def test_create_record(mint_address: str): redis_object = RedisDB() diff --git a/bot/module/pump.py b/bot/module/pump.py index b3d8a9f..2bd9b3c 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -54,6 +54,7 @@ class Redis(Enum): closeToken = "closeToken" readTraders = "readTraders" saveToken = "saveToken" + cleanTokens = "cleanTokens" class TradeRoadmap: @@ -93,11 +94,19 @@ class TradeRoadmap: "name": "START_SCANNER", "system_action": Celebrimborg.start, "criteria": { - "scanner_activity_time": 10, # How much time the scanner will be working. -1 is always + "scanner_activity_time": -1, # How much time the scanner will be working. -1 is always }, }, { "step": 1, + "name": "CLEAN_UNTRADED_TOKENS_FROM_REDIS", + "redis": Redis.cleanTokens, + "criteria": { + "delete_unchecked_tokens": True + } + }, + { + "step": 2, "name": "SCANNER", "subscription": Suscription.subscribeNewToken, "criteria": { @@ -113,7 +122,7 @@ class TradeRoadmap: } } }, - {"step": 2, "name": "exit", "system_action": Celebrimborg.exit}, + {"step": 3, "name": "exit", "system_action": Celebrimborg.exit}, ] sniper_1 = [ { @@ -325,6 +334,17 @@ async def subscribe(self, steps: list): # REDIS DB HANDLING if "redis" in step: + if step["redis"] == Redis.cleanTokens: + if "criteria" in step: + if "delete_unchecked_tokens" in step["criteria"]: + if step["criteria"]["delete_unchecked_tokens"]: + deletions = redisdb.delete_unchecked_tokens() + print("Deleted unchecked tokens from redis db: {}".format( + deletions + )) + step_index += 1 + continue + # Listen to redis change redisdb.subscribe() if step["redis"] == Redis.readToken: From 71656d193fead9367d33abde975a958a5909c27e Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 6 Dec 2024 17:46:31 +0100 Subject: [PATCH 030/103] Bug fix: trader_has_sold was receiving bad few arguments. Dockerfile: adding vim --- bot/Dockerfile | 1 + bot/libs/criterias.py | 4 ++-- bot/module/pump.py | 12 ++++++------ bot/tests/libs/test_criterias.py | 2 +- bot/tests/module/test_pump.py | 7 ++++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/bot/Dockerfile b/bot/Dockerfile index c15adf2..48789a4 100644 --- a/bot/Dockerfile +++ b/bot/Dockerfile @@ -9,6 +9,7 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y \ build-essential \ procps \ bash \ + vim \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 905a959..d037285 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -229,7 +229,7 @@ def max_seconds_between_buys(seconds: float, msg: dict, amount_traded: float = N return msg["seconds_between_buys"] >= seconds -def trader_has_sold(msg: dict, amount_traded: float = None) -> bool: +def trader_has_sold(expected: bool, msg: dict, amount_traded: float = None) -> bool: """ Checks if the trader or developer has sold their tokens. @@ -239,7 +239,7 @@ def trader_has_sold(msg: dict, amount_traded: float = None) -> bool: Returns: bool: True if the condition is met, otherwise False. """ - return msg["trader_has_sold"] + return msg["trader_has_sold"] and expected def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amount_traded: float) -> bool: diff --git a/bot/module/pump.py b/bot/module/pump.py index 2bd9b3c..3a848d3 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -94,7 +94,7 @@ class TradeRoadmap: "name": "START_SCANNER", "system_action": Celebrimborg.start, "criteria": { - "scanner_activity_time": -1, # How much time the scanner will be working. -1 is always + "scanner_activity_time": 60, # How much time the scanner will be working. -1 is always }, }, { @@ -111,7 +111,7 @@ class TradeRoadmap: "subscription": Suscription.subscribeNewToken, "criteria": { "min_initial_buy": 1.50, # Filter: we'll trade tokens with this initial buying amount at least. - "trading_amount": 0.5, # Amount to be traded by snipers + "trading_amount": 0.1, # Amount to be traded by snipers "trader": Trader.sniper.value # Who will trade the tokens }, "action":{ @@ -149,7 +149,7 @@ class TradeRoadmap: "max_consecutive_sells": 2, "max_seconds_between_buys": 2.5, "trader_has_sold": True, - "max_sols_in_token_after_buying_in_percentage": 100, + "max_sols_in_token_after_buying_in_percentage": 500, "market_inactivity": 3, "validate_trade_timedelta_exceeded": True, "seller_is_an_unknown_trader": True @@ -226,7 +226,7 @@ def increase_fees(self): self.trade_fees += appconfig.FEES_INCREASMENT def decrease_fees(self): - lower_fees = round(self.trade_fees - appconfig.FEES_INCREASMENT, 5) + lower_fees = round(self.trade_fees - appconfig.FEES_INCREASMENT, 9) print("### DECREASING FEES: from {} to {}".format( self.trade_fees, lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES @@ -473,7 +473,7 @@ async def subscribe(self, steps: list): self.log_trade_token_timestamp( mint=mint_address, txtype=TxType.buy, - timestamp=buy_time.timestamp() + trade_timestamp=buy_time.timestamp() ) # Get the token balance in wallet @@ -521,7 +521,7 @@ async def subscribe(self, steps: list): self.log_trade_token_timestamp( mint=mint_address, txtype=TxType.sell, - timestamp=buy_time.timestamp() + trade_timestamp=buy_time.timestamp() ) # Update wallet balance after selling self.get_balance(public_key=self.keypair.pubkey()) diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 4442b84..9199ffd 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -498,7 +498,7 @@ def test_trader_has_sold( token_timestamps={}, traders=traders ) - has_sold = trader_has_sold(msg=new_msg) + has_sold = trader_has_sold(expected=trader_has_sold_result, msg=new_msg) assert has_sold == trader_has_sold_result, description @pytest.mark.parametrize( diff --git a/bot/tests/module/test_pump.py b/bot/tests/module/test_pump.py index 358f445..14e2613 100644 --- a/bot/tests/module/test_pump.py +++ b/bot/tests/module/test_pump.py @@ -48,7 +48,7 @@ def test_attribute_handling( "txtype, token, amount, expected_value, retries, status_code, content, description", [ ( TxType.buy, - "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump", + {"mint": "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump"}, 1.0, None, 1, @@ -58,7 +58,7 @@ def test_attribute_handling( ), ( TxType.buy, - "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump", + {"mint": "49fRbSzsHhtjH6ZF4KQDCcmcFdSbU11uHGeVMWbSpump"}, 1.0, None, 1, @@ -104,7 +104,8 @@ def test_trade( get_balance_mocked.return_value = 5.00 pump = Pump(executor_name="test", trader_type=Trader.sniper) - txn = pump.trade(txtype=txtype, token=token, keypair=pubkey_mocked, amount=amount) + pump.add_update_token(token=token) + txn = pump.trade(txtype=txtype, token=token["mint"], keypair=pubkey_mocked, amount=amount) assert txn == expected_value, description post_mocked.assert_called_once From 06b034389e16ac8475a8ffe63b645fdebdd79c83 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 7 Dec 2024 01:05:34 +0100 Subject: [PATCH 031/103] Keeping track of the developer as an exit criteria if he/she sells. Config: increasing trading delay to 1second. Criteria: max_buys and sells are not counted for our own trades and bug fixed with max_sols_in_token_after_buying_in_percentage function. Tests: adding max_sols_in_token_after_buying_in_percentage test and updating test_trade_timedelta test. --- bot/config/__init__.py | 9 +- bot/domain/redis_db.py | 2 + bot/libs/criterias.py | 24 ++-- bot/module/pump.py | 15 ++- bot/tests/libs/test_criterias.py | 204 +++++++++++++++++++++++++++++-- 5 files changed, 227 insertions(+), 27 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index e63c0d8..c832c7c 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -12,7 +12,7 @@ class AppMode(Enum): class AuthConfig: # our code environment ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") - APPMODE = os.environ.get("APPMODE", AppMode.dummy.value) + APPMODE = os.environ.get("APPMODE", AppMode.real.value) TIME_FORMAT = "%b %-d %-I:%M:%S %p" @@ -21,9 +21,10 @@ class AuthConfig: # High volatility or limited liquidity: 1% to 3% # Very volatile or low-liquidity asset: up to 5% SLIPPAGE = os.environ.get("SLIPPAGE", 10) - FEES = float(os.environ.get("FEES", 0.00805)) - FEES_INCREASMENT = 0.002 - FEES_TIMEDELTA_IN_SECONDS = 0.5 # Tolerance to delays between buying and entering in the trade + FEES = float(os.environ.get("FEES", 0.000805)) + FEES_INCREASMENT = 0.000805 + FEES_TIMEDELTA_IN_SECONDS = 1.0 # Tolerance to delays between buying and entering in + # the trade because of lower fees FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index 31f4507..d6edbaa 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -84,6 +84,8 @@ def get_fresh_tokens(self, trader=Trader, mint_address: str = None) -> List[Dict if not is_checked and trader_match: track_traders = data["track_traders"] if "track_traders" in data else [] + # By default we'll always keeep track of the developer + track_traders.append(data["traderPublicKey"]) token = { "key": key, "mint": key.replace("token:", ""), # Mint address from key value diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index d037285..0c8ea68 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -39,18 +39,22 @@ def trading_analytics( new_msg["trade_time_delta"] = 0 # Calculating timedelta between buy/sell and received message if is_this_my_trade: + print("trading_analytics: *** THIS IS OUR TRADE ***") timestamp_key = "{}_timestamp".format(new_msg["txType"].lower()) if timestamp_key in token_timestamps: token_time = datetime.fromtimestamp(token_timestamps[timestamp_key]) message_time = datetime.fromtimestamp(new_msg["timestamp"]) new_msg["trade_time_delta"] = (message_time - token_time).total_seconds() + print(" *** trade_time_delta: {}".format(new_msg["trade_time_delta"])) + print(" *** message_time: {}".format(message_time.strftime(appconfig.TIME_FORMAT).lower())) + print(" *** token_time: {}".format(token_time.strftime(appconfig.TIME_FORMAT).lower())) # Checking if the trade position is relevant enough to be considering for criteria new_msg["is_relevant_trade"] = True # Default values that might change - new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value else 0 - new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value else 0 + new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value and not is_this_my_trade else 0 + new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value and not is_this_my_trade else 0 new_msg["seller_is_an_unknown_trader"] = not any( trade for trade in previous_trades if new_msg["traderPublicKey"] == trade["traderPublicKey"] @@ -84,7 +88,7 @@ def trading_analytics( new_msg["max_consecutive_buys"] = [ { "quantity": new_msg["consecutive_buys"], - "sols": amount_traded if aprox == 0 else aprox + "sols": amount_traded if aprox == 0 else aprox if not is_this_my_trade else 0 } ] @@ -130,8 +134,9 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() # Updating the last record - new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] - new_msg["max_consecutive_buys"][-1]["sols"] += sols + if not is_this_my_trade: + new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] + new_msg["max_consecutive_buys"][-1]["sols"] += sols else: new_msg["consecutive_buys"] = 1 @@ -161,8 +166,9 @@ def trading_analytics( new_msg["is_non_relevant_trade_count"] = 0 - new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] - new_msg["seconds_between_sells"] = (datetime.now() - last_msg_timestamp).total_seconds() + if not is_this_my_trade: + new_msg["consecutive_sells"] = 1 + last_msg["consecutive_sells"] + new_msg["seconds_between_sells"] = (datetime.now() - last_msg_timestamp).total_seconds() else: new_msg["consecutive_sells"] = 1 @@ -242,7 +248,7 @@ def trader_has_sold(expected: bool, msg: dict, amount_traded: float = None) -> b return msg["trader_has_sold"] and expected -def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amount_traded: float) -> bool: +def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amount_traded: float = None) -> bool: """ Checks if the maximum percentage of SOLs in the token after buying condition is met. @@ -255,7 +261,7 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amo """ sols = msg["max_consecutive_buys"][-1]["sols"] max_sols = amount_traded * percentage / 100 - return sols <= max_sols + return sols >= max_sols def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: float = None) -> bool: """ diff --git a/bot/module/pump.py b/bot/module/pump.py index 3a848d3..541644b 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -504,6 +504,8 @@ async def subscribe(self, steps: list): if token_data["is_closed"]: continue + # TODO: If trade_time_delta > tolerance then check if the buy txn has been done + txn = self.trade( txtype=TxType.sell, token=mint_address, @@ -610,6 +612,10 @@ async def subscribe(self, steps: list): # Getting the mint address we'll work with # CHeck mint value for each tokeb being processed mint = msg["mint"] + + # Key point: need to copy the token + token = self.tokens[mint].copy() + # We'll not pay attention to closed token trades if self.tokens[mint]["is_closed"]: continue @@ -621,17 +627,18 @@ async def subscribe(self, steps: list): if "sell_timestamp" in token: time_stamps["sell_timestamp"] = token["sell_timestamp"] + # By default we'll always track the developer + traders = token.get("track_traders", []) # Doing some analytics like how many continuous buys have happend, etc new_msg = trading_analytics( msg=msg, previous_trades=token["trades"], amount_traded=self.trading_amount, pubkey=self.keypair.pubkey(), - traders=self.tokens[mint]["track_traders"] if "track_traders" in self.tokens[mint] else [], + traders=traders, token_timestamps=time_stamps ) # Including last message with new metadata into trades list - token = self.tokens[mint].copy() # Key point: need to copy the token if not token["trades"]: token["trades"] = [new_msg] else: @@ -650,7 +657,9 @@ async def subscribe(self, steps: list): if move_to_next_step: step_index += 1 move_to_next_step = False - print("Exiting subscription") + print("Exiting subscription criteria: {}".format( + self.tokens[mint]["exit_criteria"] + )) break except asyncio.TimeoutError: diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 9199ffd..9a60d84 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -12,7 +12,8 @@ max_seconds_between_buys, trader_has_sold, validate_trade_timedelta_exceeded, - seller_is_an_unknown_trader + seller_is_an_unknown_trader, + max_sols_in_token_after_buying_in_percentage ) @@ -233,7 +234,6 @@ def test_trading_analytics_buys( assert max_consecutive_buys(buys=get_max_consecutive_buys, msg=new_msg) == max_consecutive_buys_result - @pytest.mark.parametrize( """ description, @@ -367,7 +367,6 @@ def test_max_seconds_between_buys( ) assert seconds_exceeded == max_seconds_between_buys_result, description - @pytest.mark.parametrize( """ description, @@ -578,8 +577,8 @@ def test_detect_own_trade( last_max_consecutive_buys = previous_trades[0]["max_consecutive_buys"][0]["quantity"] max_consecutive_buys = new_msg["max_consecutive_buys"][0]["quantity"] - assert max_consecutive_buys == last_max_consecutive_buys + 1, description - + # Our trades wont affect the max consecutive buys count + assert max_consecutive_buys == last_max_consecutive_buys, description @pytest.mark.parametrize( """ @@ -611,7 +610,7 @@ def test_detect_own_trade( [], 0.5, [], - 0.5, + 1.0, True ), ( @@ -632,7 +631,7 @@ def test_detect_own_trade( [], 0.5, [], - 0.4999, + 0.9999, False ), ( @@ -653,7 +652,7 @@ def test_detect_own_trade( [], 0.5, [], - 0.5, + 1.01, True ), ( @@ -674,7 +673,7 @@ def test_detect_own_trade( [], 0.5, [], - 0.4999, + 0.9999, False ), ( @@ -695,7 +694,7 @@ def test_detect_own_trade( [], 0.5, [], - 0.5, + 1.5, True ), ( @@ -743,7 +742,6 @@ def test_trade_timedelta( ) assert validate_trade_timedelta_exceeded(expected=expected_result, msg=new_msg) == expected_result, description - @pytest.mark.parametrize( """ description, @@ -877,3 +875,187 @@ def test_seller_is_an_unknown_trader( token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} ) assert seller_is_an_unknown_trader(expected=expected_result, msg=new_msg) == expected_result, description + +@pytest.mark.parametrize( + """ + description, + txType, + msg, + previous_trades, + amount, + percentage, + expected_result + """, + [ + ( + "Buy: max Solanas in token after our buy have been reached ", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + } + ], + 2.0, + 100, + True + ), + ( + "Buy: 200% of amount traded compared with bought Solanas have NOT been reached ", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + } + ], + 2.0, + 200, + False + ), + ( + "Buy: 80% of amount traded compared with bouth Solanas have been reached ", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.8 + } + ], + "seller_is_an_unknown_trader": False, + } + ], + 1.0, + 80, + True + ), + ] +) +def test_max_sols_in_token_after_buying( + get_pubkey, + description, + txType, + msg, + previous_trades, + amount, + percentage, + expected_result +): + tkey = "{}_timestamp".format(txType) + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=amount, + pubkey=get_pubkey, + traders=[], + token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} + ) + assert max_sols_in_token_after_buying_in_percentage(percentage=percentage, msg=new_msg, amount_traded=amount) == expected_result, description From bd828d31974faa16cd9342e269c786caa3e00d4a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 7 Dec 2024 17:49:13 +0100 Subject: [PATCH 032/103] Bug: seller_is_an_unknown_trader can only be set for sell tardes from unknown traders. --- bot/libs/criterias.py | 8 ++- bot/tests/libs/test_criterias.py | 100 +++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 0c8ea68..2034943 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -56,9 +56,11 @@ def trading_analytics( new_msg["consecutive_buys"] = 1 if msg["txType"].lower() == TxType.buy.value and not is_this_my_trade else 0 new_msg["consecutive_sells"] = 1 if msg["txType"].lower() == TxType.sell.value and not is_this_my_trade else 0 - new_msg["seller_is_an_unknown_trader"] = not any( - trade for trade in previous_trades if new_msg["traderPublicKey"] == trade["traderPublicKey"] - ) + new_msg["seller_is_an_unknown_trader"] = False + if new_msg["txType"].lower() == TxType.sell.value: + new_msg["seller_is_an_unknown_trader"] = not any( + trade for trade in previous_trades if new_msg["traderPublicKey"] == trade["traderPublicKey"] + ) if not previous_trades: aprox = 0 diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 9a60d84..7306f18 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -853,6 +853,106 @@ def test_trade_timedelta( [], True ), + ( + "Buy: known buyer.", + "sell", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4Xx7xxXxXX125XX3Xx2xxXxxxxXXxxxxXxXxXxxxpump", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4Xx7xxXxXX125XX3Xx2xxXxxxxXXxxxxXxXxXxxxpump", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + } + ], + 0.5, + [], + False + ), + ( + "Buy: unknown buyer.", + "sell", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4Xx7xxXxXX125XX3Xx2xxXxxxxXXxxxxXxXxXxxxpump", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":1, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + } + ], + 0.5, + [], + False + ), ] ) def test_seller_is_an_unknown_trader( From 74b5d335e730f33404ee31b2c21df868acf3b5a8 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 13 Dec 2024 07:23:17 +0100 Subject: [PATCH 033/103] Including jito bundle transactions --- bot/config/__init__.py | 7 +- bot/domain/jito_rpc.py | 130 +++++++++++++++ bot/libs/utils.py | 56 ++++++- bot/module/jito.py | 363 +++++++++++++++++++++++++++++++++++++++++ bot/module/pump.py | 221 +++++++++++++++++++------ bot/requirements.txt | 1 + 6 files changed, 720 insertions(+), 58 deletions(-) create mode 100644 bot/domain/jito_rpc.py create mode 100644 bot/module/jito.py diff --git a/bot/config/__init__.py b/bot/config/__init__.py index c832c7c..49d8c1c 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -20,8 +20,8 @@ class AuthConfig: # Moderate trading volumes: 0.5% to 1%. # High volatility or limited liquidity: 1% to 3% # Very volatile or low-liquidity asset: up to 5% - SLIPPAGE = os.environ.get("SLIPPAGE", 10) - FEES = float(os.environ.get("FEES", 0.000805)) + SLIPPAGE = os.environ.get("SLIPPAGE", 40) + FEES = float(os.environ.get("FEES", 0.0015)) FEES_INCREASMENT = 0.000805 FEES_TIMEDELTA_IN_SECONDS = 1.0 # Tolerance to delays between buying and entering in # the trade because of lower fees @@ -34,7 +34,8 @@ class AuthConfig: os.environ.get("MIN_SOL_TRADING_AMOUNT", 0.5) ) # Minimum balance a token must have for being traded RETRYING_SECONDS = 1 - RPC_URL = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + RPC_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 diff --git a/bot/domain/jito_rpc.py b/bot/domain/jito_rpc.py new file mode 100644 index 0000000..3f7b5db --- /dev/null +++ b/bot/domain/jito_rpc.py @@ -0,0 +1,130 @@ +import requests +import os +import json +import random + +# Jito JSON RPC SDK +class JitoJsonRpcSDK: + # Initialize a block engine URL + def __init__(self, url, uuid_var=None): + self.url = url + if uuid_var == None: + self.uuid_var = None + else: + self.uuid_var = self.__get_uuid(uuid_var) + + def __get_uuid(self, uuid_var): + return os.getenv(uuid_var) + + # Send a request to the Block engine url using the JSON RPC methods + def __send_request(self, endpoint, method, params=None): + if endpoint == None: + return "Error: Please enter a valid endpoint." + + if self.uuid_var == None: + headers = { + 'Content-Type': 'application/json', + "accept": "application/json" + } + else: + headers = { + 'Content-Type': 'application/json', + "accept": "application/json", + "x-jito-atuh": self.uuid_var + } + data = { + "id": 1, + "jsonrpc": "2.0", + "method": method, + "params": [params] + } + + print(data) + try: + resp = requests.post(self.url + endpoint, headers=headers, json=data) + resp.raise_for_status() + return {"success": True, "data": resp.json()} + except requests.exceptions.HTTPError as errh: + return {"success": False, "error": f"HTTP Error: {errh}"} + except requests.exceptions.ConnectionError as errc: + return {"success": False, "error": f"Error Connecting: {errc}"} + except requests.exceptions.Timeout as errt: + return {"success": False, "error": f"Timeout Error: {errt}"} + except requests.exceptions.InvalidHeader as err: + return {"success": False, "error": f"Invalid Header error: {err}"} + except requests.exceptions.InvalidURL as err: + return {"success": False, "error": f"InvalidURL error: {err}"} + except requests.exceptions.RequestException as err: + return {"success": False, "error": f"An error occurred: {err}"} + + #Bundle Endpoint + def get_tip_accounts(self, params=None): + if self.uuid_var == None: + return self.__send_request(endpoint="/bundles", method="getTipAccounts") + else: + return self.__send_request(endpoint="/bundles?uuid=" + self.uuid_var, method="getTipAccounts") + + def get_random_tip_account(self): + response = self.get_tip_accounts() + if not response['success']: + print(f"Error getting tip accounts: {response.get('error', 'Unknown error')}") + return None + + tip_accounts = response['data']['result'] + if not tip_accounts: + print("No tip accounts found.") + return None + + random_account = random.choice(tip_accounts) + return random_account + + + def get_bundle_statuses(self, bundle_uuids): + endpoint = "/bundles" + if self.uuid_var is not None: + endpoint += f"?uuid={self.uuid_var}" + + # Ensure bundle_uuids is a list + if not isinstance(bundle_uuids, list): + bundle_uuids = [bundle_uuids] + + # Correct format for the request + params = bundle_uuids + + return self.__send_request(endpoint=endpoint, method="getBundleStatuses", params=params) + + def send_bundle(self, params=None): + if self.uuid_var == None: + return self.__send_request(endpoint="/bundles",method="sendBundle", params=params) + else: + return self.__send_request(endpoint="/bundles?uuid=" + self.uuid_var, method="sendBundle", params=params) + + def get_inflight_bundle_statuses(self, bundle_uuids): + endpoint = "/bundles" + if self.uuid_var is not None: + endpoint += f"?uuid={self.uuid_var}" + + # Ensure bundle_uuids is a list + if not isinstance(bundle_uuids, list): + bundle_uuids = [bundle_uuids] + + # Correct format for the request + params = bundle_uuids + + return self.__send_request(endpoint=endpoint, method="getInflightBundleStatuses", params=params) + + # Transaction Endpoint + def send_txn(self, params=None, bundleOnly=False): + ep = "/transactions" + query_params = [] + + if bundleOnly: + query_params.append("bundleOnly=true") + + if self.uuid_var is not None: + query_params.append(f"uuid={self.uuid_var}") + + if query_params: + ep += "?" + "&".join(query_params) + + return self.__send_request(endpoint=ep, method="sendTransaction", params=params) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index a795d3e..4b9b5a0 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -6,7 +6,7 @@ from enum import Enum from solana.rpc.async_api import AsyncClient from solana.rpc.types import TokenAccountOpts -from solders.message import Message +from solders.message import Message, MessageV0 from solders.pubkey import Pubkey from bot.config import appconfig @@ -39,7 +39,7 @@ async def get_solana_balance(public_key: Pubkey) -> float: :param wallet_address: The public address of the Solana wallet. :return: Balance in SOL as a float. """ - async with AsyncClient(appconfig.RPC_URL) as client: + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: # Fetch the balance (in lamports) balance_response = await client.get_balance(public_key) @@ -59,7 +59,7 @@ async def get_own_token_balance(wallet_pubkey: Pubkey, token_mint_addres: str) - :param token_mint_pubkey: The mint address of the token to query. :return: Token balance as a float. """ - async with AsyncClient(appconfig.RPC_URL) as client: + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: # Fetch token mint public key owned by the wallet token_mint_pubkey = decode_pump_fun_token(token=token_mint_addres) @@ -166,3 +166,53 @@ def get_instructions_from_message(msg: Message): print(f"Instruction {idx} contains priceImpactPct: {decoded_data}") return parsed_instructions + + +def include_instruction(msg: MessageV0): + """ + Under development + """ + from solders.compute_budget import set_compute_unit_limit + from solana.rpc.api import Client + from solders.instruction import CompiledInstruction + + # Step 1: Add ComputeBudgetInstruction + compute_budget_ix = set_compute_unit_limit(1_000_000) # Set compute unit limit to 1M + + # Step 2: Get a fresh recent ∫blockhash + client = Client(appconfig.RPC_URL) # Assuming RpcClient is already imported and available + recent_blockhash = client.get_latest_blockhash().value.blockhash + + accountKeys = list(msg.account_keys) # Convert to mutable list + program_id_index = len(accountKeys) # Index for the newly added program ID + accountKeys.append(compute_budget_ix.program_id) # Append the program ID + + # Map the accounts in compute_budget_ix to their indices in accountKeys + accounts_indices = [ + accountKeys.index(account) for account in compute_budget_ix.accounts + ] + + new_instruction = CompiledInstruction( + program_id_index=program_id_index, + data=compute_budget_ix.data, + accounts=bytes(accounts_indices) + ) + + # Ensure the new instruction is not already in the list of instructions + if new_instruction not in msg.instructions: + # Add the new instruction to the message + updated_instructions = list(msg.instructions) # Convert to mutable list + updated_instructions.append(new_instruction) + + updated_msg = Message.new_with_compiled_instructions( + num_required_signatures=msg.header.num_required_signatures, + num_readonly_signed_accounts=msg.header.num_readonly_signed_accounts, + num_readonly_unsigned_accounts=msg.header.num_readonly_unsigned_accounts, + account_keys=accountKeys, + recent_blockhash=recent_blockhash, + instructions=updated_instructions + ) + msg = updated_msg + else: + print("Duplicate instruction detected, skipping.") + return msg diff --git a/bot/module/jito.py b/bot/module/jito.py new file mode 100644 index 0000000..743a6a4 --- /dev/null +++ b/bot/module/jito.py @@ -0,0 +1,363 @@ +from bot.config import appconfig +from bot.domain.jito_rpc import JitoJsonRpcSDK + +import asyncio +import base64 +import json +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solders.system_program import TransferParams, transfer +from solders.transaction import Transaction +from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price +from solders.transaction_status import TransactionConfirmationStatus +from solders.signature import Signature +from solders.message import Message +from solders.instruction import Instruction +from solana.rpc.async_api import AsyncClient + + +async def check_transaction_status(client: AsyncClient, signature_str: str): + print("Checking transaction status...") + max_attempts = 60 # 60 seconds + attempt = 0 + + signature = Signature.from_string(signature_str) + + while attempt < max_attempts: + try: + response = await client.get_signature_statuses([signature]) + + if response.value[0] is not None: + status = response.value[0] + slot = status.slot + confirmations = status.confirmations + err = status.err + confirmation_status = status.confirmation_status + + print(f"Slot: {slot}") + print(f"Confirmations: {confirmations}") + print(f"Confirmation status: {confirmation_status}") + + if err: + print(f"Transaction failed with error: {err}") + return False + elif confirmation_status == TransactionConfirmationStatus.Finalized: + print("Transaction is finalized.") + return True + elif confirmation_status == TransactionConfirmationStatus.Confirmed: + print("Transaction is confirmed but not yet finalized.") + elif confirmation_status == TransactionConfirmationStatus.Processed: + print("Transaction is processed but not yet confirmed or finalized.") + else: + print("Transaction status not available yet.") + + await asyncio.sleep(1) + attempt += 1 + except Exception as e: + print(f"Error checking transaction status: {e}") + await asyncio.sleep(1) + attempt += 1 + + print(f"Transaction not finalized after {max_attempts} attempts.") + return False + +async def send_transaction_with_priority_fee(sdk, solana_client, sender, receiver, amount, jito_tip_amount, priority_fee, compute_unit_limit=100_000): + try: + recent_blockhash = await solana_client.get_latest_blockhash() + + # Transfer to the known receiver + transfer_ix = transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=receiver, lamports=amount)) + + # Jito tip transfer + jito_tip_account = Pubkey.from_string(sdk.get_random_tip_account()) + jito_tip_ix = transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=jito_tip_account, lamports=jito_tip_amount)) + + # Priority Fee + priority_fee_ix = set_compute_unit_price(priority_fee) + + transaction = Transaction.new_signed_with_payer( + [priority_fee_ix, transfer_ix, jito_tip_ix], + sender.pubkey(), + [sender], + recent_blockhash.value.blockhash + ) + + serialized_transaction = base64.b64encode(bytes(transaction)).decode('ascii') + + print(f"Sending transaction with priority fee: {priority_fee} micro-lamports per compute unit") + print(f"Transfer amount: {amount} lamports to {receiver}") + print(f"Jito tip amount: {jito_tip_amount} lamports to {jito_tip_account}") + print(f"Serialized transaction: {serialized_transaction}") + + response = sdk.send_txn(params=serialized_transaction, bundleOnly=False) + + if response['success']: + print(f"Full Jito SDK response: {response}") + signature_str = response['data']['result'] + print(f"Transaction signature: {signature_str}") + + finalized = await check_transaction_status(solana_client, signature_str) + + if finalized: + print("Transaction has been finalized.") + solscan_url = f"https://solscan.io/tx/{signature_str}" + print(f"View transaction details on Solscan: {solscan_url}") + else: + print("Transaction was not finalized within the expected time.") + + return signature_str + else: + print(f"Error sending transaction: {response['error']}") + return None + + except Exception as e: + print(f"Exception occurred: {str(e)}") + return None + +### BUNDLE ### +async def confirm_landed_bundle(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempts: int = 60, delay: float = 2.0): + for attempt in range(max_attempts): + response = sdk.get_bundle_statuses([bundle_id]) + + if not response['success']: + print(f"Error confirming bundle status: {response.get('error', 'Unknown error')}") + await asyncio.sleep(delay) + + print(f"Confirmation attempt {attempt + 1}/{max_attempts}:") + print(json.dumps(response, indent=2)) + + if 'result' not in response['data']: + print(f"Unexpected response structure. 'result' not found in response data.") + await asyncio.sleep(delay) + + result = response['data']['result'] + if 'value' not in result or not result['value']: + print(f"Bundle {bundle_id} not found in confirmation response") + await asyncio.sleep(delay) + + bundle_status = result['value'][0] + if bundle_status['bundle_id'] != bundle_id: + print(f"Unexpected bundle ID in response: {bundle_status['bundle_id']}") + await asyncio.sleep(delay) + + status = bundle_status.get('confirmation_status') + + if status == 'finalized': + print(f"Bundle {bundle_id} has been finalized on-chain!") + # Extract transaction ID and construct Solscan link + if 'transactions' in bundle_status and bundle_status['transactions']: + tx_id = bundle_status['transactions'][0] + solscan_link = f"https://solscan.io/tx/{tx_id}" + print(f"Transaction details: {solscan_link}") + else: + print("Transaction ID not found in the response.") + return 'Finalized' + elif status == 'confirmed': + print(f"Bundle {bundle_id} is confirmed but not yet finalized. Checking again...") + elif status == 'processed': + print(f"Bundle {bundle_id} is processed but not yet confirmed. Checking again...") + else: + print(f"Unexpected status '{status}' during confirmation for bundle {bundle_id}") + + # Check for errors + err = bundle_status.get('err', {}).get('Ok') + if err is not None: + print(f"Error in bundle {bundle_id}: {err}") + return 'Failed' + + await asyncio.sleep(delay) + + print(f"Max confirmation attempts reached. Unable to confirm finalization of bundle {bundle_id}") + return 'Landed' + +async def check_bundle_status(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempts: int = 30, delay: float = 2.0): + for attempt in range(max_attempts): + response = sdk.get_inflight_bundle_statuses([bundle_id]) + + if not response['success']: + print(f"Error checking bundle status: {response.get('error', 'Unknown error')}") + await asyncio.sleep(delay) + continue + + print(f"Raw response (Attempt {attempt + 1}/{max_attempts}):") + print(json.dumps(response, indent=2)) + + if 'result' not in response['data']: + print(f"Unexpected response structure. 'result' not found in response data.") + await asyncio.sleep(delay) + continue + + result = response['data']['result'] + if 'value' not in result or not result['value']: + print(f"Bundle {bundle_id} not found in response") + await asyncio.sleep(delay) + continue + + bundle_status = result['value'][0] + status = bundle_status.get('status') + print(f"Attempt {attempt + 1}/{max_attempts}: Bundle status - {status}") + + if status == 'Landed': + print(f"Bundle {bundle_id} has landed on-chain! Performing additional confirmation...") + final_status = await confirm_landed_bundle(sdk, bundle_id) + return final_status + elif status == 'Failed': + print(f"Bundle {bundle_id} has failed.") + return status + elif status == 'Invalid': + if attempt < 5: # Check a few more times before giving up on Invalid(usually on start) + print(f"Bundle {bundle_id} is currently invalid. Checking again...") + else: + print(f"Bundle {bundle_id} is invalid (not in system or outside 5-minute window).") + return status + elif status == 'Pending': + print(f"Bundle {bundle_id} is still pending. Checking again in {delay} seconds...") + else: + print(f"Unknown status '{status}' for bundle {bundle_id}") + + await asyncio.sleep(delay) + + print(f"Max attempts reached. Final status of bundle {bundle_id}: {status}") + return status + +async def confirm_bundle(jito_client, bundle_id, timeout_seconds=60): + start_time = asyncio.get_event_loop().time() + + while asyncio.get_event_loop().time() - start_time < timeout_seconds: + try: + status = jito_client.get_bundle_statuses([[bundle_id]]) + print('Bundle status:', status) + + if status['success'] and bundle_id in status['data']['result']: + bundle_status = status['data']['result'][bundle_id] + if bundle_status['status'] == 'finalized': + print('Bundle has been finalized on the blockchain.') + return bundle_status + elif bundle_status['status'] == 'confirmed': + print('Bundle has been confirmed but not yet finalized.') + elif bundle_status['status'] == 'processed': + print('Bundle has been processed but not yet confirmed.') + elif bundle_status['status'] == 'failed': + raise Exception(f"Bundle failed: {bundle_status.get('error')}") + else: + print(f"Unknown bundle status: {bundle_status['status']}") + except Exception as error: + print('Error checking bundle status:', str(error)) + + # Wait for a short time before checking again + await asyncio.sleep(2) + + print(f"Bundle {bundle_id} has not finalized within {timeout_seconds}s, but it may still be in progress.") + return jito_client.get_bundle_statuses([bundle_id]) + + +async def basic_bundle(): + # Initialize connection to Solana testnet + solana_client = AsyncClient(appconfig.RPC_URL) + + wallet_keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + # Initialize JitoJsonRpcSDK + jito_client = JitoJsonRpcSDK(url="https://mainnet.block-engine.jito.wtf/api/v1") + + #Example using UUID + #jito_client = JitoJsonRpcSDK(url="https://mainnet.block-engine.jito.wtf/api/v1", uuid_var="YOUR_UUID" ) + + # Set up transaction parameters + receiver = Pubkey.from_string("EKhLZ5Kcwx6JxePx89sVBknJsyuWmLWC3pPRga2jpump") + jito_tip_account = Pubkey.from_string(jito_client.get_random_tip_account()) + jito_tip_amount = 1000 # lamports + transfer_amount = 1000 # lamports + + # Memo program ID + memo_program_id = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") + + # Create instructions + transfer_ix = transfer(TransferParams( + from_pubkey=wallet_keypair.pubkey(), + to_pubkey=receiver, + lamports=transfer_amount + )) + + tip_ix = transfer(TransferParams( + from_pubkey=wallet_keypair.pubkey(), + to_pubkey=jito_tip_account, + lamports=jito_tip_amount + )) + + memo_ix = Instruction( + program_id=memo_program_id, + accounts=[], + data=bytes("Let's Jito!", "utf-8") + ) + + # Get recent blockhash + recent_blockhash = await solana_client.get_latest_blockhash() + + # Create the transaction + message = Message.new_with_blockhash( + [transfer_ix, tip_ix, memo_ix], + wallet_keypair.pubkey(), + recent_blockhash.value.blockhash + ) + transaction = Transaction.new_unsigned(message) + + # Sign the transaction + transaction.sign([wallet_keypair], recent_blockhash.value.blockhash) + + # Serialize and base58 encode the entire signed transaction + serialized_transaction = base64.b64encode(bytes(transaction)).decode('ascii') + + try: + # Prepare the bundle request + bundle_request = [serialized_transaction] + print(f"Sending bundle request: {json.dumps(bundle_request, indent=2)}") + + # Send the bundle using sendBundle method + result = jito_client.send_bundle(bundle_request) + print('Raw API response:', json.dumps(result, indent=2)) + + if result['success']: + bundle_id = result['data']['result'] + print(f"Bundle sent successfully. Bundle ID: {bundle_id}") + + # Check the status of the bundle + final_status = await check_bundle_status(jito_client, bundle_id, max_attempts=30, delay=2.0) + + if final_status == 'Finalized': + print("Bundle has been confirmed and finalized on-chain.") + elif final_status == 'Landed': + print("Bundle has landed on-chain but could not be confirmed as finalized within the timeout period.") + else: + print(f"Bundle did not land on-chain. Final status: {final_status}") + else: + print(f"Failed to send bundle: {result.get('error', 'Unknown error')}") + + except Exception as error: + print('Error sending or confirming bundle:', str(error)) + + # Close the Solana client session + await solana_client.close() + + +async def basic_txn(): + solana_client = AsyncClient("https://api.mainnet-beta.solana.com") + sdk = JitoJsonRpcSDK(url=appconfig.RPC_URL) + + sender = Keypair.from_bytes(bytes(appconfig.PRIVKEY)) + receiver = Pubkey.from_string() + + print(f"Sender public key: {sender.pubkey()}") + print(f"Receiver public key: {receiver}") + + priority_fee = 1000 # Lamport for priority fee + amount = 1000 # Lamports to transfer to receiver + jito_tip_amount = 1000 # Lamports for Jito tip + + signature = await send_transaction_with_priority_fee(sdk, solana_client, sender, receiver, amount, jito_tip_amount, priority_fee) + + if signature: + print(f"Transaction process completed. Signature: {signature}") + + await solana_client.close() + +asyncio.run(basic_bundle()) diff --git a/bot/module/pump.py b/bot/module/pump.py index 541644b..a245997 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -57,6 +57,7 @@ class Redis(Enum): cleanTokens = "cleanTokens" + class TradeRoadmap: test = [ {"step": 0, "name": "test", "subscription": Suscription.subscribeNewToken}, @@ -200,6 +201,7 @@ def __init__( self.trade_fees = appconfig.FEES + def start_scanner(self): self.scanner_start_time = datetime.now() @@ -208,14 +210,14 @@ def get_balance(self, public_key): return balance def get_tkn_balance(self, wallet_pubkey, token_account): - # token_balance = asyncio.run(get_own_token_balance( - # wallet_pubkey=wallet_pubkey, - # token_mint_addres=token_account - # ) - # ) + token_balance = asyncio.run(get_own_token_balance( + wallet_pubkey=wallet_pubkey, + token_mint_addres=token_account + ) + ) # TODO: get balance from redis # BYPASS - token_balance = 0 + #token_balance = 0 return token_balance def increase_fees(self): @@ -816,7 +818,34 @@ def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> break return is_valid, exit_criteria - + + def prepare_data( + self, + keypair: Keypair, + txtype: TxType, + token_address: str, + amount: float = appconfig.TRADING_DEFAULT_AMOUNT, + slippage: float = appconfig.SLIPPAGE, + priority_fee: float = appconfig.FEES + ): + """ + Prepare body data when transaction will go through setted RPC endpoint + """ + amount = amount if txtype.value == TxType.buy.value else "100%" + denominated_in_sol = "true" if txtype.value == TxType.buy.value else "false" + + data = { + "publicKey": str(keypair.pubkey()), + "action": txtype.value, + "mint": token_address, + "amount": amount, # amount of SOL or tokens to trade. Can be "100%" when selling + "denominatedInSol": denominated_in_sol, # "true" if amount is amount of SOL, "false" if amount is number of tokens + "slippage": slippage, # percent slippage allowed + "priorityFee": priority_fee, # amount to use as priority fee + "pool": "pump" # exchange to trade on. "pump" or "raydium" + } + + return data def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = None) -> str: """ @@ -842,20 +871,13 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No if self.tokens[token]["exit_criteria"] == "validate_trade_timedelta_exceeded": self.increase_fees() - denominated_in_sol = "true" if txtype.value == TxType.buy.value else "false" - amount = amount if txtype.value == TxType.buy.value else "100%" - # BUYING/SELLING tokens with an amount of Solana - data = { - "publicKey": str(keypair.pubkey()), - "action": txtype.value, - "mint": token, - "amount": amount, # amount of SOL or tokens to trade. Can be "100%" when selling - "denominatedInSol": denominated_in_sol, # "true" if amount is amount of SOL, "false" if amount is number of tokens - "slippage": appconfig.SLIPPAGE, # percent slippage allowed - "priorityFee": appconfig.FEES, # amount to use as priority fee - "pool": "pump" # exchange to trade on. "pump" or "raydium" - } + data = self.prepare_data( + keypair=keypair, + txtype=txtype, + token_address=token, + amount=amount + ) response = None retries = 0 @@ -918,6 +940,9 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No vst = VersionedTransaction.from_bytes(response.content) msg = vst.message + + print(msg.to_json()) + tx = VersionedTransaction( msg, [keypair] @@ -962,21 +987,104 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No return txSignature - def market_making( - self, - token: str, - amount: int = appconfig.MARKETMAKING_SOL_BUY_AMOUNT - ): - """marketMaking will buy an amount of tokens and inmediatelly sell them + def nuke(self, token: str, keypair: Keypair, amount: float = appconfig.TRADING_DEFAULT_AMOUNT) -> List[str]: + import base58 + txSignatures = [] + try: + buy_data = self.prepare_data( + keypair=keypair, + txtype=TxType.buy, + token_address=token, + amount=amount + ) + sell_data = self.prepare_data( + keypair=keypair, + txtype=TxType.sell, + token_address=token, + amount=amount + ) + response = requests.post( + appconfig.PUMPFUN_TRANSACTION_URL, + headers={"Content-Type": "application/json"}, + json=[buy_data, sell_data] + ) - Args: - token (str): token mint address - amount (int): amount of Solana - """ - keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + if response.status_code != 200: + print("Failed to generate transactions.") + print(response.reason) + else: + encodedTransactions = response.json() + encodedSignedTransactions = [] + txSignatures = [] + + def get_tokens(message): + # Constants + from solders.pubkey import Pubkey + TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + TRANSFER_INSTRUCTION_INDEX = 3 # Index for "transfer" instruction in SPL Token Program. + + # Iterate through the instructions + for i, instruction in enumerate(message.instructions): + # Check if the program_id matches the Token Program ID + if instruction.program_id == TOKEN_PROGRAM_ID: + # Instruction data contains the operation type + data = instruction.data + if len(data) > 0 and data[0] == TRANSFER_INSTRUCTION_INDEX: + print(f"Instruction {i} is a Token Program 'transfer'") + print("Accounts involved:", instruction.accounts) + print("Instruction data (raw):", data) + + for index, encodedTransaction in enumerate(encodedTransactions): + msg = VersionedTransaction.from_bytes( + base58.b58decode(encodedTransaction) + ).message + get_tokens(message=msg) + signedTx = VersionedTransaction( + msg, + [keypair] + ) + encodedSignedTransactions.append(base58.b58encode(bytes(signedTx)).decode()) + txSignatures.append(str(signedTx.signatures[0])) - txn = self.buy(token=token, amount=amount, keypair=keypair) + jito_response = requests.post( + appconfig.RPC_URL + "/bundles", + headers={"Content-Type": "application/json"}, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "sendBundle", + "params": [ + encodedSignedTransactions + ] + } + ) + if jito_response.status_code == 200: + for i, signature in enumerate(txSignatures): + print(f'Nuke-> Transaction {i}: https://solscan.io/tx/{signature}') + else: + print("Nuke-> error sending jito: {}".format(jito_response)) + + except Exception as e: + print(e) + + return txSignatures + + +def study(): + from solders.pubkey import Pubkey + from solana.rpc.api import Client + import struct + program_id = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com" + client = Client(SOLANA_RPC_URL) + + # Token Mint IDs + token_a = Pubkey.from_string("CXXsWzhbzNZLDosyebxqMtoJtnxYLRg4hTxudgoApump") + + seeds = [bytes(token_a)] + pool_address, _ = Pubkey.find_program_address(seeds, program_id) + print("Liquidity Pool Address:", pool_address) def test(): messages = [ @@ -1114,6 +1222,7 @@ def test(): } ] + tokens = {"mint_address": {"trades": []}} mint = "mint_address" trading_amount = 0.45 @@ -1123,24 +1232,32 @@ def test(): trader_type=Trader.sniper ) - if "trades" not in tokens[mint]: - tokens[mint]["trades"] = [] - - for msg in messages: - # Doing some analytics like how many continuous buys have happend, etc - new_msg = trading_analytics( - msg=msg, - previous_trades=tokens[mint]["trades"], - amount_traded=trading_amount, - pubkey=keypair.pubkey() - ) - # Including last message with new metadata into trades list - tokens[mint]["trades"].append(new_msg) - exit_trade, exis_criteria = pump.validate_criteria(msg=new_msg, criteria=TradeRoadmap.sniper_1[2]["criteria"]) - if exit_trade: - print("Pump.test -> Criteria out: {}".format(exis_criteria)) - - - print(tokens[mint]["trades"]) + # if "trades" not in tokens[mint]: + # tokens[mint]["trades"] = [] + + # for msg in messages: + # # Doing some analytics like how many continuous buys have happend, etc + # new_msg = trading_analytics( + # msg=msg, + # previous_trades=tokens[mint]["trades"], + # amount_traded=trading_amount, + # pubkey=keypair.pubkey() + # ) + # # Including last message with new metadata into trades list + # tokens[mint]["trades"].append(new_msg) + # exit_trade, exis_criteria = pump.validate_criteria(msg=new_msg, criteria=TradeRoadmap.sniper_1[2]["criteria"]) + # if exit_trade: + # print("Pump.test -> Criteria out: {}".format(exis_criteria)) + # print(tokens[mint]["trades"]) + token={"mint": "ziFYNEyHmgJPGsmSA88W1fW5EUtH2VcxZhcuyxRpump"} + pump.add_update_token(token=token) + print("Balance before: {}".format(pump.balance)) + txn_list = pump.nuke( + token=token["mint"], + keypair=keypair, + amount=1.48 + ) + balance = pump.get_balance(public_key=keypair.pubkey()) + print("Balance after: {}".format(balance)) -#test() +#study() diff --git a/bot/requirements.txt b/bot/requirements.txt index 46c4701..89898a8 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -13,3 +13,4 @@ redis==4.2.0 redisearch==2.0.0 nest_asyncio==1.6.0 base58==2.1.1 +jito_py_rpc==0.1.0 From 9874c804db71dd2cc219844b800bd2aabac5d65a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 15 Dec 2024 21:56:19 +0100 Subject: [PATCH 034/103] Sniper2: detecting artifical pumps on dummy mode. unit testing done --- bot/config/__init__.py | 2 +- bot/libs/criterias.py | 86 ++++++- bot/libs/utils.py | 12 +- bot/main.py | 4 +- bot/module/pump.py | 123 ++++++--- bot/tests/libs/fixtures_criterias.py | 13 + bot/tests/libs/test_criterias.py | 368 ++++++++++++++++++++++++++- 7 files changed, 550 insertions(+), 58 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 49d8c1c..f1210d2 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -12,7 +12,7 @@ class AppMode(Enum): class AuthConfig: # our code environment ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") - APPMODE = os.environ.get("APPMODE", AppMode.real.value) + APPMODE = os.environ.get("APPMODE", AppMode.dummy.value) TIME_FORMAT = "%b %-d %-I:%M:%S %p" diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 2034943..732bf4a 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Dict, List from bot.config import appconfig -from bot.libs.utils import TxType +from bot.libs.utils import stamp_time, TxType from solders.pubkey import Pubkey @@ -35,7 +35,8 @@ def trading_analytics( is_this_my_trade = new_msg["traderPublicKey"] == str(pubkey) # Including timestamp in incomming message - new_msg["timestamp"] = datetime.now().timestamp() + current_time = datetime.now() + new_msg["timestamp"] = current_time.timestamp() new_msg["trade_time_delta"] = 0 # Calculating timedelta between buy/sell and received message if is_this_my_trade: @@ -90,9 +91,12 @@ def trading_analytics( new_msg["max_consecutive_buys"] = [ { "quantity": new_msg["consecutive_buys"], - "sols": amount_traded if aprox == 0 else aprox if not is_this_my_trade else 0 + "sols": amount_traded if aprox == 0 else aprox if not is_this_my_trade else 0, + "stamp_times": stamp_time(time=current_time) } ] + # This'll be used to detect possible bots producing fake pumps + new_msg["consecutive_buys_timestamps"] = stamp_time(time=current_time) else: last_msg = copy.deepcopy(previous_trades[-1]) @@ -113,12 +117,17 @@ def trading_analytics( # We just count non relevant trades with consecutives buys/sells new_msg["is_non_relevant_trade_count"] = 0 if new_msg["is_relevant_trade"] else 1 - # We always keep track of the consecutive buys + # We always keep track of the consecutive buys and their time stamps new_msg["max_consecutive_buys"] = last_msg["max_consecutive_buys"] + new_msg["consecutive_buys_timestamps"] = last_msg["consecutive_buys_timestamps"] if new_msg["txType"].lower() == TxType.buy.value: new_msg["consecutive_sells"] = 0 new_msg["seconds_between_sells"] = 0 + new_msg["consecutive_buys_timestamps"] = stamp_time( + time=current_time, + time_stored=new_msg["consecutive_buys_timestamps"] + ) if last_msg["txType"].lower() == TxType.buy.value: # Starting counter for non relevant trades with consecutives buys/sells @@ -134,7 +143,7 @@ def trading_analytics( new_msg["is_non_relevant_trade_count"] = 0 new_msg["consecutive_buys"] = 1 + last_msg["consecutive_buys"] - new_msg["seconds_between_buys"] = (datetime.now() - last_msg_timestamp).total_seconds() + new_msg["seconds_between_buys"] = (current_time - last_msg_timestamp).total_seconds() # Updating the last record if not is_this_my_trade: new_msg["max_consecutive_buys"][-1]["quantity"] = 1 + last_msg["consecutive_buys"] @@ -265,6 +274,7 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amo max_sols = amount_traded * percentage / 100 return sols >= max_sols + def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: float = None) -> bool: """ Checks if the timedelta in seconds between the buy/sell and the message received is acceptable or not @@ -280,6 +290,7 @@ def validate_trade_timedelta_exceeded(expected: bool, msg: dict, amount_traded: def seller_is_an_unknown_trader(expected: bool, msg: dict, amount_traded: float = None) -> bool: return msg["seller_is_an_unknown_trader"] and expected + def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: """ Dummy fuction as it retrieves False @@ -293,6 +304,71 @@ def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = Non """ return False +def market_inactivity(seconds: int, msg: dict = None, amount_traded: float = None) -> bool: + """ + Dummy fuction as it retrieves False + + Args: + seconds (int): The maximum number of seconds of inactivity allowed. + msg (dict): The message dictionary containing context. + + Returns: + bool: True if the condition is met, otherwise False. + """ + return False + +def buys_in_the_same_second(validations: dict, msg: dict, amount_traded: float = None) -> bool: + """ + Check if any of the timestamp values are within a given range. + + :param msg: dict, keys are timestamps, values are the counts of buy actions on each timestamp. + :param validations: dict[str: int]. Validations to be done: min buys per each timestamp and min consecutive relevant timestamps. + :return: bool, True if any value is within the range, False otherwise. + """ + min_buys = False + cbt = msg["consecutive_buys_timestamps"] + token_age = validations["seconds_since_token_genesis"] + if len(cbt) >= validations["min_consecutive_timestamps"]: + # Checking if we're among the first expected seconds of a token genesis. + sorted_keys = sorted(cbt.keys()) + first_timestamp = datetime.strptime(sorted_keys[0], "%Y%m%d%H%M%S") + last_timestamp = datetime.strptime(sorted_keys[-1], "%Y%m%d%H%M%S") + difference = (last_timestamp - first_timestamp).total_seconds() + if int(difference) <= token_age: + counter = 0 + for tmstmp, times in cbt.items(): + # We might have trades among the possible artifical pump genesis. + # If this happends then we'll not consider that timestamp + counter = counter + 1 if times >= validations["min_buys_per_timestamp"] else counter + min_buys = counter >= validations["min_consecutive_timestamps"] + + return min_buys + + +def discard_token_max_seconds_between_buys(seconds: float, msg: dict, amount_traded: float = None) -> bool: + return max_seconds_between_buys(seconds=seconds, msg=msg) + + +def max_seconds_in_market(time_in_market: int, msg: dict, amount_traded: float = None) -> bool: + """ + Check if the max expected seconds for being in the market has been reached. + + :param time_in_market: int, seconds to be in market + :param msg: dict, keys are timestamps, values are the counts of buy actions on each timestamp. + :return: bool, True if any value is within the range, False otherwise. + """ + cbt = msg["consecutive_buys_timestamps"] + sorted_keys = sorted(cbt.keys()) + first_timestamp = datetime.strptime(sorted_keys[0], "%Y%m%d%H%M%S") + current_time_in_market = (datetime.now() - first_timestamp).total_seconds() + return current_time_in_market >= time_in_market + + +def discard_max_seconds_in_market(time_in_market: int, msg: dict, amount_traded: float = None) -> bool: + return max_seconds_in_market( + time_in_market=time_in_market, + msg=msg + ) def test(): print("#######################") diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 4b9b5a0..6fcb7f3 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -3,6 +3,7 @@ import json import struct +from datetime import datetime from enum import Enum from solana.rpc.async_api import AsyncClient from solana.rpc.types import TokenAccountOpts @@ -167,7 +168,6 @@ def get_instructions_from_message(msg: Message): return parsed_instructions - def include_instruction(msg: MessageV0): """ Under development @@ -216,3 +216,13 @@ def include_instruction(msg: MessageV0): else: print("Duplicate instruction detected, skipping.") return msg + +def stamp_time(time: datetime, time_stored: dict = None) -> dict: + time_stored = {} if time_stored is None else time_stored + tstamp = time.strftime("%Y%m%d%H%M%S") + if tstamp not in time_stored: + time_stored[tstamp] = 1 + else: + time_stored[tstamp] += 1 + + return time_stored diff --git a/bot/main.py b/bot/main.py index c2e2a74..589451a 100644 --- a/bot/main.py +++ b/bot/main.py @@ -7,7 +7,7 @@ async def main(): pump = Pump( - executor_name="sniper1", + executor_name="sniper2", trader_type=Trader.sniper ) @@ -17,7 +17,7 @@ async def main(): ) tasks = [ - pump.subscribe(steps=TradeRoadmap.sniper_1), + pump.subscribe(steps=TradeRoadmap.sniper_2_detect_artifical_pump), #scanner.subscribe(steps=TradeRoadmap.scanner) ] start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() diff --git a/bot/module/pump.py b/bot/module/pump.py index a245997..92481ce 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -16,7 +16,11 @@ max_sols_in_token_after_buying_in_percentage, trader_has_sold, seller_is_an_unknown_trader, - market_inactivity + market_inactivity, + buys_in_the_same_second, + discard_token_max_seconds_between_buys, + max_seconds_in_market, + discard_max_seconds_in_market ) from bot.libs import criterias as criteria_functions from bot.libs.utils import ( @@ -119,7 +123,7 @@ class TradeRoadmap: "name": "SAVE_TOKEN_IN_REDIS", "redis": Redis.saveToken, "criteria": { - "capacity": 1 # How many tokens will be written to redis at the same time + "capacity": 10 # How many tokens will be written to redis at the same time } } }, @@ -165,6 +169,58 @@ class TradeRoadmap: {"step": 4, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, {"step": 5, "name": "CLOSE_TOKEN", "redis": Redis.closeToken}, ] + sniper_2_detect_artifical_pump = [ + { + "step": 0, + "name": "WAIT_FOR_TOKEN", + "redis": Redis.readToken, + "criteria": { + "use_all_balance": True # If True, all balance will be used if balance < trading amount + } + }, + { + "step": 1, + "name": "TOKEN_SUBSCRIPTION", + "subscription": Suscription.subscribeTokenTrade, + # Exit criteria to move on to the next step + "criteria": { + "buys_in_the_same_second": { + "min_buys_per_timestamp": 3, + "min_consecutive_timestamps": 3, + "seconds_since_token_genesis": 10, + }, + "discard_token_max_seconds_between_buys": 3, + "discard_max_seconds_in_market": 10, + "discard_market_inactivity": 20, + }, + "on_discard_token_go_to_step": 5 + }, + { + "step": 2, + "name": "TRADE_TOKEN_BUY", + "action": TxType.buy, + "on_error_go_to_step": 5 + }, + { + "step": 3, + "name": "TOKEN_SUBSCRIPTION", + "subscription": Suscription.subscribeTokenTrade, + # Exit criteria to move on to the next step + "criteria": { + "market_inactivity": 10, + "max_sols_in_token_after_buying_in_percentage": 500, + "max_seconds_in_market": 30, + } + }, + { + "step": 4, + "name": "TRADE_TOKEN_SELL", + "action": TxType.sell, + "on_error_go_to_step": 5 + }, + {"step": 5, "name": "UNSUBSCRIBE_TO_TOKEN", "subscription": Suscription.unsubscribeTokenTrade}, + {"step": 6, "name": "CLOSE_TOKEN", "redis": Redis.closeToken}, + ] # Patch the running event loop. Required to get wallet's balance @@ -210,14 +266,16 @@ def get_balance(self, public_key): return balance def get_tkn_balance(self, wallet_pubkey, token_account): - token_balance = asyncio.run(get_own_token_balance( - wallet_pubkey=wallet_pubkey, - token_mint_addres=token_account - ) - ) - # TODO: get balance from redis - # BYPASS - #token_balance = 0 + token_balance = 0 + if appconfig.APPMODE not in [AppMode.dummy.value, AppMode.simulation.value]: + # TODO: get balance from redis + # BYPASS + token_balance = 0 + # token_balance = asyncio.run(get_own_token_balance( + # wallet_pubkey=wallet_pubkey, + # token_mint_addres=token_account + # ) + # ) return token_balance def increase_fees(self): @@ -567,6 +625,8 @@ async def subscribe(self, steps: list): if "criteria" in step: if "market_inactivity" in step["criteria"]: websocket_timeout = step["criteria"]["market_inactivity"] + if "discard_market_inactivity" in step["criteria"]: + websocket_timeout = step["criteria"]["discard_market_inactivity"] if suscription.value == Suscription.subscribeAccountTrade.value: payload["keys"] = self.accounts @@ -662,14 +722,21 @@ async def subscribe(self, steps: list): print("Exiting subscription criteria: {}".format( self.tokens[mint]["exit_criteria"] )) + if "discard_" in self.tokens[mint]["exit_criteria"]: + step_index = step["on_discard_token_go_to_step"] + break except asyncio.TimeoutError: print("No trades detected during {} seconds. Moving to next step.".format( websocket_timeout )) - self.tokens[mint]["exit_criteria"] = "market_inactivity" step_index += 1 + self.tokens[mint]["exit_criteria"] = "market_inactivity" + + if "discard_market_inactivity" in step["criteria"]: + step_index = step["on_discard_token_go_to_step"] + move_to_next_step = False break @@ -764,34 +831,6 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: # TODO: relase tokens to snipers with redis recods. At this moment we're listening to one token only return move_to_next_step - - def token_copytrade_suscription(self, msg: str) -> bool: - # TODO: remove this temporal validation for testing - # if not self.traders and "txType" in msg: - # if TxType.buy.value == msg["txType"]: - # self.add_trader(trader=msg["traderPublicKey"]) - - if "traderPublicKey" in msg and msg["traderPublicKey"] in self.traders: - if TxType.buy.value == msg["txType"]: - print("Starting pumping bot. Listening to trader: {} - Buy: {}. MarketCap: {}".format( - msg["traderPublicKey"][:6], - msg["tokenAmount"], - msg["marketCapSol"] - ) - ) - - if TxType.sell.value == msg["txType"]: - print("Stoping pumping bot. Sell: {}. MarketCap: {}".format( - msg["tokenAmount"], - msg["marketCapSol"] - ) - ) - self.traders = [] - return True - - return False - - def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> bool: """ This function takes the incomming trading message from Pump.fun previouly @@ -811,8 +850,9 @@ def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> is_valid = function(parameter, msg, amount_traded) print(f"validate_criteria-> Function {function_name} returned: {is_valid}") else: - print(f"validate_criteria-> Error: {function_name} not found.") - + if appconfig.APPMODE in [AppMode.dummy.value]: + print(f"*** validate_criteria-> WARNING: {function_name} not found.") + if is_valid: exit_criteria = function_name break @@ -986,7 +1026,6 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No return txSignature - def nuke(self, token: str, keypair: Keypair, amount: float = appconfig.TRADING_DEFAULT_AMOUNT) -> List[str]: import base58 txSignatures = [] diff --git a/bot/tests/libs/fixtures_criterias.py b/bot/tests/libs/fixtures_criterias.py index bcc5d13..1005494 100644 --- a/bot/tests/libs/fixtures_criterias.py +++ b/bot/tests/libs/fixtures_criterias.py @@ -14,3 +14,16 @@ def get_max_consecutive_buys(): @pytest.fixture def get_max_seconds_between_buys(): yield 3 + +@pytest.fixture +def get_artifical_pump_validations(): + validations = { + "min_buys_per_timestamp": 3, + "min_consecutive_timestamps": 3, + "seconds_since_token_genesis": 10, + } + yield validations + +@pytest.fixture +def get_max_seconds_in_market(): + yield 30 diff --git a/bot/tests/libs/test_criterias.py b/bot/tests/libs/test_criterias.py index 7306f18..7574973 100644 --- a/bot/tests/libs/test_criterias.py +++ b/bot/tests/libs/test_criterias.py @@ -13,7 +13,9 @@ trader_has_sold, validate_trade_timedelta_exceeded, seller_is_an_unknown_trader, - max_sols_in_token_after_buying_in_percentage + max_sols_in_token_after_buying_in_percentage, + buys_in_the_same_second, + max_seconds_in_market ) @@ -193,7 +195,8 @@ def test_exit_on_first_sale(expected_result, msg, description): "quantity":1, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -285,7 +288,8 @@ def test_trading_analytics_buys( "quantity":1, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -333,7 +337,8 @@ def test_trading_analytics_buys( "quantity":1, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -418,7 +423,8 @@ def test_max_seconds_between_buys( "quantity":1, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -468,7 +474,8 @@ def test_max_seconds_between_buys( "quantity":1, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -550,7 +557,8 @@ def test_trader_has_sold( "quantity":2, "sols":1.6 } - ] + ], + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -797,6 +805,7 @@ def test_trade_timedelta( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -847,6 +856,7 @@ def test_trade_timedelta( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -897,6 +907,7 @@ def test_trade_timedelta( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -947,6 +958,7 @@ def test_trade_timedelta( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 0.5, @@ -1031,6 +1043,7 @@ def test_seller_is_an_unknown_trader( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 2.0, @@ -1081,6 +1094,7 @@ def test_seller_is_an_unknown_trader( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 2.0, @@ -1131,6 +1145,7 @@ def test_seller_is_an_unknown_trader( } ], "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": {"20241212121212": 1} } ], 1.0, @@ -1159,3 +1174,342 @@ def test_max_sols_in_token_after_buying( token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} ) assert max_sols_in_token_after_buying_in_percentage(percentage=percentage, msg=new_msg, amount_traded=amount) == expected_result, description + + +@pytest.mark.parametrize( + """ + description, + txType, + msg, + previous_trades, + expected_result + """, + [ + ( + "Buy: No artifical pump is found", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": { + (datetime.now() - timedelta(seconds=2)).strftime("%Y%m%d%H%M%S"): 2, + (datetime.now() - timedelta(seconds=1)).strftime("%Y%m%d%H%M%S"): 3, + (datetime.now() - timedelta(seconds=0)).strftime("%Y%m%d%H%M%S"): 1 + } + }, + ], + False + ), + ( + "Buy: artifical pump was found!", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": { + (datetime.now() - timedelta(seconds=3)).strftime("%Y%m%d%H%M%S"): 4, + (datetime.now() - timedelta(seconds=2)).strftime("%Y%m%d%H%M%S"): 2, + (datetime.now() - timedelta(seconds=1)).strftime("%Y%m%d%H%M%S"): 3, + (datetime.now() - timedelta(seconds=0)).strftime("%Y%m%d%H%M%S"): 7 + } + }, + ], + True + ), + + ( + "Buy: artifical pump was found OUTSIDE expected seconds since token genesis", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": { + (datetime.now() - timedelta(seconds=13)).strftime("%Y%m%d%H%M%S"): 4, + (datetime.now() - timedelta(seconds=6)).strftime("%Y%m%d%H%M%S"): 2, + (datetime.now() - timedelta(seconds=3)).strftime("%Y%m%d%H%M%S"): 3, + (datetime.now() - timedelta(seconds=0)).strftime("%Y%m%d%H%M%S"): 7 + } + }, + ], + False + ), + ] +) +def test_buys_in_the_same_second( + description, + txType, + msg, + previous_trades, + expected_result, + get_artifical_pump_validations +): + tkey = "{}_timestamp".format(txType) + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=0.1, + pubkey=get_pubkey, + traders=[], + token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} + ) + result = buys_in_the_same_second( + validations=get_artifical_pump_validations, + msg=new_msg + ) + assert result == expected_result, description + +@pytest.mark.parametrize( + """ + description, + txType, + msg, + previous_trades, + expected_result + """, + [ + ( + "Buy: Still in market", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": { + (datetime.now() - timedelta(seconds=2)).strftime("%Y%m%d%H%M%S"): 2, + (datetime.now() - timedelta(seconds=1)).strftime("%Y%m%d%H%M%S"): 3, + (datetime.now() - timedelta(seconds=0)).strftime("%Y%m%d%H%M%S"): 1 + } + }, + ], + False + ), + ( + "Buy: max seconds in market has beed reached!", + "buy", + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"4xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000.00, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.00, + "vSolInBondingCurve":32.500, + "marketCapSol":32.001 + }, + [ + { + "signature":"xxxx", + "mint":"4Wo7nxVsPV125DW3Tr2ppPrzrnNFwidiKjWyVsifpump", + "traderPublicKey":"1xxMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "txType":"buy", + "tokenAmount":10000000.0, + "newTokenBalance":30582206.734745, + "bondingCurveKey":"HPWxfYdBitgdK4VcevMgE1VHaKgpcKJWiJh9dFAsP6SE", + "vTokensInBondingCurve":200000000.0, + "vSolInBondingCurve":32.0, + "marketCapSol":33.0, + "timestamp":1732963950.837367, + "is_relevant_trade":True, + "consecutive_buys":1, + "consecutive_sells":0, + "vSolInBondingCurve_Base":30.4, + "is_non_relevant_trade_count":0, + "seconds_between_buys":0, + "seconds_between_sells":0, + "market_inactivity":0, + "max_seconds_in_market":0, + "max_consecutive_buys":[ + { + "quantity":2, + "sols":1.6 + } + ], + "seller_is_an_unknown_trader": False, + "consecutive_buys_timestamps": { + (datetime.now() - timedelta(seconds=31)).strftime("%Y%m%d%H%M%S"): 4, + (datetime.now() - timedelta(seconds=2)).strftime("%Y%m%d%H%M%S"): 2, + (datetime.now() - timedelta(seconds=1)).strftime("%Y%m%d%H%M%S"): 3, + (datetime.now() - timedelta(seconds=0)).strftime("%Y%m%d%H%M%S"): 7 + } + }, + ], + True + ), + ] +) +def test_max_seconds_in_market( + description, + txType, + msg, + previous_trades, + expected_result, + get_max_seconds_in_market +): + tkey = "{}_timestamp".format(txType) + new_msg = trading_analytics( + msg=msg, + previous_trades=previous_trades, + amount_traded=0.1, + pubkey=get_pubkey, + traders=[], + token_timestamps={tkey:(datetime.now() - timedelta(seconds=0)).timestamp()} + ) + result = max_seconds_in_market( + time_in_market=get_max_seconds_in_market, + msg=new_msg + ) + assert result == expected_result, description + From 9928fa7ce3413e6686b69822b8492b6c3282867b Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 17 Dec 2024 22:34:33 +0100 Subject: [PATCH 035/103] Solving bugs: websocket now pings back to keep conection alive. Olds tokens are noe closed. Refactoring: Subcription is now done in token_trade_subscription function. Subcription is handles in a try catch to deal with websockets exceptions. --- bot/config/__init__.py | 4 +- bot/domain/redis_db.py | 15 +- bot/libs/criterias.py | 2 +- bot/module/pump.py | 362 ++++++++++++++++++++++++++--------------- 4 files changed, 238 insertions(+), 145 deletions(-) diff --git a/bot/config/__init__.py b/bot/config/__init__.py index f1210d2..69c8a17 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -12,7 +12,7 @@ class AppMode(Enum): class AuthConfig: # our code environment ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") - APPMODE = os.environ.get("APPMODE", AppMode.dummy.value) + APPMODE = os.environ.get("APPMODE", AppMode.simulation.value) TIME_FORMAT = "%b %-d %-I:%M:%S %p" @@ -55,7 +55,7 @@ class AuthConfig: TRADING_TOKENS_AT_THE_SAME_TIME = 1 TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 TRADING_RETRIES = 3 - TRADING_TOKEN_TOO_OLD_SECONDS = 15 + TRADING_TOKEN_TOO_OLD_SECONDS = 5 TRADING_MARKETING_INACTIVITY_TIMEOUT = 60 SCANNER_MIN_TRADING_AMOUNT = 1.00 # Min Sols a token must have as first buy to be considered for trading SCANNER_WRITTING_CAPACITY = 1 # How many tokens to scanned will write at the same time in Redis diff --git a/bot/domain/redis_db.py b/bot/domain/redis_db.py index d6edbaa..30473b6 100644 --- a/bot/domain/redis_db.py +++ b/bot/domain/redis_db.py @@ -108,7 +108,7 @@ def update_token( action: TxType = None, amount: float = None, trader: Trader = None, - is_checked: bool = True, + is_checked: bool = False, is_closed: bool = False, balance: float = None, token_balance: float = None, @@ -122,22 +122,19 @@ def update_token( #data["timestamp"] = data["timestamp"].isoformat() # Prepare the data to update - trading_time = datetime.now().timestamp() + current_time = datetime.now().timestamp() new_data = {} - if not is_checked: + if is_checked: new_data = { - "checked_time": trading_time, - "is_checked": True, + "checked_time": current_time, + "is_checked": is_checked, "is_traded": False, "is_closed": is_closed, "trades": [] # We'll keep track of every buy/sell during our trade } else: - for trade in trades: - trade["timestamp"] = trade["timestamp"] - new_data = { - "{}_time".format(action.value): trading_time, # New trading time as timestamp + "{}_time".format(action.value): current_time, # New trading time as timestamp "{}_txn".format(action.value): txn, # New transaction string "is_traded": True, "{}_amount".format(action.value): amount, # Specifying the amount of buy/sell action diff --git a/bot/libs/criterias.py b/bot/libs/criterias.py index 732bf4a..50cfd18 100644 --- a/bot/libs/criterias.py +++ b/bot/libs/criterias.py @@ -270,7 +270,7 @@ def max_sols_in_token_after_buying_in_percentage(percentage: int, msg: dict, amo Returns: bool: True if the condition is met, otherwise False. """ - sols = msg["max_consecutive_buys"][-1]["sols"] + sols = msg["sols_in_token_after_buying"] max_sols = amount_traded * percentage / 100 return sols >= max_sols diff --git a/bot/module/pump.py b/bot/module/pump.py index 92481ce..46744a9 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -6,6 +6,7 @@ import websockets import ssl import certifi +import websockets.connection import websockets.exceptions from bot.libs.criterias import ( @@ -175,7 +176,8 @@ class TradeRoadmap: "name": "WAIT_FOR_TOKEN", "redis": Redis.readToken, "criteria": { - "use_all_balance": True # If True, all balance will be used if balance < trading amount + "use_all_balance": True, # If True, all balance will be used if balance < trading amount + "age_tolerance ": 2, # Acceptable seconds since token creation in redis } }, { @@ -191,7 +193,7 @@ class TradeRoadmap: }, "discard_token_max_seconds_between_buys": 3, "discard_max_seconds_in_market": 10, - "discard_market_inactivity": 20, + "discard_market_inactivity": 10, }, "on_discard_token_go_to_step": 5 }, @@ -209,8 +211,8 @@ class TradeRoadmap: "criteria": { "market_inactivity": 10, "max_sols_in_token_after_buying_in_percentage": 500, - "max_seconds_in_market": 30, - } + "max_seconds_in_market": 60, + }, }, { "step": 4, @@ -351,12 +353,20 @@ async def subscribe(self, steps: list): # Keepalive main loop while not self.stop_app: try: - # SSL context is required in Mac and not on windows - async with websockets.connect(self.uri_data, ssl=self.ssl_context) as websocket: + # SSL context is required on Mac and not on windows + async with websockets.connect( + self.uri_data, + ssl=self.ssl_context, + ping_interval=5 + ) as websocket: # Handle WebSocket communication while True: - # Step index reset if necessary - step_index = 0 if step_index >= len(steps) else step_index + # Starting again from step 0 and closing current websocket connection + if step_index >= len(steps): + step_index = 0 + await websocket.close() + break + roadmap_name = steps[step_index]["name"] if step_index == 0: print("subscribe -> restarting roadmap '{}'\n".format(roadmap_name)) @@ -435,11 +445,20 @@ async def subscribe(self, steps: list): # FILTERING BY TOKEN'S CREATION TIME token_creation_time = datetime.fromtimestamp(token["timestamp"]) token_age = (datetime.now() - token_creation_time).total_seconds() - if token_age >= appconfig.TRADING_TOKEN_TOO_OLD_SECONDS and \ - appconfig.APPMODE not in [AppMode.dummy.value, AppMode.simulation.value]: - print("Warning: susbscribe -> Token {} is too old for trading.".format( + + age_tolerance = appconfig.TRADING_TOKEN_TOO_OLD_SECONDS + if "age_tolerance" in step["criteria"]: + age_tolerance = step["criteria"]["age_tolerance"] + + if token_age >= age_tolerance and appconfig.APPMODE not in [AppMode.dummy.value]: + print("Warning: susbscribe -> Token {} is too old for trading. Checking it and moving on".format( token["name"] )) + redisdb.update_token( + token=token, + is_checked=True, + is_closed=True + ) continue enough_balance = self.balance >= amount @@ -471,7 +490,7 @@ async def subscribe(self, steps: list): # Also closing the token if there's not enough balance for trading token = redisdb.update_token( token=token, - is_checked=False, + is_checked=True, is_closed=not enough_balance ) self.add_update_token(token=token) @@ -613,140 +632,128 @@ async def subscribe(self, steps: list): step_index += 1 - if "subscription" in step: - suscription = step["subscription"] - - payload = { - "method": suscription.value, - } - - websocket_timeout = appconfig.TRADING_MARKETING_INACTIVITY_TIMEOUT - if "criteria" in step: - if "market_inactivity" in step["criteria"]: - websocket_timeout = step["criteria"]["market_inactivity"] - if "discard_market_inactivity" in step["criteria"]: - websocket_timeout = step["criteria"]["discard_market_inactivity"] - - if suscription.value == Suscription.subscribeAccountTrade.value: - payload["keys"] = self.accounts - - if suscription.value == Suscription.subscribeTokenTrade.value: - payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_checked"] and self.tokens[mint]["is_traded"]] - - if suscription.value == Suscription.unsubscribeTokenTrade.value: - payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] - - await websocket.send(json.dumps(payload)) - - ##################### - # ADD HERE THE BUYING TRADEs in separate threads and fix the step so Buy and Sell are inside subscription - # NOTE: test buy and sell in real before having them in threads - ##################### - for mint, token_data in self.tokens.items(): - if not self.tokens[mint]["is_traded"] and \ - self.tokens[mint]["is_checked"] and \ - not self.tokens[mint]["is_closed"]: - pass - - while True: - try: - message = await asyncio.wait_for( - websocket.recv(), - timeout=websocket_timeout - ) - msg = json.loads(message) - move_to_next_step = False - - # This is the first message we get when we connect - if "message" in msg: - print(msg["message"]) - # If the message is about unsibscribing, then we move on to the next step - if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): - move_to_next_step = True - else: - continue + try: + suscription = step["subscription"] - if suscription.value == Suscription.subscribeNewToken.value: - move_to_next_step = self.new_token_suscription(msg=msg, step=step, redisdb=redisdb) + payload = { + "method": suscription.value, + } - if suscription.value == Suscription.subscribeTokenTrade.value: - # Getting the mint address we'll work with - # CHeck mint value for each tokeb being processed - mint = msg["mint"] + websocket_timeout = appconfig.TRADING_MARKETING_INACTIVITY_TIMEOUT + if "criteria" in step: + if "market_inactivity" in step["criteria"]: + websocket_timeout = step["criteria"]["market_inactivity"] + if "discard_market_inactivity" in step["criteria"]: + websocket_timeout = step["criteria"]["discard_market_inactivity"] - # Key point: need to copy the token - token = self.tokens[mint].copy() + if suscription.value == Suscription.subscribeAccountTrade.value: + payload["keys"] = self.accounts - # We'll not pay attention to closed token trades - if self.tokens[mint]["is_closed"]: - continue + if suscription.value == Suscription.subscribeTokenTrade.value: + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_checked"] and not self.tokens[mint]["is_traded"]] - time_stamps = {"buy_timestamp": None, "sell_timestamp": None} - if "buy_timestamp" in token: - time_stamps["buy_timestamp"] = token["buy_timestamp"] - - if "sell_timestamp" in token: - time_stamps["sell_timestamp"] = token["sell_timestamp"] - - # By default we'll always track the developer - traders = token.get("track_traders", []) - # Doing some analytics like how many continuous buys have happend, etc - new_msg = trading_analytics( - msg=msg, - previous_trades=token["trades"], - amount_traded=self.trading_amount, - pubkey=self.keypair.pubkey(), - traders=traders, - token_timestamps=time_stamps - ) - # Including last message with new metadata into trades list - if not token["trades"]: - token["trades"] = [new_msg] - else: - token["trades"].append(new_msg) - self.add_update_token(token=token) - - move_to_next_step, criteria = self.validate_criteria( - msg=new_msg, - amount_traded=self.trading_amount, - criteria=step["criteria"] - ) - # Including exit criteria in token for further analytics - self.tokens[mint]["exit_criteria"] = criteria + if suscription.value == Suscription.unsubscribeTokenTrade.value: + payload["keys"] = [mint for mint, _ in self.tokens.items() if self.tokens[mint]["is_closed"]] - # Moving to the next step - if move_to_next_step: - step_index += 1 + await websocket.send(json.dumps(payload)) + + ##################### + # ADD HERE THE BUYING TRADEs in separate threads and fix the step so Buy and Sell are inside subscription + # NOTE: test buy and sell in real before having them in threads + ##################### + for mint, token_data in self.tokens.items(): + if not self.tokens[mint]["is_traded"] and \ + self.tokens[mint]["is_checked"] and \ + not self.tokens[mint]["is_closed"]: + pass + + while True: + try: + message = await asyncio.wait_for( + websocket.recv(), + timeout=websocket_timeout + ) + msg = json.loads(message) move_to_next_step = False - print("Exiting subscription criteria: {}".format( - self.tokens[mint]["exit_criteria"] - )) - if "discard_" in self.tokens[mint]["exit_criteria"]: - step_index = step["on_discard_token_go_to_step"] - break + # This is the first message we get when we connect + if "message" in msg: + print(msg["message"]) + # If the message is about unsibscribing, then we move on to the next step + if suscription.value == Suscription.unsubscribeTokenTrade.value and "unsubscribed" in msg["message"].lower(): + move_to_next_step = True + else: + continue - except asyncio.TimeoutError: - print("No trades detected during {} seconds. Moving to next step.".format( - websocket_timeout - )) - step_index += 1 - self.tokens[mint]["exit_criteria"] = "market_inactivity" + if suscription.value == Suscription.subscribeNewToken.value: + move_to_next_step = self.new_token_suscription(msg=msg, step=step, redisdb=redisdb) - if "discard_market_inactivity" in step["criteria"]: - step_index = step["on_discard_token_go_to_step"] + if suscription.value == Suscription.subscribeTokenTrade.value: + # Getting the mint address we'll work with + # CHeck mint value for each tokeb being processed + mint = msg["mint"] + + # We'll not pay attention to closed token trades + if self.tokens[mint]["is_closed"]: + continue + + # Key point: need to copy the token + token = self.tokens[mint].copy() + + move_to_next_step, exit_criteria = self.token_trade_subscription( + token=token, + msg=msg, + step=step + ) + + # Including exit criteria in token for further analytics + self.tokens[mint]["exit_criteria"] = exit_criteria + + # Moving to the next step + if move_to_next_step: + step_index += 1 + move_to_next_step = False + + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + + print("Exiting subscription criteria: {} at {}".format( + self.tokens[mint]["exit_criteria"], + current_time + )) + if suscription.value == Suscription.subscribeTokenTrade.value: + if "discard_" in self.tokens[mint]["exit_criteria"]: + step_index = step["on_discard_token_go_to_step"] + + break - move_to_next_step = False - break + except asyncio.TimeoutError: + print("No trades detected during {} seconds. Moving to next step.".format( + websocket_timeout + )) + step_index += 1 + self.tokens[mint]["exit_criteria"] = "market_inactivity" + + if "discard_market_inactivity" in step["criteria"]: + step_index = step["on_discard_token_go_to_step"] + + move_to_next_step = False + break - # TODO: add a safely exit way of ending the program + except Exception as e: + print("Exception in websocket: {}".format(e)) + await websocket.close() + # TODO: add a safely exit way of ending the program + break except websockets.exceptions.ConnectionClosedError: print("Connection lost, reconnecting...") await asyncio.sleep(5) # Wait before reconnecting + except websockets.exceptions.ConnectionClosedOK: + print("Closing connection as expected") + break except Exception as e: print(f"Unexpected error: {e}. Exiting program abruptly.") break # Exit on non-recoverable errors @@ -831,6 +838,54 @@ def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: # TODO: relase tokens to snipers with redis recods. At this moment we're listening to one token only return move_to_next_step + def token_trade_subscription(self, token: dict, msg: dict, step: dict) -> tuple[bool, str]: + """ + Perform analysis on current and past trades and evaluate criteria to move on to the next step + :param token[dict]: token stored in current PUmp pbject + :param msg[dict]: message from Pump.fun when listening to trading tokens and modified by trading_analytics funciton. + :param step[dict]: current step in trade roadmap list of steps. + :return: move_to_next_step[bool] and exit_criteria[str] which is the reason why the trade subscription must be finished + """ + time_stamps = {"buy_timestamp": None, "sell_timestamp": None} + if "buy_timestamp" in token: + time_stamps["buy_timestamp"] = token["buy_timestamp"] + + if "sell_timestamp" in token: + time_stamps["sell_timestamp"] = token["sell_timestamp"] + + # By default we'll always track the developer + traders = token.get("track_traders", []) + + if appconfig.APPMODE in [AppMode.dummy.value and AppMode.simulation.value]: + if not token.get("trades", []): + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print(" First trade received at: {}".format(current_time)) + + # Doing some analytics like how many continuous buys have happend, etc + new_msg = trading_analytics( + msg=msg, + previous_trades=token["trades"], + amount_traded=self.trading_amount, + pubkey=self.keypair.pubkey(), + traders=traders, + token_timestamps=time_stamps + ) + # Including last message with new metadata into trades list + if not token["trades"]: + token["trades"] = [new_msg] + else: + token["trades"].append(new_msg) + self.add_update_token(token=token) + + move_to_next_step, criteria = self.validate_criteria( + msg=new_msg, + amount_traded=self.trading_amount, + criteria=step["criteria"] + ) + + return move_to_next_step, criteria + + def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> bool: """ This function takes the incomming trading message from Pump.fun previouly @@ -848,10 +903,10 @@ def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> if callable(function): # Call the function with the parameter and msg is_valid = function(parameter, msg, amount_traded) - print(f"validate_criteria-> Function {function_name} returned: {is_valid}") + # print(f"validate_criteria-> Function {function_name} returned: {is_valid}") else: - if appconfig.APPMODE in [AppMode.dummy.value]: - print(f"*** validate_criteria-> WARNING: {function_name} not found.") + if appconfig.APPMODE in [AppMode.dummy.value and AppMode.simulation.value]: + print(f"validate_criteria-> WARNING: {function_name} function was not found.") if is_valid: exit_criteria = function_name @@ -924,7 +979,12 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No while True: # Faking transaction for none real modes if appconfig.APPMODE not in [AppMode.real.value]: - print("trade -> {} MODE: returning dummy transaction".format(appconfig.APPMODE)) + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("simulate trade -> {} MODE: returning dummy transaction at {}".format( + appconfig.APPMODE, + current_time + ) + ) txSignature = "txn_dummy_{}".format(txtype.value) break @@ -1000,10 +1060,13 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No data=txPayload.to_json() ) + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + txSignature = response.json()['result'] - print("Trade->{} Transaction: https://solscan.io/tx/{}".format( + print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( txtype.value, - txSignature + txSignature, + current_time )) break @@ -1299,4 +1362,37 @@ def test(): balance = pump.get_balance(public_key=keypair.pubkey()) print("Balance after: {}".format(balance)) + +def test_ws(): + import asyncio +import json +import websockets + +async def subscribe_to_trades(): + uri = "wss://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" # Replace with your RPC provider's WebSocket URL + mint_address = "65ckwQJV8x8byusvMbGh4AWtRmvH8FiD3qVBHAdW1MRP" + + async with websockets.connect(uri) as websocket: + # Subscription payload for the token account + payload = { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [ + {"mentions": [mint_address]}, + {"encoding": "base64"} + ] + } + + # Send subscription request + await websocket.send(json.dumps(payload)) + + # Listen for messages + while True: + message = await websocket.recv() + print("Received trade update:", json.loads(message)) + +# Run the async function +#asyncio.run(subscribe_to_trades()) + #study() From 3edc33d083a631e9d812948138ee596b9e9a6491 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 22 Dec 2024 01:53:30 +0100 Subject: [PATCH 036/103] For associated token accounts: count them and get basic metadata to tell if the accounts holds dust tokens. Adding Quicknode RCP endpoint to config --- .vscode/launch.json | 13 +++ bot/config/__init__.py | 1 + bot/libs/utils.py | 219 ++++++++++++++++++++++++++++++++++++----- bot/module/pump.py | 5 +- 4 files changed, 212 insertions(+), 26 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 943489d..9b81315 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -51,6 +51,19 @@ "ENVIRONMENT": "DUMMY" }, "justMyCode": false + }, + { + "name": "Debug utils.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/libs/utils.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false } ] } diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 69c8a17..cf8d443 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -35,6 +35,7 @@ class AuthConfig: ) # Minimum balance a token must have for being traded RETRYING_SECONDS = 1 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" RPC_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 6fcb7f3..1116fe7 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -1,14 +1,17 @@ import base58 import base64 import json +import requests import struct from datetime import datetime from enum import Enum +from solana.rpc.api import Client from solana.rpc.async_api import AsyncClient from solana.rpc.types import TokenAccountOpts from solders.message import Message, MessageV0 from solders.pubkey import Pubkey +from solders.rpc.responses import GetTokenAccountsByOwnerResp from bot.config import appconfig @@ -52,49 +55,188 @@ async def get_solana_balance(public_key: Pubkey) -> float: print(f"Error fetching balance: {e.error_msg}") return 0.0 -async def get_own_token_balance(wallet_pubkey: Pubkey, token_mint_addres: str) -> float: + +def get_token_mint_decimals(mint_address: str) -> int: """ - Fetches the balance of a specific token in a given Solana wallet. + Fetches the decimals value for a given token mint. + + Parameters: + mint_address (str): The mint address of the token. + + Returns: + int: The number of decimals for the token. + """ + client = Client(appconfig.RPC_URL_QUICKNODE) + + mint_pubkey = Pubkey.from_string(mint_address) + + # Fetch the mint account info + response = client.get_account_info(mint_pubkey) + if not response.value: + raise ValueError(f"Mint account {mint_address} not found") + + from spl.token._layouts import MINT_LAYOUT, ACCOUNT_LAYOUT + mint_data = MINT_LAYOUT.parse(response.value.data) + decimals = mint_data.decimals + + return decimals + + +async def count_associated_token_accounts( + wallet_pubkey: Pubkey +) -> int: + """ + Fetch the amount of associated token accounts an address holds :param wallet_pubkey: The public address of the Solana wallet. - :param token_mint_pubkey: The mint address of the token to query. - :return: Token balance as a float. + :return: amount of associated token accounts """ - async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + async with AsyncClient(appconfig.RPC_URL_QUICKNODE) as client: try: - # Fetch token mint public key owned by the wallet - token_mint_pubkey = decode_pump_fun_token(token=token_mint_addres) + program_id = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") opts = TokenAccountOpts( - mint=token_mint_pubkey, # The mint address of the token - program_id=None, # (Optional) SPL token program ID + mint=None, + program_id=program_id, # (Optional) SPL token program ID encoding="base64" # (Optional) Response encoding ) + response = await client.get_token_accounts_by_owner( owner=wallet_pubkey, opts=opts, ) + await asyncio.sleep(1) + token_accounts_response = GetTokenAccountsByOwnerResp.from_json(response.to_json()) + + if not token_accounts_response.value: + return 0 + + return len(token_accounts_response.value) + + except: + return 0 + +async def detect_dust_token_accounts( + wallet_pubkey: Pubkey, + token_mint_addres: str = None +) -> list[dict]: + """ + Fetches the balance of a specific token in a given Solana wallet. - if len(response.value) == 0: - raise Exception("No token accounts found for this mint address.") + :param wallet_pubkey: The public address of the Solana wallet. + :param token_mint_pubkey: The mint address of the token to query. + :return: Token balance as a float. + """ + min_token_balance = 1 + max_retries = 5 + async with AsyncClient(appconfig.RPC_URL_QUICKNODE) as client: + try: - # Assume the first token account holds the balance (adjust for multisig if needed) - token_pubkey = response.value[0].pubkey - # Fetch the token balance - balance_response = await client.get_token_account_balance( - token_pubkey + program_id = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + opts = TokenAccountOpts( + mint=None, + program_id=program_id, # (Optional) SPL token program ID + encoding="base64" # (Optional) Response encoding ) - balance_data = balance_response.value - raw_balance = int(balance_data.amount) - decimals = int(balance_data.decimals) - # Convert to human-readable balance - readable_balance = raw_balance / (10 ** decimals) - return readable_balance + response = await client.get_token_accounts_by_owner( + owner=wallet_pubkey, + opts=opts, + ) + await asyncio.sleep(1) + token_accounts_response = GetTokenAccountsByOwnerResp.from_json(response.to_json()) + + if not token_accounts_response.value: + return [] + + # Fetch the current Solana price + sol_price = get_solana_price() + + accounts = [] + counter = 0 + for account in token_accounts_response.value: + counter +=1 + + associated_token_account = str(account.pubkey) + if token_mint_addres and token_mint_addres != associated_token_account: + continue + + data = account.account.data + if len(data) != 165: + raise ValueError("Invalid data length for an SPL token account") + + # Unpack the data using the SPL token account layout + ( + mint, # 32 bytes + owner, # 32 bytes + amount_lamports, # 8 bytes (token balance) + delegate_option, # 4 bytes (delegate option: 0 or 1) + delegate, # 32 bytes (delegate public key) + state, # 1 byte (state of the account) + is_native_option, # 4 bytes (is_native option: 0 or 1) + is_native, # 8 bytes (amount of native SOL if is_native is set) + delegated_amount, # 8 bytes (amount of tokens delegated) + close_authority_option, # 4 bytes (close authority option: 0 or 1) + close_authority # 32 bytes (close authority public key) + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) + + mint_address = str(Pubkey.from_bytes(mint)) + + retries_counter = 0 + decimals = 0 + while True: + try: + if retries_counter >= max_retries: + print("detect_dust_token_accounts-> Max retries of {} reached when calling get_token_mint_decimals") + return accounts + decimals = get_token_mint_decimals(mint_address=mint_address) + break + except: + print("sleeping 1sec at counter {}...".format(counter)) + await asyncio.sleep(1) + retries_counter += 1 + amount = amount_lamports / 10**decimals + + # Fetch SOL balance of the associated token account + retries_counter = 0 + sol_balance_response = 0 + while True: + try: + if retries_counter >= max_retries: + print("detect_dust_token_accounts-> Max retries of {} reached when calling get_balance") + return accounts + sol_balance_response = await client.get_balance(Pubkey.from_string(associated_token_account)) + break + except: + print("sleeping at counter {}...".format(counter)) + await asyncio.sleep(1) + retries_counter += 1 + + sol_balance = sol_balance_response.value / 1e9 # Convert lamports to SOL + sol_balance_usd = sol_balance * sol_price # Calculate SOL value in USD + + accounts.append( + { + "token_mint": str(Pubkey(mint)), + "associated_token_account": associated_token_account, + "owner": str(Pubkey(owner)), + "token_balance": amount, + "sol_balance": sol_balance, + "sol_balance_usd": sol_balance_usd, + "is_dust": amount < min_token_balance, + "delegate": str(Pubkey(delegate)) if delegate_option != b"\x00" else None, + "state": state, + "is_native": is_native == b"\x01", + "delegated_amount": delegated_amount, + "close_authority": str(Pubkey(close_authority)) if close_authority_option != b"\x00" else None, + } + ) + + return accounts except Exception as e: print(f"Error fetching token '{token_mint_addres}' balance: {e}") - return 0.0 + return [] def decode_pump_fun_token(token: str) -> Pubkey: """ @@ -226,3 +368,34 @@ def stamp_time(time: datetime, time_stored: dict = None) -> dict: time_stored[tstamp] += 1 return time_stored + + +def get_solana_price() -> float: + """ + Retrieves the current Solana price in USD using a public API. + + Returns: + float: Current Solana price in USD. + """ + try: + response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") + response.raise_for_status() + price_data = response.json() + return price_data["solana"]["usd"] + except Exception as e: + raise RuntimeError(f"Failed to fetch Solana price: {e}") + +async def test(): + solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" + solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" + account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" + account = None + token_accounts = await count_associated_token_accounts(wallet_pubkey=solana_address) + print("Accounts: {}".format(token_accounts)) + token_accounts_data = await detect_dust_token_accounts(wallet_pubkey=solana_address, token_mint_addres=account) + print("USD being held: {}".format( + sum(account["sol_balance_usd"] for account in token_accounts_data if account["is_dust"]) + )) + +import asyncio +asyncio.run(test()) \ No newline at end of file diff --git a/bot/module/pump.py b/bot/module/pump.py index 46744a9..afc534d 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1041,15 +1041,14 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No vst = VersionedTransaction.from_bytes(response.content) msg = vst.message - print(msg.to_json()) - tx = VersionedTransaction( msg, [keypair] ) config = RpcSendTransactionConfig( - preflight_commitment=CommitmentLevel.Confirmed + preflight_commitment=CommitmentLevel.Confirmed, + skip_preflight=True ) txPayload = SendVersionedTransaction(tx, config) From 6b02fb4604f692b105686c5fb10bc20b7e5e4578 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 23 Dec 2024 15:54:06 +0100 Subject: [PATCH 037/103] utils: count_associated_token_accounts now returns a dic with accounts, burnable accounts, value held in solanas and usd --- bot/libs/utils.py | 238 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 206 insertions(+), 32 deletions(-) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 1116fe7..988e7c7 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -1,8 +1,10 @@ import base58 import base64 import json +import random import requests import struct +import time from datetime import datetime from enum import Enum @@ -82,6 +84,47 @@ def get_token_mint_decimals(mint_address: str) -> int: return decimals +async def get_token_accounts_by_owner(wallet_address=str)->dict: + response = {} + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getTokenAccountsByOwner", + "params": [ + wallet_address, + { + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "encoding": "jsonParsed" + } + ] + } + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("get_token_accounts_by_owner: Bad status code '{}' recevied for token {}".format( + response.status_code, + wallet_address + ) + ) + return response + + content = response.json() + response = content["result"]["value"] + + return response + except Exception as e: + print("get_token_accounts_by_owner-> Error: {}".format(e)) + return response + + + async def count_associated_token_accounts( wallet_pubkey: Pubkey ) -> int: @@ -91,30 +134,87 @@ async def count_associated_token_accounts( :param wallet_pubkey: The public address of the Solana wallet. :return: amount of associated token accounts """ - async with AsyncClient(appconfig.RPC_URL_QUICKNODE) as client: - try: + total = { + "total_accounts": 0, + "burnable_accounts": 0, + "accounts_for_manual_review": 0, + "recoverable_balance": 0, + "recoverable_balance_usd": 0 + } + min_token_value = 1 + account_samples = 5 + retries = 5 + + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + usd_sol_value = 0 + + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] - program_id = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") - opts = TokenAccountOpts( - mint=None, - program_id=program_id, # (Optional) SPL token program ID - encoding="base64" # (Optional) Response encoding - ) + if accounts: + usd_sol_value = get_solana_price() + if usd_sol_value == 0: + return total + # Calculating accounts balance as AN APROXIMATION to speed up this process + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + average_balance = sum_balance / len(account_sample_list) + + total["recoverable_balance"] = average_balance * len(accounts) + total["recoverable_balance_usd"] = total["recoverable_balance"] * usd_sol_value + + accounts_for_manual_review = 0 + for account in accounts: + mint = account["account"]["data"]["parsed"]["info"]["mint"] + token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + + # # Get token value + # counter = 0 + # metadata = {} + # while True: + # metadata = get_token_metadata(token_address=mint) + # if metadata: + # retries = 0 + # break + # counter += 1 + # time.sleep(counter) + + # if not metadata: + # # Forcing out with what we have + # return total + + # if metadata["insufficient_data"]: + # accounts_for_manual_review += 1 + # continue + + # token_value = metadata["price_info"]["price_per_token"] + + token_value = 1 + total["total_accounts"] += 1 + if token_ammount * token_value < min_token_value: + total["burnable_accounts"] += 1 + else: + print("token {} has significan value".format(mint)) + + total["accounts_for_manual_review"] = accounts_for_manual_review + + return total - response = await client.get_token_accounts_by_owner( - owner=wallet_pubkey, - opts=opts, - ) - await asyncio.sleep(1) - token_accounts_response = GetTokenAccountsByOwnerResp.from_json(response.to_json()) - - if not token_accounts_response.value: - return 0 - - return len(token_accounts_response.value) - - except: - return 0 async def detect_dust_token_accounts( wallet_pubkey: Pubkey, @@ -129,7 +229,7 @@ async def detect_dust_token_accounts( """ min_token_balance = 1 max_retries = 5 - async with AsyncClient(appconfig.RPC_URL_QUICKNODE) as client: + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: program_id = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") @@ -369,7 +469,6 @@ def stamp_time(time: datetime, time_stored: dict = None) -> dict: return time_stored - def get_solana_price() -> float: """ Retrieves the current Solana price in USD using a public API. @@ -377,25 +476,100 @@ def get_solana_price() -> float: Returns: float: Current Solana price in USD. """ + retries = 5 + price = 0 + counter = 0 + while True: + try: + if counter >= retries: + break + + response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") + response.raise_for_status() + price_data = response.json() + price = price_data["solana"]["usd"] + break + except Exception as e: + counter += 1 + print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( + counter, + retries + )) + time.sleep(counter) + + return price + +def get_token_metadata(token_address: str)->dict: + metadata = {} + + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getAsset", + "params": { + "id": token_address + } + } try: - response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") - response.raise_for_status() - price_data = response.json() - return price_data["solana"]["usd"] + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("get_metadata: Bad status code '{}' recevied for token {}".format( + response.status_code, + token_address + ) + ) + return metadata + content = response.json() + metadata.update(content["result"]["content"]["files"][0]) + metadata.update(content["result"]["content"]["metadata"]) + metadata["authority"] = content["result"]["authorities"][0]["address"] + metadata["supply"] = content["result"]["token_info"]["supply"] + metadata["decimals"] = content["result"]["token_info"]["decimals"] + metadata["token_program"] = content["result"]["token_info"]["token_program"] + metadata["insufficient_data"] = False + + if "price_info" in content["result"]["token_info"]: + metadata["price_info"] = content["result"]["token_info"]["price_info"] + else: + # It might happen that the token comes with no price info. IF so, we'll mark the token + metadata["price_info"] = 0 + metadata["insufficient_data"] = True + except Exception as e: - raise RuntimeError(f"Failed to fetch Solana price: {e}") + print("get_metadata: Error retrieving metadata for token {}".format(token_address)) + return metadata + + + async def test(): solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" - solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" + #solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" + accounts = await get_token_accounts_by_owner(wallet_address=solana_address) + + token_address = "FFqR2bk3ULB1WuYECRbooxPpbYvvZBp3z9Uc94Pqpump" + metadata = get_token_metadata(token_address=token_address) + print(metadata) + + account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" account = None - token_accounts = await count_associated_token_accounts(wallet_pubkey=solana_address) + token_accounts = await count_associated_token_accounts(wallet_pubkey=Pubkey.from_string(solana_address)) print("Accounts: {}".format(token_accounts)) - token_accounts_data = await detect_dust_token_accounts(wallet_pubkey=solana_address, token_mint_addres=account) + token_accounts_data = await detect_dust_token_accounts( + wallet_pubkey=Pubkey.from_string(solana_address), + token_mint_addres=account + ) print("USD being held: {}".format( sum(account["sol_balance_usd"] for account in token_accounts_data if account["is_dust"]) )) + print("Sols being held: {}".format( + sum(account["sol_balance"] for account in token_accounts_data if account["is_dust"]) + )) import asyncio asyncio.run(test()) \ No newline at end of file From 5ece621203f32d35bff29e82694e2be7b6b61d6b Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 24 Dec 2024 12:46:52 +0100 Subject: [PATCH 038/103] detect_dust_token_accounts implementation with latest changes. --- bot/libs/utils.py | 286 ++++++++++++++++++++++------------------------ 1 file changed, 134 insertions(+), 152 deletions(-) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 988e7c7..a37f6f1 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -138,12 +138,11 @@ async def count_associated_token_accounts( "total_accounts": 0, "burnable_accounts": 0, "accounts_for_manual_review": 0, - "recoverable_balance": 0, - "recoverable_balance_usd": 0 + "rent_balance": 0, + "rent_balance_usd": 0 } min_token_value = 1 account_samples = 5 - retries = 5 # TODO: implement token's black list (like USDC, etc) token_blacklist = [] @@ -175,35 +174,14 @@ async def count_associated_token_accounts( average_balance = sum_balance / len(account_sample_list) - total["recoverable_balance"] = average_balance * len(accounts) - total["recoverable_balance_usd"] = total["recoverable_balance"] * usd_sol_value + total["rent_balance"] = average_balance * len(accounts) + total["rent_balance_usd"] = total["rent_balance"] * usd_sol_value accounts_for_manual_review = 0 for account in accounts: mint = account["account"]["data"]["parsed"]["info"]["mint"] token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] - # # Get token value - # counter = 0 - # metadata = {} - # while True: - # metadata = get_token_metadata(token_address=mint) - # if metadata: - # retries = 0 - # break - # counter += 1 - # time.sleep(counter) - - # if not metadata: - # # Forcing out with what we have - # return total - - # if metadata["insufficient_data"]: - # accounts_for_manual_review += 1 - # continue - - # token_value = metadata["price_info"]["price_per_token"] - token_value = 1 total["total_accounts"] += 1 if token_ammount * token_value < min_token_value: @@ -218,125 +196,109 @@ async def count_associated_token_accounts( async def detect_dust_token_accounts( wallet_pubkey: Pubkey, - token_mint_addres: str = None + token_mint_addres: str = None, + do_balance_aproximation: bool = True ) -> list[dict]: """ Fetches the balance of a specific token in a given Solana wallet. :param wallet_pubkey: The public address of the Solana wallet. - :param token_mint_pubkey: The mint address of the token to query. :return: Token balance as a float. """ - min_token_balance = 1 - max_retries = 5 + min_token_value = 1 + account_samples = 5 + working_balance = 0 async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] - program_id = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") - opts = TokenAccountOpts( - mint=None, - program_id=program_id, # (Optional) SPL token program ID - encoding="base64" # (Optional) Response encoding - ) - - response = await client.get_token_accounts_by_owner( - owner=wallet_pubkey, - opts=opts, - ) - await asyncio.sleep(1) - token_accounts_response = GetTokenAccountsByOwnerResp.from_json(response.to_json()) - - if not token_accounts_response.value: + if not accounts: return [] # Fetch the current Solana price sol_price = get_solana_price() + if sol_price == 0: + return [] + + if do_balance_aproximation: + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + working_balance = sum_balance / len(account_sample_list) - accounts = [] counter = 0 - for account in token_accounts_response.value: + account_output = [] + for account in accounts: counter +=1 - - associated_token_account = str(account.pubkey) - if token_mint_addres and token_mint_addres != associated_token_account: - continue - data = account.account.data - if len(data) != 165: - raise ValueError("Invalid data length for an SPL token account") - - # Unpack the data using the SPL token account layout - ( - mint, # 32 bytes - owner, # 32 bytes - amount_lamports, # 8 bytes (token balance) - delegate_option, # 4 bytes (delegate option: 0 or 1) - delegate, # 32 bytes (delegate public key) - state, # 1 byte (state of the account) - is_native_option, # 4 bytes (is_native option: 0 or 1) - is_native, # 8 bytes (amount of native SOL if is_native is set) - delegated_amount, # 8 bytes (amount of tokens delegated) - close_authority_option, # 4 bytes (close authority option: 0 or 1) - close_authority # 32 bytes (close authority public key) - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) - - mint_address = str(Pubkey.from_bytes(mint)) - - retries_counter = 0 - decimals = 0 - while True: - try: - if retries_counter >= max_retries: - print("detect_dust_token_accounts-> Max retries of {} reached when calling get_token_mint_decimals") - return accounts - decimals = get_token_mint_decimals(mint_address=mint_address) - break - except: - print("sleeping 1sec at counter {}...".format(counter)) - await asyncio.sleep(1) - retries_counter += 1 - amount = amount_lamports / 10**decimals - - # Fetch SOL balance of the associated token account - retries_counter = 0 - sol_balance_response = 0 - while True: - try: - if retries_counter >= max_retries: - print("detect_dust_token_accounts-> Max retries of {} reached when calling get_balance") - return accounts - sol_balance_response = await client.get_balance(Pubkey.from_string(associated_token_account)) - break - except: - print("sleeping at counter {}...".format(counter)) - await asyncio.sleep(1) - retries_counter += 1 - - sol_balance = sol_balance_response.value / 1e9 # Convert lamports to SOL - sol_balance_usd = sol_balance * sol_price # Calculate SOL value in USD + mint = account["account"]["data"]["parsed"]["info"]["mint"] + owner = account["account"]["data"]["parsed"]["info"]["owner"] + token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] + associated_token_account = account["pubkey"] + + metadata = get_token_metadata(token_address=mint) - accounts.append( + token_price = metadata["price_info"]["price_per_token"] + token_value = token_price * token_amount + + uri = metadata["uri"] + cdn_uri = metadata["cdn_uri"] + mime = metadata["mime"] + description = metadata["description"] if "description" in metadata else "" + name = metadata["name"].strip() + symbol = metadata["symbol"].strip() + authority = metadata["authority"] + supply = metadata["supply"] + token_program = metadata["token_program"] + insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price + + account_output.append( { - "token_mint": str(Pubkey(mint)), + "token_mint": mint, "associated_token_account": associated_token_account, - "owner": str(Pubkey(owner)), - "token_balance": amount, - "sol_balance": sol_balance, - "sol_balance_usd": sol_balance_usd, - "is_dust": amount < min_token_balance, - "delegate": str(Pubkey(delegate)) if delegate_option != b"\x00" else None, - "state": state, - "is_native": is_native == b"\x01", - "delegated_amount": delegated_amount, - "close_authority": str(Pubkey(close_authority)) if close_authority_option != b"\x00" else None, + "owner": owner, + "token_amount": token_amount, + "token_price": token_price, + "token_value": token_value, + "decimals": decimals, + "sol_balance": working_balance, + "sol_balance_usd": working_balance * sol_price, + "is_dust": token_value < min_token_value, + "uri": uri, + "cdn_uri": cdn_uri, + "mime": mime, + "description": description, + "name": name, + "symbol": symbol, + "authority": authority, + "supply": supply, + "token_program": token_program, + "insufficient_data": insufficient_data, } ) - return accounts + return account_output except Exception as e: print(f"Error fetching token '{token_mint_addres}' balance: {e}") - return [] + return account_output def decode_pump_fun_token(token: str) -> Pubkey: """ @@ -510,56 +472,76 @@ def get_token_metadata(token_address: str)->dict: "id": token_address } } - try: - response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - print("get_metadata: Bad status code '{}' recevied for token {}".format( - response.status_code, - token_address + retries = 5 + counter = 0 + while True: + if counter >= retries: + break + + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + counter += 1 + print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( + response.status_code, + token_address, + counter, + retries + ) + ) + time.sleep(counter) + continue + + content = response.json() + metadata.update(content["result"]["content"]["files"][0]) + metadata.update(content["result"]["content"]["metadata"]) + metadata["authority"] = content["result"]["authorities"][0]["address"] + metadata["supply"] = content["result"]["token_info"]["supply"] + metadata["decimals"] = content["result"]["token_info"]["decimals"] + metadata["token_program"] = content["result"]["token_info"]["token_program"] + metadata["insufficient_data"] = False + + if "price_info" in content["result"]["token_info"]: + metadata["price_info"] = content["result"]["token_info"]["price_info"] + else: + # It might happen that the token comes with no price info. IF so, we'll mark the token + metadata["price_info"] = {"price_per_token": 0} + + metadata["insufficient_data"] = True + break + + except Exception as e: + counter += 1 + print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( + token_address, + counter, + retries ) ) - return metadata - content = response.json() - metadata.update(content["result"]["content"]["files"][0]) - metadata.update(content["result"]["content"]["metadata"]) - metadata["authority"] = content["result"]["authorities"][0]["address"] - metadata["supply"] = content["result"]["token_info"]["supply"] - metadata["decimals"] = content["result"]["token_info"]["decimals"] - metadata["token_program"] = content["result"]["token_info"]["token_program"] - metadata["insufficient_data"] = False - - if "price_info" in content["result"]["token_info"]: - metadata["price_info"] = content["result"]["token_info"]["price_info"] - else: - # It might happen that the token comes with no price info. IF so, we'll mark the token - metadata["price_info"] = 0 - metadata["insufficient_data"] = True + time.sleep(counter) - except Exception as e: - print("get_metadata: Error retrieving metadata for token {}".format(token_address)) return metadata - async def test(): solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" #solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" - accounts = await get_token_accounts_by_owner(wallet_address=solana_address) - token_address = "FFqR2bk3ULB1WuYECRbooxPpbYvvZBp3z9Uc94Pqpump" - metadata = get_token_metadata(token_address=token_address) - print(metadata) + token_address = "7AeGaYhyhvDRdPkH7Wg7vup8D1SjNRZc1fXLZNcYpump" + + # metadata = get_token_metadata(token_address=token_address) + # print(metadata) - account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" account = None token_accounts = await count_associated_token_accounts(wallet_pubkey=Pubkey.from_string(solana_address)) print("Accounts: {}".format(token_accounts)) + time.sleep(2) token_accounts_data = await detect_dust_token_accounts( wallet_pubkey=Pubkey.from_string(solana_address), token_mint_addres=account From 7fe34f3812c0655587d8f82806711b1022dccb43 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 25 Dec 2024 17:57:00 +0100 Subject: [PATCH 039/103] Burn associated token account tokens implementation --- bot/libs/utils.py | 144 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index a37f6f1..4012b2b 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -8,12 +8,23 @@ from datetime import datetime from enum import Enum + from solana.rpc.api import Client from solana.rpc.async_api import AsyncClient -from solana.rpc.types import TokenAccountOpts +from solana.rpc.types import TxOpts +from solana.rpc.commitment import Confirmed + from solders.message import Message, MessageV0 from solders.pubkey import Pubkey -from solders.rpc.responses import GetTokenAccountsByOwnerResp +from solders.keypair import Keypair +from solders.compute_budget import set_compute_unit_price +from solders.transaction import Transaction + +from spl.token.instructions import ( + burn_checked, + get_associated_token_address, + BurnCheckedParams, +) from bot.config import appconfig @@ -208,6 +219,7 @@ async def detect_dust_token_accounts( min_token_value = 1 account_samples = 5 working_balance = 0 + # Refactor this: remove this line of code and test as client is not being used async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: # TODO: implement token's black list (like USDC, etc) @@ -527,6 +539,123 @@ def get_token_metadata(token_address: str)->dict: return metadata +async def burn_associated_token_account( + token: Pubkey, + keypair: Keypair, + token_authority: Pubkey, + decimals: int = 0, + amount: float = None +) -> str: + """ + Burs all tokens from the associated token account + :param token[Pubkey]: associated token account + :param keypair[Keypair]: signer and account owner + :param token_authority[Pubkey]: token authority + :param decimals: [int] The number of decimals for the token. + :param amount[float] = None. Burn the exact amount + :return: [str] transaction signature. + """ + txn = None + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + token_programm = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") # SPL Token program ID + # Get associated token account + associated_token_account = get_associated_token_address( + owner=keypair.pubkey(), + mint=token + ) + + # Check if the associated token account exists + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + return txn + + data = account_info.data + if len(data) != 165: + print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + str(associated_token_account), + str(token) + )) + ( + mint, + owner, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + lamports = amount_lamports + owner = owner + # Convert amount to integer based on token decimals + if amount is None: + amount = lamports + else: + amount = int(amount * (10 ** decimals)) + + # Construct the burn instruction + params = BurnCheckedParams( + program_id=token_programm, + mint=token, + account=associated_token_account, + owner=keypair.pubkey(), + amount=amount, + decimals=decimals, + signers=[keypair.pubkey()] + ) + burn_ix = burn_checked(params=params) + + # Create and sign transaction + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + + msg = Message( + instructions=[set_compute_unit_price(1_000), burn_ix], + payer=keypair.pubkey() + ) + tx_signature = await client.send_transaction( + txn=Transaction([keypair], msg, recent_blockhash), + opts=TxOpts(preflight_commitment=Confirmed), + ) + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( + "Burn", + tx_signature.value, + current_time + )) + await client.confirm_transaction(tx_signature.value, commitment="confirmed") + print("Transaction confirmed") + + # SECOND WAY: POSTING TRANSACTION + # config = RpcSendTransactionConfig( + # preflight_commitment=CommitmentLevel.Confirmed + # ) + # tx = SendLegacyTransaction(tx=Transaction([keypair], msg, recent_blockhash),config=config) + # response = requests.post( + # url=appconfig.RPC_URL_HELIUS, + # headers={"Content-Type": "application/json"}, + # data=tx.to_json() + # ) + # txSignature = response.json()['result'] + # current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + # print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( + # "Burn", + # txSignature, + # current_time + # )) + + except Exception as e: + print("burn_associated_token_account Error: {}".format(e)) + + return txn async def test(): solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" @@ -536,6 +665,13 @@ async def test(): # metadata = get_token_metadata(token_address=token_address) # print(metadata) + txn = await burn_associated_token_account( + token=Pubkey.from_string("DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump"), + keypair=Keypair.from_base58_string(appconfig.PRIVKEY), + token_authority=Pubkey.from_string("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"), + decimals=6, + amount=0.02 + ) account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" account = None @@ -553,5 +689,5 @@ async def test(): sum(account["sol_balance"] for account in token_accounts_data if account["is_dust"]) )) -import asyncio -asyncio.run(test()) \ No newline at end of file +#import asyncio +#asyncio.run(test()) \ No newline at end of file From a33a97613e29b13fee1bb81c46cb3ed1bdec1517 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 27 Dec 2024 15:59:10 +0100 Subject: [PATCH 040/103] Utils: reorganizing functions --- bot/libs/utils.py | 459 +++++++++++++++++++++++----------------------- 1 file changed, 227 insertions(+), 232 deletions(-) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 4012b2b..7c11e28 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -69,6 +69,167 @@ async def get_solana_balance(public_key: Pubkey) -> float: return 0.0 +def decode_pump_fun_token(token: str) -> Pubkey: + """ + Decodes a 44-character token from pump.fun to retrieve its mint address. + + :param token: 44-character token. + :return: Token mint address (base58 encoded). + """ + try: + decoded_bytes = base58.b58decode(token) + return Pubkey(decoded_bytes) + except Exception as e: + raise ValueError(f"Failed to decode token: {e}") + +def initial_buy_calculator(sol_in_bonding_curve: float): + pump_dont_fun_initial_fund = appconfig.SCANNER_PUMPDONTFUN_INITIAL_FUND + sols = round(sol_in_bonding_curve - pump_dont_fun_initial_fund, 3) + return sols + +def flatten_data(data): + if isinstance(data, list): + return b"".join(flatten_data(item) for item in data) + elif isinstance(data, int): + return data.to_bytes(1, 'little') # Convert integer to a single byte + return b"" + +def decode_instruction(raw_data): + if len(raw_data) >= 5: # Ensure at least enough data for a float + # Assume the first byte is the Field ID + field_id = raw_data[0] + # Interpret the next 4 bytes as a float (little-endian) + try: + price_impact = struct.unpack(' dict: + time_stored = {} if time_stored is None else time_stored + tstamp = time.strftime("%Y%m%d%H%M%S") + if tstamp not in time_stored: + time_stored[tstamp] = 1 + else: + time_stored[tstamp] += 1 + + return time_stored + +def get_solana_price() -> float: + """ + Retrieves the current Solana price in USD using a public API. + + Returns: + float: Current Solana price in USD. + """ + retries = 5 + price = 0 + counter = 0 + while True: + try: + if counter >= retries: + break + + response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") + response.raise_for_status() + price_data = response.json() + price = price_data["solana"]["usd"] + break + except Exception as e: + counter += 1 + print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( + counter, + retries + )) + time.sleep(counter) + + return price + def get_token_mint_decimals(mint_address: str) -> int: """ Fetches the decimals value for a given token mint. @@ -94,7 +255,6 @@ def get_token_mint_decimals(mint_address: str) -> int: return decimals - async def get_token_accounts_by_owner(wallet_address=str)->dict: response = {} async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: @@ -134,7 +294,70 @@ async def get_token_accounts_by_owner(wallet_address=str)->dict: print("get_token_accounts_by_owner-> Error: {}".format(e)) return response - +def get_token_metadata(token_address: str)->dict: + metadata = {} + + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getAsset", + "params": { + "id": token_address + } + } + retries = 5 + counter = 0 + while True: + if counter >= retries: + break + + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + counter += 1 + print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( + response.status_code, + token_address, + counter, + retries + ) + ) + time.sleep(counter) + continue + + content = response.json() + metadata.update(content["result"]["content"]["files"][0]) + metadata.update(content["result"]["content"]["metadata"]) + metadata["authority"] = content["result"]["authorities"][0]["address"] + metadata["supply"] = content["result"]["token_info"]["supply"] + metadata["decimals"] = content["result"]["token_info"]["decimals"] + metadata["token_program"] = content["result"]["token_info"]["token_program"] + metadata["insufficient_data"] = False + + if "price_info" in content["result"]["token_info"]: + metadata["price_info"] = content["result"]["token_info"]["price_info"] + else: + # It might happen that the token comes with no price info. IF so, we'll mark the token + metadata["price_info"] = {"price_per_token": 0} + + metadata["insufficient_data"] = True + break + + except Exception as e: + counter += 1 + print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( + token_address, + counter, + retries + ) + ) + time.sleep(counter) + + return metadata async def count_associated_token_accounts( wallet_pubkey: Pubkey @@ -204,7 +427,6 @@ async def count_associated_token_accounts( return total - async def detect_dust_token_accounts( wallet_pubkey: Pubkey, token_mint_addres: str = None, @@ -312,233 +534,6 @@ async def detect_dust_token_accounts( print(f"Error fetching token '{token_mint_addres}' balance: {e}") return account_output -def decode_pump_fun_token(token: str) -> Pubkey: - """ - Decodes a 44-character token from pump.fun to retrieve its mint address. - - :param token: 44-character token. - :return: Token mint address (base58 encoded). - """ - try: - decoded_bytes = base58.b58decode(token) - return Pubkey(decoded_bytes) - except Exception as e: - raise ValueError(f"Failed to decode token: {e}") - -def initial_buy_calculator(sol_in_bonding_curve: float): - pump_dont_fun_initial_fund = appconfig.SCANNER_PUMPDONTFUN_INITIAL_FUND - sols = round(sol_in_bonding_curve - pump_dont_fun_initial_fund, 3) - return sols - -def flatten_data(data): - if isinstance(data, list): - return b"".join(flatten_data(item) for item in data) - elif isinstance(data, int): - return data.to_bytes(1, 'little') # Convert integer to a single byte - return b"" - -def decode_instruction(raw_data): - if len(raw_data) >= 5: # Ensure at least enough data for a float - # Assume the first byte is the Field ID - field_id = raw_data[0] - # Interpret the next 4 bytes as a float (little-endian) - try: - price_impact = struct.unpack(' dict: - time_stored = {} if time_stored is None else time_stored - tstamp = time.strftime("%Y%m%d%H%M%S") - if tstamp not in time_stored: - time_stored[tstamp] = 1 - else: - time_stored[tstamp] += 1 - - return time_stored - -def get_solana_price() -> float: - """ - Retrieves the current Solana price in USD using a public API. - - Returns: - float: Current Solana price in USD. - """ - retries = 5 - price = 0 - counter = 0 - while True: - try: - if counter >= retries: - break - - response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") - response.raise_for_status() - price_data = response.json() - price = price_data["solana"]["usd"] - break - except Exception as e: - counter += 1 - print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( - counter, - retries - )) - time.sleep(counter) - - return price - -def get_token_metadata(token_address: str)->dict: - metadata = {} - - data = { - "jsonrpc": "2.0", - "id": "test", - "method": "getAsset", - "params": { - "id": token_address - } - } - retries = 5 - counter = 0 - while True: - if counter >= retries: - break - - try: - response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - counter += 1 - print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( - response.status_code, - token_address, - counter, - retries - ) - ) - time.sleep(counter) - continue - - content = response.json() - metadata.update(content["result"]["content"]["files"][0]) - metadata.update(content["result"]["content"]["metadata"]) - metadata["authority"] = content["result"]["authorities"][0]["address"] - metadata["supply"] = content["result"]["token_info"]["supply"] - metadata["decimals"] = content["result"]["token_info"]["decimals"] - metadata["token_program"] = content["result"]["token_info"]["token_program"] - metadata["insufficient_data"] = False - - if "price_info" in content["result"]["token_info"]: - metadata["price_info"] = content["result"]["token_info"]["price_info"] - else: - # It might happen that the token comes with no price info. IF so, we'll mark the token - metadata["price_info"] = {"price_per_token": 0} - - metadata["insufficient_data"] = True - break - - except Exception as e: - counter += 1 - print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( - token_address, - counter, - retries - ) - ) - time.sleep(counter) - - return metadata - - async def burn_associated_token_account( token: Pubkey, keypair: Keypair, @@ -689,5 +684,5 @@ async def test(): sum(account["sol_balance"] for account in token_accounts_data if account["is_dust"]) )) -#import asyncio -#asyncio.run(test()) \ No newline at end of file +# import asyncio +# asyncio.run(test()) From 31883efbfffb3ca8bad51da9add46e6eb6d0dca2 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 27 Dec 2024 22:34:47 +0100 Subject: [PATCH 041/103] burn and close_and_burn functions implemented --- bot/libs/utils.py | 126 +++++++++++++++++++++++++++++++++++++++++-- bot/module/JSON.yaml | 71 ++++++++++++++++++++++++ bot/module/pump.py | 1 + 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 bot/module/JSON.yaml diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 7c11e28..2cc08c2 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -652,6 +652,111 @@ async def burn_associated_token_account( return txn + +from solders.pubkey import Pubkey +from spl.token.instructions import close_account, CloseAccountParams +async def burn_and_close_associated_token_account( + associated_token_account: Pubkey, + token_mint: Pubkey, + decimals: int, + keypair: Keypair +) -> str: + """ + Burs all tokens from the associated token account + :param associated_token_account[Pubkey]: associated token account + :param token_mint[Pubkey]: tokens to get burned + :param keypair[Keypair]: signer and account owner + :param decimals[int]: The token's decimals (default 9 for SOL). + :return: [str] transaction signature. + """ + txn_signature = None + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + return txn_signature + + # Derive the associated token account address + TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + + # BURN + data = account_info.data + if len(data) != 165: + print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + str(associated_token_account), + str(token_mint) + )) + ( + mint, + owner, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + lamports = amount_lamports + owner = owner + amount = lamports + + # Construct the burn instruction + params = BurnCheckedParams( + program_id=TOKEN_PROGRAM_ID, + mint=token_mint, + account=associated_token_account, + owner=keypair.pubkey(), + amount=amount, + decimals=decimals, + signers=[keypair.pubkey()] + ) + burn_ix = burn_checked(params=params) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=keypair.pubkey(), + owner=keypair.pubkey() + ) + ) + + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + tx = Transaction.new_signed_with_payer( + instructions=[set_compute_unit_price(1_000), burn_ix, close_ix], + payer=keypair.pubkey(), + signing_keypairs=[keypair], + recent_blockhash=recent_blockhash + ) + tx_signature = await client.send_transaction( + txn=tx, + opts=TxOpts(preflight_commitment=Confirmed), + ) + + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( + "Transfer", + tx_signature.value, + current_time + )) + await client.confirm_transaction(tx_signature.value, commitment="confirmed") + print("Transaction confirmed") + + except Exception as e: + print("transfer_solanas Error: {}".format(e)) + + return txn_signature + async def test(): solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" #solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" @@ -660,14 +765,25 @@ async def test(): # metadata = get_token_metadata(token_address=token_address) # print(metadata) - txn = await burn_associated_token_account( - token=Pubkey.from_string("DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump"), + # txn = await burn_associated_token_account( + # token=Pubkey.from_string("DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump"), + # keypair=Keypair.from_base58_string(appconfig.PRIVKEY), + # token_authority=Pubkey.from_string("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"), + # decimals=6, + # amount=None + # ) + + associated_token_account = "D4fHjGpq7yzVm7RSGVkfkMGfixR8HTErJgLbD3GV5k6J" + token_address = "ErNXfdqTCXBE3H4MAHqULWdZsTbnrjsHcoCGPnkmpump" + txn = await burn_and_close_associated_token_account( + associated_token_account=Pubkey.from_string(associated_token_account), + token_mint=Pubkey.from_string(token_address), keypair=Keypair.from_base58_string(appconfig.PRIVKEY), - token_authority=Pubkey.from_string("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"), - decimals=6, - amount=0.02 + decimals=6 ) + + account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" account = None token_accounts = await count_associated_token_accounts(wallet_pubkey=Pubkey.from_string(solana_address)) diff --git a/bot/module/JSON.yaml b/bot/module/JSON.yaml new file mode 100644 index 0000000..33aa837 --- /dev/null +++ b/bot/module/JSON.yaml @@ -0,0 +1,71 @@ +JSON.SET "token:4YFNRqzX1oNnXQZifn5p6Kj2T8ZztjXSjfhcF9Xapump" $ '{"amount":0.004,"is_traded":false,"timestamp":1690228321, "trader": "sniper", "name": "CELEBRINBORG", "symbol": "CBB", "track_traders": ["trader1", "trader2"]}' + +json.set "token:U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump" $ '{"amount":1.0,"is_checked":true,"is_traded":true,"is_closed":true,"timestamp":1733065571.934685,"trader":"sniper","signature":"o1bjZbyJ9SpTfaWvwecgCGGG2f5dTxDRQ4vdSi8dN3di3gX3w5CRqCHNs1ETLArMFt3azZ3Fk4qWbQPv2aT38pW","mint":"U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump","traderPublicKey":"5oLwPx3p8j8AK3P6fxdD23CRA7i3iUmCVwBi4wpsWLMP","txType":"create","initialBuy":67062499.999999,"bondingCurveKey":"5rv2wnm2H5MsYrgHauSfYRirQEGSkpPe7vfNkoqNB4WY","vTokensInBondingCurve":1005937500.000001,"vSolInBondingCurve":31.999999999999968,"marketCapSol":31.811121466293816,"name":"Tung Distilled AI","symbol":"Tung","uri":"https://ipfs.io/ipfs/QmXNFxxQPagxTvMdjJCtah9u6abwfdt77PtjS8sgWcYbGG"}' + + +json.set "token:5SzQBTqXTbFYmPaamAKj5ajbzJk9ANNqJf2gaVZ1pump" $ '{"amount":1.0,"is_checked":true,"is_traded":true,"is_closed":true,"timestamp":1733065571.934685,"trader":"sniper","signature":"o1bjZbyJ9SpTfaWvwecgCGGG2f5dTxDRQ4vdSi8dN3di3gX3w5CRqCHNs1ETLArMFt3azZ3Fk4qWbQPv2aT38pW","mint":"U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump","traderPublicKey":"5oLwPx3p8j8AK3P6fxdD23CRA7i3iUmCVwBi4wpsWLMP","txType":"create","initialBuy":67062499.999999,"bondingCurveKey":"5rv2wnm2H5MsYrgHauSfYRirQEGSkpPe7vfNkoqNB4WY","vTokensInBondingCurve":1005937500.000001,"vSolInBondingCurve":31.999999999999968,"marketCapSol":31.811121466293816,"name":"Tung Distilled AI","symbol":"Tung","uri":"https://ipfs.io/ipfs/QmXNFxxQPagxTvMdjJCtah9u6abwfdt77PtjS8sgWcYbGG"}' + + +# TODO +√ Step1: return this + - total associated token accounts + - total burnable accounts + - amount in solanas (aproximation) + - amount in usd + + Notes: + - Ver el request directo de get token account by owner + y así tengo los decimales de una + - Obtener el valors del token y multiplicarlo por los tokens + para decidir si el token es quemable o no + +√ Step2: return a list of associated token accounts with: + - Token description + - Name + - Symbol + - uri + - cdn_uri + - mime + - authority + - tokens + - price per token + - account balance in sol + - account balance in usd + +√ Step 3: Burn token for a particular account + +√ Step 5: close account + +- Step 6: get close account info and validate that it's deleted. + + + + + + + + +- BUG: when a timeout occurs during subscription, the next step is + BUY and should be unsubscribe to token. + +- BUG: When token is too old, It should hear again from redisdb + Now is continuing the steps. + + + +- Fees service: + - Create a service that will subscribe to + sniper pubkey and get any changes in its state + and log it in redis. + - Create a new index for this + +- Scanner: + * will delete all tokens -> is_checked == False + -> Work with black list and discard any token made by blacked developers + +- Docker: run automatically +- Bash: apply command arguments to choose + how to run the celebrimborg: Sniper, messaging + - Send messsages when things goes wrong + +- Replace prints with Logging with timestamp \ No newline at end of file diff --git a/bot/module/pump.py b/bot/module/pump.py index afc534d..cdaf278 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -1364,6 +1364,7 @@ def test(): def test_ws(): import asyncio + import json import websockets From 66099bdc770eea1ca791d150b7e512832433f2de Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 27 Dec 2024 22:36:36 +0100 Subject: [PATCH 042/103] adding todo.yaml --- bot/{module/JSON.yaml => todo.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bot/{module/JSON.yaml => todo.yaml} (100%) diff --git a/bot/module/JSON.yaml b/bot/todo.yaml similarity index 100% rename from bot/module/JSON.yaml rename to bot/todo.yaml From 58d0058645a6f8dfae41dd333114d3366a981207 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 28 Dec 2024 11:02:37 +0100 Subject: [PATCH 043/103] Including backend FastAPI skeleton draft and openAPI spceficication for GET endpoints --- bot/app/__init__.py | 0 bot/app/main.py | 10 ++ bot/app/openapi/__init__.py | 0 bot/app/openapi/external_endpoints.yaml | 204 ++++++++++++++++++++++++ bot/app/requirements.txt | 2 + bot/app/routes.py | 6 + bot/libs/utils.py | 8 +- 7 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 bot/app/__init__.py create mode 100644 bot/app/main.py create mode 100644 bot/app/openapi/__init__.py create mode 100644 bot/app/openapi/external_endpoints.yaml create mode 100644 bot/app/requirements.txt create mode 100644 bot/app/routes.py diff --git a/bot/app/__init__.py b/bot/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/main.py b/bot/app/main.py new file mode 100644 index 0000000..60a61c6 --- /dev/null +++ b/bot/app/main.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from routes import get_quote + +# Create the FastAPI app +app = FastAPI() + +# Define routes +@app.post("/get_quote") +async def fetch_quote(): + return await get_quote() diff --git a/bot/app/openapi/__init__.py b/bot/app/openapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml new file mode 100644 index 0000000..165de22 --- /dev/null +++ b/bot/app/openapi/external_endpoints.yaml @@ -0,0 +1,204 @@ +openapi: 3.0.3 +info: + title: Solana API + description: API for interacting with Solana-related functionalities. + version: 1.0.0 +servers: + - url: http://127.0.0.1:8000 +paths: + /associated_token_accounts/count: + get: + summary: Get all associated token account for a fiven Solana account + parameters: + - name: wallet_pubkey + in: query + required: true + description: The Solana associated token account address. + schema: + type: string + pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ + description: > + Must be a valid Solana address format with 44 characters, + excluding the letters `l`, `I`, and `O` to avoid ambiguity. + responses: + '200': + description: Account statistics for the given address. + content: + application/json: + schema: + $ref: '#/components/schemas/CountAssociatedTokenAccounts' + example: + total_accounts: 44 + burnable_accounts: 43 + accounts_for_manual_review: 0 + rent_balance: 0.08972832 + rent_balance_usd: 16.3628564352 + '404': + description: Account not found. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: "Account not found." + + /associated_token_accounts: + get: + summary: Retrieve associated token accounts + parameters: + - name: wallet_pubkey + in: query + required: true + description: The wallet public key (Solana format). + schema: + type: string + pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ + description: > + Must be a valid Solana public key format with 44 characters, + excluding the letters `l`, `I`, and `O` to avoid ambiguity. + - name: token_mint_address + in: query + required: true + description: The token mint address (Solana format). + schema: + type: string + pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ + description: > + Must be a valid Solana address format with 44 characters, + excluding the letters `l`, `I`, and `O` to avoid ambiguity. + responses: + '200': + description: List of associated token accounts. + content: + application/json: + schema: + $ref: '#/components/schemas/AssociatedTokenAccounts' + example: + - token_mint: "DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump" + associated_token_account: "7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi" + owner: "5154KiZXfvicBnbETPruMrvEabpUWF7KxTpTPmCH6ncn" + token_amount: 0.457711 + token_price: 0 + token_value: 0.0 + decimals: 6 + sol_balance: 0.00203928 + sol_balance_usd: 0.39113390400000003 + is_dust: true + uri: "https://ipfs.io/ipfs/QmVDb2EBo7HtcsNn9AYprcBGvvS4eLF4JiU7g8sWURPaaY" + cdn_uri: "https://cdn.helius-rpc.com/cdn-cgi/image//https://ipfs.io/ipfs/QmVDb2EBo7HtcsNn9AYprcBGvvS4eLF4JiU7g8sWURPaaY" + mime: "image/png" + description: >- + "Nail it! (NILT)" is a vibrant and community-driven meme + token inspired by the iconic nail as a symbol of precision + and determination. With its fun and engaging theme, the + token aims to bring joy to crypto enthusiasts while offering + unique opportunities for community engagement and creative + use cases. Join the "Nail it!" movement and hammer down on + your goals! + name: "Nail it!" + symbol: "NILT" + authority: "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM" + supply: 999999999694079 + token_program: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + insufficient_data: true + +components: + schemas: + CountAssociatedTokenAccounts: + type: object + properties: + total_accounts: + type: integer + description: Total number of associated accounts. + burnable_accounts: + type: integer + description: Number of accounts eligible for burn. + accounts_for_manual_review: + type: integer + description: Number of accounts requiring manual review. + rent_balance: + type: number + format: float + description: Total rent balance for the accounts. + rent_balance_usd: + type: number + format: float + description: Total rent balance converted to USD. + + AssociatedTokenAccount: + type: object + properties: + token_mint: + type: string + description: The mint address of the token. + associated_token_account: + type: string + description: The associated token account address. + owner: + type: string + description: The owner of the associated token account. + token_amount: + type: number + format: float + description: The amount of tokens held. + token_price: + type: number + format: float + description: The price of the token. + token_value: + type: number + format: float + description: The total value of the tokens held. + decimals: + type: integer + description: The number of decimals for the token. + sol_balance: + type: number + format: float + description: The balance of SOL in the associated account. + sol_balance_usd: + type: number + format: float + description: The balance of SOL converted to USD. + is_dust: + type: boolean + description: Whether the token is considered "dust." + uri: + type: string + format: uri + description: URI pointing to the token's metadata. + cdn_uri: + type: string + format: uri + description: CDN URI pointing to cached metadata. + mime: + type: string + description: The MIME type of the token's metadata file. + description: + type: string + description: A description of the token. + name: + type: string + description: The name of the token. + symbol: + type: string + description: The symbol of the token. + authority: + type: string + description: The token authority address. + supply: + type: integer + description: The total supply of the token. + token_program: + type: string + description: The Solana token program ID. + insufficient_data: + type: boolean + description: Whether there is insufficient data for this token. + + AssociatedTokenAccounts: + type: array + items: + $ref: '#/components/schemas/AssociatedTokenAccount' diff --git a/bot/app/requirements.txt b/bot/app/requirements.txt new file mode 100644 index 0000000..97dc7cd --- /dev/null +++ b/bot/app/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn diff --git a/bot/app/routes.py b/bot/app/routes.py new file mode 100644 index 0000000..8cfe825 --- /dev/null +++ b/bot/app/routes.py @@ -0,0 +1,6 @@ +async def get_quote(): + """ + A mock function to simulate retrieving a quote. + Replace this with actual business logic. + """ + return {"quote": "Keep it simple, but significant."} diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 2cc08c2..70909cd 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -429,7 +429,7 @@ async def count_associated_token_accounts( async def detect_dust_token_accounts( wallet_pubkey: Pubkey, - token_mint_addres: str = None, + token_mint_address: str = None, do_balance_aproximation: bool = True ) -> list[dict]: """ @@ -531,7 +531,7 @@ async def detect_dust_token_accounts( return account_output except Exception as e: - print(f"Error fetching token '{token_mint_addres}' balance: {e}") + print(f"Error fetching token '{token_mint_address}' balance: {e}") return account_output async def burn_associated_token_account( @@ -782,8 +782,6 @@ async def test(): decimals=6 ) - - account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" account = None token_accounts = await count_associated_token_accounts(wallet_pubkey=Pubkey.from_string(solana_address)) @@ -791,7 +789,7 @@ async def test(): time.sleep(2) token_accounts_data = await detect_dust_token_accounts( wallet_pubkey=Pubkey.from_string(solana_address), - token_mint_addres=account + token_mint_address=account ) print("USD being held: {}".format( sum(account["sol_balance_usd"] for account in token_accounts_data if account["is_dust"]) From ee0689b4d584f624a63aca8ce0848221bbd40428 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 28 Dec 2024 18:26:14 +0100 Subject: [PATCH 044/103] Adding outer count_associated_token_accounts and fetch_associated_token_accounts GET endpoints created from openAPI specs. --- bot/app/api/__init__.py | 0 bot/app/api/models/__init__.py | 0 bot/app/api/models/outer_models.py | 87 +++++++++++++++++++ bot/app/api/routes/__init__.py | 0 bot/app/api/routes/outer/__init__.py | 0 .../routes/outer/associated_token_accounts.py | 10 +++ .../outer/count_associated_token_accounts.py | 37 ++++++++ bot/app/main.py | 17 ++-- bot/app/requirements.txt | 1 + bot/app/routes.py | 6 -- bot/todo.yaml | 33 +------ 11 files changed, 147 insertions(+), 44 deletions(-) create mode 100644 bot/app/api/__init__.py create mode 100644 bot/app/api/models/__init__.py create mode 100644 bot/app/api/models/outer_models.py create mode 100644 bot/app/api/routes/__init__.py create mode 100644 bot/app/api/routes/outer/__init__.py create mode 100644 bot/app/api/routes/outer/associated_token_accounts.py create mode 100644 bot/app/api/routes/outer/count_associated_token_accounts.py delete mode 100644 bot/app/routes.py diff --git a/bot/app/api/__init__.py b/bot/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/models/__init__.py b/bot/app/api/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py new file mode 100644 index 0000000..f6ef442 --- /dev/null +++ b/bot/app/api/models/outer_models.py @@ -0,0 +1,87 @@ +from __future__ import annotations +from typing import Annotated, List, Optional +from pydantic import AnyUrl, BaseModel, Field, RootModel, constr + + +AccountAddressType = Annotated[ + str, + Field( + pattern=r"^[1-9A-HJ-NP-Za-km-z]{44}$", + description=( + "The Solana associated token account address. Must be a valid address with 44 characters, " + "excluding the letters `l`, `I`, and `O` to avoid ambiguity." + ) + ) +] + +class CountAssociatedTokenAccounts(BaseModel): + total_accounts: int = Field( + 0, description='Total number of associated accounts.' + ) + burnable_accounts: int = Field( + 0, description='Number of accounts eligible for burn.' + ) + accounts_for_manual_review: int = Field( + 0, description='Number of accounts requiring manual review.' + ) + rent_balance: int = Field( + 0, description='Total rent balance for the accounts.' + ) + rent_balance_usd: int = Field( + 0, description='Total rent balance converted to USD.' + ) + + +class AssociatedTokenAccount(BaseModel): + token_mint: Optional[str] = Field( + None, description='The mint address of the token.' + ) + associated_token_account: Optional[str] = Field( + None, description='The associated token account address.' + ) + owner: Optional[str] = Field( + None, description='The owner of the associated token account.' + ) + token_amount: Optional[float] = Field( + None, description='The amount of tokens held.' + ) + token_price: Optional[float] = Field(None, description='The price of the token.') + token_value: Optional[float] = Field( + None, description='The total value of the tokens held.' + ) + decimals: Optional[int] = Field( + None, description='The number of decimals for the token.' + ) + sol_balance: Optional[float] = Field( + None, description='The balance of SOL in the associated account.' + ) + sol_balance_usd: Optional[float] = Field( + None, description='The balance of SOL converted to USD.' + ) + is_dust: Optional[bool] = Field( + None, description='Whether the token is considered "dust."' + ) + uri: Optional[AnyUrl] = Field( + None, description="URI pointing to the token's metadata." + ) + cdn_uri: Optional[AnyUrl] = Field( + None, description='CDN URI pointing to cached metadata.' + ) + mime: Optional[str] = Field( + None, description="The MIME type of the token's metadata file." + ) + description: Optional[str] = Field(None, description='A description of the token.') + name: Optional[str] = Field(None, description='The name of the token.') + symbol: Optional[str] = Field(None, description='The symbol of the token.') + authority: Optional[str] = Field(None, description='The token authority address.') + supply: Optional[int] = Field(None, description='The total supply of the token.') + token_program: Optional[str] = Field( + None, description='The Solana token program ID.' + ) + insufficient_data: Optional[bool] = Field( + None, description='Whether there is insufficient data for this token.' + ) + + +class AssociatedTokenAccounts(RootModel[List[AssociatedTokenAccount]]): + pass \ No newline at end of file diff --git a/bot/app/api/routes/__init__.py b/bot/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/routes/outer/__init__.py b/bot/app/api/routes/outer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py new file mode 100644 index 0000000..06997b2 --- /dev/null +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +# Create a router for the associated_token_accounts routes +router = APIRouter() + +@router.get("/associated_token_accounts") +async def fetch_associated_token_accounts(): + response = {"detail": "Dummy response"} + return response + diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py new file mode 100644 index 0000000..98254a1 --- /dev/null +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, HTTPException, Query, Depends +from pydantic import ValidationError, Field, constr + +from api.models.outer_models import AccountAddressType, CountAssociatedTokenAccounts + +# Create a router for the associated_token_accounts routes +router = APIRouter() + + +# Dependency for pre-validation +def validate_account_address(account_address: AccountAddressType) -> str: + return account_address + +@router.get( + path="/associated_token_accounts/count", + response_model=CountAssociatedTokenAccounts, + summary="Get account statistics for a Solana associated token account", + description="Retrieve statistics for a given Solana associated token account address." +) +async def count_associated_token_accounts( + account_address: str = Depends(validate_account_address) +): + # Mock logic for checking account existence (replace with actual logic) + if account_address != "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb": + raise HTTPException( + status_code=404, + detail="Account not found." + ) + response = CountAssociatedTokenAccounts( + total_accounts=1, + burnable_accounts=1, + accounts_for_manual_review=0, + rent_balance=1.0, + rent_balance_usd=225.0 + ) + return response + diff --git a/bot/app/main.py b/bot/app/main.py index 60a61c6..ce97cc3 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -1,10 +1,13 @@ from fastapi import FastAPI -from routes import get_quote - +from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route +from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route # Create the FastAPI app -app = FastAPI() +app = FastAPI( + debug=False, + title="Ghost Funds Solana's API", + version="0.1" +) -# Define routes -@app.post("/get_quote") -async def fetch_quote(): - return await get_quote() +# Including routes +app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) +app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) diff --git a/bot/app/requirements.txt b/bot/app/requirements.txt index 97dc7cd..ef5948c 100644 --- a/bot/app/requirements.txt +++ b/bot/app/requirements.txt @@ -1,2 +1,3 @@ fastapi uvicorn +datamodel-code-generator diff --git a/bot/app/routes.py b/bot/app/routes.py deleted file mode 100644 index 8cfe825..0000000 --- a/bot/app/routes.py +++ /dev/null @@ -1,6 +0,0 @@ -async def get_quote(): - """ - A mock function to simulate retrieving a quote. - Replace this with actual business logic. - """ - return {"quote": "Keep it simple, but significant."} diff --git a/bot/todo.yaml b/bot/todo.yaml index 33aa837..2140915 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -7,37 +7,8 @@ json.set "token:5SzQBTqXTbFYmPaamAKj5ajbzJk9ANNqJf2gaVZ1pump" $ '{"amount":1.0," # TODO -√ Step1: return this - - total associated token accounts - - total burnable accounts - - amount in solanas (aproximation) - - amount in usd - - Notes: - - Ver el request directo de get token account by owner - y así tengo los decimales de una - - Obtener el valors del token y multiplicarlo por los tokens - para decidir si el token es quemable o no - -√ Step2: return a list of associated token accounts with: - - Token description - - Name - - Symbol - - uri - - cdn_uri - - mime - - authority - - tokens - - price per token - - account balance in sol - - account balance in usd - -√ Step 3: Burn token for a particular account - -√ Step 5: close account - -- Step 6: get close account info and validate that it's deleted. - +- Create API schema and generate pydantic models for + - get From 3559c700e40fa593e7b32c9da65b002962cc564e Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 28 Dec 2024 21:40:18 +0100 Subject: [PATCH 045/103] /associated_token_accounts/count endpoint implementation. Adding debug to uvicorn service --- .vscode/launch.json | 18 +- bot/app/api/config/__init__.py | 35 ++ bot/app/api/libs/__init__.py | 0 bot/app/api/libs/utils.py | 451 ++++++++++++++++++ bot/app/api/models/outer_models.py | 4 +- .../outer/count_associated_token_accounts.py | 27 +- 6 files changed, 520 insertions(+), 15 deletions(-) create mode 100644 bot/app/api/config/__init__.py create mode 100644 bot/app/api/libs/__init__.py create mode 100644 bot/app/api/libs/utils.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 9b81315..919e8ac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -64,7 +64,23 @@ "ENVIRONMENT": "DUMMY" }, "justMyCode": false - } + }, + { + "name": "Debug FastAPI with Uvicorn", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": [ + "main:app", + "--port", + "5001", + "--reload" + ], + "jinja": true, + "env": { + "PYTHONPATH": "${workspaceFolder}/bot/app" + } + } ] } \ No newline at end of file diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py new file mode 100644 index 0000000..1cd0d13 --- /dev/null +++ b/bot/app/api/config/__init__.py @@ -0,0 +1,35 @@ +import os +from enum import Enum + + +class AppMode(Enum): + development = "DEVELOPMENT" + real = "PRODUCTION" + + +class AuthConfig: + # our code environment + ENVIRONMENT = os.environ.get("ENVIRONMENT", "local") + APPMODE = os.environ.get("APPMODE", AppMode.development.value) + + TIME_FORMAT = "%b %-d %-I:%M:%S %p" + + # Stable and liquid pairs: 0.1% to 0.5%. + # Moderate trading volumes: 0.5% to 1%. + # High volatility or limited liquidity: 1% to 3% + # Very volatile or low-liquidity asset: up to 5% + SLIPPAGE = os.environ.get("SLIPPAGE", 40) + FEES = float(os.environ.get("FEES", 0.0015)) + FEES_INCREASMENT = 0.000805 + FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS + + RETRIES = 5 + RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" + RPC_JITO_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" + #DB + REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") + REDIS_PORT = os.environ.get("REDIS_PORT", "6379") + + +appconfig: AuthConfig = AuthConfig() diff --git a/bot/app/api/libs/__init__.py b/bot/app/api/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py new file mode 100644 index 0000000..677d612 --- /dev/null +++ b/bot/app/api/libs/utils.py @@ -0,0 +1,451 @@ +import random +import requests +import struct +import time + +from datetime import datetime +from solana.rpc.async_api import AsyncClient +from solana.rpc.types import TxOpts +from solana.rpc.commitment import Confirmed + +from solders.pubkey import Pubkey +from spl.token.instructions import close_account, CloseAccountParams +from solders.pubkey import Pubkey +from solders.keypair import Keypair +from solders.compute_budget import set_compute_unit_price +from solders.transaction import Transaction +from spl.token.instructions import ( + burn_checked, + BurnCheckedParams, +) +from api.config import appconfig + + +async def get_solana_balance(public_key: Pubkey) -> float: + """ + Fetches the SOL balance of a given Solana wallet address. + + :param wallet_address: The public address of the Solana wallet. + :return: Balance in SOL as a float. + """ + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + # Fetch the balance (in lamports) + balance_response = await client.get_balance(public_key) + # Balance is returned in lamports (1 SOL = 10^9 lamports) + lamports = balance_response.value + sol_balance = lamports / 1e9 + return sol_balance + except Exception as e: + print(f"Error fetching balance: {e.error_msg}") + return 0.0 + +def get_solana_price() -> float: + """ + Retrieves the current Solana price in USD using a public API. + + Returns: + float: Current Solana price in USD. + """ + retries = appconfig.RETRIES + price = 0 + counter = 0 + while True: + try: + if counter >= retries: + break + + response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") + response.raise_for_status() + price_data = response.json() + price = price_data["solana"]["usd"] + break + except Exception as e: + counter += 1 + print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( + counter, + retries + )) + time.sleep(counter) + + return price + +async def get_token_accounts_by_owner(wallet_address=str)->dict: + response = {} + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getTokenAccountsByOwner", + "params": [ + wallet_address, + { + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "encoding": "jsonParsed" + } + ] + } + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("get_token_accounts_by_owner: Bad status code '{}' recevied for token {}".format( + response.status_code, + wallet_address + ) + ) + return response + + content = response.json() + response = content["result"]["value"] + + return response + except Exception as e: + print("get_token_accounts_by_owner-> Error: {}".format(e)) + return response + +def get_token_metadata(token_address: str)->dict: + metadata = {} + + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getAsset", + "params": { + "id": token_address + } + } + retries = appconfig.RETRIES + counter = 0 + while True: + if counter >= retries: + break + + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + counter += 1 + print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( + response.status_code, + token_address, + counter, + retries + ) + ) + time.sleep(counter) + continue + + content = response.json() + metadata.update(content["result"]["content"]["files"][0]) + metadata.update(content["result"]["content"]["metadata"]) + metadata["authority"] = content["result"]["authorities"][0]["address"] + metadata["supply"] = content["result"]["token_info"]["supply"] + metadata["decimals"] = content["result"]["token_info"]["decimals"] + metadata["token_program"] = content["result"]["token_info"]["token_program"] + metadata["insufficient_data"] = False + + if "price_info" in content["result"]["token_info"]: + metadata["price_info"] = content["result"]["token_info"]["price_info"] + else: + # It might happen that the token comes with no price info. IF so, we'll mark the token + metadata["price_info"] = {"price_per_token": 0} + + metadata["insufficient_data"] = True + break + + except Exception as e: + counter += 1 + print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( + token_address, + counter, + retries + ) + ) + time.sleep(counter) + + return metadata + +async def count_associated_token_accounts( + wallet_pubkey: Pubkey +) -> int: + """ + Fetch the amount of associated token accounts an address holds + :param wallet_pubkey: The public address of the Solana wallet. + :return: amount of associated token accounts + """ + total = { + "total_accounts": 0, + "burnable_accounts": 0, + "accounts_for_manual_review": 0, + "rent_balance": 0, + "rent_balance_usd": 0 + } + min_token_value = 1 + account_samples = 5 + + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + usd_sol_value = 0 + + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] + + if accounts: + usd_sol_value = get_solana_price() + if usd_sol_value == 0: + return total + # Calculating accounts balance as AN APROXIMATION to speed up this process + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + average_balance = sum_balance / len(account_sample_list) + + total["rent_balance"] = average_balance * len(accounts) + total["rent_balance_usd"] = total["rent_balance"] * usd_sol_value + + accounts_for_manual_review = 0 + for account in accounts: + mint = account["account"]["data"]["parsed"]["info"]["mint"] + token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + + token_value = 1 + total["total_accounts"] += 1 + if token_ammount * token_value < min_token_value: + total["burnable_accounts"] += 1 + else: + print("token {} has significan value".format(mint)) + + total["accounts_for_manual_review"] = accounts_for_manual_review + + return total + +async def detect_dust_token_accounts( + wallet_pubkey: Pubkey, + token_mint_address: str = None, + do_balance_aproximation: bool = True +) -> list[dict]: + """ + Fetches the balance of a specific token in a given Solana wallet. + + :param wallet_pubkey: The public address of the Solana wallet. + :return: Token balance as a float. + """ + min_token_value = 1 + account_samples = 5 + working_balance = 0 + # Refactor this: remove this line of code and test as client is not being used + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] + + if not accounts: + return [] + + # Fetch the current Solana price + sol_price = get_solana_price() + if sol_price == 0: + return [] + + if do_balance_aproximation: + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + working_balance = sum_balance / len(account_sample_list) + + counter = 0 + account_output = [] + for account in accounts: + counter +=1 + + mint = account["account"]["data"]["parsed"]["info"]["mint"] + owner = account["account"]["data"]["parsed"]["info"]["owner"] + token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] + associated_token_account = account["pubkey"] + + metadata = get_token_metadata(token_address=mint) + + token_price = metadata["price_info"]["price_per_token"] + token_value = token_price * token_amount + + uri = metadata["uri"] + cdn_uri = metadata["cdn_uri"] + mime = metadata["mime"] + description = metadata["description"] if "description" in metadata else "" + name = metadata["name"].strip() + symbol = metadata["symbol"].strip() + authority = metadata["authority"] + supply = metadata["supply"] + token_program = metadata["token_program"] + insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price + + account_output.append( + { + "token_mint": mint, + "associated_token_account": associated_token_account, + "owner": owner, + "token_amount": token_amount, + "token_price": token_price, + "token_value": token_value, + "decimals": decimals, + "sol_balance": working_balance, + "sol_balance_usd": working_balance * sol_price, + "is_dust": token_value < min_token_value, + "uri": uri, + "cdn_uri": cdn_uri, + "mime": mime, + "description": description, + "name": name, + "symbol": symbol, + "authority": authority, + "supply": supply, + "token_program": token_program, + "insufficient_data": insufficient_data, + } + ) + + return account_output + + except Exception as e: + print(f"Error fetching token '{token_mint_address}' balance: {e}") + return account_output + +async def burn_and_close_associated_token_account( + associated_token_account: Pubkey, + token_mint: Pubkey, + decimals: int, + keypair: Keypair +) -> str: + """ + Burs all tokens from the associated token account + :param associated_token_account[Pubkey]: associated token account + :param token_mint[Pubkey]: tokens to get burned + :param keypair[Keypair]: signer and account owner + :param decimals[int]: The token's decimals (default 9 for SOL). + :return: [str] transaction signature. + """ + txn_signature = None + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + return txn_signature + + # Derive the associated token account address + TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + + # BURN + data = account_info.data + if len(data) != 165: + print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + str(associated_token_account), + str(token_mint) + )) + ( + mint, + owner, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + lamports = amount_lamports + owner = owner + amount = lamports + + # Construct the burn instruction + params = BurnCheckedParams( + program_id=TOKEN_PROGRAM_ID, + mint=token_mint, + account=associated_token_account, + owner=keypair.pubkey(), + amount=amount, + decimals=decimals, + signers=[keypair.pubkey()] + ) + burn_ix = burn_checked(params=params) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=keypair.pubkey(), + owner=keypair.pubkey() + ) + ) + + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + tx = Transaction.new_signed_with_payer( + instructions=[set_compute_unit_price(1_000), burn_ix, close_ix], + payer=keypair.pubkey(), + signing_keypairs=[keypair], + recent_blockhash=recent_blockhash + ) + tx_signature = await client.send_transaction( + txn=tx, + opts=TxOpts(preflight_commitment=Confirmed), + ) + + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( + "Transfer", + tx_signature.value, + current_time + )) + await client.confirm_transaction(tx_signature.value, commitment="confirmed") + print("Transaction confirmed") + + except Exception as e: + print("transfer_solanas Error: {}".format(e)) + + return txn_signature diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index f6ef442..a716be4 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -24,10 +24,10 @@ class CountAssociatedTokenAccounts(BaseModel): accounts_for_manual_review: int = Field( 0, description='Number of accounts requiring manual review.' ) - rent_balance: int = Field( + rent_balance: float = Field( 0, description='Total rent balance for the accounts.' ) - rent_balance_usd: int = Field( + rent_balance_usd: float = Field( 0, description='Total rent balance converted to USD.' ) diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 98254a1..041bb57 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -2,7 +2,8 @@ from pydantic import ValidationError, Field, constr from api.models.outer_models import AccountAddressType, CountAssociatedTokenAccounts - +from api.libs.utils import count_associated_token_accounts +from solders.pubkey import Pubkey # Create a router for the associated_token_accounts routes router = APIRouter() @@ -17,21 +18,23 @@ def validate_account_address(account_address: AccountAddressType) -> str: summary="Get account statistics for a Solana associated token account", description="Retrieve statistics for a given Solana associated token account address." ) -async def count_associated_token_accounts( +async def associated_token_accounts_count( account_address: str = Depends(validate_account_address) ): # Mock logic for checking account existence (replace with actual logic) - if account_address != "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb": - raise HTTPException( - status_code=404, - detail="Account not found." - ) + # if account_address != "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb": + # raise HTTPException( + # status_code=404, + # detail="Account not found." + # ) + wallet_pubkey = Pubkey.from_string(account_address) + data = await count_associated_token_accounts(wallet_pubkey=wallet_pubkey) response = CountAssociatedTokenAccounts( - total_accounts=1, - burnable_accounts=1, - accounts_for_manual_review=0, - rent_balance=1.0, - rent_balance_usd=225.0 + total_accounts=data["total_accounts"], + burnable_accounts=data["burnable_accounts"], + accounts_for_manual_review=data["accounts_for_manual_review"], + rent_balance=data["rent_balance"], + rent_balance_usd=data["rent_balance_usd"] ) return response From e744a95329dd6ca916292ed5474b3ff0dce21038 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 28 Dec 2024 22:16:02 +0100 Subject: [PATCH 046/103] Implmenting fetch_associated_token_accounts endpoint, updating requirements and cleaning imports --- bot/app/api/libs/utils.py | 3 +- .../routes/outer/associated_token_accounts.py | 52 +++++++++++++++++-- .../outer/count_associated_token_accounts.py | 3 +- bot/app/openapi/external_endpoints.yaml | 14 +---- bot/app/requirements.txt | 8 +-- 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 677d612..9970ea9 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -243,7 +243,6 @@ async def count_associated_token_accounts( async def detect_dust_token_accounts( wallet_pubkey: Pubkey, - token_mint_address: str = None, do_balance_aproximation: bool = True ) -> list[dict]: """ @@ -345,7 +344,7 @@ async def detect_dust_token_accounts( return account_output except Exception as e: - print(f"Error fetching token '{token_mint_address}' balance: {e}") + print(f"Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") return account_output async def burn_and_close_associated_token_account( diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py index 06997b2..2dde072 100644 --- a/bot/app/api/routes/outer/associated_token_accounts.py +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -1,10 +1,52 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from typing import List +from api.models.outer_models import AccountAddressType, AssociatedTokenAccount, AssociatedTokenAccounts +from api.libs.utils import detect_dust_token_accounts +from solders.pubkey import Pubkey # Create a router for the associated_token_accounts routes router = APIRouter() -@router.get("/associated_token_accounts") -async def fetch_associated_token_accounts(): - response = {"detail": "Dummy response"} - return response + +# Dependency for pre-validation +def validate_account_address(account_address: AccountAddressType) -> str: + return account_address + +@router.get( + path="/associated_token_accounts", + response_model=AssociatedTokenAccounts, + summary="Get account statistics for a Solana associated token account", + description="Retrieve statistics for a given Solana associated token account address." +) +async def fetch_associated_token_accounts( + account_address: str = Depends(validate_account_address) +): + wallet_pubkey = Pubkey.from_string(account_address) + ata_list = await detect_dust_token_accounts(wallet_pubkey=wallet_pubkey) + accounts: List[AssociatedTokenAccount] = [] + for ata in ata_list: + account = AssociatedTokenAccount( + token_mint=ata["token_mint"], + associated_token_account=ata["associated_token_account"], + owner=ata["owner"], + token_amount=ata["token_amount"], + token_price=ata["token_price"], + token_value=ata["token_value"], + decimals=ata["decimals"], + sol_balance=ata["sol_balance"], + sol_balance_usd=ata["sol_balance_usd"], + is_dust=ata["is_dust"], + uri=ata["uri"], + cdn_uri=ata["cdn_uri"], + mime=ata["mime"], + description=ata["description"], + name=ata["name"], + symbol=ata["symbol"], + authority=ata["authority"], + supply=ata["supply"], + token_program=ata["token_program"], + insufficient_data=ata["insufficient_data"] + ) + accounts.append(account) + return accounts diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 041bb57..92bdf65 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -1,5 +1,4 @@ -from fastapi import APIRouter, HTTPException, Query, Depends -from pydantic import ValidationError, Field, constr +from fastapi import APIRouter, Depends from api.models.outer_models import AccountAddressType, CountAssociatedTokenAccounts from api.libs.utils import count_associated_token_accounts diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 165de22..4141996 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -10,7 +10,7 @@ paths: get: summary: Get all associated token account for a fiven Solana account parameters: - - name: wallet_pubkey + - name: account_address in: query required: true description: The Solana associated token account address. @@ -48,7 +48,7 @@ paths: get: summary: Retrieve associated token accounts parameters: - - name: wallet_pubkey + - name: account_address in: query required: true description: The wallet public key (Solana format). @@ -58,16 +58,6 @@ paths: description: > Must be a valid Solana public key format with 44 characters, excluding the letters `l`, `I`, and `O` to avoid ambiguity. - - name: token_mint_address - in: query - required: true - description: The token mint address (Solana format). - schema: - type: string - pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ - description: > - Must be a valid Solana address format with 44 characters, - excluding the letters `l`, `I`, and `O` to avoid ambiguity. responses: '200': description: List of associated token accounts. diff --git a/bot/app/requirements.txt b/bot/app/requirements.txt index ef5948c..53b0fa6 100644 --- a/bot/app/requirements.txt +++ b/bot/app/requirements.txt @@ -1,3 +1,5 @@ -fastapi -uvicorn -datamodel-code-generator +fastapi==0.115.6 +uvicorn==0.34.0 +datamodel-code-generator==0.26.4 +solders==0.21.0 +solana==0.35.0 From e7d3efcceae3635285cc97f5ced529d605bb4676 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 2 Jan 2025 17:37:19 +0100 Subject: [PATCH 047/103] AWS Docker image, EC2 config step by step and api gateway config --- .gitignore | 2 + bot/app/AWS/Dockerfile | 22 ++++++++++ bot/app/AWS/api_gateway_dev_config.json | 53 +++++++++++++++++++++++++ bot/app/AWS/aws_ec2_userdata.sh | 6 +++ bot/app/AWS/steps.md | 38 ++++++++++++++++++ bot/app/api/libs/utils.py | 3 +- bot/app/api/routes/outer/test.py | 2 + bot/app/main.py | 3 ++ bot/app/requirements.txt | 2 + 9 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 bot/app/AWS/Dockerfile create mode 100644 bot/app/AWS/api_gateway_dev_config.json create mode 100644 bot/app/AWS/aws_ec2_userdata.sh create mode 100644 bot/app/AWS/steps.md create mode 100644 bot/app/api/routes/outer/test.py diff --git a/.gitignore b/.gitignore index 0b1c76b..536d785 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ test-ledger dist/ +site-packages/ + .keys*/ .keys*/** diff --git a/bot/app/AWS/Dockerfile b/bot/app/AWS/Dockerfile new file mode 100644 index 0000000..ef6ca8f --- /dev/null +++ b/bot/app/AWS/Dockerfile @@ -0,0 +1,22 @@ +# Use an official Python runtime as a parent image +#FROM python:3.10-slim +FROM python:3.12-slim + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container +COPY . /app + +# Install any needed packages specified in requirements.txt +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + +# Make port 80 available to the world outside this container +EXPOSE 8000 + +# Define environment variable +ENV NAME=World + +# Run app.py when the container launches +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/bot/app/AWS/api_gateway_dev_config.json b/bot/app/AWS/api_gateway_dev_config.json new file mode 100644 index 0000000..71981ee --- /dev/null +++ b/bot/app/AWS/api_gateway_dev_config.json @@ -0,0 +1,53 @@ +{ + "openapi" : "3.0.1", + "info" : { + "title" : "Solana API", + "description" : "API for interacting with Solana-related functionalities.", + "version" : "1.0.0" + }, + "servers" : [ { + "url" : "https://14s40zm2d2.execute-api.eu-west-1.amazonaws.com/{basePath}", + "variables" : { + "basePath" : { + "default" : "dev" + } + } + } ], + "paths" : { + "/associated_token_accounts" : { + "get" : { + "responses" : { + "default" : { + "description" : "Default response for GET /associated_token_accounts" + } + }, + "x-amazon-apigateway-integration" : { + "payloadFormatVersion" : "1.0", + "type" : "http_proxy", + "httpMethod" : "GET", + "uri" : "http://54.154.161.137:443/api/associated_token_accounts", + "connectionType" : "INTERNET", + "timeoutInMillis" : 15000 + } + } + }, + "/associated_token_accounts/count" : { + "get" : { + "responses" : { + "default" : { + "description" : "Default response for GET /associated_token_accounts/count" + } + }, + "x-amazon-apigateway-integration" : { + "payloadFormatVersion" : "1.0", + "type" : "http_proxy", + "httpMethod" : "GET", + "uri" : "http://54.154.161.137:443/api/associated_token_accounts/count", + "connectionType" : "INTERNET", + "timeoutInMillis" : 15000 + } + } + } + }, + "x-amazon-apigateway-importexport-version" : "1.0" +} \ No newline at end of file diff --git a/bot/app/AWS/aws_ec2_userdata.sh b/bot/app/AWS/aws_ec2_userdata.sh new file mode 100644 index 0000000..6a3b4ad --- /dev/null +++ b/bot/app/AWS/aws_ec2_userdata.sh @@ -0,0 +1,6 @@ +#!/bin/bash +yum update -y +yum install docker -y +service docker start +usermod -aG docker ec2-user +chkconfig docker on diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md new file mode 100644 index 0000000..02c87ad --- /dev/null +++ b/bot/app/AWS/steps.md @@ -0,0 +1,38 @@ +# AWS +# cli login +aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 237733826785.dkr.ecr.eu-west-1.amazonaws.com +# Docker build image +## Note: must provide --provenance=false to create lambda function from latest image. If not it'll fail. +docker build --platform linux/arm64/v8 --provenance=false -t ghostfunds-ata-service . +# Run and test locally +docker run -p 8000:8000 ghostfunds-ata-service +# Docker tag and push image into AWS ECR +docker tag ghostfunds-ata-service:latest 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest +docker push 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest + +# EC2 and docker +## Install libraries +sudo yum update -y +sudo yum install docker -y +sudo service docker start + +## Configure the client and include the keys for your AWS user +aws configure + +## cli login +aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 237733826785.dkr.ecr.eu-west-1.amazonaws.com +## Add Your User to the Docker Group: +sudo usermod -aG docker $(whoami) +newgrp docker +### Verify Group Membership +groups +### Check docker's permission. Should be something like this: srw-rw---- 1 root docker 0 Dec 9 13:11 /var/run/docker.sock +ls -l /var/run/docker.sock +## restart +sudo systemctl restart docker +# Pull the image +docker pull 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest +## List images +aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 +# Run the docker container +docker run -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 9970ea9..db20850 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -395,9 +395,8 @@ async def burn_and_close_associated_token_account( close_authority_option, close_authority ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) - lamports = amount_lamports owner = owner - amount = lamports + amount = amount_lamports # Construct the burn instruction params = BurnCheckedParams( diff --git a/bot/app/api/routes/outer/test.py b/bot/app/api/routes/outer/test.py new file mode 100644 index 0000000..09b55ec --- /dev/null +++ b/bot/app/api/routes/outer/test.py @@ -0,0 +1,2 @@ +async def test(): + return {"detail": "This is a layer test"} diff --git a/bot/app/main.py b/bot/app/main.py index ce97cc3..8e3e0c4 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from mangum import Mangum from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route # Create the FastAPI app @@ -11,3 +12,5 @@ # Including routes app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) + +handler = Mangum(app) diff --git a/bot/app/requirements.txt b/bot/app/requirements.txt index 53b0fa6..5a52ce6 100644 --- a/bot/app/requirements.txt +++ b/bot/app/requirements.txt @@ -3,3 +3,5 @@ uvicorn==0.34.0 datamodel-code-generator==0.26.4 solders==0.21.0 solana==0.35.0 +mangum==0.19.0 +requests==2.32.3 \ No newline at end of file From 28c90c7205daca608cb6fe98806bde59bce1b689 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 3 Jan 2025 00:05:43 +0100 Subject: [PATCH 048/103] new generate_vanity_wallet function --- bot/libs/utils.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 70909cd..73b75b2 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -757,7 +757,41 @@ async def burn_and_close_associated_token_account( return txn_signature + +def generate_vanity_wallet(prefix: str) -> Keypair: + counter = 0 + start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("starting generate_vanity_wallet at {}".format(start_time)) + while True: + counter += 1 + # Generate a new keypair + keypair = Keypair() + public_key = str(keypair.pubkey()) + + # Check if the public key starts with the desired prefix + if public_key.lower().startswith(prefix.lower()): + stop_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Pubkey: {}".format(public_key)) + pk = base58.b58encode(bytes(keypair)).decode() + print("PK: {}".format(pk)) + print("finishing generate_vanity_wallet at {} after {} iterations".format( + stop_time, + counter + ) + ) + return keypair + + async def test(): + prefix = "Ghost" + + # Generate the vanity wallet + print(f"Generating wallet with prefix '{prefix}'...") + keypair = generate_vanity_wallet(prefix) + + if keypair: + return + solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" #solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" token_address = "FFqR2bk3ULB1WuYECRbooxPpbYvvZBp3z9Uc94Pqpump" From ff9bb9cc9e6c9db61114e34d745074d4bd0947cd Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Fri, 3 Jan 2025 14:43:59 +0100 Subject: [PATCH 049/103] INtroducing pagination for detect_dust_token_accounts and fetch_associated_token_accounts functions --- bot/app/api/config/__init__.py | 5 ++ bot/app/api/libs/utils.py | 38 +++++++++++--- bot/app/api/models/outer_models.py | 26 ++++++++-- .../routes/outer/associated_token_accounts.py | 51 ++++++++++++++++--- bot/todo.yaml | 40 ++++++++++----- 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 1cd0d13..8f67fb8 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -27,6 +27,11 @@ class AuthConfig: RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" RPC_JITO_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" + + #PAGINATION + DEFAULT_PAGE = 1 + DEFAULT_ITEMS_PER_PAGE = 10 + #DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index db20850..743bf26 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -1,3 +1,4 @@ +import math import random import requests import struct @@ -243,8 +244,10 @@ async def count_associated_token_accounts( async def detect_dust_token_accounts( wallet_pubkey: Pubkey, - do_balance_aproximation: bool = True -) -> list[dict]: + do_balance_aproximation: bool = True, + page: int = appconfig.DEFAULT_PAGE, + items_per_page: int = appconfig.DEFAULT_ITEMS_PER_PAGE +) -> tuple[list[dict], int, int]: """ Fetches the balance of a specific token in a given Solana wallet. @@ -257,6 +260,7 @@ async def detect_dust_token_accounts( # Refactor this: remove this line of code and test as client is not being used async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: + total_items = 0 # TODO: implement token's black list (like USDC, etc) token_blacklist = [] original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) @@ -266,12 +270,12 @@ async def detect_dust_token_accounts( ] if not accounts: - return [] + return [], page, total_items # Fetch the current Solana price sol_price = get_solana_price() if sol_price == 0: - return [] + return [], page, total_items if do_balance_aproximation: account_sample_list = accounts @@ -291,7 +295,26 @@ async def detect_dust_token_accounts( counter = 0 account_output = [] - for account in accounts: + # Sort ATAs by mint address (this will help with the pagination) + sorted_accounts = sorted( + accounts, + key=lambda x: x["account"]["data"]["parsed"]["info"]["mint"] + ) + + # Pagination + total_items = len(sorted_accounts) + # We can't expect to have more items in a page than what we have. Ex: can't have 50 page items on 10 items in total + total_pages = math.ceil(total_items / items_per_page) if items_per_page < total_items else total_items + # can't work with pages greater than the total pages we're dealing with + page = total_pages if page > total_pages else page + + start_index = (page - 1) * items_per_page + end_index = start_index + items_per_page if start_index + items_per_page < total_items else total_items + + # Paginate the list + sorted_accounts = sorted_accounts[start_index:end_index] + + for account in sorted_accounts: counter +=1 mint = account["account"]["data"]["parsed"]["info"]["mint"] @@ -341,11 +364,11 @@ async def detect_dust_token_accounts( } ) - return account_output + return account_output, page, total_items except Exception as e: print(f"Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") - return account_output + return account_output, page, total_items async def burn_and_close_associated_token_account( associated_token_account: Pubkey, @@ -447,3 +470,4 @@ async def burn_and_close_associated_token_account( print("transfer_solanas Error: {}".format(e)) return txn_signature + diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index a716be4..054a502 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -1,6 +1,6 @@ from __future__ import annotations from typing import Annotated, List, Optional -from pydantic import AnyUrl, BaseModel, Field, RootModel, constr +from pydantic import AnyUrl, BaseModel, Field, model_validator, ValidationError AccountAddressType = Annotated[ @@ -83,5 +83,25 @@ class AssociatedTokenAccount(BaseModel): ) -class AssociatedTokenAccounts(RootModel[List[AssociatedTokenAccount]]): - pass \ No newline at end of file +class AssociatedTokenAccounts(BaseModel): + page: int = Field( + ..., gt=0, description='The current page being retrieved (must be greater or equal than 0).' + ) + items: int = Field( + ..., gt=0, description='The number of items per page (must be greater or equal than 0).' + ) + total_items: int = Field( + ..., gt=-1, description='The total number of retrievable items (must be non-negative).' + ) + accounts: List[AssociatedTokenAccount] = Field( + description="The list of associated token accounts.", + default=[] + ) + + # @model_validator(mode="before") + # def validate_pagination(cls, values): + # page = values.get("page") + # items = values.get("items") + # if page <= 0 or items <= 0: + # raise ValidationError("Both 'page' and 'items' must be greater than 0.") + # return values diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py index 2dde072..cd52294 100644 --- a/bot/app/api/routes/outer/associated_token_accounts.py +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -1,8 +1,10 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from typing import List +from pydantic import PositiveInt from api.models.outer_models import AccountAddressType, AssociatedTokenAccount, AssociatedTokenAccounts from api.libs.utils import detect_dust_token_accounts +from api.config import appconfig from solders.pubkey import Pubkey # Create a router for the associated_token_accounts routes router = APIRouter() @@ -19,13 +21,38 @@ def validate_account_address(account_address: AccountAddressType) -> str: description="Retrieve statistics for a given Solana associated token account address." ) async def fetch_associated_token_accounts( - account_address: str = Depends(validate_account_address) + account_address: str = Depends(validate_account_address), + page: PositiveInt = Query( + default=appconfig.DEFAULT_PAGE, + description="The current page to retrieve (must be greater than 0)." + ), + items: PositiveInt = Query( + default=appconfig.DEFAULT_ITEMS_PER_PAGE, + description="The number of items per page (must be greater than 0)." + ), ): wallet_pubkey = Pubkey.from_string(account_address) - ata_list = await detect_dust_token_accounts(wallet_pubkey=wallet_pubkey) - accounts: List[AssociatedTokenAccount] = [] - for ata in ata_list: - account = AssociatedTokenAccount( + page = page if page >= appconfig.DEFAULT_PAGE else appconfig.DEFAULT_PAGE + items = items if items > 1 else appconfig.DEFAULT_ITEMS_PER_PAGE + + ata_list, page, total_items = await detect_dust_token_accounts( + wallet_pubkey=wallet_pubkey, + page=page, + items_per_page=items + ) + + # Calculate pagination details + if total_items == 0: + # If the list is empty, return a response with metadata and an empty accounts list + return AssociatedTokenAccounts( + page=page, + items=items, + total_items=0, + accounts=[] + ) + + accounts: List[AssociatedTokenAccount] = [ + AssociatedTokenAccount( token_mint=ata["token_mint"], associated_token_account=ata["associated_token_account"], owner=ata["owner"], @@ -47,6 +74,14 @@ async def fetch_associated_token_accounts( token_program=ata["token_program"], insufficient_data=ata["insufficient_data"] ) - accounts.append(account) - return accounts + for ata in ata_list + ] + + ata_accounts = AssociatedTokenAccounts( + page=page, + items=items, + total_items=total_items, + accounts=accounts + ) + return ata_accounts diff --git a/bot/todo.yaml b/bot/todo.yaml index 2140915..c82beed 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -1,16 +1,32 @@ -JSON.SET "token:4YFNRqzX1oNnXQZifn5p6Kj2T8ZztjXSjfhcF9Xapump" $ '{"amount":0.004,"is_traded":false,"timestamp":1690228321, "trader": "sniper", "name": "CELEBRINBORG", "symbol": "CBB", "track_traders": ["trader1", "trader2"]}' - -json.set "token:U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump" $ '{"amount":1.0,"is_checked":true,"is_traded":true,"is_closed":true,"timestamp":1733065571.934685,"trader":"sniper","signature":"o1bjZbyJ9SpTfaWvwecgCGGG2f5dTxDRQ4vdSi8dN3di3gX3w5CRqCHNs1ETLArMFt3azZ3Fk4qWbQPv2aT38pW","mint":"U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump","traderPublicKey":"5oLwPx3p8j8AK3P6fxdD23CRA7i3iUmCVwBi4wpsWLMP","txType":"create","initialBuy":67062499.999999,"bondingCurveKey":"5rv2wnm2H5MsYrgHauSfYRirQEGSkpPe7vfNkoqNB4WY","vTokensInBondingCurve":1005937500.000001,"vSolInBondingCurve":31.999999999999968,"marketCapSol":31.811121466293816,"name":"Tung Distilled AI","symbol":"Tung","uri":"https://ipfs.io/ipfs/QmXNFxxQPagxTvMdjJCtah9u6abwfdt77PtjS8sgWcYbGG"}' - - -json.set "token:5SzQBTqXTbFYmPaamAKj5ajbzJk9ANNqJf2gaVZ1pump" $ '{"amount":1.0,"is_checked":true,"is_traded":true,"is_closed":true,"timestamp":1733065571.934685,"trader":"sniper","signature":"o1bjZbyJ9SpTfaWvwecgCGGG2f5dTxDRQ4vdSi8dN3di3gX3w5CRqCHNs1ETLArMFt3azZ3Fk4qWbQPv2aT38pW","mint":"U3BHBrAf7pZoQLbbFgp9qx7JcJin8ovT7EBqeeapump","traderPublicKey":"5oLwPx3p8j8AK3P6fxdD23CRA7i3iUmCVwBi4wpsWLMP","txType":"create","initialBuy":67062499.999999,"bondingCurveKey":"5rv2wnm2H5MsYrgHauSfYRirQEGSkpPe7vfNkoqNB4WY","vTokensInBondingCurve":1005937500.000001,"vSolInBondingCurve":31.999999999999968,"marketCapSol":31.811121466293816,"name":"Tung Distilled AI","symbol":"Tung","uri":"https://ipfs.io/ipfs/QmXNFxxQPagxTvMdjJCtah9u6abwfdt77PtjS8sgWcYbGG"}' - - # TODO -- Create API schema and generate pydantic models for - - get - - +- app + - Pagination: + - Speed up: Add pagination to detect_dust_token_accounts function + +- EC2: + - Deploy app in + - Mini and check if it works + - Small and check if it works + - Have a permanent IP address + +- New endopints + - Post: get burn and close quote + - Create openAPI specs + - Create pydantic classes + - Create enpoint /associated_token_accounts/burn_and_close/get_quote + - Will call burn_and_close_associated_token_account without keypair + - WIll include 5% fees as a new instruction + - Check lookuptables + - Will return a Transaction.new_unsigned (create the Message) + - encode in base58 and return + + - Post: send transaction (hablar con Jorge primero) + - Create openAPI specs + - Create pydantic classes + - Create enpoint /associated_token_accounts/burn_and_close + - Will receive the signed transaction in base 58 and decode it + - Will send the transaction and return the signature. + - The front will take care of confirming the txn. From da2bd09da4bb0312c6358abcf09d587c086a5424 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 5 Jan 2025 01:13:43 +0100 Subject: [PATCH 050/103] adding new end point POST /associated_token_accounts/burn_and_close/get_transaction to yaml file. Adding docker start script for EC2 instance --- bot/app/AWS/start-docker-container.sh | 16 +++++++ bot/app/AWS/steps.md | 20 ++++++++- bot/app/{AWS => }/Dockerfile | 2 +- bot/app/openapi/external_endpoints.yaml | 59 ++++++++++++++++++++++--- bot/todo.yaml | 20 ++++----- 5 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 bot/app/AWS/start-docker-container.sh rename bot/app/{AWS => }/Dockerfile (97%) diff --git a/bot/app/AWS/start-docker-container.sh b/bot/app/AWS/start-docker-container.sh new file mode 100644 index 0000000..dee49b2 --- /dev/null +++ b/bot/app/AWS/start-docker-container.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# chkconfig: 2345 20 80 +# description: Start Docker container for Ghostfunds service +# processname: start-docker-container + +### BEGIN INIT INFO +# Provides: start-docker-container +# Required-Start: $network $docker +# Required-Stop: $network $docker +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start Ghostfunds Docker container +# Description: Runs a Docker container for the Ghostfunds service on startup +### END INIT INFO + +docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index 02c87ad..bb0820a 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -35,4 +35,22 @@ docker pull 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service: ## List images aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 # Run the docker container -docker run -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest +docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest + +# Start Docker when EC2 instance starts +## Create start-docker-container.sh and add the code +### - look into /spp/AWS/start-docker-container.sh +### - Adjust if dockerimage id changes +sudo vim /etc/init.d/start-docker-container.sh + +## Make executable script +sudo chmod +x /etc/init.d/start-docker-container.sh +## Add script to chkconfig +sudo chkconfig --add start-docker-container.sh +## Enable the script at startup +sudo chkconfig start-docker-container.sh on +## Test the script +sudo service start-docker-container.sh start +docker ps + + diff --git a/bot/app/AWS/Dockerfile b/bot/app/Dockerfile similarity index 97% rename from bot/app/AWS/Dockerfile rename to bot/app/Dockerfile index ef6ca8f..bd2575f 100644 --- a/bot/app/AWS/Dockerfile +++ b/bot/app/Dockerfile @@ -13,7 +13,7 @@ RUN pip install --upgrade pip RUN pip install --no-cache-dir -r requirements.txt # Make port 80 available to the world outside this container -EXPOSE 8000 +EXPOSE 443 # Define environment variable ENV NAME=World diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 4141996..0dcdd0f 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -93,7 +93,41 @@ paths: supply: 999999999694079 token_program: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" insufficient_data: true - + + /associated_token_accounts/burn_and_close/get_transaction: + post: + summary: Retrieve a transaction with both burn and close intructions for an associated token account + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetTransaction' + example: + quote: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" + responses: + '200': + description: Burn and close quote successfully retrieved. + content: + application/json: + schema: + type: object + properties: + quote: + type: string + format: base64 + description: A base64-encoded string representing the quote. + '404': + description: Account not found. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: "Associated token Account not found or miss match." + components: schemas: CountAssociatedTokenAccounts: @@ -116,7 +150,12 @@ components: type: number format: float description: Total rent balance converted to USD. - + + AssociatedTokenAccounts: + type: array + items: + $ref: '#/components/schemas/AssociatedTokenAccount' + AssociatedTokenAccount: type: object properties: @@ -188,7 +227,15 @@ components: type: boolean description: Whether there is insufficient data for this token. - AssociatedTokenAccounts: - type: array - items: - $ref: '#/components/schemas/AssociatedTokenAccount' + GetTransaction: + type: object + properties: + associated_token_account: + type: string + description: The associated token account address. + token_mint: + type: string + description: The mint address of the token. + decimals: + type: integer + description: The number of decimals for the token. \ No newline at end of file diff --git a/bot/todo.yaml b/bot/todo.yaml index c82beed..a042f85 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -4,8 +4,8 @@ - Speed up: Add pagination to detect_dust_token_accounts function - EC2: - - Deploy app in - - Mini and check if it works + √ Deploy app in + √√ Mini and check if it works - Small and check if it works - Have a permanent IP address @@ -13,20 +13,20 @@ - Post: get burn and close quote - Create openAPI specs - Create pydantic classes - - Create enpoint /associated_token_accounts/burn_and_close/get_quote + - Create enpoint /associated_token_accounts/burn_and_close/quote - Will call burn_and_close_associated_token_account without keypair - WIll include 5% fees as a new instruction - Check lookuptables - Will return a Transaction.new_unsigned (create the Message) - encode in base58 and return - - Post: send transaction (hablar con Jorge primero) - - Create openAPI specs - - Create pydantic classes - - Create enpoint /associated_token_accounts/burn_and_close - - Will receive the signed transaction in base 58 and decode it - - Will send the transaction and return the signature. - - The front will take care of confirming the txn. + # - Post: send transaction (hablar con Jorge primero) + # - Create openAPI specs + # - Create pydantic classes + # - Create enpoint /associated_token_accounts/burn_and_close + # - Will receive the signed transaction in base 58 and decode it + # - Will send the transaction and return the signature. + # - The front will take care of confirming the txn. From 23532897a49ec8692bdf98b8a9986a8b540ec1b6 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 6 Jan 2025 13:04:46 +0100 Subject: [PATCH 051/103] Adding skeleton for new POST endpoint to retreive transaction for ata close and burn --- bot/app/api/models/outer_models.py | 23 +++++++++++++- .../api/routes/outer/request_transaction.py | 25 +++++++++++++++ bot/app/main.py | 2 ++ bot/app/openapi/external_endpoints.yaml | 31 ++++++++++++------- bot/todo.yaml | 6 ++-- 5 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 bot/app/api/routes/outer/request_transaction.py diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 054a502..5571963 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -1,6 +1,6 @@ from __future__ import annotations from typing import Annotated, List, Optional -from pydantic import AnyUrl, BaseModel, Field, model_validator, ValidationError +from pydantic import AnyUrl, BaseModel, Field, constr AccountAddressType = Annotated[ @@ -105,3 +105,24 @@ class AssociatedTokenAccounts(BaseModel): # if page <= 0 or items <= 0: # raise ValidationError("Both 'page' and 'items' must be greater than 0.") # return values +class RequestTransaction(BaseModel): + associated_token_account: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( + Field( + ..., + description='The associated token account address.', + example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', + ) + ) + token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = Field( + ..., + description='The mint address of the token.', + example='DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump', + ) + decimals: int = Field( + ..., description='The number of decimals for the token.', example=6 + ) + +class Quote(BaseModel): + quote: Optional[str] = Field( + None, description='A base64-encoded string representing the quote.' + ) \ No newline at end of file diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py new file mode 100644 index 0000000..859848d --- /dev/null +++ b/bot/app/api/routes/outer/request_transaction.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from api.models.outer_models import Quote, RequestTransaction + +router = APIRouter() + +@router.post( + path="/associated_token_accounts/burn_and_close/transaction", + response_model=Quote, + summary="Retrieve a transaction with intructions to close an associated token account", + description="Retrieve a transaction with both burn and close intructions for an associated token account" +) +async def request_close_ata_transaction( + body: RequestTransaction +): + txn = None + ata = body.associated_token_account + token = body.token_mint + decimals = body.decimals + + + quote = Quote( + quote=txn + ) + return quote diff --git a/bot/app/main.py b/bot/app/main.py index 8e3e0c4..d1af701 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -2,6 +2,7 @@ from mangum import Mangum from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route +from api.routes.outer.request_transaction import router as request_ata_transaction_route # Create the FastAPI app app = FastAPI( debug=False, @@ -12,5 +13,6 @@ # Including routes app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) +app.include_router(router=request_ata_transaction_route, prefix="/api", tags=["Request Associated Token Account Transaction"]) handler = Mangum(app) diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 0dcdd0f..29e5c9e 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -94,7 +94,7 @@ paths: token_program: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" insufficient_data: true - /associated_token_accounts/burn_and_close/get_transaction: + /associated_token_accounts/burn_and_close/transaction: post: summary: Retrieve a transaction with both burn and close intructions for an associated token account requestBody: @@ -103,20 +103,16 @@ paths: application/json: schema: $ref: '#/components/schemas/GetTransaction' - example: - quote: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" responses: '200': description: Burn and close quote successfully retrieved. content: application/json: schema: - type: object - properties: - quote: - type: string - format: base64 - description: A base64-encoded string representing the quote. + $ref: '#/components/schemas/Quote' + example: + quote: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" + '404': description: Account not found. content: @@ -126,7 +122,7 @@ paths: properties: detail: type: string - example: "Associated token Account not found or miss match." + example: "Associated token Account not found or mismatch." components: schemas: @@ -233,9 +229,22 @@ components: associated_token_account: type: string description: The associated token account address. + pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ + example: "7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi" token_mint: type: string description: The mint address of the token. + pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ + example: "DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump" decimals: type: integer - description: The number of decimals for the token. \ No newline at end of file + description: The number of decimals for the token. + example: 6 + + Quote: + type: object + properties: + quote: + type: string + format: base64 + description: A base64-encoded string representing the quote. diff --git a/bot/todo.yaml b/bot/todo.yaml index a042f85..193908f 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -6,13 +6,13 @@ - EC2: √ Deploy app in √√ Mini and check if it works - - Small and check if it works + # - Small and check if it works - Have a permanent IP address - New endopints - Post: get burn and close quote - - Create openAPI specs - - Create pydantic classes + √ Create openAPI specs + √∫ Create pydantic classes - Create enpoint /associated_token_accounts/burn_and_close/quote - Will call burn_and_close_associated_token_account without keypair - WIll include 5% fees as a new instruction From 030803df87f8f57be2ade557d5433edb7ac40c0a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 6 Jan 2025 14:28:53 +0100 Subject: [PATCH 052/103] request_close_ata_transaction implementation and adding exception handling to app --- bot/app/api/handlers/__init__.py | 0 bot/app/api/handlers/exceptions.py | 14 ++ bot/app/api/libs/utils.py | 121 +++++++++++++++++- bot/app/api/models/outer_models.py | 10 +- .../api/routes/outer/request_transaction.py | 32 +++-- bot/app/main.py | 4 + bot/app/openapi/external_endpoints.yaml | 2 +- bot/todo.yaml | 2 +- 8 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 bot/app/api/handlers/__init__.py create mode 100644 bot/app/api/handlers/exceptions.py diff --git a/bot/app/api/handlers/__init__.py b/bot/app/api/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/handlers/exceptions.py b/bot/app/api/handlers/exceptions.py new file mode 100644 index 0000000..3e00797 --- /dev/null +++ b/bot/app/api/handlers/exceptions.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse + +# Define custom exception +class EntityNotFoundException(HTTPException): + def __init__(self, detail: str): + super().__init__(status_code=404, detail=detail) + +# Exception handler for EntityNotFoundException +def entity_not_found_exception_handler(request: Request, exc: EntityNotFoundException): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + ) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 743bf26..00864f9 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -19,7 +19,9 @@ burn_checked, BurnCheckedParams, ) +from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID from api.config import appconfig +from api.handlers.exceptions import EntityNotFoundException async def get_solana_balance(public_key: Pubkey) -> float: @@ -81,7 +83,7 @@ async def get_token_accounts_by_owner(wallet_address=str)->dict: "params": [ wallet_address, { - "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "programId": TOKEN_PROGRAM_ID }, { "encoding": "jsonParsed" @@ -393,10 +395,7 @@ async def burn_and_close_associated_token_account( print("Associated token account {} does not exist.".format( associated_token_account )) - return txn_signature - - # Derive the associated token account address - TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + raise txn_signature # BURN data = account_info.data @@ -423,7 +422,7 @@ async def burn_and_close_associated_token_account( # Construct the burn instruction params = BurnCheckedParams( - program_id=TOKEN_PROGRAM_ID, + program_id=Pubkey.from_string(TOKEN_PROGRAM_ID), mint=token_mint, account=associated_token_account, owner=keypair.pubkey(), @@ -471,3 +470,113 @@ async def burn_and_close_associated_token_account( return txn_signature + +def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: + """Derives the associated token address for the given wallet address and token mint. + + Returns: + The public key of the derived associated token address. + """ + key, _ = Pubkey.find_program_address( + seeds=[bytes(owner), bytes(TOKEN_PROGRAM_ID), bytes(mint)], + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return key + + +async def close_ata_transaction( + owner: Pubkey, + token_mint: Pubkey, + decimals: int, + encode_base64: bool = True +) -> str: + """ + Burs all tokens from the associated token account + :param associated_token_account[Pubkey]: associated token account + :param token_mint[Pubkey]: tokens to get burned + :param decimals[int]: The token's decimals (default 9 for SOL). + :return: [str] base64 transaction object by default. + """ + txn = None + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + associated_token_account = get_associated_token_address( + owner=owner, + mint=token_mint + ) + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + raise EntityNotFoundException( + detail="Associated token account {} nor found.".format( + str(associated_token_account) + ) + ) + + # BURN + data = account_info.data + if len(data) != 165: + print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + str(associated_token_account), + str(token_mint) + )) + ( + mint, + owner_data, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + #owner = owner + amount = amount_lamports + + # Construct the burn instruction + params = BurnCheckedParams( + program_id=TOKEN_PROGRAM_ID, + mint=token_mint, + account=associated_token_account, + owner=owner, + amount=amount, + decimals=decimals, + signers=[owner] + ) + burn_ix = burn_checked(params=params) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=owner, + owner=owner + ) + ) + + from solders.message import Message + import base64 + msg = Message( + instructions=[set_compute_unit_price(1_000), burn_ix, close_ix], + payer=owner + ) + + tx = Transaction.new_unsigned(message=msg) + txn = bytes(tx) + if encode_base64: + txn = base64.b64encode(txn).decode('ascii') + except EntityNotFoundException as enfe: + raise enfe + except Exception as e: + print("request_close_ata_transaction Error: {}".format(e)) + + return txn + diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 5571963..dd632d4 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -106,17 +106,17 @@ class AssociatedTokenAccounts(BaseModel): # raise ValidationError("Both 'page' and 'items' must be greater than 0.") # return values class RequestTransaction(BaseModel): - associated_token_account: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( + owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( Field( ..., - description='The associated token account address.', + description='Owner account address.', example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', ) ) - token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = Field( + token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( ..., - description='The mint address of the token.', - example='DCmtjvp36JDAmsURBRY4jz5A8PoEXsxaFEAgu7CBpump', + description='The mint address of the token. Supports standard Solana mint format and extended formats.', + example='bpMAcs5cEDu33kbCgTcBu7HtuZwsoNwsMH839jupump', ) decimals: int = Field( ..., description='The number of decimals for the token.', example=6 diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 859848d..a5ca5f8 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -1,7 +1,10 @@ from fastapi import APIRouter -from api.models.outer_models import Quote, RequestTransaction +from solders.pubkey import Pubkey +from api.handlers.exceptions import EntityNotFoundException +from api.models.outer_models import Quote, RequestTransaction +from api.libs.utils import close_ata_transaction router = APIRouter() @router.post( @@ -11,15 +14,26 @@ description="Retrieve a transaction with both burn and close intructions for an associated token account" ) async def request_close_ata_transaction( - body: RequestTransaction + body: RequestTransaction, ): txn = None - ata = body.associated_token_account - token = body.token_mint - decimals = body.decimals + try: + owner = body.owner + token = body.token_mint + decimals = body.decimals + transaction = await close_ata_transaction( + owner=Pubkey.from_string(owner), + token_mint=Pubkey.from_string(token), + decimals=decimals, + encode_base64=True + ) - quote = Quote( - quote=txn - ) - return quote + quote = Quote( + quote=transaction + ) + return quote + except EntityNotFoundException as e: + raise EntityNotFoundException( + detail=e.detail + ) diff --git a/bot/app/main.py b/bot/app/main.py index d1af701..28c484a 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from mangum import Mangum +from api.handlers.exceptions import entity_not_found_exception_handler, EntityNotFoundException from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route from api.routes.outer.request_transaction import router as request_ata_transaction_route @@ -15,4 +16,7 @@ app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) app.include_router(router=request_ata_transaction_route, prefix="/api", tags=["Request Associated Token Account Transaction"]) +# Register the exception handler without decorator +app.add_exception_handler(EntityNotFoundException, entity_not_found_exception_handler) + handler = Mangum(app) diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 29e5c9e..4bbc30e 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -226,7 +226,7 @@ components: GetTransaction: type: object properties: - associated_token_account: + owner: type: string description: The associated token account address. pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ diff --git a/bot/todo.yaml b/bot/todo.yaml index 193908f..386ddb1 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -12,7 +12,7 @@ - New endopints - Post: get burn and close quote √ Create openAPI specs - √∫ Create pydantic classes + √ Create pydantic classes - Create enpoint /associated_token_accounts/burn_and_close/quote - Will call burn_and_close_associated_token_account without keypair - WIll include 5% fees as a new instruction From 1c223d21050c23886fc1b93df047145ea1c3a5ea Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 6 Jan 2025 14:30:16 +0100 Subject: [PATCH 053/103] updating todo list --- bot/todo.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bot/todo.yaml b/bot/todo.yaml index 386ddb1..6355694 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -13,13 +13,12 @@ - Post: get burn and close quote √ Create openAPI specs √ Create pydantic classes - - Create enpoint /associated_token_accounts/burn_and_close/quote - - Will call burn_and_close_associated_token_account without keypair - - WIll include 5% fees as a new instruction - - Check lookuptables - - Will return a Transaction.new_unsigned (create the Message) - - encode in base58 and return - + √ Create enpoint /associated_token_accounts/burn_and_close/quote + √ Will call burn_and_close_associated_token_account without keypair + √ Will return a Transaction.new_unsigned (create the Message) + √ encode in base64 and return + - WIll include 5% fees as a new instruction + - Check lookuptables # - Post: send transaction (hablar con Jorge primero) # - Create openAPI specs # - Create pydantic classes From 3b26caf9fc74729a11f110a1aded7294fb6ed390 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 6 Jan 2025 22:30:23 +0100 Subject: [PATCH 054/103] app: adding recover_rent_client function for testing --- bot/app/api/libs/utils.py | 97 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 00864f9..d9acc4e 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -1,3 +1,4 @@ +import base64 import math import random import requests @@ -10,18 +11,19 @@ from solana.rpc.commitment import Confirmed from solders.pubkey import Pubkey -from spl.token.instructions import close_account, CloseAccountParams -from solders.pubkey import Pubkey +from solders.message import Message from solders.keypair import Keypair from solders.compute_budget import set_compute_unit_price from solders.transaction import Transaction from spl.token.instructions import ( burn_checked, BurnCheckedParams, + close_account, + CloseAccountParams ) from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from api.config import appconfig -from api.handlers.exceptions import EntityNotFoundException +from app.api.config import appconfig +from app.api.handlers.exceptions import EntityNotFoundException async def get_solana_balance(public_key: Pubkey) -> float: @@ -562,8 +564,6 @@ async def close_ata_transaction( ) ) - from solders.message import Message - import base64 msg = Message( instructions=[set_compute_unit_price(1_000), burn_ix, close_ix], payer=owner @@ -573,6 +573,8 @@ async def close_ata_transaction( txn = bytes(tx) if encode_base64: txn = base64.b64encode(txn).decode('ascii') + + except EntityNotFoundException as enfe: raise enfe except Exception as e: @@ -580,3 +582,86 @@ async def close_ata_transaction( return txn + +async def recover_rent_client(): + from bot.config import appconfig + from solders.instruction import Instruction, AccountMeta + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + + body = { + "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "token_mint": "bpMAcs5cEDu33kbCgTcBu7HtuZwsoNwsMH839jupump", + "decimals": 6 + } + try: + response = requests.post( + url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", + json=body, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("recover_rent_client: Bad status code '{}' recevied".format( + response.status_code + ) + ) + return response + + content = response.json() + txn_base64 = content["quote"] + + # Decode the Base64 string to bytes + txn_bytes = base64.b64decode(txn_base64) + + # Recreate the Transaction object from bytes + vst = Transaction.from_bytes(txn_bytes) + + # Get the message from the transaction + msg = vst.message + + instructions = [ + Instruction( + program_id=msg.account_keys[ci.program_id_index], + accounts=[ + AccountMeta(pubkey=msg.account_keys[idx], is_signer=idx == 0, is_writable=True) + for idx in ci.accounts + ], + data=ci.data + ) + for ci in msg.instructions + ] + + # Send the signed transaction (example assumes using a Solana RPC client) + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + + signed_tx = Transaction.new_signed_with_payer( + instructions=instructions, + payer=keypair.pubkey(), + signing_keypairs=[keypair], + recent_blockhash=recent_blockhash + ) + + send_result = None + tx_signature = await client.send_transaction( + txn=signed_tx, + opts=TxOpts(preflight_commitment=Confirmed) + ) + print(f"Transaction sent successfully: {tx_signature}") + + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Test->{} Transaction: https://solscan.io/tx/{} at {}".format( + "Transfer", + tx_signature.value, + current_time + )) + await client.confirm_transaction(tx_signature.value, commitment="confirmed") + print("Transaction confirmed") + + return send_result + except Exception as e: + print("get_token_accounts_by_owner-> Error: {}".format(e)) + return response + +# import asyncio +# asyncio.run(recover_rent_client()) From 052f08f9d3531404d23b33be8b28610fd44429ca Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 9 Jan 2025 11:00:19 +0100 Subject: [PATCH 055/103] Adding new POST endpoint: /associated_token_accounts/burn_and_close/instructions. SOlving bugs in app.libs.utils. Adding "Remove previous images" in step.md file --- .vscode/launch.json | 4 +- bot/app/AWS/steps.md | 2 + bot/app/Dockerfile | 2 +- bot/app/api/libs/utils.py | 198 +++++++++++++++++- bot/app/api/models/outer_models.py | 7 +- .../api/routes/outer/request_instructions.py | 46 ++++ bot/app/main.py | 2 + bot/app/openapi/external_endpoints.yaml | 40 +++- 8 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 bot/app/api/routes/outer/request_instructions.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 919e8ac..7289bcc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -53,10 +53,10 @@ "justMyCode": false }, { - "name": "Debug utils.py", + "name": "Debug app utils.py", "type": "python", "request": "launch", - "program": "${workspaceFolder}/bot/libs/utils.py", + "program": "${workspaceFolder}/bot/app/api/libs/utils.py", "console": "integratedTerminal", "cwd": "${workspaceFolder}", "env": { diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index bb0820a..dff2ff1 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -34,6 +34,8 @@ sudo systemctl restart docker docker pull 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest ## List images aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 +### Remove previous images +docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -v ':latest' | awk '{print $2}' | xargs docker rmi -f # Run the docker container docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest diff --git a/bot/app/Dockerfile b/bot/app/Dockerfile index bd2575f..e918482 100644 --- a/bot/app/Dockerfile +++ b/bot/app/Dockerfile @@ -12,7 +12,7 @@ COPY . /app RUN pip install --upgrade pip RUN pip install --no-cache-dir -r requirements.txt -# Make port 80 available to the world outside this container +# Make port 443 available to the world outside this container EXPOSE 443 # Define environment variable diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index d9acc4e..c11409c 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -1,4 +1,5 @@ import base64 +import json import math import random import requests @@ -15,6 +16,7 @@ from solders.keypair import Keypair from solders.compute_budget import set_compute_unit_price from solders.transaction import Transaction +from solders.instruction import Instruction, AccountMeta from spl.token.instructions import ( burn_checked, BurnCheckedParams, @@ -22,8 +24,8 @@ CloseAccountParams ) from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from app.api.config import appconfig -from app.api.handlers.exceptions import EntityNotFoundException +from api.config import appconfig +from api.handlers.exceptions import EntityNotFoundException async def get_solana_balance(public_key: Pubkey) -> float: @@ -85,7 +87,7 @@ async def get_token_accounts_by_owner(wallet_address=str)->dict: "params": [ wallet_address, { - "programId": TOKEN_PROGRAM_ID + "programId": str(TOKEN_PROGRAM_ID) }, { "encoding": "jsonParsed" @@ -424,7 +426,7 @@ async def burn_and_close_associated_token_account( # Construct the burn instruction params = BurnCheckedParams( - program_id=Pubkey.from_string(TOKEN_PROGRAM_ID), + program_id=TOKEN_PROGRAM_ID, mint=token_mint, account=associated_token_account, owner=keypair.pubkey(), @@ -583,9 +585,120 @@ async def close_ata_transaction( return txn -async def recover_rent_client(): +async def close_burn_ata_instructions( + owner: Pubkey, + token_mint: Pubkey, + decimals: int +) -> str: + """ + Burs all tokens from the associated token account + :param associated_token_account[Pubkey]: associated token account + :param token_mint[Pubkey]: tokens to get burned + :param decimals[int]: The token's decimals (default 9 for SOL). + :return: [str] base64 list with all the instructions in hex format. + """ + instructions = None + + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + try: + associated_token_account = get_associated_token_address( + owner=owner, + mint=token_mint + ) + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + raise EntityNotFoundException( + detail="Associated token account {} nor found.".format( + str(associated_token_account) + ) + ) + + # BURN + data = account_info.data + if len(data) != 165: + print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + str(associated_token_account), + str(token_mint) + )) + ( + mint, + owner_data, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + #owner = owner + amount = amount_lamports + + # Construct the burn instruction + params = BurnCheckedParams( + program_id=TOKEN_PROGRAM_ID, + mint=token_mint, + account=associated_token_account, + owner=owner, + amount=amount, + decimals=decimals, + signers=[owner] + ) + burn_ix = burn_checked(params=params) + burn_ix_bytes = bytes(burn_ix) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=owner, + owner=owner + ) + ) + close_ix_bytes = bytes(close_ix) + + compute_unit_ix = set_compute_unit_price(3_000) + compute_unit_ix_bytes = bytes(compute_unit_ix) + + # Construct the transfer instruction (charging 0.001 SOL) + from solders.system_program import transfer, TransferParams + lamports_to_charge = int(0.001 * 10**9) # Convert SOL to lamports + fix_fees_params = TransferParams( + from_pubkey=owner, + to_pubkey=Pubkey.from_string("5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD"), + lamports=lamports_to_charge + ) + fix_fees_ix = transfer(params=fix_fees_params) + fix_fees_ix_bytes = bytes(fix_fees_ix) + + # Package both instructions into a single JSON object + instructions = [ + compute_unit_ix_bytes.hex(), + burn_ix_bytes.hex(), # Convert to hex for compatibility + close_ix_bytes.hex(), # Convert to hex for compatibility + fix_fees_ix_bytes.hex() + ] + + + + except EntityNotFoundException as enfe: + raise enfe + except Exception as e: + print("request_close_ata_instruction Error: {}".format(e)) + + return instructions + + +async def recover_rent_client_from_transaction(): from bot.config import appconfig - from solders.instruction import Instruction, AccountMeta keypair = Keypair.from_base58_string(appconfig.PRIVKEY) body = { @@ -660,8 +773,77 @@ async def recover_rent_client(): return send_result except Exception as e: - print("get_token_accounts_by_owner-> Error: {}".format(e)) + print("recover_rent_client_from_transaction-> Error: {}".format(e)) + return response + +async def recover_rent_client_from_instructions(): + from bot.config import appconfig + keypair = Keypair.from_base58_string(appconfig.PRIVKEY) + + body = { + "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "token_mint": "6fMG6HBSgfKcgar8SmUMPnfaFCVnPhQn1ZgHhJ27ELWK", + "decimals": 6 + } + try: + response = requests.post( + url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", + json=body, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("recover_rent_client: Bad status code '{}' recevied".format( + response.status_code + ) + ) + return response + + content = response.json() + txn_base64 = content["response"] + + # Decode the Base64 string to bytes + decoded_bytes = base64.b64decode(txn_base64) + + # Step 2: Deserialize JSON to get the list of instructions + instructions_data = json.loads(decoded_bytes.decode("utf-8")) + + # Step 3: Convert each hex string back to bytes + instructions_bytes = [bytes.fromhex(instruction) for instruction in instructions_data] + + instructions = [Instruction.from_bytes(ix_bytes) for ix_bytes in instructions_bytes] + + # Send the signed transaction (example assumes using a Solana RPC client) + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + + signed_tx = Transaction.new_signed_with_payer( + instructions=instructions, + payer=keypair.pubkey(), + signing_keypairs=[keypair], + recent_blockhash=recent_blockhash + ) + + send_result = None + tx_signature = await client.send_transaction( + txn=signed_tx, + opts=TxOpts(preflight_commitment=Confirmed) + ) + print(f"Transaction sent successfully: {tx_signature}") + + current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("Test->{} Transaction: https://solscan.io/tx/{} at {}".format( + "Transfer", + tx_signature.value, + current_time + )) + await client.confirm_transaction(tx_signature.value, commitment="confirmed") + print("Transaction confirmed") + + return send_result + except Exception as e: + print("recover_rent_client_from_instructions-> Error: {}".format(e)) return response # import asyncio -# asyncio.run(recover_rent_client()) +# asyncio.run(recover_rent_client_from_instructions()) diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index dd632d4..b2fc3ba 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -125,4 +125,9 @@ class RequestTransaction(BaseModel): class Quote(BaseModel): quote: Optional[str] = Field( None, description='A base64-encoded string representing the quote.' - ) \ No newline at end of file + ) + +class Instructions(BaseModel): + response: Optional[str] = Field( + None, description='A base64-encoded string representing the list of instructions in hex format.' + ) diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py new file mode 100644 index 0000000..ca7d761 --- /dev/null +++ b/bot/app/api/routes/outer/request_instructions.py @@ -0,0 +1,46 @@ +import base64 +import json + +from fastapi import APIRouter +from solders.pubkey import Pubkey + +from api.handlers.exceptions import EntityNotFoundException +from api.models.outer_models import Instructions, RequestTransaction +from api.libs.utils import close_burn_ata_instructions +router = APIRouter() + +@router.post( + path="/associated_token_accounts/burn_and_close/instructions", + response_model=Instructions, + summary="Retrieve the intructions to close an associated token account", + description="Retrieve both burn and close and fees intructions to close an associated token account" +) +async def request_close_ata_instructions( + body: RequestTransaction, +): + try: + owner = body.owner + token = body.token_mint + decimals = body.decimals + + instructions = await close_burn_ata_instructions( + owner=Pubkey.from_string(owner), + token_mint=Pubkey.from_string(token), + decimals=decimals + ) + + # Serialize JSON object to bytes + instructions_json = json.dumps(instructions) + instructions_bytes = instructions_json.encode("utf-8") + + # Encode the entire byte string as Base64 + instructions_base64 = base64.b64encode(instructions_bytes).decode("utf-8") + + instructions = Instructions( + response=instructions_base64 + ) + return instructions + except EntityNotFoundException as e: + raise EntityNotFoundException( + detail=e.detail + ) diff --git a/bot/app/main.py b/bot/app/main.py index 28c484a..3a92424 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -4,6 +4,7 @@ from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route from api.routes.outer.request_transaction import router as request_ata_transaction_route +from api.routes.outer.request_instructions import router as request_ata_instructions_route # Create the FastAPI app app = FastAPI( debug=False, @@ -15,6 +16,7 @@ app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) app.include_router(router=request_ata_transaction_route, prefix="/api", tags=["Request Associated Token Account Transaction"]) +app.include_router(router=request_ata_instructions_route, prefix="/api", tags=["Request Associated Token Account Instructions"]) # Register the exception handler without decorator app.add_exception_handler(EntityNotFoundException, entity_not_found_exception_handler) diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 4bbc30e..0428f12 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -106,6 +106,35 @@ paths: responses: '200': description: Burn and close quote successfully retrieved. + content: + application/json: + schema: + $ref: '#/components/schemas/Instructions' + example: + quote: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" + + '404': + description: Account not found. + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: "Associated token Account not found or mismatch." + /associated_token_accounts/burn_and_close/instructions: + post: + summary: Retrieve a list of instructions in base64 to clse an associated token account + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetTransaction' + responses: + '200': + description: INstrucions successfully retrieved. content: application/json: schema: @@ -123,7 +152,6 @@ paths: detail: type: string example: "Associated token Account not found or mismatch." - components: schemas: CountAssociatedTokenAccounts: @@ -226,7 +254,7 @@ components: GetTransaction: type: object properties: - owner: + associated_token_account: type: string description: The associated token account address. pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ @@ -248,3 +276,11 @@ components: type: string format: base64 description: A base64-encoded string representing the quote. + + Instructions: + type: object + properties: + response: + type: string + format: base64 + description: A base64-encoded string representing the list of Instructions. From 105c4f9e67a978c746a6b0c46823d28bd29a630f Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 9 Jan 2025 23:24:46 +0100 Subject: [PATCH 056/103] Including variable fees for GhostFunds --- bot/app/api/config/__init__.py | 9 ++ bot/app/api/libs/utils.py | 120 +++++++++++++----- bot/app/api/models/outer_models.py | 10 ++ .../outer/count_associated_token_accounts.py | 9 +- .../api/routes/outer/request_instructions.py | 6 +- bot/app/openapi/external_endpoints.yaml | 19 ++- bot/todo.yaml | 24 ++-- 7 files changed, 142 insertions(+), 55 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 8f67fb8..baa8fc0 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -22,6 +22,15 @@ class AuthConfig: FEES = float(os.environ.get("FEES", 0.0015)) FEES_INCREASMENT = 0.000805 FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS + GHOSTFUNDS_FIX_FEES = 0.00001 + GHOSTFUNDS_FEES_PERCENTAGES = { + 1: 0.05, + 100: 0.048, + 500: 0.045, + 1000: 0.04 + } + GHOSTFUNDS_FIX_FEES_RECEIVER = "5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD" + GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD" RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index c11409c..26195d6 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -15,6 +15,7 @@ from solders.message import Message from solders.keypair import Keypair from solders.compute_budget import set_compute_unit_price +from solders.system_program import transfer, TransferParams from solders.transaction import Transaction from solders.instruction import Instruction, AccountMeta from spl.token.instructions import ( @@ -77,8 +78,8 @@ def get_solana_price() -> float: return price -async def get_token_accounts_by_owner(wallet_address=str)->dict: - response = {} +async def get_token_accounts_by_owner(wallet_address=str)->list[dict]: + value = [] async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: data = { "jsonrpc": "2.0", @@ -101,20 +102,20 @@ async def get_token_accounts_by_owner(wallet_address=str)->dict: headers={"Content-Type": "application/json"} ) if response.status_code != 200: - print("get_token_accounts_by_owner: Bad status code '{}' recevied for token {}".format( + print("get_token_accounts_by_owner: Bad status code '{}' received for token {}".format( response.status_code, wallet_address ) ) - return response + return value content = response.json() - response = content["result"]["value"] + value = content["result"]["value"] - return response + return value except Exception as e: print("get_token_accounts_by_owner-> Error: {}".format(e)) - return response + return value def get_token_metadata(token_address: str)->dict: metadata = {} @@ -181,9 +182,31 @@ def get_token_metadata(token_address: str)->dict: return metadata +def get_current_ghostfunds_fees(burnable_accounts: int)->float: + """ + Return GhostFunds fee based on how many ata can be burned. + :param burnable_accounts [int]: how many atas + :return [float]: fee percentage + """ + fee = 0 + # Validate input + if burnable_accounts <= 0: + return fee + + # Iterate through the fee tiers to find the applicable fee + for upper_limit, ghost_fee in sorted(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.items()): + if burnable_accounts > upper_limit: + fee = ghost_fee + continue + return fee + + # If the burnable_accounts exceed the highest limit, return the lowest fee + return appconfig.GHOSTFUNDS_FEES_PERCENTAGES[max(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.keys())] + + async def count_associated_token_accounts( wallet_pubkey: Pubkey -) -> int: +) -> dict: """ Fetch the amount of associated token accounts an address holds :param wallet_pubkey: The public address of the Solana wallet. @@ -194,14 +217,25 @@ async def count_associated_token_accounts( "burnable_accounts": 0, "accounts_for_manual_review": 0, "rent_balance": 0, - "rent_balance_usd": 0 + "rent_balance_usd": 0, + "fee": 0 } min_token_value = 1 account_samples = 5 # TODO: implement token's black list (like USDC, etc) token_blacklist = [] - original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + + original_accounts = [] + counter = 0 + while True: + if counter >= appconfig.RETRIES: + break + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + if original_accounts: + break + counter += 1 + usd_sol_value = 0 accounts = [ @@ -246,6 +280,9 @@ async def count_associated_token_accounts( total["accounts_for_manual_review"] = accounts_for_manual_review + if total["burnable_accounts"] > 0: + total["fee"] = get_current_ghostfunds_fees(burnable_accounts=total["burnable_accounts"]) + return total async def detect_dust_token_accounts( @@ -585,16 +622,43 @@ async def close_ata_transaction( return txn +def get_fee_instructions( + fee: float, + balance: float, + owner: Pubkey +)->list[Instruction]: + # Fix fee + lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * 10**9) # Convert SOL to lamports + fix_fees_params = TransferParams( + from_pubkey=owner, + to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_FIX_FEES_RECEIVER), + lamports=lamports_to_charge + ) + fix_fees_ix = transfer(params=fix_fees_params) + # Variable fee + lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports + variable_fees_params = TransferParams( + from_pubkey=owner, + to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_VARIABLE_FEES_RECEIVER), + lamports=lamports_to_charge + ) + variable_fees_ix = transfer(params=variable_fees_params) + return [fix_fees_ix, variable_fees_ix] + async def close_burn_ata_instructions( owner: Pubkey, token_mint: Pubkey, - decimals: int -) -> str: + decimals: int, + balance: float, + fee: float +) -> list[hex]: """ Burs all tokens from the associated token account :param associated_token_account[Pubkey]: associated token account :param token_mint[Pubkey]: tokens to get burned :param decimals[int]: The token's decimals (default 9 for SOL). + :param balance[float]: The ATA Sol balance (for fee calculation) + :param fee[float]: GhostFunds fee to be charge to user. :return: [str] base64 list with all the instructions in hex format. """ instructions = None @@ -665,29 +729,21 @@ async def close_burn_ata_instructions( ) close_ix_bytes = bytes(close_ix) - compute_unit_ix = set_compute_unit_price(3_000) + compute_unit_ix = set_compute_unit_price(5_000) compute_unit_ix_bytes = bytes(compute_unit_ix) - # Construct the transfer instruction (charging 0.001 SOL) - from solders.system_program import transfer, TransferParams - lamports_to_charge = int(0.001 * 10**9) # Convert SOL to lamports - fix_fees_params = TransferParams( - from_pubkey=owner, - to_pubkey=Pubkey.from_string("5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD"), - lamports=lamports_to_charge - ) - fix_fees_ix = transfer(params=fix_fees_params) - fix_fees_ix_bytes = bytes(fix_fees_ix) + # Variable fee + fee_ix_list = get_fee_instructions(fee=fee, balance=balance, owner=owner) + fee_ix_list_bytes = [bytes(fee_ix) for fee_ix in fee_ix_list] + fee_ix_list_hex = [fee_ix_bytes.hex() for fee_ix_bytes in fee_ix_list_bytes] # Package both instructions into a single JSON object instructions = [ - compute_unit_ix_bytes.hex(), - burn_ix_bytes.hex(), # Convert to hex for compatibility - close_ix_bytes.hex(), # Convert to hex for compatibility - fix_fees_ix_bytes.hex() + compute_unit_ix_bytes.hex(), # Convert to hex for compatibility + burn_ix_bytes.hex(), + close_ix_bytes.hex(), ] - - + instructions.extend(fee_ix_list_hex) except EntityNotFoundException as enfe: raise enfe @@ -782,8 +838,10 @@ async def recover_rent_client_from_instructions(): body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "6fMG6HBSgfKcgar8SmUMPnfaFCVnPhQn1ZgHhJ27ELWK", - "decimals": 6 + "token_mint": "5PTGpK7mEPHsWwTigA8CRv5vzdEPsSHee5crUj8Cpump", + "decimals": 6, + "balance": 0.002039, + "fee": 0.045 } try: response = requests.post( diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index b2fc3ba..7a5f190 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -30,6 +30,9 @@ class CountAssociatedTokenAccounts(BaseModel): rent_balance_usd: float = Field( 0, description='Total rent balance converted to USD.' ) + fee: float = Field( + 0, description='GhostFunds fee.' + ) class AssociatedTokenAccount(BaseModel): @@ -105,6 +108,7 @@ class AssociatedTokenAccounts(BaseModel): # if page <= 0 or items <= 0: # raise ValidationError("Both 'page' and 'items' must be greater than 0.") # return values + class RequestTransaction(BaseModel): owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( Field( @@ -121,6 +125,12 @@ class RequestTransaction(BaseModel): decimals: int = Field( ..., description='The number of decimals for the token.', example=6 ) + balance: float = Field( + ..., description='ATA SOls balance' + ) + fee: float = Field( + ..., description='GhostFunds fee.' + ) class Quote(BaseModel): quote: Optional[str] = Field( diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 92bdf65..9cb515e 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -20,12 +20,6 @@ def validate_account_address(account_address: AccountAddressType) -> str: async def associated_token_accounts_count( account_address: str = Depends(validate_account_address) ): - # Mock logic for checking account existence (replace with actual logic) - # if account_address != "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb": - # raise HTTPException( - # status_code=404, - # detail="Account not found." - # ) wallet_pubkey = Pubkey.from_string(account_address) data = await count_associated_token_accounts(wallet_pubkey=wallet_pubkey) response = CountAssociatedTokenAccounts( @@ -33,7 +27,8 @@ async def associated_token_accounts_count( burnable_accounts=data["burnable_accounts"], accounts_for_manual_review=data["accounts_for_manual_review"], rent_balance=data["rent_balance"], - rent_balance_usd=data["rent_balance_usd"] + rent_balance_usd=data["rent_balance_usd"], + fee=data["fee"] ) return response diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index ca7d761..ec744ef 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -22,11 +22,15 @@ async def request_close_ata_instructions( owner = body.owner token = body.token_mint decimals = body.decimals + balance = body.balance + fee = body.fee instructions = await close_burn_ata_instructions( owner=Pubkey.from_string(owner), token_mint=Pubkey.from_string(token), - decimals=decimals + decimals=decimals, + balance=balance, + fee=fee ) # Serialize JSON object to bytes diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index 0428f12..b739035 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -33,6 +33,7 @@ paths: accounts_for_manual_review: 0 rent_balance: 0.08972832 rent_balance_usd: 16.3628564352 + fee: 0.05 '404': description: Account not found. content: @@ -138,9 +139,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/Instructions' example: - quote: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" + response: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" '404': description: Account not found. @@ -174,6 +175,10 @@ components: type: number format: float description: Total rent balance converted to USD. + fee: + type: number + format: float + description: Fees to be applied based on burnable accounts AssociatedTokenAccounts: type: array @@ -268,6 +273,16 @@ components: type: integer description: The number of decimals for the token. example: 6 + balance: + type: number + format: float + description: ATA Sol balance + example: 0.00203928 + fee: + type: number + format: float + description: Fees to be applied based on burnable accounts + example: 0.05 Quote: type: object diff --git a/bot/todo.yaml b/bot/todo.yaml index 6355694..4a6e89b 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -1,7 +1,5 @@ # TODO -- app - - Pagination: - - Speed up: Add pagination to detect_dust_token_accounts function +- TODO: fees. Send instruction request in base64 to avoid "otros" to see our fees - EC2: √ Deploy app in @@ -13,22 +11,20 @@ - Post: get burn and close quote √ Create openAPI specs √ Create pydantic classes - √ Create enpoint /associated_token_accounts/burn_and_close/quote + √ Create enpoint /associated_token_accounts/burn_and_close/transaction √ Will call burn_and_close_associated_token_account without keypair √ Will return a Transaction.new_unsigned (create the Message) √ encode in base64 and return - WIll include 5% fees as a new instruction - Check lookuptables - # - Post: send transaction (hablar con Jorge primero) - # - Create openAPI specs - # - Create pydantic classes - # - Create enpoint /associated_token_accounts/burn_and_close - # - Will receive the signed transaction in base 58 and decode it - # - Will send the transaction and return the signature. - # - The front will take care of confirming the txn. - - - + √ Create new function get_current_ghostfunds_fees(ata_to_be_closed: int)->float + √ Return the fees in the count endpoint + √ Get the fee as a new input variable in: + /associated_token_accounts/burn_and_close/instructions + √ Create a new function get_fee_instructions(fees: float)->[Instruction] + √ Business logic will be defined here + √ Phase1: All 5% will go to Ghost account + - Phase2: When we have a token. We'll sent some % to another Ghost account to finally buy our own tokens. - BUG: when a timeout occurs during subscription, the next step is From 2e40c7942bab043db11662a9be745fbaed2d1931 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 12 Jan 2025 00:35:06 +0100 Subject: [PATCH 057/103] updating utils functions --- bot/app/api/libs/utils.py | 16 ++++++++-------- bot/libs/utils.py | 8 +++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 26195d6..10181f4 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -15,9 +15,9 @@ from solders.message import Message from solders.keypair import Keypair from solders.compute_budget import set_compute_unit_price -from solders.system_program import transfer, TransferParams from solders.transaction import Transaction from solders.instruction import Instruction, AccountMeta +from solders.system_program import transfer, TransferParams from spl.token.instructions import ( burn_checked, BurnCheckedParams, @@ -194,14 +194,14 @@ def get_current_ghostfunds_fees(burnable_accounts: int)->float: return fee # Iterate through the fee tiers to find the applicable fee - for upper_limit, ghost_fee in sorted(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.items()): + for upper_limit, ghost_fee in sorted(appconfig.GHOSTFUNDS_FEES.items()): if burnable_accounts > upper_limit: fee = ghost_fee continue return fee # If the burnable_accounts exceed the highest limit, return the lowest fee - return appconfig.GHOSTFUNDS_FEES_PERCENTAGES[max(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.keys())] + return appconfig.GHOSTFUNDS_FEES[max(appconfig.GHOSTFUNDS_FEES.keys())] async def count_associated_token_accounts( @@ -628,7 +628,7 @@ def get_fee_instructions( owner: Pubkey )->list[Instruction]: # Fix fee - lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * 10**9) # Convert SOL to lamports + lamports_to_charge = int(0.001 * 10**9) # Convert SOL to lamports fix_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_FIX_FEES_RECEIVER), @@ -636,7 +636,7 @@ def get_fee_instructions( ) fix_fees_ix = transfer(params=fix_fees_params) # Variable fee - lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports + lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports variable_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_VARIABLE_FEES_RECEIVER), @@ -729,7 +729,7 @@ async def close_burn_ata_instructions( ) close_ix_bytes = bytes(close_ix) - compute_unit_ix = set_compute_unit_price(5_000) + compute_unit_ix = set_compute_unit_price(3_000) compute_unit_ix_bytes = bytes(compute_unit_ix) # Variable fee @@ -764,7 +764,7 @@ async def recover_rent_client_from_transaction(): } try: response = requests.post( - url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", + url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", json=body, headers={"Content-Type": "application/json"} ) @@ -838,7 +838,7 @@ async def recover_rent_client_from_instructions(): body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "5PTGpK7mEPHsWwTigA8CRv5vzdEPsSHee5crUj8Cpump", + "token_mint": "DZp2GZjhgLTD2hyLowCZ8pu1evbA9wivV9JURts8LmNt", "decimals": 6, "balance": 0.002039, "fee": 0.045 diff --git a/bot/libs/utils.py b/bot/libs/utils.py index 73b75b2..25b2587 100644 --- a/bot/libs/utils.py +++ b/bot/libs/utils.py @@ -758,7 +758,7 @@ async def burn_and_close_associated_token_account( return txn_signature -def generate_vanity_wallet(prefix: str) -> Keypair: +def generate_vanity_wallet(prefix: str, key_sensitive: bool = False) -> Keypair: counter = 0 start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("starting generate_vanity_wallet at {}".format(start_time)) @@ -769,7 +769,9 @@ def generate_vanity_wallet(prefix: str) -> Keypair: public_key = str(keypair.pubkey()) # Check if the public key starts with the desired prefix - if public_key.lower().startswith(prefix.lower()): + validator = public_key.lower().startswith(prefix.lower()) if not key_sensitive else public_key.startswith(prefix) + + if validator: stop_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("Pubkey: {}".format(public_key)) pk = base58.b58encode(bytes(keypair)).decode() @@ -787,7 +789,7 @@ async def test(): # Generate the vanity wallet print(f"Generating wallet with prefix '{prefix}'...") - keypair = generate_vanity_wallet(prefix) + keypair = generate_vanity_wallet(prefix, key_sensitive=True) if keypair: return From 7101257520a7718c3726ed0a3330a284456d12b0 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 12 Jan 2025 01:13:51 +0100 Subject: [PATCH 058/103] app config: adding request_heap_frame and set_compute_unit_limit instructions. Adding Ghost fee accounts --- .vscode/launch.json | 13 ++++ bot/app/api/config/__init__.py | 4 +- bot/app/api/libs/utils.py | 105 +++++++++++++++++++++------------ 3 files changed, 83 insertions(+), 39 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7289bcc..a96b967 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,6 +52,19 @@ }, "justMyCode": false }, + { + "name": "Debug bot utils.py", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/libs/utils.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, { "name": "Debug app utils.py", "type": "python", diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index baa8fc0..5d2c083 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -29,8 +29,8 @@ class AuthConfig: 500: 0.045, 1000: 0.04 } - GHOSTFUNDS_FIX_FEES_RECEIVER = "5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD" - GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "5ySkForhyx7CmPjZvJMn323uuN9xnLw4KVHgdepYgmRD" + GHOSTFUNDS_FIX_FEES_RECEIVER = "GhoStvfwEx5FYEX7jMEpsu6R13xJFJdTLs4BxEpB9qxQ" + GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "Ghost5UYkXcgLdja6Uhyac3gTnuefrx7TuSFat5JUVdW" RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 10181f4..d2f7183 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -11,13 +11,18 @@ from solana.rpc.types import TxOpts from solana.rpc.commitment import Confirmed -from solders.pubkey import Pubkey -from solders.message import Message -from solders.keypair import Keypair -from solders.compute_budget import set_compute_unit_price -from solders.transaction import Transaction +from solders.compute_budget import ( + request_heap_frame, + set_compute_unit_limit, + set_compute_unit_price +) from solders.instruction import Instruction, AccountMeta +from solders.keypair import Keypair +from solders.message import Message +from solders.pubkey import Pubkey from solders.system_program import transfer, TransferParams +from solders.transaction import Transaction + from spl.token.instructions import ( burn_checked, BurnCheckedParams, @@ -25,9 +30,10 @@ CloseAccountParams ) from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from api.config import appconfig -from api.handlers.exceptions import EntityNotFoundException - +# from api.config import appconfig +# from api.handlers.exceptions import EntityNotFoundException +from bot.app.api.config import appconfig +from bot.app.api.handlers.exceptions import EntityNotFoundException async def get_solana_balance(public_key: Pubkey) -> float: """ @@ -194,14 +200,14 @@ def get_current_ghostfunds_fees(burnable_accounts: int)->float: return fee # Iterate through the fee tiers to find the applicable fee - for upper_limit, ghost_fee in sorted(appconfig.GHOSTFUNDS_FEES.items()): + for upper_limit, ghost_fee in sorted(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.items()): if burnable_accounts > upper_limit: fee = ghost_fee continue return fee # If the burnable_accounts exceed the highest limit, return the lowest fee - return appconfig.GHOSTFUNDS_FEES[max(appconfig.GHOSTFUNDS_FEES.keys())] + return appconfig.GHOSTFUNDS_FEES_PERCENTAGES[max(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.keys())] async def count_associated_token_accounts( @@ -628,7 +634,7 @@ def get_fee_instructions( owner: Pubkey )->list[Instruction]: # Fix fee - lamports_to_charge = int(0.001 * 10**9) # Convert SOL to lamports + lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * 10**9) # Convert SOL to lamports fix_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_FIX_FEES_RECEIVER), @@ -636,7 +642,7 @@ def get_fee_instructions( ) fix_fees_ix = transfer(params=fix_fees_params) # Variable fee - lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports + lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports variable_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_VARIABLE_FEES_RECEIVER), @@ -729,8 +735,16 @@ async def close_burn_ata_instructions( ) close_ix_bytes = bytes(close_ix) - compute_unit_ix = set_compute_unit_price(3_000) - compute_unit_ix_bytes = bytes(compute_unit_ix) + compute_unit_price_ix = set_compute_unit_price(5_000) + compute_unit_price_ix_bytes = bytes(compute_unit_price_ix) + + compute_unit_limit_ix = set_compute_unit_limit(units=9_000) + compute_unit_limit_ix_bytes = bytes(compute_unit_limit_ix) + + # Although this is redundant, we're keeping it. + heap_memory_size = 32 * 1024 + request_heap_frame_ix = request_heap_frame(bytes_=heap_memory_size) + request_heap_frame_ix_bytes = bytes(request_heap_frame_ix) # Variable fee fee_ix_list = get_fee_instructions(fee=fee, balance=balance, owner=owner) @@ -739,7 +753,9 @@ async def close_burn_ata_instructions( # Package both instructions into a single JSON object instructions = [ - compute_unit_ix_bytes.hex(), # Convert to hex for compatibility + compute_unit_price_ix_bytes.hex(), # Convert to hex for compatibility + compute_unit_limit_ix_bytes.hex(), + request_heap_frame_ix_bytes.hex(), burn_ix_bytes.hex(), close_ix_bytes.hex(), ] @@ -752,7 +768,6 @@ async def close_burn_ata_instructions( return instructions - async def recover_rent_client_from_transaction(): from bot.config import appconfig keypair = Keypair.from_base58_string(appconfig.PRIVKEY) @@ -764,7 +779,7 @@ async def recover_rent_client_from_transaction(): } try: response = requests.post( - url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", + url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", json=body, headers={"Content-Type": "application/json"} ) @@ -832,39 +847,50 @@ async def recover_rent_client_from_transaction(): print("recover_rent_client_from_transaction-> Error: {}".format(e)) return response -async def recover_rent_client_from_instructions(): +async def recover_rent_client_from_instructions(go_local: bool = True): from bot.config import appconfig keypair = Keypair.from_base58_string(appconfig.PRIVKEY) body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "DZp2GZjhgLTD2hyLowCZ8pu1evbA9wivV9JURts8LmNt", + "token_mint": "B7Bo6nKHLewLXm9YEyyfjPECVnPx74ga5YH4r9Tfpump", "decimals": 6, "balance": 0.002039, "fee": 0.045 } try: - response = requests.post( - url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", - json=body, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - print("recover_rent_client: Bad status code '{}' recevied".format( - response.status_code + instructions = [] + if not go_local: + response = requests.post( + url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", + json=body, + headers={"Content-Type": "application/json"} ) - ) - return response + if response.status_code != 200: + print("recover_rent_client: Bad status code '{}' recevied".format( + response.status_code + ) + ) + return response - content = response.json() - txn_base64 = content["response"] + content = response.json() + txn_base64 = content["response"] - # Decode the Base64 string to bytes - decoded_bytes = base64.b64decode(txn_base64) + # Decode the Base64 string to bytes + decoded_bytes = base64.b64decode(txn_base64) - # Step 2: Deserialize JSON to get the list of instructions - instructions_data = json.loads(decoded_bytes.decode("utf-8")) + # Step 2: Deserialize JSON to get the list of instructions + instructions_data = json.loads(decoded_bytes.decode("utf-8")) + else: + instructions_data = await close_burn_ata_instructions( + owner=Pubkey.from_string(body["owner"]), + token_mint=Pubkey.from_string(body["token_mint"]), + decimals=body["decimals"], + balance=body["balance"], + fee=body["fee"] + ) + # Step 3: Convert each hex string back to bytes instructions_bytes = [bytes.fromhex(instruction) for instruction in instructions_data] @@ -882,6 +908,11 @@ async def recover_rent_client_from_instructions(): recent_blockhash=recent_blockhash ) + # Simluating the transaction + simulation_result = await client.simulate_transaction(signed_tx) + if simulation_result.value.err: + print("Simulation error:", simulation_result.value.err) + send_result = None tx_signature = await client.send_transaction( txn=signed_tx, @@ -901,7 +932,7 @@ async def recover_rent_client_from_instructions(): return send_result except Exception as e: print("recover_rent_client_from_instructions-> Error: {}".format(e)) - return response + # import asyncio -# asyncio.run(recover_rent_client_from_instructions()) +# asyncio.run(recover_rent_client_from_instructions(go_local=False)) From 63b587559cacf45d41afa428730f1425788efb0c Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 12 Jan 2025 01:36:04 +0100 Subject: [PATCH 059/103] adding steps to stop, pull and publish latest docker image in AWS ec2 instance --- bot/app/AWS/steps.md | 11 +++++++--- bot/app/api/libs/utils.py | 42 +++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index dff2ff1..e0f4c22 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -30,18 +30,23 @@ groups ls -l /var/run/docker.sock ## restart sudo systemctl restart docker -# Pull the image + +# Pull the image ########################### docker pull 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest ## List images -aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 +#### aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 +### Stop current docker conatiner +CONTAINER_ID=$(docker ps | awk 'NR==2 {print $1}') +docker stop $CONTAINER_ID ### Remove previous images docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -v ':latest' | awk '{print $2}' | xargs docker rmi -f # Run the docker container docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest +### ##################################### # Start Docker when EC2 instance starts ## Create start-docker-container.sh and add the code -### - look into /spp/AWS/start-docker-container.sh +### - look into /app/AWS/start-docker-container.sh ### - Adjust if dockerimage id changes sudo vim /etc/init.d/start-docker-container.sh diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index d2f7183..276d132 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -30,10 +30,10 @@ CloseAccountParams ) from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -# from api.config import appconfig -# from api.handlers.exceptions import EntityNotFoundException -from bot.app.api.config import appconfig -from bot.app.api.handlers.exceptions import EntityNotFoundException +from api.config import appconfig +from api.handlers.exceptions import EntityNotFoundException +# from bot.app.api.config import appconfig +# from bot.app.api.handlers.exceptions import EntityNotFoundException async def get_solana_balance(public_key: Pubkey) -> float: """ @@ -81,7 +81,7 @@ def get_solana_price() -> float: retries )) time.sleep(counter) - + return price async def get_token_accounts_by_owner(wallet_address=str)->list[dict]: @@ -205,7 +205,7 @@ def get_current_ghostfunds_fees(burnable_accounts: int)->float: fee = ghost_fee continue return fee - + # If the burnable_accounts exceed the highest limit, return the lowest fee return appconfig.GHOSTFUNDS_FEES_PERCENTAGES[max(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.keys())] @@ -241,11 +241,11 @@ async def count_associated_token_accounts( if original_accounts: break counter += 1 - + usd_sol_value = 0 accounts = [ - account for account in original_accounts + account for account in original_accounts if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist ] @@ -314,13 +314,13 @@ async def detect_dust_token_accounts( token_blacklist = [] original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) accounts = [ - account for account in original_accounts + account for account in original_accounts if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist ] if not accounts: return [], page, total_items - + # Fetch the current Solana price sol_price = get_solana_price() if sol_price == 0: @@ -346,10 +346,10 @@ async def detect_dust_token_accounts( account_output = [] # Sort ATAs by mint address (this will help with the pagination) sorted_accounts = sorted( - accounts, + accounts, key=lambda x: x["account"]["data"]["parsed"]["info"]["mint"] ) - + # Pagination total_items = len(sorted_accounts) # We can't expect to have more items in a page than what we have. Ex: can't have 50 page items on 10 items in total @@ -376,7 +376,7 @@ async def detect_dust_token_accounts( token_price = metadata["price_info"]["price_per_token"] token_value = token_price * token_amount - + uri = metadata["uri"] cdn_uri = metadata["cdn_uri"] mime = metadata["mime"] @@ -465,7 +465,7 @@ async def burn_and_close_associated_token_account( close_authority ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) owner = owner - amount = amount_lamports + amount = amount_lamports # Construct the burn instruction params = BurnCheckedParams( @@ -584,7 +584,7 @@ async def close_ata_transaction( close_authority ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) #owner = owner - amount = amount_lamports + amount = amount_lamports # Construct the burn instruction params = BurnCheckedParams( @@ -708,7 +708,7 @@ async def close_burn_ata_instructions( close_authority ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) #owner = owner - amount = amount_lamports + amount = amount_lamports # Construct the burn instruction params = BurnCheckedParams( @@ -756,7 +756,7 @@ async def close_burn_ata_instructions( compute_unit_price_ix_bytes.hex(), # Convert to hex for compatibility compute_unit_limit_ix_bytes.hex(), request_heap_frame_ix_bytes.hex(), - burn_ix_bytes.hex(), + burn_ix_bytes.hex(), close_ix_bytes.hex(), ] instructions.extend(fee_ix_list_hex) @@ -853,7 +853,7 @@ async def recover_rent_client_from_instructions(go_local: bool = True): body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "B7Bo6nKHLewLXm9YEyyfjPECVnPx74ga5YH4r9Tfpump", + "token_mint": "BHKtPF6iZxUG6GePWx2UgNM9xefkk8sTE4gfUsaypump", "decimals": 6, "balance": 0.002039, "fee": 0.045 @@ -862,7 +862,7 @@ async def recover_rent_client_from_instructions(go_local: bool = True): instructions = [] if not go_local: response = requests.post( - url="http://localhost:5001/api/associated_token_accounts/burn_and_close/instructions", + url="http://3.255.102.199:443/api/associated_token_accounts/burn_and_close/instructions", json=body, headers={"Content-Type": "application/json"} ) @@ -890,7 +890,7 @@ async def recover_rent_client_from_instructions(go_local: bool = True): balance=body["balance"], fee=body["fee"] ) - + # Step 3: Convert each hex string back to bytes instructions_bytes = [bytes.fromhex(instruction) for instruction in instructions_data] @@ -932,7 +932,7 @@ async def recover_rent_client_from_instructions(go_local: bool = True): return send_result except Exception as e: print("recover_rent_client_from_instructions-> Error: {}".format(e)) - + # import asyncio # asyncio.run(recover_rent_client_from_instructions(go_local=False)) From 9931de0bdecbecbcea72aa248edfaf6861a43c24 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 13 Jan 2025 09:45:26 +0100 Subject: [PATCH 060/103] adding flake8. having app.api.libs.utils.py flake8 complient --- bot/app/api/libs/utils.py | 336 +++++++++++++++++++------------------- bot/requirements.txt | 1 + setup.cfg | 2 + 3 files changed, 175 insertions(+), 164 deletions(-) create mode 100644 setup.cfg diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 276d132..4e6abf9 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -35,6 +35,7 @@ # from bot.app.api.config import appconfig # from bot.app.api.handlers.exceptions import EntityNotFoundException + async def get_solana_balance(public_key: Pubkey) -> float: """ Fetches the SOL balance of a given Solana wallet address. @@ -43,16 +44,17 @@ async def get_solana_balance(public_key: Pubkey) -> float: :return: Balance in SOL as a float. """ async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - try: - # Fetch the balance (in lamports) - balance_response = await client.get_balance(public_key) - # Balance is returned in lamports (1 SOL = 10^9 lamports) - lamports = balance_response.value - sol_balance = lamports / 1e9 - return sol_balance - except Exception as e: - print(f"Error fetching balance: {e.error_msg}") - return 0.0 + try: + # Fetch the balance (in lamports) + balance_response = await client.get_balance(public_key) + # Balance is returned in lamports (1 SOL = 10^9 lamports) + lamports = balance_response.value + sol_balance = lamports / 1e9 + return sol_balance + except Exception as e: + print(f"Error fetching balance: {e.error_msg}") + return 0.0 + def get_solana_price() -> float: """ @@ -74,7 +76,7 @@ def get_solana_price() -> float: price_data = response.json() price = price_data["solana"]["usd"] break - except Exception as e: + except Exception: counter += 1 print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( counter, @@ -84,46 +86,47 @@ def get_solana_price() -> float: return price -async def get_token_accounts_by_owner(wallet_address=str)->list[dict]: + +async def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: value = [] - async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - data = { - "jsonrpc": "2.0", - "id": "test", - "method": "getTokenAccountsByOwner", - "params": [ - wallet_address, - { - "programId": str(TOKEN_PROGRAM_ID) - }, - { - "encoding": "jsonParsed" - } - ] - } - try: - response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - print("get_token_accounts_by_owner: Bad status code '{}' received for token {}".format( - response.status_code, - wallet_address - ) + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getTokenAccountsByOwner", + "params": [ + wallet_address, + { + "programId": str(TOKEN_PROGRAM_ID) + }, + { + "encoding": "jsonParsed" + } + ] + } + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("get_token_accounts_by_owner: Bad status code '{}' received for token {}".format( + response.status_code, + wallet_address ) - return value + ) + return value - content = response.json() - value = content["result"]["value"] + content = response.json() + value = content["result"]["value"] - return value - except Exception as e: - print("get_token_accounts_by_owner-> Error: {}".format(e)) - return value + return value + except Exception as e: + print("get_token_accounts_by_owner-> Error: {}".format(e)) + return value -def get_token_metadata(token_address: str)->dict: + +def get_token_metadata(token_address: str) -> dict: metadata = {} data = { @@ -176,7 +179,7 @@ def get_token_metadata(token_address: str)->dict: metadata["insufficient_data"] = True break - except Exception as e: + except Exception: counter += 1 print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( token_address, @@ -188,7 +191,8 @@ def get_token_metadata(token_address: str)->dict: return metadata -def get_current_ghostfunds_fees(burnable_accounts: int)->float: + +def get_current_ghostfunds_fees(burnable_accounts: int) -> float: """ Return GhostFunds fee based on how many ata can be burned. :param burnable_accounts [int]: how many atas @@ -291,6 +295,7 @@ async def count_associated_token_accounts( return total + async def detect_dust_token_accounts( wallet_pubkey: Pubkey, do_balance_aproximation: bool = True, @@ -307,117 +312,118 @@ async def detect_dust_token_accounts( account_samples = 5 working_balance = 0 # Refactor this: remove this line of code and test as client is not being used - async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - try: - total_items = 0 - # TODO: implement token's black list (like USDC, etc) - token_blacklist = [] - original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) - accounts = [ - account for account in original_accounts - if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist - ] + try: + total_items = 0 + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] + + if not accounts: + return [], page, total_items + + # Fetch the current Solana price + sol_price = get_solana_price() + if sol_price == 0: + return [], page, total_items + + if do_balance_aproximation: + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + working_balance = sum_balance / len(account_sample_list) + + counter = 0 + account_output = [] + # Sort ATAs by mint address (this will help with the pagination) + sorted_accounts = sorted( + accounts, + key=lambda x: x["account"]["data"]["parsed"]["info"]["mint"] + ) + + # Pagination + total_items = len(sorted_accounts) + # We can't expect to have more items in a page than what we have. + # Ex: can't have 50 page items on 10 items in total + total_pages = math.ceil(total_items / items_per_page) if items_per_page < total_items else total_items + # can't work with pages greater than the total pages we're dealing with + page = total_pages if page > total_pages else page + + start_index = (page - 1) * items_per_page + end_index = start_index + items_per_page if start_index + items_per_page < total_items else total_items + + # Paginate the list + sorted_accounts = sorted_accounts[start_index:end_index] + + for account in sorted_accounts: + counter += 1 - if not accounts: - return [], page, total_items - - # Fetch the current Solana price - sol_price = get_solana_price() - if sol_price == 0: - return [], page, total_items - - if do_balance_aproximation: - account_sample_list = accounts - if len(accounts) > account_samples: - my_list = list(range(len(accounts))) - # Generate X random positions - random_positions = random.sample(range(len(my_list)), account_samples) - # Get the 5 random values from the list - account_sample_list = [my_list[pos] for pos in random_positions] - - sum_balance = 0 - for index in account_sample_list: - associated_tokan_account = accounts[index]["pubkey"] - sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) - - working_balance = sum_balance / len(account_sample_list) - - counter = 0 - account_output = [] - # Sort ATAs by mint address (this will help with the pagination) - sorted_accounts = sorted( - accounts, - key=lambda x: x["account"]["data"]["parsed"]["info"]["mint"] + mint = account["account"]["data"]["parsed"]["info"]["mint"] + owner = account["account"]["data"]["parsed"]["info"]["owner"] + token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] + associated_token_account = account["pubkey"] + + metadata = get_token_metadata(token_address=mint) + + token_price = metadata["price_info"]["price_per_token"] + token_value = token_price * token_amount + + uri = metadata["uri"] + cdn_uri = metadata["cdn_uri"] + mime = metadata["mime"] + description = metadata["description"] if "description" in metadata else "" + name = metadata["name"].strip() + symbol = metadata["symbol"].strip() + authority = metadata["authority"] + supply = metadata["supply"] + token_program = metadata["token_program"] + insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price + + account_output.append( + { + "token_mint": mint, + "associated_token_account": associated_token_account, + "owner": owner, + "token_amount": token_amount, + "token_price": token_price, + "token_value": token_value, + "decimals": decimals, + "sol_balance": working_balance, + "sol_balance_usd": working_balance * sol_price, + "is_dust": token_value < min_token_value, + "uri": uri, + "cdn_uri": cdn_uri, + "mime": mime, + "description": description, + "name": name, + "symbol": symbol, + "authority": authority, + "supply": supply, + "token_program": token_program, + "insufficient_data": insufficient_data, + } ) - # Pagination - total_items = len(sorted_accounts) - # We can't expect to have more items in a page than what we have. Ex: can't have 50 page items on 10 items in total - total_pages = math.ceil(total_items / items_per_page) if items_per_page < total_items else total_items - # can't work with pages greater than the total pages we're dealing with - page = total_pages if page > total_pages else page - - start_index = (page - 1) * items_per_page - end_index = start_index + items_per_page if start_index + items_per_page < total_items else total_items - - # Paginate the list - sorted_accounts = sorted_accounts[start_index:end_index] - - for account in sorted_accounts: - counter +=1 - - mint = account["account"]["data"]["parsed"]["info"]["mint"] - owner = account["account"]["data"]["parsed"]["info"]["owner"] - token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] - decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] - associated_token_account = account["pubkey"] - - metadata = get_token_metadata(token_address=mint) - - token_price = metadata["price_info"]["price_per_token"] - token_value = token_price * token_amount - - uri = metadata["uri"] - cdn_uri = metadata["cdn_uri"] - mime = metadata["mime"] - description = metadata["description"] if "description" in metadata else "" - name = metadata["name"].strip() - symbol = metadata["symbol"].strip() - authority = metadata["authority"] - supply = metadata["supply"] - token_program = metadata["token_program"] - insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price - - account_output.append( - { - "token_mint": mint, - "associated_token_account": associated_token_account, - "owner": owner, - "token_amount": token_amount, - "token_price": token_price, - "token_value": token_value, - "decimals": decimals, - "sol_balance": working_balance, - "sol_balance_usd": working_balance * sol_price, - "is_dust": token_value < min_token_value, - "uri": uri, - "cdn_uri": cdn_uri, - "mime": mime, - "description": description, - "name": name, - "symbol": symbol, - "authority": authority, - "supply": supply, - "token_program": token_program, - "insufficient_data": insufficient_data, - } - ) + return account_output, page, total_items - return account_output, page, total_items + except Exception as e: + print(f"Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") + return account_output, page, total_items - except Exception as e: - print(f"Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") - return account_output, page, total_items async def burn_and_close_associated_token_account( associated_token_account: Pubkey, @@ -447,7 +453,7 @@ async def burn_and_close_associated_token_account( # BURN data = account_info.data if len(data) != 165: - print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + print("burn_associated_token_account-> Error: Invalid data length for ATA {} for token {}".format( str(associated_token_account), str(token_mint) )) @@ -463,7 +469,7 @@ async def burn_and_close_associated_token_account( delegated_amount, close_authority_option, close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) owner = owner amount = amount_lamports @@ -566,7 +572,7 @@ async def close_ata_transaction( # BURN data = account_info.data if len(data) != 165: - print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + print("close_ata_transaction-> Error: Invalid data length for ATA {} for token {}".format( str(associated_token_account), str(token_mint) )) @@ -582,8 +588,8 @@ async def close_ata_transaction( delegated_amount, close_authority_option, close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) - #owner = owner + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) + amount = amount_lamports # Construct the burn instruction @@ -619,7 +625,6 @@ async def close_ata_transaction( if encode_base64: txn = base64.b64encode(txn).decode('ascii') - except EntityNotFoundException as enfe: raise enfe except Exception as e: @@ -632,7 +637,7 @@ def get_fee_instructions( fee: float, balance: float, owner: Pubkey -)->list[Instruction]: +) -> list[Instruction]: # Fix fee lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * 10**9) # Convert SOL to lamports fix_fees_params = TransferParams( @@ -651,6 +656,7 @@ def get_fee_instructions( variable_fees_ix = transfer(params=variable_fees_params) return [fix_fees_ix, variable_fees_ix] + async def close_burn_ata_instructions( owner: Pubkey, token_mint: Pubkey, @@ -690,7 +696,7 @@ async def close_burn_ata_instructions( # BURN data = account_info.data if len(data) != 165: - print("burn_associated_token_account-> Error: Invalid data length for SPL associated account {} for token {}".format( + print("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( str(associated_token_account), str(token_mint) )) @@ -706,8 +712,8 @@ async def close_burn_ata_instructions( delegated_amount, close_authority_option, close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s",data) - #owner = owner + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) + amount = amount_lamports # Construct the burn instruction @@ -768,6 +774,7 @@ async def close_burn_ata_instructions( return instructions + async def recover_rent_client_from_transaction(): from bot.config import appconfig keypair = Keypair.from_base58_string(appconfig.PRIVKEY) @@ -847,6 +854,7 @@ async def recover_rent_client_from_transaction(): print("recover_rent_client_from_transaction-> Error: {}".format(e)) return response + async def recover_rent_client_from_instructions(go_local: bool = True): from bot.config import appconfig keypair = Keypair.from_base58_string(appconfig.PRIVKEY) diff --git a/bot/requirements.txt b/bot/requirements.txt index 89898a8..adcaf86 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -14,3 +14,4 @@ redisearch==2.0.0 nest_asyncio==1.6.0 base58==2.1.1 jito_py_rpc==0.1.0 +flake8 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aa079ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120 From 5c2c67d2963def103025e2a150ecca947e9eff72 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 13 Jan 2025 09:52:44 +0100 Subject: [PATCH 061/103] having app project flake8 compliant --- bot/app/api/config/__init__.py | 10 +++++----- bot/app/api/handlers/exceptions.py | 4 +++- bot/app/api/models/outer_models.py | 8 ++++++-- .../routes/outer/associated_token_accounts.py | 4 ++-- .../outer/count_associated_token_accounts.py | 2 +- .../api/routes/outer/request_instructions.py | 1 + .../api/routes/outer/request_transaction.py | 2 +- bot/app/main.py | 18 +++++++++++++++--- 8 files changed, 34 insertions(+), 15 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 5d2c083..155cd71 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -34,14 +34,14 @@ class AuthConfig: RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" - RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" + RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" # noqa: E501 RPC_JITO_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" - - #PAGINATION + + # PAGINATION DEFAULT_PAGE = 1 DEFAULT_ITEMS_PER_PAGE = 10 - - #DB + + # DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/bot/app/api/handlers/exceptions.py b/bot/app/api/handlers/exceptions.py index 3e00797..2023ee6 100644 --- a/bot/app/api/handlers/exceptions.py +++ b/bot/app/api/handlers/exceptions.py @@ -1,11 +1,13 @@ -from fastapi import FastAPI, HTTPException, Request +from fastapi import HTTPException, Request from fastapi.responses import JSONResponse + # Define custom exception class EntityNotFoundException(HTTPException): def __init__(self, detail: str): super().__init__(status_code=404, detail=detail) + # Exception handler for EntityNotFoundException def entity_not_found_exception_handler(request: Request, exc: EntityNotFoundException): return JSONResponse( diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 7a5f190..af47008 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -14,6 +14,7 @@ ) ] + class CountAssociatedTokenAccounts(BaseModel): total_accounts: int = Field( 0, description='Total number of associated accounts.' @@ -109,15 +110,16 @@ class AssociatedTokenAccounts(BaseModel): # raise ValidationError("Both 'page' and 'items' must be greater than 0.") # return values + class RequestTransaction(BaseModel): - owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( + owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( # noqa: F722 Field( ..., description='Owner account address.', example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', ) ) - token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( + token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( # noqa: F722 ..., description='The mint address of the token. Supports standard Solana mint format and extended formats.', example='bpMAcs5cEDu33kbCgTcBu7HtuZwsoNwsMH839jupump', @@ -132,11 +134,13 @@ class RequestTransaction(BaseModel): ..., description='GhostFunds fee.' ) + class Quote(BaseModel): quote: Optional[str] = Field( None, description='A base64-encoded string representing the quote.' ) + class Instructions(BaseModel): response: Optional[str] = Field( None, description='A base64-encoded string representing the list of instructions in hex format.' diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py index cd52294..46d7ad5 100644 --- a/bot/app/api/routes/outer/associated_token_accounts.py +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -14,6 +14,7 @@ def validate_account_address(account_address: AccountAddressType) -> str: return account_address + @router.get( path="/associated_token_accounts", response_model=AssociatedTokenAccounts, @@ -34,7 +35,7 @@ async def fetch_associated_token_accounts( wallet_pubkey = Pubkey.from_string(account_address) page = page if page >= appconfig.DEFAULT_PAGE else appconfig.DEFAULT_PAGE items = items if items > 1 else appconfig.DEFAULT_ITEMS_PER_PAGE - + ata_list, page, total_items = await detect_dust_token_accounts( wallet_pubkey=wallet_pubkey, page=page, @@ -84,4 +85,3 @@ async def fetch_associated_token_accounts( accounts=accounts ) return ata_accounts - diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 9cb515e..086a46b 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -11,6 +11,7 @@ def validate_account_address(account_address: AccountAddressType) -> str: return account_address + @router.get( path="/associated_token_accounts/count", response_model=CountAssociatedTokenAccounts, @@ -31,4 +32,3 @@ async def associated_token_accounts_count( fee=data["fee"] ) return response - diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index ec744ef..8871670 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -9,6 +9,7 @@ from api.libs.utils import close_burn_ata_instructions router = APIRouter() + @router.post( path="/associated_token_accounts/burn_and_close/instructions", response_model=Instructions, diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index a5ca5f8..d5b4b48 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -7,6 +7,7 @@ from api.libs.utils import close_ata_transaction router = APIRouter() + @router.post( path="/associated_token_accounts/burn_and_close/transaction", response_model=Quote, @@ -16,7 +17,6 @@ async def request_close_ata_transaction( body: RequestTransaction, ): - txn = None try: owner = body.owner token = body.token_mint diff --git a/bot/app/main.py b/bot/app/main.py index 3a92424..274b305 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -14,9 +14,21 @@ # Including routes app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) -app.include_router(router=count_associated_token_accounts_route, prefix="/api", tags=["Count Associated Token Accounts"]) -app.include_router(router=request_ata_transaction_route, prefix="/api", tags=["Request Associated Token Account Transaction"]) -app.include_router(router=request_ata_instructions_route, prefix="/api", tags=["Request Associated Token Account Instructions"]) +app.include_router( + router=count_associated_token_accounts_route, + prefix="/api", + tags=["Count Associated Token Accounts"] +) +app.include_router( + router=request_ata_transaction_route, + prefix="/api", + tags=["Request Associated Token Account Transaction"] +) +app.include_router( + router=request_ata_instructions_route, + prefix="/api", + tags=["Request Associated Token Account Instructions"] +) # Register the exception handler without decorator app.add_exception_handler(EntityNotFoundException, entity_not_found_exception_handler) From b1f3d9d609aa92f4c6030bc903a8963984a78eea Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 13 Jan 2025 10:08:23 +0100 Subject: [PATCH 062/103] more flake8 validations --- bot/app/api/libs/utils.py | 69 +++++++++---------- bot/app/api/models/outer_models.py | 8 +-- .../routes/outer/associated_token_accounts.py | 12 ++-- bot/config/__init__.py | 8 +-- setup.cfg | 8 +++ 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 4e6abf9..e3f1e1d 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -105,16 +105,15 @@ async def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: } try: response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) if response.status_code != 200: print("get_token_accounts_by_owner: Bad status code '{}' received for token {}".format( - response.status_code, - wallet_address - ) - ) + response.status_code, + wallet_address + )) return value content = response.json() @@ -145,19 +144,18 @@ def get_token_metadata(token_address: str) -> dict: try: response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) if response.status_code != 200: counter += 1 print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( - response.status_code, - token_address, - counter, - retries - ) - ) + response.status_code, + token_address, + counter, + retries + )) time.sleep(counter) continue @@ -182,11 +180,10 @@ def get_token_metadata(token_address: str) -> dict: except Exception: counter += 1 print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( - token_address, - counter, - retries - ) - ) + token_address, + counter, + retries + )) time.sleep(counter) return metadata @@ -786,15 +783,14 @@ async def recover_rent_client_from_transaction(): } try: response = requests.post( - url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", - json=body, - headers={"Content-Type": "application/json"} - ) + url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", + json=body, + headers={"Content-Type": "application/json"} + ) if response.status_code != 200: print("recover_rent_client: Bad status code '{}' recevied".format( - response.status_code - ) - ) + response.status_code + )) return response content = response.json() @@ -870,15 +866,14 @@ async def recover_rent_client_from_instructions(go_local: bool = True): instructions = [] if not go_local: response = requests.post( - url="http://3.255.102.199:443/api/associated_token_accounts/burn_and_close/instructions", - json=body, - headers={"Content-Type": "application/json"} - ) + url="http://3.255.102.199:443/api/associated_token_accounts/burn_and_close/instructions", + json=body, + headers={"Content-Type": "application/json"} + ) if response.status_code != 200: print("recover_rent_client: Bad status code '{}' recevied".format( - response.status_code - ) - ) + response.status_code + )) return response content = response.json() diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index af47008..96c33de 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -28,10 +28,10 @@ class CountAssociatedTokenAccounts(BaseModel): rent_balance: float = Field( 0, description='Total rent balance for the accounts.' ) - rent_balance_usd: float = Field( + rent_balance_usd: float = Field( 0, description='Total rent balance converted to USD.' ) - fee: float = Field( + fee: float = Field( 0, description='GhostFunds fee.' ) @@ -112,14 +112,14 @@ class AssociatedTokenAccounts(BaseModel): class RequestTransaction(BaseModel): - owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( # noqa: F722 + owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( Field( ..., description='Owner account address.', example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', ) ) - token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( # noqa: F722 + token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( ..., description='The mint address of the token. Supports standard Solana mint format and extended formats.', example='bpMAcs5cEDu33kbCgTcBu7HtuZwsoNwsMH839jupump', diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py index 46d7ad5..8d96228 100644 --- a/bot/app/api/routes/outer/associated_token_accounts.py +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -24,13 +24,13 @@ def validate_account_address(account_address: AccountAddressType) -> str: async def fetch_associated_token_accounts( account_address: str = Depends(validate_account_address), page: PositiveInt = Query( - default=appconfig.DEFAULT_PAGE, - description="The current page to retrieve (must be greater than 0)." - ), + default=appconfig.DEFAULT_PAGE, + description="The current page to retrieve (must be greater than 0)." + ), items: PositiveInt = Query( - default=appconfig.DEFAULT_ITEMS_PER_PAGE, - description="The number of items per page (must be greater than 0)." - ), + default=appconfig.DEFAULT_ITEMS_PER_PAGE, + description="The number of items per page (must be greater than 0)." + ), ): wallet_pubkey = Pubkey.from_string(account_address) page = page if page >= appconfig.DEFAULT_PAGE else appconfig.DEFAULT_PAGE diff --git a/bot/config/__init__.py b/bot/config/__init__.py index cf8d443..e82ac63 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -23,8 +23,8 @@ class AuthConfig: SLIPPAGE = os.environ.get("SLIPPAGE", 40) FEES = float(os.environ.get("FEES", 0.0015)) FEES_INCREASMENT = 0.000805 - FEES_TIMEDELTA_IN_SECONDS = 1.0 # Tolerance to delays between buying and entering in - # the trade because of lower fees + FEES_TIMEDELTA_IN_SECONDS = 1.0 # Tolerance to delays between buying and entering in + # the trade because of lower fees # noqa: E116 FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", @@ -52,7 +52,7 @@ class AuthConfig: "TRADING_CRITERIA_CONSECUTIVES_NON_RELEVANT_TRADES_TOLERANCE", 3 ) - ) + ) TRADING_TOKENS_AT_THE_SAME_TIME = 1 TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 TRADING_RETRIES = 3 @@ -63,7 +63,7 @@ class AuthConfig: SCANNER_TRADING_AMOUNT = 1.00 # Sols the sniper will trade SCANNER_WORKING_TIME = 600 # Seconds the scanner will be working SCANNER_PUMPDONTFUN_INITIAL_FUND = 30 # Sols placed by pump.fun to launch a token - #DB + # DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") diff --git a/setup.cfg b/setup.cfg index aa079ec..e13ce20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,10 @@ [flake8] max-line-length=120 +exclude = # Exclude specific files or directories + .git, + __pycache__, + docs/conf.py, + old, + build, + dist +ignore = F722 From 5324f058eccafc564d1dca01c64d40500bc8c41a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 13 Jan 2025 13:53:07 +0100 Subject: [PATCH 063/103] new todo: week 2 2025 --- bot/todo.yaml | 76 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/bot/todo.yaml b/bot/todo.yaml index 4a6e89b..7d5fbb8 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -1,32 +1,60 @@ # TODO -- TODO: fees. Send instruction request in base64 to avoid "otros" to see our fees +- AssociatedTokenAccount + return if it's blacklisted -- EC2: - √ Deploy app in - √√ Mini and check if it works - # - Small and check if it works - - Have a permanent IP address +- associated_token_accounts/burn_and_close/instructions + - Si el fee tiene un vallor raro, entonces será 5% por defecto + +- Instructions: + - have up to 10 burn and close accounts at the same time + +- FRONT BEHAVIOUR: + - Front will handle the Black list + - Front will disable blacklisted ATAs and prevent them from being claimed. + - Front will do 10 ATAs per transaction based on fix page size. + +- FRONT: + - Not connected: + - Info section: + - Big Message: "Claim your hidden funds now!" + - Show steps + - Pagination section: + - PLace holder message saying: Not counts available to shown. + - Widget: + - Claim History + - Show Ghost icon + - Place holder message: No history available + - When connected: + - Info Section: + - Claimable Sol balance in big + - Claimable Usd balance in small right above + - Total holding accounts + - Not claimable accounts + - Pagination Section: + - Columns: + - Selected + - Token image -> token link to solscan + - Token full name -> token link to solscan + - Token symbol -> token link to solscan + - ATA balance in Sols + - ATA balance in USD + - Footer: + - Fix 10 page pagination + - Widget: + - Claim History. + - Total claimed in Sols in big + - Total claimed in usd + - Accounts closed (check a better term for this) + - List of actions (no pagination) + - Columns: + - Abreviated Signature -> transaction link to solscan + - Accounts -> transaction link to solscan -- New endopints - - Post: get burn and close quote - √ Create openAPI specs - √ Create pydantic classes - √ Create enpoint /associated_token_accounts/burn_and_close/transaction - √ Will call burn_and_close_associated_token_account without keypair - √ Will return a Transaction.new_unsigned (create the Message) - √ encode in base64 and return - - WIll include 5% fees as a new instruction - - Check lookuptables - √ Create new function get_current_ghostfunds_fees(ata_to_be_closed: int)->float - √ Return the fees in the count endpoint - √ Get the fee as a new input variable in: - /associated_token_accounts/burn_and_close/instructions - √ Create a new function get_fee_instructions(fees: float)->[Instruction] - √ Business logic will be defined here - √ Phase1: All 5% will go to Ghost account - - Phase2: When we have a token. We'll sent some % to another Ghost account to finally buy our own tokens. +- EC2: + - Have a permanent IP address + - BUG: when a timeout occurs during subscription, the next step is BUY and should be unsubscribe to token. From 17eada0ec0a1ac806befeaac86136e03a5977d26 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 14 Jan 2025 17:45:26 +0100 Subject: [PATCH 064/103] Adding fee value validation when fetching instructions --- bot/app/api/routes/outer/request_instructions.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index 8871670..6f15669 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -4,9 +4,11 @@ from fastapi import APIRouter from solders.pubkey import Pubkey +from api.config import appconfig from api.handlers.exceptions import EntityNotFoundException -from api.models.outer_models import Instructions, RequestTransaction from api.libs.utils import close_burn_ata_instructions +from api.models.outer_models import Instructions, RequestTransaction + router = APIRouter() @@ -26,6 +28,17 @@ async def request_close_ata_instructions( balance = body.balance fee = body.fee + last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] + if fee not in appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values() or fee < last_fee: + # Assigning unknown or lower fee + print("Warning: unknown fee of {} received from account {} on token {}. Adjusting fee to be {}".format( + fee, + owner, + token, + appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] + )) + fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] + instructions = await close_burn_ata_instructions( owner=Pubkey.from_string(owner), token_mint=Pubkey.from_string(token), From 1f2ea3d94fda32b32eb45bab4ab7487e065febd9 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 14 Jan 2025 20:05:46 +0100 Subject: [PATCH 065/103] close_burn_ata_instructions now receives a list of tokens --- bot/app/api/libs/utils.py | 242 +++++++++++++++++++++++++------------- 1 file changed, 158 insertions(+), 84 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index e3f1e1d..df31cb8 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -633,10 +633,11 @@ async def close_ata_transaction( def get_fee_instructions( fee: float, balance: float, - owner: Pubkey + owner: Pubkey, + atas: int ) -> list[Instruction]: - # Fix fee - lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * 10**9) # Convert SOL to lamports + # Fix fee: we're charing a base fix fee for every ATA we're burning and closing. + lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * atas * 10**9) # Convert SOL to lamports fix_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_FIX_FEES_RECEIVER), @@ -654,19 +655,32 @@ def get_fee_instructions( return [fix_fees_ix, variable_fees_ix] +def calculate_compute_units(atas: int): + # Units consumed by each instruction + burn_checked_units = 4742 + close_account_units = 2916 + other_instruction_units = 1000 # Adjust based on your use case + + # Total units required + total_units = atas * (burn_checked_units + close_account_units) + other_instruction_units + + # Add a buffer for safety + buffer_units = 1000 + return total_units + buffer_units + + async def close_burn_ata_instructions( owner: Pubkey, - token_mint: Pubkey, - decimals: int, - balance: float, + tokens: list[dict], fee: float ) -> list[hex]: """ Burs all tokens from the associated token account - :param associated_token_account[Pubkey]: associated token account - :param token_mint[Pubkey]: tokens to get burned - :param decimals[int]: The token's decimals (default 9 for SOL). - :param balance[float]: The ATA Sol balance (for fee calculation) + :param owner[Pubkey]: ATA account owner + :param tokens[list[dict]]: List of dicts that will hold the following information: + token_mint[Pubkey]: tokens to get burned + decimals[int]: The token's decimals (default 9 for SOL). + balance[float]: The ATA Sol balance (for fee calculation) :param fee[float]: GhostFunds fee to be charge to user. :return: [str] base64 list with all the instructions in hex format. """ @@ -674,94 +688,107 @@ async def close_burn_ata_instructions( async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: - associated_token_account = get_associated_token_address( - owner=owner, - mint=token_mint - ) - response = await client.get_account_info(associated_token_account) - account_info = response.value - if not account_info: - print("Associated token account {} does not exist.".format( - associated_token_account - )) - raise EntityNotFoundException( - detail="Associated token account {} nor found.".format( - str(associated_token_account) - ) + # Will set both burn and close intructions for every ATA + burn_close_instructions_hex = [] + for token in tokens: + associated_token_account = get_associated_token_address( + owner=owner, + mint=Pubkey.from_string(token["token_mint"]) ) + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + raise EntityNotFoundException( + detail="Associated token account {} nor found.".format( + str(associated_token_account) + ) + ) - # BURN - data = account_info.data - if len(data) != 165: - print("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( - str(associated_token_account), - str(token_mint) - )) - ( - mint, - owner_data, - amount_lamports, - delegate_option, - delegate, - state, - is_native_option, - is_native, - delegated_amount, - close_authority_option, - close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) - - amount = amount_lamports - - # Construct the burn instruction - params = BurnCheckedParams( - program_id=TOKEN_PROGRAM_ID, - mint=token_mint, - account=associated_token_account, - owner=owner, - amount=amount, - decimals=decimals, - signers=[owner] - ) - burn_ix = burn_checked(params=params) - burn_ix_bytes = bytes(burn_ix) - - # CLOSE - # Create the close account instruction - close_ix = close_account( - CloseAccountParams( + # BURN + data = account_info.data + if len(data) != 165: + print("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( + str(associated_token_account), + token["token_mint"] + )) + ( + mint, + owner_data, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) + + amount = amount_lamports + + # Construct the burn instruction + params = BurnCheckedParams( program_id=TOKEN_PROGRAM_ID, + mint=Pubkey.from_string(token["token_mint"]), account=associated_token_account, - dest=owner, - owner=owner + owner=owner, + amount=amount, + decimals=token["decimals"], + signers=[owner] ) - ) - close_ix_bytes = bytes(close_ix) + burn_ix = burn_checked(params=params) + burn_ix_bytes = bytes(burn_ix) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=owner, + owner=owner + ) + ) + close_ix_bytes = bytes(close_ix) + + burn_close_instructions_hex.append(burn_ix_bytes.hex()) + burn_close_instructions_hex.append(close_ix_bytes.hex()) + # Required instructions to set compute unit limit and price compute_unit_price_ix = set_compute_unit_price(5_000) compute_unit_price_ix_bytes = bytes(compute_unit_price_ix) - compute_unit_limit_ix = set_compute_unit_limit(units=9_000) + compute_units = calculate_compute_units(atas=len(tokens)) + compute_unit_limit_ix = set_compute_unit_limit(units=compute_units) compute_unit_limit_ix_bytes = bytes(compute_unit_limit_ix) # Although this is redundant, we're keeping it. - heap_memory_size = 32 * 1024 + heap_memory_size = 64 * 1024 request_heap_frame_ix = request_heap_frame(bytes_=heap_memory_size) request_heap_frame_ix_bytes = bytes(request_heap_frame_ix) # Variable fee - fee_ix_list = get_fee_instructions(fee=fee, balance=balance, owner=owner) + # Summing all ATAs balances to calculate variable fee + fee_ix_list = get_fee_instructions( + fee=fee, + balance=sum(token.get("balance", 0) for token in tokens), + owner=owner, + atas=len(tokens) + ) fee_ix_list_bytes = [bytes(fee_ix) for fee_ix in fee_ix_list] fee_ix_list_hex = [fee_ix_bytes.hex() for fee_ix_bytes in fee_ix_list_bytes] - # Package both instructions into a single JSON object + # Package all instructions as hex values from bytes into a single list instructions = [ compute_unit_price_ix_bytes.hex(), # Convert to hex for compatibility compute_unit_limit_ix_bytes.hex(), - request_heap_frame_ix_bytes.hex(), - burn_ix_bytes.hex(), - close_ix_bytes.hex(), + # request_heap_frame_ix_bytes.hex(), ] + instructions.extend(burn_close_instructions_hex) instructions.extend(fee_ix_list_hex) except EntityNotFoundException as enfe: @@ -857,9 +884,58 @@ async def recover_rent_client_from_instructions(go_local: bool = True): body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "BHKtPF6iZxUG6GePWx2UgNM9xefkk8sTE4gfUsaypump", - "decimals": 6, - "balance": 0.002039, + "tokens": [ + { + "token_mint": "A8ZWXHAG94pse8yYBgjnsCMc6GKhcM83CJfvTB6bpump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "HkRrx8bdhGgoQLShM2YTzSHAYDdp6pLNXQE69m7Lpump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "3DnBVVMB41h1iR37kV6tbGTuaQnv7xTgjA2QUog7pump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "4QpUXvGqNHJiBYWeoNR3Z9hACrVvwTHwYzPwFkDepump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "7UXAqkx3rtUDHepzEVzBdBCempwAj31SQjx3TgKMpump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "2KTN3QZPpBDNvaFzWGNZ58E3SiyafDGo4K4YDnN1pump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "8phGbDQfP5E7cv7bZJDJr1KkxnZaPJa4RzbNVZhTpump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "HBSJQcuUgPESsznJ7iMVKxYecLU6cQuKPGgoam6cpump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "55iqkACwZ3ceGqUXnaPUhA4RTKv9XUFZAKy3gPM2pump", + "decimals": 6, + "balance": 0.002039 + }, + { + "token_mint": "M3M3pSFptfpZYnWNUgAbyWzKKgPo5d1eWmX6tbiSF2K", + "decimals": 9, + "balance": 0.002039 + }, + ], "fee": 0.045 } try: @@ -888,12 +964,10 @@ async def recover_rent_client_from_instructions(go_local: bool = True): else: instructions_data = await close_burn_ata_instructions( owner=Pubkey.from_string(body["owner"]), - token_mint=Pubkey.from_string(body["token_mint"]), - decimals=body["decimals"], - balance=body["balance"], + tokens=body["tokens"], fee=body["fee"] ) - + print("*** Recovering funds from {} ATAs ***".format(len(instructions_data))) # Step 3: Convert each hex string back to bytes instructions_bytes = [bytes.fromhex(instruction) for instruction in instructions_data] @@ -911,7 +985,7 @@ async def recover_rent_client_from_instructions(go_local: bool = True): recent_blockhash=recent_blockhash ) - # Simluating the transaction + # Simulating the transaction simulation_result = await client.simulate_transaction(signed_tx) if simulation_result.value.err: print("Simulation error:", simulation_result.value.err) @@ -938,4 +1012,4 @@ async def recover_rent_client_from_instructions(go_local: bool = True): # import asyncio -# asyncio.run(recover_rent_client_from_instructions(go_local=False)) +# asyncio.run(recover_rent_client_from_instructions(go_local=True)) From 498511186f250e331aeb94fac903d681c00629a1 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 14 Jan 2025 20:53:39 +0100 Subject: [PATCH 066/103] now request_close_ata_instructions receives and process many instructions --- bot/app/AWS/steps.md | 7 ++- bot/app/api/libs/utils.py | 61 +++---------------- bot/app/api/models/outer_models.py | 25 +++++--- .../api/routes/outer/request_instructions.py | 18 +++--- bot/app/openapi/external_endpoints.yaml | 26 +++++--- bot/todo.yaml | 13 ++-- 6 files changed, 62 insertions(+), 88 deletions(-) diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index e0f4c22..cdb01e9 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -31,7 +31,10 @@ ls -l /var/run/docker.sock ## restart sudo systemctl restart docker -# Pull the image ########################### +# ##################################################### +# Copy and paste in EC2 terminal +# login and Pull the image ########################### +aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 237733826785.dkr.ecr.eu-west-1.amazonaws.com docker pull 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest ## List images #### aws ecr list-images --repository-name ghostfunds-ata-service --region eu-west-1 @@ -42,7 +45,7 @@ docker stop $CONTAINER_ID docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -v ':latest' | awk '{print $2}' | xargs docker rmi -f # Run the docker container docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest -### ##################################### +### ################################################## # Start Docker when EC2 instance starts ## Create start-docker-container.sh and add the code diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index df31cb8..1834f24 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -12,7 +12,7 @@ from solana.rpc.commitment import Confirmed from solders.compute_budget import ( - request_heap_frame, + # request_heap_frame, set_compute_unit_limit, set_compute_unit_price ) @@ -767,9 +767,9 @@ async def close_burn_ata_instructions( compute_unit_limit_ix_bytes = bytes(compute_unit_limit_ix) # Although this is redundant, we're keeping it. - heap_memory_size = 64 * 1024 - request_heap_frame_ix = request_heap_frame(bytes_=heap_memory_size) - request_heap_frame_ix_bytes = bytes(request_heap_frame_ix) + # heap_memory_size = 64 * 1024 + # request_heap_frame_ix = request_heap_frame(bytes_=heap_memory_size) + # request_heap_frame_ix_bytes = bytes(request_heap_frame_ix) # Variable fee # Summing all ATAs balances to calculate variable fee @@ -884,59 +884,14 @@ async def recover_rent_client_from_instructions(go_local: bool = True): body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "fee": 0.045, "tokens": [ { - "token_mint": "A8ZWXHAG94pse8yYBgjnsCMc6GKhcM83CJfvTB6bpump", + "token_mint": "G1jFTkA5XCw8b9Sut9PW6cUzaoTfhmW58fRXqa78pump", "decimals": 6, "balance": 0.002039 - }, - { - "token_mint": "HkRrx8bdhGgoQLShM2YTzSHAYDdp6pLNXQE69m7Lpump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "3DnBVVMB41h1iR37kV6tbGTuaQnv7xTgjA2QUog7pump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "4QpUXvGqNHJiBYWeoNR3Z9hACrVvwTHwYzPwFkDepump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "7UXAqkx3rtUDHepzEVzBdBCempwAj31SQjx3TgKMpump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "2KTN3QZPpBDNvaFzWGNZ58E3SiyafDGo4K4YDnN1pump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "8phGbDQfP5E7cv7bZJDJr1KkxnZaPJa4RzbNVZhTpump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "HBSJQcuUgPESsznJ7iMVKxYecLU6cQuKPGgoam6cpump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "55iqkACwZ3ceGqUXnaPUhA4RTKv9XUFZAKy3gPM2pump", - "decimals": 6, - "balance": 0.002039 - }, - { - "token_mint": "M3M3pSFptfpZYnWNUgAbyWzKKgPo5d1eWmX6tbiSF2K", - "decimals": 9, - "balance": 0.002039 - }, + } ], - "fee": 0.045 } try: instructions = [] @@ -1012,4 +967,4 @@ async def recover_rent_client_from_instructions(go_local: bool = True): # import asyncio -# asyncio.run(recover_rent_client_from_instructions(go_local=True)) +# asyncio.run(recover_rent_client_from_instructions(go_local=False)) diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 96c33de..ce7f3a2 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -111,14 +111,7 @@ class AssociatedTokenAccounts(BaseModel): # return values -class RequestTransaction(BaseModel): - owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( - Field( - ..., - description='Owner account address.', - example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', - ) - ) +class RequestTransactionToken(BaseModel): token_mint: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{32,50}$') = Field( ..., description='The mint address of the token. Supports standard Solana mint format and extended formats.', @@ -130,6 +123,22 @@ class RequestTransaction(BaseModel): balance: float = Field( ..., description='ATA SOls balance' ) + + +class RequestTransaction(BaseModel): + owner: constr(pattern=r'^[1-9A-HJ-NP-Za-km-z]{44}$') = ( + Field( + ..., + description='Owner account address.', + example='7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi', + ) + ) + tokens: list[RequestTransactionToken] = ( + Field( + ..., + description='A list of tokens addresses with ATA balance and token decimals.', + ) + ) fee: float = Field( ..., description='GhostFunds fee.' ) diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index 6f15669..5ee57b7 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -23,27 +23,29 @@ async def request_close_ata_instructions( ): try: owner = body.owner - token = body.token_mint - decimals = body.decimals - balance = body.balance fee = body.fee + tokens = body.tokens + if not tokens: + instructions = Instructions( + response=None + ) + return instructions + + tokens = [token.model_dump() for token in tokens] last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] if fee not in appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values() or fee < last_fee: # Assigning unknown or lower fee - print("Warning: unknown fee of {} received from account {} on token {}. Adjusting fee to be {}".format( + print("Warning: unknown fee of {} received from account {}. Adjusting fee to be {}".format( fee, owner, - token, appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] )) fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] instructions = await close_burn_ata_instructions( owner=Pubkey.from_string(owner), - token_mint=Pubkey.from_string(token), - decimals=decimals, - balance=balance, + tokens=tokens, fee=fee ) diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index b739035..c09a2b7 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -103,7 +103,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GetTransaction' + $ref: '#/components/schemas/RequestTransaction' responses: '200': description: Burn and close quote successfully retrieved. @@ -132,7 +132,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GetTransaction' + $ref: '#/components/schemas/RequestTransaction' responses: '200': description: INstrucions successfully retrieved. @@ -256,14 +256,27 @@ components: type: boolean description: Whether there is insufficient data for this token. - GetTransaction: + RequestTransaction: type: object properties: - associated_token_account: + owner: type: string description: The associated token account address. pattern: ^[1-9A-HJ-NP-Za-km-z]{44}$ example: "7dLn2WU6vX6Yk1BeMoAAumx7grc79TdcUgrpqvA9CvFi" + fee: + type: number + format: float + description: Fees to be applied based on burnable accounts + example: 0.05 + tokens: + type: array + items: + $ref: '#/components/schemas/RequestTransactionToken' + + RequestTransactionToken: + type: object + properties: token_mint: type: string description: The mint address of the token. @@ -278,11 +291,6 @@ components: format: float description: ATA Sol balance example: 0.00203928 - fee: - type: number - format: float - description: Fees to be applied based on burnable accounts - example: 0.05 Quote: type: object diff --git a/bot/todo.yaml b/bot/todo.yaml index 7d5fbb8..617df91 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -1,18 +1,15 @@ # TODO -- AssociatedTokenAccount - return if it's blacklisted +√ associated_token_accounts/burn_and_close/instructions + √ Si el fee tiene un vallor raro, entonces será 5% por defecto -- associated_token_accounts/burn_and_close/instructions - - Si el fee tiene un vallor raro, entonces será 5% por defecto - -- Instructions: - - have up to 10 burn and close accounts at the same time +√ Instructions: + √ have up to 10 burn and close accounts at the same time - FRONT BEHAVIOUR: - Front will handle the Black list - Front will disable blacklisted ATAs and prevent them from being claimed. - Front will do 10 ATAs per transaction based on fix page size. - + - FRONT: - Not connected: - Info section: From b4f05a62b5c3c722151d4bc891c9768b1d28e55a Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 14 Jan 2025 22:43:40 +0100 Subject: [PATCH 067/103] Adding TooManyInstructionsException if submitted instructions exceed the BACKEND_MAX_INSTRUCTIONS of 10. Updating OpenAPI specs --- bot/app/api/config/__init__.py | 1 + bot/app/api/handlers/exceptions.py | 7 ++++++- bot/app/api/routes/outer/request_instructions.py | 9 ++++++++- bot/app/openapi/external_endpoints.yaml | 16 +++++++++++++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 155cd71..b0e5718 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -31,6 +31,7 @@ class AuthConfig: } GHOSTFUNDS_FIX_FEES_RECEIVER = "GhoStvfwEx5FYEX7jMEpsu6R13xJFJdTLs4BxEpB9qxQ" GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "Ghost5UYkXcgLdja6Uhyac3gTnuefrx7TuSFat5JUVdW" + BACKEND_MAX_INSTRUCTIONS = 10 RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" diff --git a/bot/app/api/handlers/exceptions.py b/bot/app/api/handlers/exceptions.py index 2023ee6..402b455 100644 --- a/bot/app/api/handlers/exceptions.py +++ b/bot/app/api/handlers/exceptions.py @@ -8,8 +8,13 @@ def __init__(self, detail: str): super().__init__(status_code=404, detail=detail) +class TooManyInstructionsException(HTTPException): + def __init__(self, detail: str): + super().__init__(status_code=400, detail=detail) + + # Exception handler for EntityNotFoundException -def entity_not_found_exception_handler(request: Request, exc: EntityNotFoundException): +def entity_not_found_exception_handler(request: Request, exc: HTTPException): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index 5ee57b7..4d04740 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -5,7 +5,7 @@ from solders.pubkey import Pubkey from api.config import appconfig -from api.handlers.exceptions import EntityNotFoundException +from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException from api.libs.utils import close_burn_ata_instructions from api.models.outer_models import Instructions, RequestTransaction @@ -31,6 +31,9 @@ async def request_close_ata_instructions( ) return instructions + if len(tokens) > appconfig.BACKEND_MAX_INSTRUCTIONS: + raise TooManyInstructionsException(detail="Too many instructions") + tokens = [token.model_dump() for token in tokens] last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] @@ -64,3 +67,7 @@ async def request_close_ata_instructions( raise EntityNotFoundException( detail=e.detail ) + except TooManyInstructionsException as te: + raise TooManyInstructionsException( + detail=te.detail + ) diff --git a/bot/app/openapi/external_endpoints.yaml b/bot/app/openapi/external_endpoints.yaml index c09a2b7..1b37ea7 100644 --- a/bot/app/openapi/external_endpoints.yaml +++ b/bot/app/openapi/external_endpoints.yaml @@ -103,7 +103,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RequestTransaction' + $ref: '#/components/schemas/GetTransaction' responses: '200': description: Burn and close quote successfully retrieved. @@ -132,7 +132,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RequestTransaction' + $ref: '#/components/schemas/GetTransaction' responses: '200': description: INstrucions successfully retrieved. @@ -143,6 +143,16 @@ paths: example: response: "VGhpcyBpcyBhIHNhbXBsZSBxdW90ZSBmb3IgYnVybiBhbmQgY2xvc2U=" + '400': + description: "Too many instructions." + content: + application/json: + schema: + type: object + properties: + detail: + type: string + example: "Too many instructions." '404': description: Account not found. content: @@ -256,7 +266,7 @@ components: type: boolean description: Whether there is insufficient data for this token. - RequestTransaction: + GetTransaction: type: object properties: owner: From e3110702995a2cef04698eea35804a98c35dd784 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 19 Jan 2025 01:06:57 +0100 Subject: [PATCH 068/103] increasing fees and adding MIN_TOKEN_VALUE = 0.0001 to config file --- bot/app/api/config/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index b0e5718..6fe9b0b 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -24,15 +24,17 @@ class AuthConfig: FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS GHOSTFUNDS_FIX_FEES = 0.00001 GHOSTFUNDS_FEES_PERCENTAGES = { - 1: 0.05, - 100: 0.048, - 500: 0.045, - 1000: 0.04 + 1: 0.1, + 100: 0.09, + 500: 0.08, + 1000: 0.07 } GHOSTFUNDS_FIX_FEES_RECEIVER = "GhoStvfwEx5FYEX7jMEpsu6R13xJFJdTLs4BxEpB9qxQ" GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "Ghost5UYkXcgLdja6Uhyac3gTnuefrx7TuSFat5JUVdW" BACKEND_MAX_INSTRUCTIONS = 10 + MIN_TOKEN_VALUE = 0.0001 # Min value of a token to not be considered dust + RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" # noqa: E501 From 3bf321c168156331e486ffa9043d3c5e73f377d9 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 20 Jan 2025 11:28:00 +0100 Subject: [PATCH 069/103] APP: adding middleware, controlling max retrievable accounts, --- bot/app/AWS/steps.md | 2 +- bot/app/api/config/__init__.py | 2 ++ bot/app/api/libs/utils.py | 20 ++++++++++-- bot/app/api/models/outer_models.py | 3 ++ .../outer/count_associated_token_accounts.py | 3 +- bot/app/main.py | 11 +++++++ bot/todo.yaml | 31 +++++++++++++++---- 7 files changed, 61 insertions(+), 11 deletions(-) diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index cdb01e9..d8f8328 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -44,7 +44,7 @@ docker stop $CONTAINER_ID ### Remove previous images docker images --format '{{.Repository}}:{{.Tag}} {{.ID}}' | grep -v ':latest' | awk '{print $2}' | xargs docker rmi -f # Run the docker container -docker run --memory=512m --memory-swap=512m -d -p 443:8000 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-ata-service:latest +sudo service start-docker-container.sh start ### ################################################## # Start Docker when EC2 instance starts diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 6fe9b0b..99e46fe 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -34,6 +34,8 @@ class AuthConfig: BACKEND_MAX_INSTRUCTIONS = 10 MIN_TOKEN_VALUE = 0.0001 # Min value of a token to not be considered dust + MAX_RETRIEVABLE_ACCOUNTS = 1100 # Safety. To avoid api server to crash + MAX_RETRIEVABLE_ACCOUNTS_MESSAGE = "TOO_MANY_ATAS" RETRIES = 5 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 1834f24..878b236 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -225,9 +225,10 @@ async def count_associated_token_accounts( "accounts_for_manual_review": 0, "rent_balance": 0, "rent_balance_usd": 0, - "fee": 0 + "fee": 0, + "msg": None } - min_token_value = 1 + min_token_value = appconfig.MIN_TOKEN_VALUE account_samples = 5 # TODO: implement token's black list (like USDC, etc) @@ -250,6 +251,12 @@ async def count_associated_token_accounts( if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist ] + # Safety trimming: big accounts can crash the api server + total["total_accounts"] = len(accounts) + if len(accounts) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: + accounts = accounts[0: appconfig.MAX_RETRIEVABLE_ACCOUNTS] + total["msg"] = appconfig.MAX_RETRIEVABLE_ACCOUNTS_MESSAGE + if accounts: usd_sol_value = get_solana_price() if usd_sol_value == 0: @@ -305,7 +312,7 @@ async def detect_dust_token_accounts( :param wallet_pubkey: The public address of the Solana wallet. :return: Token balance as a float. """ - min_token_value = 1 + min_token_value = appconfig.MIN_TOKEN_VALUE account_samples = 5 working_balance = 0 # Refactor this: remove this line of code and test as client is not being used @@ -322,6 +329,13 @@ async def detect_dust_token_accounts( if not accounts: return [], page, total_items + if len(accounts) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: + print("Warning: Big account detected {} holding {} atas".format( + str(wallet_pubkey), + len(accounts) + )) + accounts = accounts[0: appconfig.MAX_RETRIEVABLE_ACCOUNTS] + # Fetch the current Solana price sol_price = get_solana_price() if sol_price == 0: diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index ce7f3a2..adbbb4b 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -34,6 +34,9 @@ class CountAssociatedTokenAccounts(BaseModel): fee: float = Field( 0, description='GhostFunds fee.' ) + msg: Optional[str] = Field( + None, description='Message from backend server' + ) class AssociatedTokenAccount(BaseModel): diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 086a46b..7201b78 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -29,6 +29,7 @@ async def associated_token_accounts_count( accounts_for_manual_review=data["accounts_for_manual_review"], rent_balance=data["rent_balance"], rent_balance_usd=data["rent_balance_usd"], - fee=data["fee"] + fee=data["fee"], + msg=data["msg"], ) return response diff --git a/bot/app/main.py b/bot/app/main.py index 274b305..236649e 100644 --- a/bot/app/main.py +++ b/bot/app/main.py @@ -1,10 +1,13 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from mangum import Mangum + from api.handlers.exceptions import entity_not_found_exception_handler, EntityNotFoundException from api.routes.outer.associated_token_accounts import router as associated_token_accounts_route from api.routes.outer.count_associated_token_accounts import router as count_associated_token_accounts_route from api.routes.outer.request_transaction import router as request_ata_transaction_route from api.routes.outer.request_instructions import router as request_ata_instructions_route + # Create the FastAPI app app = FastAPI( debug=False, @@ -12,6 +15,14 @@ version="0.1" ) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods (GET, POST, etc.) + allow_headers=["*"], # Allows all headers +) + # Including routes app.include_router(router=associated_token_accounts_route, prefix="/api", tags=["Associated Token Accounts"]) app.include_router( diff --git a/bot/todo.yaml b/bot/todo.yaml index 617df91..5f1a912 100644 --- a/bot/todo.yaml +++ b/bot/todo.yaml @@ -1,6 +1,25 @@ # TODO -√ associated_token_accounts/burn_and_close/instructions - √ Si el fee tiene un vallor raro, entonces será 5% por defecto +- Backend: + √ Test that fetching dust accounts includes a max + √ test this in local docker and upload changes to AWS +- Fontend: + - Deal with none 200 responses like 503 Service Unavailable + - Solve spinner bug: when fetching takes too long the spinner keps going after page refreshes + - Create main page template with + - Upper menu + - Ghost icon left 20% width + - Menu 60% right ident + - About us + - GhostFunds app + - Connect Wallet top right 20% + + - right sidebar 20% + - Widget info section of 20% height + - Transaction list of 80% height + - Middle section of 80% + - Information section 20% of height + - Pagination section 80% of height + √ Instructions: √ have up to 10 burn and close accounts at the same time @@ -18,7 +37,7 @@ - Pagination section: - PLace holder message saying: Not counts available to shown. - Widget: - - Claim History + - Claimed History - Show Ghost icon - Place holder message: No history available - When connected: @@ -38,14 +57,14 @@ - Footer: - Fix 10 page pagination - Widget: - - Claim History. + - Claimed History. - Total claimed in Sols in big - Total claimed in usd - - Accounts closed (check a better term for this) + - Managed Accounts (check a better term for this) - List of actions (no pagination) - Columns: - Abreviated Signature -> transaction link to solscan - - Accounts -> transaction link to solscan + - Accounts invoilved -> transaction link to solscan From 1904b6c3ba06e47a4808a878b40666dd54280c13 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 29 Jan 2025 19:02:02 +0100 Subject: [PATCH 070/103] Adding endpoints rotation when getting solana price in USD. Adding file handling when asking for token metadata to helius --- bot/app/api/config/__init__.py | 10 ++++++++++ bot/app/api/libs/utils.py | 20 +++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 99e46fe..331ed1c 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -41,6 +41,16 @@ class AuthConfig: RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" # noqa: E501 RPC_JITO_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" + SOL_USD_QUOTE = [ + { + "url": "https://quote-api.jup.ag/v6/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=1000000000&slippageBps=1", + "vendor": "jupiter", + }, + { + "url": "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd", + "vendor": "coingecko", + } + ] # PAGINATION DEFAULT_PAGE = 1 diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 878b236..215835e 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -66,19 +66,28 @@ def get_solana_price() -> float: retries = appconfig.RETRIES price = 0 counter = 0 + current_source = 0 while True: try: if counter >= retries: break - response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd") + source = appconfig.SOL_USD_QUOTE[current_source] + response = requests.get(source["url"]) response.raise_for_status() price_data = response.json() - price = price_data["solana"]["usd"] + price = 0 + match source["vendor"]: + case "coingecko": + price = price_data["solana"]["usd"] + case "jupiter": + price = round(float(price_data["swapUsdValue"]), 2) break except Exception: counter += 1 - print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( + current_source = 0 if current_source >= len(appconfig.SOL_USD_QUOTE) else current_source + 1 + print("get_solana_price Error: Failed to fetch Solana price. Retriying now with {}. Retrying {} of {} times".format( + appconfig.SOL_USD_QUOTE[current_source]["vendor"], counter, retries )) @@ -160,7 +169,9 @@ def get_token_metadata(token_address: str) -> dict: continue content = response.json() - metadata.update(content["result"]["content"]["files"][0]) + file_dict = {'uri': None, 'cdn_uri': None, 'mime': None} + files = content["result"]["content"]["files"] + metadata.update(files[0] if files else file_dict) metadata.update(content["result"]["content"]["metadata"]) metadata["authority"] = content["result"]["authorities"][0]["address"] metadata["supply"] = content["result"]["token_info"]["supply"] @@ -286,7 +297,6 @@ async def count_associated_token_accounts( token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] token_value = 1 - total["total_accounts"] += 1 if token_ammount * token_value < min_token_value: total["burnable_accounts"] += 1 else: From b0c4db91b3a8da2c7cdfadc1bc3208e675a404fa Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 5 Feb 2025 22:57:39 +0100 Subject: [PATCH 071/103] abli.libs.utils.count_associated_token_accounts: unecessary print was removed. close_ata_transaction implementation for testing. Endpoint request_close_ata_transaction now receives a list of tokens with a fee. --- bot/app/api/libs/utils.py | 273 +++++++++++------- .../api/routes/outer/request_transaction.py | 30 +- 2 files changed, 191 insertions(+), 112 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 215835e..47dd264 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -282,7 +282,7 @@ async def count_associated_token_accounts( account_sample_list = [my_list[pos] for pos in random_positions] sum_balance = 0 - for index in account_sample_list: + for index in range(len(account_sample_list)): associated_tokan_account = accounts[index]["pubkey"] sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) @@ -293,14 +293,14 @@ async def count_associated_token_accounts( accounts_for_manual_review = 0 for account in accounts: - mint = account["account"]["data"]["parsed"]["info"]["mint"] + # mint = account["account"]["data"]["parsed"]["info"]["mint"] token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] token_value = 1 if token_ammount * token_value < min_token_value: total["burnable_accounts"] += 1 - else: - print("token {} has significan value".format(mint)) + # else: + # print("token {} has significan value".format(mint)) total["accounts_for_manual_review"] = accounts_for_manual_review @@ -560,8 +560,8 @@ def get_associated_token_address(owner: Pubkey, mint: Pubkey) -> Pubkey: async def close_ata_transaction( owner: Pubkey, - token_mint: Pubkey, - decimals: int, + tokens: list[dict], + fee: float, encode_base64: bool = True ) -> str: """ @@ -574,71 +574,110 @@ async def close_ata_transaction( txn = None async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: - associated_token_account = get_associated_token_address( - owner=owner, - mint=token_mint - ) - response = await client.get_account_info(associated_token_account) - account_info = response.value - if not account_info: - print("Associated token account {} does not exist.".format( - associated_token_account - )) - raise EntityNotFoundException( - detail="Associated token account {} nor found.".format( - str(associated_token_account) - ) + # Will set both burn and close intructions for every ATA + burn_close_instructions = [] + for token in tokens: + associated_token_account = get_associated_token_address( + owner=owner, + mint=Pubkey.from_string(token.token_mint) ) + response = await client.get_account_info(associated_token_account) + account_info = response.value + if not account_info: + print("Associated token account {} does not exist.".format( + associated_token_account + )) + raise EntityNotFoundException( + detail="Associated token account {} nor found.".format( + str(associated_token_account) + ) + ) - # BURN - data = account_info.data - if len(data) != 165: - print("close_ata_transaction-> Error: Invalid data length for ATA {} for token {}".format( - str(associated_token_account), - str(token_mint) - )) - ( - mint, - owner_data, - amount_lamports, - delegate_option, - delegate, - state, - is_native_option, - is_native, - delegated_amount, - close_authority_option, - close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) - - amount = amount_lamports + # BURN + data = account_info.data + if len(data) != 165: + print("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( + str(associated_token_account), + token.token_mint + )) + ( + mint, + owner_data, + amount_lamports, + delegate_option, + delegate, + state, + is_native_option, + is_native, + delegated_amount, + close_authority_option, + close_authority + ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) - # Construct the burn instruction - params = BurnCheckedParams( - program_id=TOKEN_PROGRAM_ID, - mint=token_mint, - account=associated_token_account, - owner=owner, - amount=amount, - decimals=decimals, - signers=[owner] - ) - burn_ix = burn_checked(params=params) + amount = amount_lamports + mint = token.token_mint + decimals = token.decimals - # CLOSE - # Create the close account instruction - close_ix = close_account( - CloseAccountParams( + # Construct the burn instruction + params = BurnCheckedParams( program_id=TOKEN_PROGRAM_ID, + mint=Pubkey.from_string(mint), account=associated_token_account, - dest=owner, - owner=owner + owner=owner, + amount=amount, + decimals=decimals, + signers=[owner] ) + burn_ix = burn_checked(params=params) + # burn_ix_bytes = bytes(burn_ix) + + # CLOSE + # Create the close account instruction + close_ix = close_account( + CloseAccountParams( + program_id=TOKEN_PROGRAM_ID, + account=associated_token_account, + dest=owner, + owner=owner + ) + ) + # close_ix_bytes = bytes(close_ix) + + burn_close_instructions.append(burn_ix) + burn_close_instructions.append(close_ix) + + # Required instructions to set compute unit limit and price + compute_unit_price_ix = set_compute_unit_price(5_000) + # compute_unit_price_ix_bytes = bytes(compute_unit_price_ix) + + compute_units = calculate_compute_units(atas=len(tokens)) + compute_unit_limit_ix = set_compute_unit_limit(units=compute_units) + # compute_unit_limit_ix_bytes = bytes(compute_unit_limit_ix) + + # Variable fee + # Summing all ATAs balances to calculate variable fee + fee_ix_list = get_fee_instructions( + fee=fee, + balance=sum(token.balance for token in tokens), + owner=owner, + atas=len(tokens) ) + # fee_ix_list_bytes = [bytes(fee_ix) for fee_ix in fee_ix_list] - msg = Message( - instructions=[set_compute_unit_price(1_000), burn_ix, close_ix], - payer=owner + # Package all instructions as hex values from bytes into a single list + instructions = [ + compute_unit_price_ix, # Convert to hex for compatibility + compute_unit_limit_ix, + ] + instructions.extend(burn_close_instructions) + instructions.extend(fee_ix_list) + + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash + msg = Message.new_with_blockhash( + instructions=instructions, + payer=owner, + blockhash=recent_blockhash ) tx = Transaction.new_unsigned(message=msg) @@ -646,10 +685,19 @@ async def close_ata_transaction( if encode_base64: txn = base64.b64encode(txn).decode('ascii') + # from solders.message import MessageV0 + # from solders.transaction import VersionedTransaction + + # tx = VersionedTransaction.populate(message=msg, signatures=[]) + # # tx = VersionedTransaction(message=msg, keypairs=[]) + # txn = bytes(tx) + # if encode_base64: + # txn = base64.b64encode(txn).decode('ascii') + except EntityNotFoundException as enfe: raise enfe except Exception as e: - print("request_close_ata_transaction Error: {}".format(e)) + print("request_close_ata_instruction Error: {}".format(e)) return txn @@ -696,7 +744,7 @@ def calculate_compute_units(atas: int): async def close_burn_ata_instructions( owner: Pubkey, tokens: list[dict], - fee: float + fee: float, ) -> list[hex]: """ Burs all tokens from the associated token account @@ -823,64 +871,77 @@ async def close_burn_ata_instructions( return instructions -async def recover_rent_client_from_transaction(): +async def recover_rent_client_from_transaction(go_local: bool = True): from bot.config import appconfig keypair = Keypair.from_base58_string(appconfig.PRIVKEY) body = { "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", - "token_mint": "bpMAcs5cEDu33kbCgTcBu7HtuZwsoNwsMH839jupump", - "decimals": 6 + "fee": 0.045, + "tokens": [ + { + "token_mint": "56B2cJQBdwQvambDvfZZuFp7PPBYMDw71UkgbH1Ppump", + "decimals": 6, + "balance": 0.002039 + } + ], } try: - response = requests.post( - url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", - json=body, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - print("recover_rent_client: Bad status code '{}' recevied".format( - response.status_code - )) - return response + if not go_local: + response = requests.post( + url="http://localhost:443/api/associated_token_accounts/burn_and_close/transaction", + json=body, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("recover_rent_client: Bad status code '{}' recevied".format( + response.status_code + )) + return response - content = response.json() - txn_base64 = content["quote"] + content = response.json() + txn_base64 = content["quote"] + else: + txn_base64 = await close_ata_transaction( + owner=Pubkey.from_string(body["owner"]), + tokens=body["tokens"], + fee=body["fee"] + ) # Decode the Base64 string to bytes txn_bytes = base64.b64decode(txn_base64) - # Recreate the Transaction object from bytes - vst = Transaction.from_bytes(txn_bytes) - - # Get the message from the transaction - msg = vst.message - - instructions = [ - Instruction( - program_id=msg.account_keys[ci.program_id_index], - accounts=[ - AccountMeta(pubkey=msg.account_keys[idx], is_signer=idx == 0, is_writable=True) - for idx in ci.accounts - ], - data=ci.data - ) - for ci in msg.instructions - ] + # # Get the message from the transaction + # msg = vst.message + + # instructions = [ + # Instruction( + # program_id=msg.account_keys[ci.program_id_index], + # accounts=[ + # AccountMeta(pubkey=msg.account_keys[idx], is_signer=idx == 0, is_writable=True) + # for idx in ci.accounts + # ], + # data=ci.data + # ) + # for ci in msg.instructions + # ] # Send the signed transaction (example assumes using a Solana RPC client) async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: blockhash = await client.get_latest_blockhash() recent_blockhash = blockhash.value.blockhash - signed_tx = Transaction.new_signed_with_payer( - instructions=instructions, - payer=keypair.pubkey(), - signing_keypairs=[keypair], - recent_blockhash=recent_blockhash - ) + # Recreate the Transaction object from bytes + signed_tx = Transaction.from_bytes(txn_bytes) + signed_tx.sign(keypairs=[keypair], recent_blockhash=recent_blockhash) + + # signed_tx = Transaction.new_signed_with_payer( + # instructions=instructions, + # payer=keypair.pubkey(), + # signing_keypairs=[keypair], + # recent_blockhash=recent_blockhash + # ) - send_result = None tx_signature = await client.send_transaction( txn=signed_tx, opts=TxOpts(preflight_commitment=Confirmed) @@ -896,10 +957,10 @@ async def recover_rent_client_from_transaction(): await client.confirm_transaction(tx_signature.value, commitment="confirmed") print("Transaction confirmed") - return send_result + return except Exception as e: print("recover_rent_client_from_transaction-> Error: {}".format(e)) - return response + return async def recover_rent_client_from_instructions(go_local: bool = True): @@ -991,4 +1052,4 @@ async def recover_rent_client_from_instructions(go_local: bool = True): # import asyncio -# asyncio.run(recover_rent_client_from_instructions(go_local=False)) +# asyncio.run(recover_rent_client_from_transaction(go_local=False)) diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index d5b4b48..1900d94 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -2,7 +2,8 @@ from solders.pubkey import Pubkey -from api.handlers.exceptions import EntityNotFoundException +from api.config import appconfig +from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException from api.models.outer_models import Quote, RequestTransaction from api.libs.utils import close_ata_transaction router = APIRouter() @@ -19,14 +20,31 @@ async def request_close_ata_transaction( ): try: owner = body.owner - token = body.token_mint - decimals = body.decimals + fee = body.fee + tokens = body.tokens + if not tokens: + instructions = Quote( + quote=None + ) + return instructions + + if len(tokens) > appconfig.BACKEND_MAX_INSTRUCTIONS: + raise TooManyInstructionsException(detail="Too many instructions") + + last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] + if fee not in appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values() or fee < last_fee: + # Assigning unknown or lower fee + print("Warning: unknown fee of {} received from account {}. Adjusting fee to be {}".format( + fee, + owner, + appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] + )) + fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] transaction = await close_ata_transaction( owner=Pubkey.from_string(owner), - token_mint=Pubkey.from_string(token), - decimals=decimals, - encode_base64=True + tokens=tokens, + fee=fee ) quote = Quote( From 2ca9fb91cda26b20ddbd244200b4dd887bc889fa Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 5 Feb 2025 23:31:36 +0100 Subject: [PATCH 072/103] refixing bug when calculation the average_balance when doing aproximation --- bot/app/api/libs/utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 47dd264..d3c8978 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -273,7 +273,7 @@ async def count_associated_token_accounts( if usd_sol_value == 0: return total # Calculating accounts balance as AN APROXIMATION to speed up this process - account_sample_list = accounts + account_sample_list = list(range(len(accounts))) if len(accounts) > account_samples: my_list = list(range(len(accounts))) # Generate X random positions @@ -282,7 +282,7 @@ async def count_associated_token_accounts( account_sample_list = [my_list[pos] for pos in random_positions] sum_balance = 0 - for index in range(len(account_sample_list)): + for index in account_sample_list: associated_tokan_account = accounts[index]["pubkey"] sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) @@ -325,6 +325,7 @@ async def detect_dust_token_accounts( min_token_value = appconfig.MIN_TOKEN_VALUE account_samples = 5 working_balance = 0 + account_output = [] # Refactor this: remove this line of code and test as client is not being used try: total_items = 0 @@ -351,8 +352,8 @@ async def detect_dust_token_accounts( if sol_price == 0: return [], page, total_items + account_sample_list = list(range(len(accounts))) if do_balance_aproximation: - account_sample_list = accounts if len(accounts) > account_samples: my_list = list(range(len(accounts))) # Generate X random positions @@ -360,15 +361,13 @@ async def detect_dust_token_accounts( # Get the 5 random values from the list account_sample_list = [my_list[pos] for pos in random_positions] - sum_balance = 0 - for index in account_sample_list: - associated_tokan_account = accounts[index]["pubkey"] - sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) - working_balance = sum_balance / len(account_sample_list) + working_balance = sum_balance / len(account_sample_list) - counter = 0 - account_output = [] # Sort ATAs by mint address (this will help with the pagination) sorted_accounts = sorted( accounts, @@ -389,6 +388,7 @@ async def detect_dust_token_accounts( # Paginate the list sorted_accounts = sorted_accounts[start_index:end_index] + counter = 0 for account in sorted_accounts: counter += 1 From 4eb6eac61d0903908e72604cdd36031df5af1252 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Mon, 17 Feb 2025 15:33:14 +0100 Subject: [PATCH 073/103] AWS Lambda Function arm64 implementation with logging --- bot/app/AWS/steps.md | 13 ++ bot/app/Dockerfile | 27 +-- bot/app/Dockerfile_ec2 | 22 ++ bot/app/api/handlers/exceptions.py | 5 + bot/app/api/libs/utils.py | 196 +++++++++++------- .../outer/count_associated_token_accounts.py | 35 ++-- .../api/routes/outer/request_transaction.py | 3 +- 7 files changed, 196 insertions(+), 105 deletions(-) create mode 100644 bot/app/Dockerfile_ec2 diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index d8f8328..f924d91 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -63,4 +63,17 @@ sudo chkconfig start-docker-container.sh on sudo service start-docker-container.sh start docker ps +## ################ +## Lambda Functions +aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 237733826785.dkr.ecr.eu-west-1.amazonaws.com +##### cd bot/app/AWS/lambda_functions/ +docker build --no-cache --provenance=false --platform linux/arm64/v8 -t ghostfunds-backend-fastapi . +## Docker tag and push image into AWS ECR +docker tag ghostfunds-backend-fastapi:latest 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest +docker push 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest +## Updaing Lambda function docker image +aws lambda update-function-code \ + --function-name ghostfunds-backend-fastapi-docker \ + --image-uri 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest +aws lambda get-function-configuration --function-name ghostfunds-backend-fastapi-docker \ No newline at end of file diff --git a/bot/app/Dockerfile b/bot/app/Dockerfile index e918482..dea31cb 100644 --- a/bot/app/Dockerfile +++ b/bot/app/Dockerfile @@ -1,22 +1,17 @@ -# Use an official Python runtime as a parent image -#FROM python:3.10-slim -FROM python:3.12-slim +FROM public.ecr.aws/lambda/python:3.12-arm64 -# Set the working directory -WORKDIR /app +WORKDIR /var/task +# Install dependencies +RUN dnf update -y && dnf install -y zip gcc gcc-c++ make glibc-devel && dnf clean all -# Copy the current directory contents into the container -COPY . /app +COPY requirements.txt . +COPY ./api ${LAMBDA_TASK_ROOT}/api +COPY main.py ${LAMBDA_TASK_ROOT} -# Install any needed packages specified in requirements.txt +# Install Solders RUN pip install --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -# Make port 443 available to the world outside this container -EXPOSE 443 -# Define environment variable -ENV NAME=World - -# Run app.py when the container launches -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +# Command for Lambda +CMD ["main.handler"] diff --git a/bot/app/Dockerfile_ec2 b/bot/app/Dockerfile_ec2 new file mode 100644 index 0000000..e918482 --- /dev/null +++ b/bot/app/Dockerfile_ec2 @@ -0,0 +1,22 @@ +# Use an official Python runtime as a parent image +#FROM python:3.10-slim +FROM python:3.12-slim + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container +COPY . /app + +# Install any needed packages specified in requirements.txt +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt + +# Make port 443 available to the world outside this container +EXPOSE 443 + +# Define environment variable +ENV NAME=World + +# Run app.py when the container launches +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/bot/app/api/handlers/exceptions.py b/bot/app/api/handlers/exceptions.py index 402b455..ecbbc5e 100644 --- a/bot/app/api/handlers/exceptions.py +++ b/bot/app/api/handlers/exceptions.py @@ -13,6 +13,11 @@ def __init__(self, detail: str): super().__init__(status_code=400, detail=detail) +class ErrorProcessingData(HTTPException): + def __init__(self, detail: str): + super().__init__(status_code=502, detail=detail) + + # Exception handler for EntityNotFoundException def entity_not_found_exception_handler(request: Request, exc: HTTPException): return JSONResponse( diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index d3c8978..ab4688b 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -1,11 +1,14 @@ import base64 import json +import logging import math import random import requests import struct import time +from api.handlers.exceptions import ErrorProcessingData + from datetime import datetime from solana.rpc.async_api import AsyncClient from solana.rpc.types import TxOpts @@ -52,7 +55,7 @@ async def get_solana_balance(public_key: Pubkey) -> float: sol_balance = lamports / 1e9 return sol_balance except Exception as e: - print(f"Error fetching balance: {e.error_msg}") + logging.error(f"get_solana_balance: Error fetching balance: {e.error_msg}") return 0.0 @@ -85,8 +88,8 @@ def get_solana_price() -> float: break except Exception: counter += 1 - current_source = 0 if current_source >= len(appconfig.SOL_USD_QUOTE) else current_source + 1 - print("get_solana_price Error: Failed to fetch Solana price. Retriying now with {}. Retrying {} of {} times".format( + current_source = 0 if current_source >= len(appconfig.SOL_USD_QUOTE) - 1 else current_source + 1 + logging.warning("get_solana_price Error: Failed to fetch Solana price. Retriying now with {}. Retrying {} of {} times".format( appconfig.SOL_USD_QUOTE[current_source]["vendor"], counter, retries @@ -96,7 +99,8 @@ def get_solana_price() -> float: return price -async def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: +def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: + print_trace = True value = [] data = { "jsonrpc": "2.0", @@ -113,13 +117,17 @@ async def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: ] } try: + if print_trace: + logging.info("Requesting getTokenAccountsByOwner for account {}".format(wallet_address)) + response = requests.post( url=appconfig.RPC_URL_HELIUS, json=data, headers={"Content-Type": "application/json"} ) + if response.status_code != 200: - print("get_token_accounts_by_owner: Bad status code '{}' received for token {}".format( + logging.error("get_token_accounts_by_owner: Bad status code '{}' received for wallet {}".format( response.status_code, wallet_address )) @@ -128,10 +136,20 @@ async def get_token_accounts_by_owner(wallet_address=str) -> list[dict]: content = response.json() value = content["result"]["value"] + if len(value) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: + logging.warning("get_token_accounts_by_owner. Warning: Big account detected {} holding {} atas".format( + str(wallet_address), + len(value) + )) + value = value[0: appconfig.MAX_RETRIEVABLE_ACCOUNTS] + + if print_trace: + logging.info("getTokenAccountsByOwner: all good, retrieving {} accounts".format(len(value))) + return value except Exception as e: - print("get_token_accounts_by_owner-> Error: {}".format(e)) - return value + logging.error("get_token_accounts_by_owner-> Error: {}".format(e)) + raise ErrorProcessingData(detail=str(e)) def get_token_metadata(token_address: str) -> dict: @@ -159,7 +177,7 @@ def get_token_metadata(token_address: str) -> dict: ) if response.status_code != 200: counter += 1 - print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( + logging.warning("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( response.status_code, token_address, counter, @@ -190,7 +208,7 @@ def get_token_metadata(token_address: str) -> dict: except Exception: counter += 1 - print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( + logging.error("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( token_address, counter, retries @@ -239,75 +257,95 @@ async def count_associated_token_accounts( "fee": 0, "msg": None } - min_token_value = appconfig.MIN_TOKEN_VALUE - account_samples = 5 + try: + min_token_value = appconfig.MIN_TOKEN_VALUE + account_samples = 3 - # TODO: implement token's black list (like USDC, etc) - token_blacklist = [] + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] - original_accounts = [] - counter = 0 - while True: - if counter >= appconfig.RETRIES: - break - original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) - if original_accounts: - break - counter += 1 - - usd_sol_value = 0 - - accounts = [ - account for account in original_accounts - if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist - ] - - # Safety trimming: big accounts can crash the api server - total["total_accounts"] = len(accounts) - if len(accounts) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: - accounts = accounts[0: appconfig.MAX_RETRIEVABLE_ACCOUNTS] - total["msg"] = appconfig.MAX_RETRIEVABLE_ACCOUNTS_MESSAGE - - if accounts: - usd_sol_value = get_solana_price() - if usd_sol_value == 0: - return total - # Calculating accounts balance as AN APROXIMATION to speed up this process - account_sample_list = list(range(len(accounts))) - if len(accounts) > account_samples: - my_list = list(range(len(accounts))) - # Generate X random positions - random_positions = random.sample(range(len(my_list)), account_samples) - # Get the 5 random values from the list - account_sample_list = [my_list[pos] for pos in random_positions] + original_accounts = [] + counter = 0 + while True: + if counter >= appconfig.RETRIES: + break + original_accounts = get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + if original_accounts: + break + counter += 1 - sum_balance = 0 - for index in account_sample_list: - associated_tokan_account = accounts[index]["pubkey"] - sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + usd_sol_value = 0 - average_balance = sum_balance / len(account_sample_list) + logging.info("count_associated_token_accounts: Accounts recovered: {} after {} loops".format( + len(original_accounts), + counter + )) - total["rent_balance"] = average_balance * len(accounts) - total["rent_balance_usd"] = total["rent_balance"] * usd_sol_value + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] - accounts_for_manual_review = 0 - for account in accounts: - # mint = account["account"]["data"]["parsed"]["info"]["mint"] - token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + # Safety trimming: big accounts can crash the api server + total["total_accounts"] = len(accounts) + if len(accounts) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: + accounts = accounts[0: appconfig.MAX_RETRIEVABLE_ACCOUNTS] + total["msg"] = appconfig.MAX_RETRIEVABLE_ACCOUNTS_MESSAGE + logging.info("count_associated_token_accounts: Whale detected: retrieving {} from {} accounts".format( + len(accounts), + total["total_accounts"] + )) - token_value = 1 - if token_ammount * token_value < min_token_value: - total["burnable_accounts"] += 1 - # else: - # print("token {} has significan value".format(mint)) + if accounts: + logging.info("count_associated_token_accounts: retrieving solana price.") + usd_sol_value = get_solana_price() + logging.info("count_associated_token_accounts: solana price is {}.".format(usd_sol_value)) + if usd_sol_value == 0: + return total + # Calculating accounts balance as AN APROXIMATION to speed up this process + account_sample_list = list(range(len(accounts))) + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] - total["accounts_for_manual_review"] = accounts_for_manual_review + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) - if total["burnable_accounts"] > 0: - total["fee"] = get_current_ghostfunds_fees(burnable_accounts=total["burnable_accounts"]) + average_balance = sum_balance / len(account_sample_list) - return total + total["rent_balance"] = average_balance * len(accounts) + total["rent_balance_usd"] = total["rent_balance"] * usd_sol_value + + logging.info("count_associated_token_accounts: Rent balance was calcualted to {}".format( + total["rent_balance"]) + ) + + accounts_for_manual_review = 0 + for account in accounts: + # mint = account["account"]["data"]["parsed"]["info"]["mint"] + token_ammount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + + token_value = 1 + if token_ammount * token_value < min_token_value: + total["burnable_accounts"] += 1 + # else: + # print("token {} has significan value".format(mint)) + + total["accounts_for_manual_review"] = accounts_for_manual_review + + if total["burnable_accounts"] > 0: + total["fee"] = get_current_ghostfunds_fees(burnable_accounts=total["burnable_accounts"]) + + logging.info("count_associated_token_accounts: Total: {}".format(total)) + return total + except Exception as e: + logging.info("count_associated_token_accounts-> Error: {}".format(str(e))) + raise ErrorProcessingData(detail=str(e)) async def detect_dust_token_accounts( @@ -331,7 +369,7 @@ async def detect_dust_token_accounts( total_items = 0 # TODO: implement token's black list (like USDC, etc) token_blacklist = [] - original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + original_accounts = get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) accounts = [ account for account in original_accounts if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist @@ -341,7 +379,7 @@ async def detect_dust_token_accounts( return [], page, total_items if len(accounts) >= appconfig.MAX_RETRIEVABLE_ACCOUNTS: - print("Warning: Big account detected {} holding {} atas".format( + logging.warning("detect_dust_token_accounts. Warning: Big account detected {} holding {} atas".format( str(wallet_pubkey), len(accounts) )) @@ -442,7 +480,7 @@ async def detect_dust_token_accounts( return account_output, page, total_items except Exception as e: - print(f"Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") + logging.error(f"detect_dust_token_accounts. Error fetching associated token accounts for account '{str(wallet_pubkey)}' balance: {e}") # noqa: 501 return account_output, page, total_items @@ -466,7 +504,7 @@ async def burn_and_close_associated_token_account( response = await client.get_account_info(associated_token_account) account_info = response.value if not account_info: - print("Associated token account {} does not exist.".format( + logging.warning("Associated token account {} does not exist.".format( associated_token_account )) raise txn_signature @@ -474,7 +512,7 @@ async def burn_and_close_associated_token_account( # BURN data = account_info.data if len(data) != 165: - print("burn_associated_token_account-> Error: Invalid data length for ATA {} for token {}".format( + logging.error("burn_associated_token_account-> Error: Invalid data length for ATA {} for token {}".format( # noqa: 501 str(associated_token_account), str(token_mint) )) @@ -576,6 +614,8 @@ async def close_ata_transaction( try: # Will set both burn and close intructions for every ATA burn_close_instructions = [] + METRIC = {'ACCOUNTS': len(tokens), 'CLAIMED': sum(token.balance for token in tokens)} + logging.info("close_ata_transaction-> METRIC: {}".format(METRIC)) for token in tokens: associated_token_account = get_associated_token_address( owner=owner, @@ -584,7 +624,7 @@ async def close_ata_transaction( response = await client.get_account_info(associated_token_account) account_info = response.value if not account_info: - print("Associated token account {} does not exist.".format( + logging.error("close_ata_transaction: Associated token account {} does not exist.".format( associated_token_account )) raise EntityNotFoundException( @@ -596,10 +636,13 @@ async def close_ata_transaction( # BURN data = account_info.data if len(data) != 165: - print("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( + logging.error("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( # noqa: 501 str(associated_token_account), token.token_mint )) + raise ErrorProcessingData( + detail="Unprocessed Token" + ) ( mint, owner_data, @@ -697,7 +740,8 @@ async def close_ata_transaction( except EntityNotFoundException as enfe: raise enfe except Exception as e: - print("request_close_ata_instruction Error: {}".format(e)) + logging.error("request_close_ata_instruction Error: {}".format(e)) + raise ErrorProcessingData(detail="Internal Server Error") return txn diff --git a/bot/app/api/routes/outer/count_associated_token_accounts.py b/bot/app/api/routes/outer/count_associated_token_accounts.py index 7201b78..18274c0 100644 --- a/bot/app/api/routes/outer/count_associated_token_accounts.py +++ b/bot/app/api/routes/outer/count_associated_token_accounts.py @@ -1,8 +1,11 @@ from fastapi import APIRouter, Depends +import logging +from api.handlers.exceptions import ErrorProcessingData from api.models.outer_models import AccountAddressType, CountAssociatedTokenAccounts from api.libs.utils import count_associated_token_accounts from solders.pubkey import Pubkey + # Create a router for the associated_token_accounts routes router = APIRouter() @@ -21,15 +24,23 @@ def validate_account_address(account_address: AccountAddressType) -> str: async def associated_token_accounts_count( account_address: str = Depends(validate_account_address) ): - wallet_pubkey = Pubkey.from_string(account_address) - data = await count_associated_token_accounts(wallet_pubkey=wallet_pubkey) - response = CountAssociatedTokenAccounts( - total_accounts=data["total_accounts"], - burnable_accounts=data["burnable_accounts"], - accounts_for_manual_review=data["accounts_for_manual_review"], - rent_balance=data["rent_balance"], - rent_balance_usd=data["rent_balance_usd"], - fee=data["fee"], - msg=data["msg"], - ) - return response + + try: + wallet_pubkey = Pubkey.from_string(account_address) + data = await count_associated_token_accounts(wallet_pubkey=wallet_pubkey) + response = CountAssociatedTokenAccounts( + total_accounts=data["total_accounts"], + burnable_accounts=data["burnable_accounts"], + accounts_for_manual_review=data["accounts_for_manual_review"], + rent_balance=data["rent_balance"], + rent_balance_usd=data["rent_balance_usd"], + fee=data["fee"], + msg=data["msg"], + ) + logging.info("Endpoint associated_token_accounts_count: all good. Retrieving data: {}".format(response)) + return response + except ErrorProcessingData as e: + logging.exception("Endpoint associated_token_accounts_count: {}".format(str(e))) + raise ErrorProcessingData( + detail="Internal Server Error" + ) diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 1900d94..19ff401 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -1,4 +1,5 @@ from fastapi import APIRouter +import logging from solders.pubkey import Pubkey @@ -34,7 +35,7 @@ async def request_close_ata_transaction( last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] if fee not in appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values() or fee < last_fee: # Assigning unknown or lower fee - print("Warning: unknown fee of {} received from account {}. Adjusting fee to be {}".format( + logging.warning("request_close_ata_transaction: unknown fee of {} received from account {}. Adjusting fee to be {}".format( fee, owner, appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] From 49c138e9fa3b1c8ae045a236609085e65e1e269d Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 18 Feb 2025 14:13:07 +0100 Subject: [PATCH 074/103] Adding token_amount_lamports and removing get_account_info for each token to be burned --- .vscode/launch.json | 15 +++- bot/app/api/libs/utils.py | 72 ++++++++++--------- bot/app/api/models/outer_models.py | 6 ++ .../routes/outer/associated_token_accounts.py | 1 + bot/requirements.txt | 4 ++ 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a96b967..cbcae98 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -93,7 +93,20 @@ "env": { "PYTHONPATH": "${workspaceFolder}/bot/app" } - } + }, + { + "name": "Debug PUMP buy", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/libs/pump_buy.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, ] } \ No newline at end of file diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index ab4688b..a3c7f45 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -432,6 +432,7 @@ async def detect_dust_token_accounts( mint = account["account"]["data"]["parsed"]["info"]["mint"] owner = account["account"]["data"]["parsed"]["info"]["owner"] + amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["amount"] token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] associated_token_account = account["pubkey"] @@ -457,6 +458,7 @@ async def detect_dust_token_accounts( "token_mint": mint, "associated_token_account": associated_token_account, "owner": owner, + "token_amount_lamports": int(amount), "token_amount": token_amount, "token_price": token_price, "token_value": token_value, @@ -621,43 +623,43 @@ async def close_ata_transaction( owner=owner, mint=Pubkey.from_string(token.token_mint) ) - response = await client.get_account_info(associated_token_account) - account_info = response.value - if not account_info: - logging.error("close_ata_transaction: Associated token account {} does not exist.".format( - associated_token_account - )) - raise EntityNotFoundException( - detail="Associated token account {} nor found.".format( - str(associated_token_account) - ) - ) + # response = await client.get_account_info(associated_token_account) + # account_info = response.value + # if not account_info: + # logging.error("close_ata_transaction: Associated token account {} does not exist.".format( + # associated_token_account + # )) + # raise EntityNotFoundException( + # detail="Associated token account {} nor found.".format( + # str(associated_token_account) + # ) + # ) # BURN - data = account_info.data - if len(data) != 165: - logging.error("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( # noqa: 501 - str(associated_token_account), - token.token_mint - )) - raise ErrorProcessingData( - detail="Unprocessed Token" - ) - ( - mint, - owner_data, - amount_lamports, - delegate_option, - delegate, - state, - is_native_option, - is_native, - delegated_amount, - close_authority_option, - close_authority - ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) - - amount = amount_lamports + # data = account_info.data + # if len(data) != 165: + # logging.error("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( # noqa: 501 + # str(associated_token_account), + # token.token_mint + # )) + # raise ErrorProcessingData( + # detail="Unprocessed Token" + # ) + # ( + # mint, + # owner_data, + # amount_lamports, + # delegate_option, + # delegate, + # state, + # is_native_option, + # is_native, + # delegated_amount, + # close_authority_option, + # close_authority + # ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) + + amount = token.token_amount_lamports mint = token.token_mint decimals = token.decimals diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index adbbb4b..30cf8c4 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -49,6 +49,9 @@ class AssociatedTokenAccount(BaseModel): owner: Optional[str] = Field( None, description='The owner of the associated token account.' ) + token_amount_lamports: Optional[int] = Field( + None, description='The amount of tokens held in lamports.' + ) token_amount: Optional[float] = Field( None, description='The amount of tokens held.' ) @@ -126,6 +129,9 @@ class RequestTransactionToken(BaseModel): balance: float = Field( ..., description='ATA SOls balance' ) + token_amount_lamports: int = Field( + ..., description='Token amount in lamports' + ) class RequestTransaction(BaseModel): diff --git a/bot/app/api/routes/outer/associated_token_accounts.py b/bot/app/api/routes/outer/associated_token_accounts.py index 8d96228..911442a 100644 --- a/bot/app/api/routes/outer/associated_token_accounts.py +++ b/bot/app/api/routes/outer/associated_token_accounts.py @@ -57,6 +57,7 @@ async def fetch_associated_token_accounts( token_mint=ata["token_mint"], associated_token_account=ata["associated_token_account"], owner=ata["owner"], + token_amount_lamports=ata["token_amount_lamports"], token_amount=ata["token_amount"], token_price=ata["token_price"], token_value=ata["token_value"], diff --git a/bot/requirements.txt b/bot/requirements.txt index adcaf86..f1bb2cc 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -15,3 +15,7 @@ nest_asyncio==1.6.0 base58==2.1.1 jito_py_rpc==0.1.0 flake8 +base58 +websockets +fastapi +mangum From 3f014b050a6d4fb4659bf0217f1153a3c2ffede5 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 18 Feb 2025 20:23:40 +0100 Subject: [PATCH 075/103] request_close_ata_transaction: adding ErrorProcessingData exception handling --- bot/app/api/routes/outer/request_transaction.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 19ff401..195cd39 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -4,7 +4,7 @@ from solders.pubkey import Pubkey from api.config import appconfig -from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException +from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException, ErrorProcessingData from api.models.outer_models import Quote, RequestTransaction from api.libs.utils import close_ata_transaction router = APIRouter() @@ -56,3 +56,7 @@ async def request_close_ata_transaction( raise EntityNotFoundException( detail=e.detail ) + except ErrorProcessingData as e: + raise ErrorProcessingData( + detail=e.detail + ) From b6d63079a293d0cca39921fdc5ca29c78221d3db Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 22 Feb 2025 18:26:48 +0100 Subject: [PATCH 076/103] Adding new "close to the metal" buy and sell and token scanner --- bot/config/__init__.py | 21 +- bot/idl/pump_fun_idl.json | 662 +++++++++++++++++++++++++++++++++++ bot/libs/pump_buy.py | 716 ++++++++++++++++++++++++++++++++++++++ bot/libs/utils.py | 185 ++++++++-- 4 files changed, 1545 insertions(+), 39 deletions(-) create mode 100644 bot/idl/pump_fun_idl.json create mode 100644 bot/libs/pump_buy.py diff --git a/bot/config/__init__.py b/bot/config/__init__.py index e82ac63..cf2678e 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -1,5 +1,6 @@ import os from enum import Enum +from solders.pubkey import Pubkey class AppMode(Enum): @@ -35,12 +36,13 @@ class AuthConfig: ) # Minimum balance a token must have for being traded RETRYING_SECONDS = 1 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + WSS_URL_QUICKNODE = "wss://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" RPC_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 - TRADING_DEFAULT_AMOUNT = float(os.environ.get("TRADING_DEFAULT_AMOUNT", 0.05)) + TRADING_DEFAULT_AMOUNT = float(os.environ.get("TRADING_DEFAULT_AMOUNT", 0.01)) TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT = float( os.environ.get( "TRADING_CRITERIA_TRADE_RELEVANT_AMOUNT", @@ -55,7 +57,7 @@ class AuthConfig: ) TRADING_TOKENS_AT_THE_SAME_TIME = 1 TRADING_EXPECTED_GAIN_IN_PERCENTAGE = 0.5 - TRADING_RETRIES = 3 + TRADING_RETRIES = 6 TRADING_TOKEN_TOO_OLD_SECONDS = 5 TRADING_MARKETING_INACTIVITY_TIMEOUT = 60 SCANNER_MIN_TRADING_AMOUNT = 1.00 # Min Sols a token must have as first buy to be considered for trading @@ -66,6 +68,21 @@ class AuthConfig: # DB REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") + # BUY and SELL + BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying + SELL_SLIPPAGE = 0.2 + LAMPORTS_PER_SOL = 1_000_000_000 + PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf") + PUMP_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1") + PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM") + SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") + SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") + SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111") + SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") + TOKEN_DECIMALS = 6 + TRADING_TIME = 35 appconfig: AuthConfig = AuthConfig() diff --git a/bot/idl/pump_fun_idl.json b/bot/idl/pump_fun_idl.json new file mode 100644 index 0000000..a3dd67e --- /dev/null +++ b/bot/idl/pump_fun_idl.json @@ -0,0 +1,662 @@ +{ + "version": "0.1.0", + "name": "pump", + "instructions": [ + { + "name": "initialize", + "docs": [ + "Creates the global state." + ], + "accounts": [ + { + "name": "global", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setParams", + "docs": [ + "Sets the global state parameters." + ], + "accounts": [ + { + "name": "global", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "feeRecipient", + "type": "publicKey" + }, + { + "name": "initialVirtualTokenReserves", + "type": "u64" + }, + { + "name": "initialVirtualSolReserves", + "type": "u64" + }, + { + "name": "initialRealTokenReserves", + "type": "u64" + }, + { + "name": "tokenTotalSupply", + "type": "u64" + }, + { + "name": "feeBasisPoints", + "type": "u64" + } + ] + }, + { + "name": "create", + "docs": [ + "Creates a new coin and bonding curve." + ], + "accounts": [ + { + "name": "mint", + "isMut": true, + "isSigner": true + }, + { + "name": "mintAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "bondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedBondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "global", + "isMut": false, + "isSigner": false + }, + { + "name": "mplTokenMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "metadata", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + } + ] + }, + { + "name": "buy", + "docs": [ + "Buys tokens from a bonding curve." + ], + "accounts": [ + { + "name": "global", + "isMut": false, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "bondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedBondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedUser", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "maxSolCost", + "type": "u64" + } + ] + }, + { + "name": "sell", + "docs": [ + "Sells tokens into a bonding curve." + ], + "accounts": [ + { + "name": "global", + "isMut": false, + "isSigner": false + }, + { + "name": "feeRecipient", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "bondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedBondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedUser", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "minSolOutput", + "type": "u64" + } + ] + }, + { + "name": "withdraw", + "docs": [ + "Allows the admin to withdraw liquidity for a migration once the bonding curve completes" + ], + "accounts": [ + { + "name": "global", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "bondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedBondingCurve", + "isMut": true, + "isSigner": false + }, + { + "name": "associatedUser", + "isMut": true, + "isSigner": false + }, + { + "name": "user", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "Global", + "type": { + "kind": "struct", + "fields": [ + { + "name": "initialized", + "type": "bool" + }, + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "feeRecipient", + "type": "publicKey" + }, + { + "name": "initialVirtualTokenReserves", + "type": "u64" + }, + { + "name": "initialVirtualSolReserves", + "type": "u64" + }, + { + "name": "initialRealTokenReserves", + "type": "u64" + }, + { + "name": "tokenTotalSupply", + "type": "u64" + }, + { + "name": "feeBasisPoints", + "type": "u64" + } + ] + } + }, + { + "name": "BondingCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "virtualTokenReserves", + "type": "u64" + }, + { + "name": "virtualSolReserves", + "type": "u64" + }, + { + "name": "realTokenReserves", + "type": "u64" + }, + { + "name": "realSolReserves", + "type": "u64" + }, + { + "name": "tokenTotalSupply", + "type": "u64" + }, + { + "name": "complete", + "type": "bool" + } + ] + } + } + ], + "events": [ + { + "name": "CreateEvent", + "fields": [ + { + "name": "name", + "type": "string", + "index": false + }, + { + "name": "symbol", + "type": "string", + "index": false + }, + { + "name": "uri", + "type": "string", + "index": false + }, + { + "name": "mint", + "type": "publicKey", + "index": false + }, + { + "name": "bondingCurve", + "type": "publicKey", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "TradeEvent", + "fields": [ + { + "name": "mint", + "type": "publicKey", + "index": false + }, + { + "name": "solAmount", + "type": "u64", + "index": false + }, + { + "name": "tokenAmount", + "type": "u64", + "index": false + }, + { + "name": "isBuy", + "type": "bool", + "index": false + }, + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "timestamp", + "type": "i64", + "index": false + }, + { + "name": "virtualSolReserves", + "type": "u64", + "index": false + }, + { + "name": "virtualTokenReserves", + "type": "u64", + "index": false + } + ] + }, + { + "name": "CompleteEvent", + "fields": [ + { + "name": "user", + "type": "publicKey", + "index": false + }, + { + "name": "mint", + "type": "publicKey", + "index": false + }, + { + "name": "bondingCurve", + "type": "publicKey", + "index": false + }, + { + "name": "timestamp", + "type": "i64", + "index": false + } + ] + }, + { + "name": "SetParamsEvent", + "fields": [ + { + "name": "feeRecipient", + "type": "publicKey", + "index": false + }, + { + "name": "initialVirtualTokenReserves", + "type": "u64", + "index": false + }, + { + "name": "initialVirtualSolReserves", + "type": "u64", + "index": false + }, + { + "name": "initialRealTokenReserves", + "type": "u64", + "index": false + }, + { + "name": "tokenTotalSupply", + "type": "u64", + "index": false + }, + { + "name": "feeBasisPoints", + "type": "u64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "NotAuthorized", + "msg": "The given account is not authorized to execute this instruction." + }, + { + "code": 6001, + "name": "AlreadyInitialized", + "msg": "The program is already initialized." + }, + { + "code": 6002, + "name": "TooMuchSolRequired", + "msg": "slippage: Too much SOL required to buy the given amount of tokens." + }, + { + "code": 6003, + "name": "TooLittleSolReceived", + "msg": "slippage: Too little SOL received to sell the given amount of tokens." + }, + { + "code": 6004, + "name": "MintDoesNotMatchBondingCurve", + "msg": "The mint does not match the bonding curve." + }, + { + "code": 6005, + "name": "BondingCurveComplete", + "msg": "The bonding curve has completed and liquidity migrated to raydium." + }, + { + "code": 6006, + "name": "BondingCurveNotComplete", + "msg": "The bonding curve has not completed." + }, + { + "code": 6007, + "name": "NotInitialized", + "msg": "The program is not initialized." + } + ], + "metadata": { + "address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + } +} \ No newline at end of file diff --git a/bot/libs/pump_buy.py b/bot/libs/pump_buy.py new file mode 100644 index 0000000..b8aef99 --- /dev/null +++ b/bot/libs/pump_buy.py @@ -0,0 +1,716 @@ +import asyncio +import base58 +import base64 +import json +import struct +import time +import websockets + +from construct import Struct, Int64ul, Flag +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solders.compute_budget import ( + set_compute_unit_limit, + set_compute_unit_price +) +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import Message +from solders.pubkey import Pubkey +from solders.transaction import Transaction, VersionedTransaction +from spl.token.instructions import get_associated_token_address +import spl.token.instructions as spl_token + +from bot.config import appconfig +from bot.libs.utils import get_account_information, get_token_accounts_by_owner + + +EXPECTED_DISCRIMINATOR = struct.pack(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + + +def get_pump_curve_state(curve_address: Pubkey) -> BondingCurveState: + response = get_account_information(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + sols = (curve_state.virtual_sol_reserves / appconfig.LAMPORTS_PER_SOL) + tokens = (curve_state.virtual_token_reserves / 10 ** appconfig.TOKEN_DECIMALS) + return sols / tokens + + +def calculate_pump_curve_price_local(token_data: dict) -> float: + buyers = token_data.get("buyers") + sols_traded = sum(buyer.get("sol_traded") for buyer in buyers) + tokens_bought = sum(buyer.get("tokens_bought") for buyer in buyers) + + # Initial virtual reserves + initial_sol_reserves = 30 # Initial SOL in the virtual pool + initial_token_reserves = 1_073_000_191 # Initial tokens in the virtual pool + token_price = initial_token_reserves - 32_290_005_730 / (initial_sol_reserves + sols_traded) + # ####### + + # Current reserves after trades + current_sol_reserves = initial_sol_reserves + sols_traded + current_token_reserves = initial_token_reserves - tokens_bought + + # Calculate the token price per token + token_price_per_token = current_sol_reserves / current_token_reserves + + # Calculate the price per 10 million tokens + token_price = token_price_per_token + + return token_price + + +def calculate_compute_units(): + # Units consumed by each instruction + create_ata_and_swap = 80000 + # Add a buffer for safety + buffer_units = 1000 + return create_ata_and_swap + buffer_units + + +async def buy_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + amount: float, + slippage: float = 0.01, + token_price_sol_local: float = 0 +): + """This code assumes that no ATA exist when buying a Pump.fun token + + Args: + mint (Pubkey): _description_ + bonding_curve (Pubkey): _description_ + associated_bonding_curve (Pubkey): _description_ + amount (float): _description_ + slippage (float, optional): _description_. Defaults to 0.01. + max_retries (int, optional): _description_. Defaults to 5. + """ + private_key = base58.b58decode(appconfig.PRIVKEY) + payer = Keypair.from_bytes(private_key) + + print("Buy-> mint is {}".format(str(mint))) + + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + associated_token_account = get_associated_token_address( + owner=payer.pubkey(), + mint=mint + ) + amount_lamports = int(amount * appconfig.LAMPORTS_PER_SOL) + + if token_price_sol_local > 0: + # Manually calculating token price + token_amount = amount / token_price_sol_local + print("** Token amount local: {}".format(token_amount)) + else: + # Fetch the token price + curve_state = get_pump_curve_state(bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + token_amount = amount / token_price_sol + print("** Token amount: {}".format(token_amount)) + + # Calculate maximum SOL to spend with slippage + max_amount_lamports = int(amount_lamports * (1 + slippage)) + + # ### Instructions + compute_unit_price_ix = set_compute_unit_price(20_000) + + compute_units = calculate_compute_units() + compute_unit_limit_ix = set_compute_unit_limit(units=compute_units) + + create_ata_ix = spl_token.create_associated_token_account( + payer=payer.pubkey(), + owner=payer.pubkey(), + mint=mint + ) + BUY_ACCOUNTS = [ + AccountMeta(pubkey=appconfig.PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=appconfig.SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.SYSTEM_RENT, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + discriminator = struct.pack(" get_token_balance return nothing and some retries must be implemented + remote_token_balance = await get_token_balance(client, associated_token_account) + if remote_token_balance > 0: + token_balance = remote_token_balance + else: + token_balance = int(token_balance * 10 ** appconfig.TOKEN_DECIMALS) + + token_balance_decimal = token_balance / 10**appconfig.TOKEN_DECIMALS + + print(f"Sell-> Token balance decimals: {token_balance_decimal}") + if token_balance == 0: + print("No tokens to sell.") + return + + # Fetch the token price + curve_state = get_pump_curve_state(bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + print(f"Sell-> Price per Token: {token_price_sol:.20f} SOL") + + # Calculate minimum SOL output + amount = token_balance + min_sol_output = float(token_balance_decimal) * float(token_price_sol) + slippage_factor = 1 - slippage + min_sol_output = int((min_sol_output * slippage_factor) * appconfig.LAMPORTS_PER_SOL) + + print(f"Sell-> Selling {token_balance_decimal} tokens") + print(f"Sell-> Minimum SOL output: {min_sol_output / appconfig.LAMPORTS_PER_SOL:.10f} SOL") + + for attempt in range(max_retries): + try: + accounts = [ + AccountMeta(pubkey=appconfig.PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=appconfig.SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=appconfig.PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + discriminator = struct.pack(" Transaction sent: https://solscan.io/tx/{tx_sell.value}") + + confirmation = await client.confirm_transaction( + tx_sell.value, + commitment="confirmed", + last_valid_block_height=last_valid_block_height + ) + if confirmation.value: + print("Sell-> Transaction confirmed") + return tx_sell.value + else: + print("Sell-> Transaction fail. Retrying {} of {}".format(attempt + 1, max_retries)) + attempt += 1 + + except Exception as e: + print(f"Sell-> Attempt {attempt + 1} failed: {str(e)}") + if "AccountNotInitialized" in str(e): + print("Sell-> AccountNotInitialized. Nothing to sell...") + break + elif attempt < max_retries - 1: + wait_time = 2 ** attempt + print(f"Retrying in {wait_time} seconds...") + await asyncio.sleep(wait_time) + attempt += 1 + else: + print("Max retries reached. Unable to complete the transaction.") + + return None + + +def load_idl(file_path): + with open(file_path, 'r') as f: + return json.load(f) + + +def decode_create_instruction(ix_data, ix_def, accounts): + args = {} + offset = 8 # Skip 8-byte discriminator + try: + for arg in ix_def['args']: + if arg['type'] == 'string': + length = struct.unpack_from(' Error: {}".format(e)) + + return args + + +def decode_buy_instruction(ix_data, ix_def, accounts): + args = {} + offset = 8 # Skip 8-byte discriminator + + try: + for arg in ix_def['args']: + arg_type = arg['type'] + arg_name = arg['name'] + + if arg_type == 'string': + length = struct.unpack_from(' Error: {}".format(e)) + # args['token_account'] = str(accounts[2]) + + return args + + +async def listen_for_create_transaction(websocket): + idl = load_idl('bot/idl/pump_fun_idl.json') + create_discriminator = 8576854823835016728 + buy_discriminator = 16927863322537952870 + + subscription_message = json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": "blockSubscribe", + "params": [ + {"mentionsAccountOrProgram": str(appconfig.PUMP_PROGRAM)}, + { + "commitment": "confirmed", + "encoding": "base64", + "showRewards": False, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0 + } + ] + }) + await websocket.send(subscription_message) + print(f"Subscribed to blocks mentioning program: {appconfig.PUMP_PROGRAM}") + + ping_interval = 20 + last_ping_time = time.time() + + keep_loop = True + token_data = {} + decoded_args = {} + buyers = [] + threshold = 400_000_000 + tradable_tokens = [] + + while keep_loop: + try: + current_time = time.time() + if current_time - last_ping_time > ping_interval: + await websocket.ping() + last_ping_time = current_time + + response = await asyncio.wait_for(websocket.recv(), timeout=10) + data = json.loads(response) + + if 'method' in data and data['method'] == 'blockNotification': + if 'params' in data and 'result' in data['params']: + block_data = data['params']['result'] + if 'value' in block_data and 'block' in block_data['value']: + block = block_data['value']['block'] + if 'transactions' in block: + for tx in block['transactions']: + if isinstance(tx, dict) and 'transaction' in tx: + tx_data_decoded = base64.b64decode(tx['transaction'][0]) + transaction = VersionedTransaction.from_bytes(tx_data_decoded) + + for ix in transaction.message.instructions: + program_id = str(transaction.message.account_keys[ix.program_id_index]) + if program_id == str(appconfig.PUMP_PROGRAM): + ix_data = bytes(ix.data) + discriminator = struct.unpack(' New token: {} at block: {}".format( + decoded_args["mint"], + decoded_args["block"] + )) + + continue + + # We'll check on BUYs after having a new token created + if decoded_args and discriminator == buy_discriminator: + buy_ix = next( + instr for instr in idl['instructions'] if instr['name'] == 'buy' + ) + try: + account_keys = [ + str(transaction.message.account_keys[index]) for index in ix.accounts + ] + except Exception: + print("Buy Error: Mismatch between ix.accounts(${}) and account_keys".format( + len(ix.accounts)), + len(transaction.message.account_keys) + ) + continue + + decoded_buy_args = decode_buy_instruction( + ix_data, + buy_ix, + account_keys + ) + + if decoded_buy_args.get('mint', '') in token_data: + buyer = decoded_buy_args.get('buyer') + tokens_bought = decoded_buy_args.get('amount') + sol_traded = decoded_buy_args.get('maxSolCost') + + developer = decoded_args.get("developer") + trader = "developer" if developer == buyer else "sniper" + if buyer not in token_data[decoded_buy_args.get('mint')]["seen_buyers"]: + # Add the buyer to the set + token_data[decoded_buy_args.get('mint')]["seen_buyers"].add(buyer) + token_data[decoded_buy_args.get('mint')]["buyers"].append({ + 'buyer': buyer, + 'trader': trader, + 'tokens_bought': tokens_bought / 10**6, + 'sol_traded': (sol_traded / 10**9) / 1.01 # taking out Pump.fun fee + }) + + print("Buyer: {}, Tokens Bought: {}, SOL Traded: {}".format( + buyer, + tokens_bought / 10**6, + (sol_traded / 10**9) / 1.01 + )) + continue + # End of transaction loop + # if token_data: + # for token, data in token_data.items(): + # total_tokens_bought = sum(buyer["tokens_bought"] for buyer in data.get("buyers", [])) + # if total_tokens_bought > threshold: + # print(f"Token {token} exceeds the threshold with {total_tokens_bought} tokens bought.") + # break + + # Ending block loop + if token_data: + if len(list(token_data.keys())) > 1: + print("-> Tokens: {}".format(len(list(token_data.keys())))) + for token, data in token_data.items(): + print("-> Checking on token {}".format(token)) + total_tokens_bought = sum( + buyer["tokens_bought"] for buyer in data.get("buyers", []) + ) + if total_tokens_bought >= threshold: + if (len(data.get("buyers", [])) > 5): + print(" Scam 'BIG' token found. Tokens bought {}".format( + total_tokens_bought + )) + tradable_tokens.append(data) + elif (len(buyers) > 1): + print(" Scam 'MID' token found. Discarting token...") + else: + print(" Whale scam found. Discarting token...") + else: + print("Discarting token...") + if tradable_tokens: + keep_loop = False + break # Breaking transactions loop + decoded_args = {} + token_data = {} + + except asyncio.TimeoutError: + print("No data received for 10 seconds, sending ping...") + await websocket.ping() + last_ping_time = time.time() + except websockets.exceptions.ConnectionClosed: + print("WebSocket connection closed. Reconnecting...") + raise + + return tradable_tokens + + +async def trade(websocket: websockets, trades: int): + + token_data_list = await listen_for_create_transaction(websocket) + token_data = token_data_list[0] # Note: As we're trading only one token at the time + + mint = Pubkey.from_string(token_data['mint']) + bonding_curve = Pubkey.from_string(token_data['bondingCurve']) + associated_bonding_curve = Pubkey.from_string(token_data['associatedBondingCurve']) + + token_price_sol_local = calculate_pump_curve_price_local(token_data=token_data) + print("** Token amount local: {}".format(token_price_sol_local)) + + print("Buying {:.6f} SOL worth of the new token with {:.1f}% slippage tolerance...".format( + appconfig.TRADING_DEFAULT_AMOUNT, + appconfig.BUY_SLIPPAGE * 100 + )) + buy_tx_hash, confirmation_stamp, token_amount = await buy_token( + mint, + bonding_curve, + associated_bonding_curve, + appconfig.TRADING_DEFAULT_AMOUNT, + appconfig.BUY_SLIPPAGE, + token_price_sol_local=token_price_sol_local + ) + if buy_tx_hash: + print("Pump trade: https://pump.fun/coin/{}".format(mint)) + else: + print("** Failed to buy {}. Looking for another new token".format(mint)) + return None + + # Check if we're in market by getting all ATAs and check if we're current ATA exists + # private_key = base58.b58decode(appconfig.PRIVKEY) + # owner = Keypair.from_bytes(private_key) + # associated_token_account = get_associated_token_address( + # owner=owner.pubkey(), + # mint=mint + # ) + # print("Check-> Validating if ATA {} exists".format(str(associated_token_account))) + # async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: + # # Get associated token account + # associated_token_account = get_associated_token_address( + # owner=owner.pubkey(), + # mint=mint + # ) + + # # Check if the associated token account exists + # response = await client.get_account_info(associated_token_account) + # account_info = response.value + # if not account_info: + # print("Associated token account {} does not exist.".format( + # associated_token_account + # )) + # continue + + + # Calculate time delta: Get timestamp from buy block and timestamp from creation block + time_delta = abs(confirmation_stamp - token_data["blockTime"]) + sleep_time = 0 if appconfig.TRADING_TIME - time_delta < 0 else appconfig.TRADING_TIME - time_delta + data = { + "sleep_time": sleep_time, + "mint": mint, + "token_balance": token_amount, + "bonding_curve": bonding_curve, + "associated_bonding_curve": associated_bonding_curve, + "slippage": appconfig.BUY_SLIPPAGE, + "max_retries": appconfig.TRADING_RETRIES + } + return data + + +async def main(): + # import hashlib + # hash_bytes = hashlib.sha256(b'global:buy').digest() + # # Extract the first 8 bytes and unpack them as a little-endian unsigned long long integer + # buy_discriminator = struct.unpack_from(' float: """ Fetches the SOL balance of a given Solana wallet address. @@ -82,11 +89,13 @@ def decode_pump_fun_token(token: str) -> Pubkey: except Exception as e: raise ValueError(f"Failed to decode token: {e}") + def initial_buy_calculator(sol_in_bonding_curve: float): pump_dont_fun_initial_fund = appconfig.SCANNER_PUMPDONTFUN_INITIAL_FUND sols = round(sol_in_bonding_curve - pump_dont_fun_initial_fund, 3) return sols + def flatten_data(data): if isinstance(data, list): return b"".join(flatten_data(item) for item in data) @@ -94,6 +103,7 @@ def flatten_data(data): return data.to_bytes(1, 'little') # Convert integer to a single byte return b"" + def decode_instruction(raw_data): if len(raw_data) >= 5: # Ensure at least enough data for a float # Assume the first byte is the Field ID @@ -107,6 +117,7 @@ def decode_instruction(raw_data): else: return None, None + def get_instructions_from_message(msg: Message): """ This function parse the Message from Quote and gets all intructions @@ -141,6 +152,7 @@ def get_instructions_from_message(msg: Message): return parsed_instructions + def include_instruction(msg: MessageV0): """ Under development @@ -190,6 +202,7 @@ def include_instruction(msg: MessageV0): print("Duplicate instruction detected, skipping.") return msg + def stamp_time(time: datetime, time_stored: dict = None) -> dict: time_stored = {} if time_stored is None else time_stored tstamp = time.strftime("%Y%m%d%H%M%S") @@ -200,6 +213,7 @@ def stamp_time(time: datetime, time_stored: dict = None) -> dict: return time_stored + def get_solana_price() -> float: """ Retrieves the current Solana price in USD using a public API. @@ -230,6 +244,7 @@ def get_solana_price() -> float: return price + def get_token_mint_decimals(mint_address: str) -> int: """ Fetches the decimals value for a given token mint. @@ -255,46 +270,46 @@ def get_token_mint_decimals(mint_address: str) -> int: return decimals -async def get_token_accounts_by_owner(wallet_address=str)->dict: + +async def get_token_accounts_by_owner(wallet_address=str) -> dict: response = {} - async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - data = { - "jsonrpc": "2.0", - "id": "test", - "method": "getTokenAccountsByOwner", - "params": [ - wallet_address, - { - "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "encoding": "jsonParsed" - } - ] - } - try: - response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) - if response.status_code != 200: - print("get_token_accounts_by_owner: Bad status code '{}' recevied for token {}".format( - response.status_code, - wallet_address - ) - ) - return response + data = { + "jsonrpc": "2.0", + "id": "test", + "method": "getTokenAccountsByOwner", + "params": [ + wallet_address, + { + "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "encoding": "jsonParsed" + } + ] + } + try: + response = requests.post( + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) + if response.status_code != 200: + print("get_token_accounts_by_owner: Bad status code '{}' recevied for token {}".format( + response.status_code, + wallet_address + )) + return response - content = response.json() - response = content["result"]["value"] + content = response.json() + response = content["result"]["value"] - return response - except Exception as e: - print("get_token_accounts_by_owner-> Error: {}".format(e)) - return response + return response + except Exception as e: + print("get_token_accounts_by_owner-> Error: {}".format(e)) + return response -def get_token_metadata(token_address: str)->dict: + +def get_token_metadata(token_address: str) -> dict: metadata = {} data = { @@ -359,6 +374,7 @@ def get_token_metadata(token_address: str)->dict: return metadata + async def count_associated_token_accounts( wallet_pubkey: Pubkey ) -> int: @@ -427,6 +443,7 @@ async def count_associated_token_accounts( return total + async def detect_dust_token_accounts( wallet_pubkey: Pubkey, token_mint_address: str = None, @@ -534,6 +551,7 @@ async def detect_dust_token_accounts( print(f"Error fetching token '{token_mint_address}' balance: {e}") return account_output + async def burn_associated_token_account( token: Pubkey, keypair: Keypair, @@ -679,7 +697,7 @@ async def burn_and_close_associated_token_account( associated_token_account )) return txn_signature - + # Derive the associated token account address TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") @@ -782,7 +800,100 @@ def generate_vanity_wallet(prefix: str, key_sensitive: bool = False) -> Keypair: ) ) return keypair + + +def get_account_information(account: Pubkey) -> GetAccountInfoResp: + """ + Fetch account information from Solana RPC and return it as a GetAccountInfoResp object. + + Args: + rpc_url (str): The Solana RPC URL. + account (str): The account public key in string format. + Returns: + GetAccountInfoResp: The account information as a GetAccountInfoResp object. + """ + # Prepare the JSON body for the POST request + json_body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getAccountInfo", + "params": [ + str(account), # The account public key + {"encoding": "base64"} # Response encoding + ] + } + + json_response = [] + loop_counter = 0 + while loop_counter < appconfig.TRADING_RETRIES: + try: + # Send the POST request + response = requests.post( + appconfig.RPC_URL_HELIUS, + headers={"Content-Type": "application/json"}, + json=json_body + ) + response.raise_for_status() # Raise HTTPStatusError for non-2xx responses + + # Parse the JSON response + json_response = response.json() + if "result" not in json_response: + loop_counter += 1 + time.sleep(2) + continue + if "value" in json_response["result"] and not json_response["result"]["value"]: + loop_counter += 1 + time.sleep(2) + continue + + break + except Exception as e: + print("get_account_information-> Exception: {}".format(str(e))) + loop_counter += 1 + + # Validate the response structure + if "result" not in json_response: + raise ValueError("Invalid response: 'result' field not found") + + result = json_response["result"] + if "value" not in result or "context" not in result: + raise ValueError("Invalid response: 'value' or 'context' field not found") + + # Extract and process account information + context = RpcResponseContext(slot=result["context"]["slot"]) + + # Check if account data exists + account_data = result["value"] + if account_data is None: + account_info = None + else: + # Decode the account information + lamports = account_data["lamports"] + owner = Pubkey.from_string(account_data["owner"]) + executable = account_data["executable"] + rent_epoch = account_data["rentEpoch"] + data = account_data["data"][0] # Account data in base64 format + encoding = account_data["data"][1] # Should be "base64" + + if encoding != "base64": + raise ValueError(f"Unexpected encoding: {encoding}") + + # Create AccountInfo object + account_info = Account( + lamports=lamports, + owner=owner, + executable=executable, + rent_epoch=rent_epoch, + data=base64.b64decode(data) + ) + + # Construct the GetAccountInfoResp object + account_info_resp = GetAccountInfoResp(context=context, value=account_info) + + return account_info_resp + + async def test(): prefix = "Ghost" From 5f266dc11631d24f1af7ff97511aa7c3833d9d9b Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 9 Mar 2025 00:31:40 +0100 Subject: [PATCH 077/103] latest jito implelentation on bot --- bot/Dockerfile | 10 +- bot/config/__init__.py | 3 +- bot/domain/jito_rpc.py | 261 +++++++++++++++++++----------------- bot/libs/pump_buy.py | 131 ++++++++++++------ bot/libs/utils.py | 292 +++++++++++++++++++---------------------- bot/module/jito.py | 143 +++++++++++--------- 6 files changed, 447 insertions(+), 393 deletions(-) diff --git a/bot/Dockerfile b/bot/Dockerfile index 48789a4..cb97fc5 100644 --- a/bot/Dockerfile +++ b/bot/Dockerfile @@ -1,5 +1,6 @@ # Use an official Python 3.10 image as the base -FROM python:3.10-slim +FROM python:3.12-slim + # Set the working directory inside the container WORKDIR /bot @@ -44,4 +45,9 @@ EXPOSE 5000 6379 CMD ["bash"] #CMD ["python", "bot/main.py"] #CMD ["python", "-m", "bot.main"] -CMD ["PYTHONPATH=/", "python", "main.py"] +# CMD ["PYTHONPATH=/", "python", "main.py"] +# CMD ["bash", "-c", "PYTHONPATH=/ python main.py"] +WORKDIR / +ENV PYTHONPATH=/ +#CMD ["python", "bot/main.py"] +#CMD ["bash", "-c", "python bot/main.py 2>&1 | tee /bot/tax_collector_bot.log"] diff --git a/bot/config/__init__.py b/bot/config/__init__.py index cf2678e..52cee50 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -36,9 +36,10 @@ class AuthConfig: ) # Minimum balance a token must have for being traded RETRYING_SECONDS = 1 RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + WSS_URL_HELIUS = "wss://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" WSS_URL_QUICKNODE = "wss://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" RPC_URL_QUICKNODE = "https://orbital-hardworking-knowledge.solana-mainnet.quiknode.pro/be0d348509d4f9ae26cd7371cd7a08b7d784324d" - RPC_URL = "https://amsterdam.mainnet.block-engine.jito.wtf/api/v1" + JITO_RPC_URL = "https://amsterdam.mainnet.block-engine.jito.wtf" PUMPFUN_TRANSACTION_URL = "https://pumpportal.fun/api/trade-local" PUMPFUN_WEBSOCKET = "wss://pumpportal.fun/api/data" MARKETMAKING_SOL_BUY_AMOUNT = 0.03 diff --git a/bot/domain/jito_rpc.py b/bot/domain/jito_rpc.py index 3f7b5db..d503d69 100644 --- a/bot/domain/jito_rpc.py +++ b/bot/domain/jito_rpc.py @@ -1,130 +1,143 @@ import requests import os -import json import random + # Jito JSON RPC SDK class JitoJsonRpcSDK: - # Initialize a block engine URL - def __init__(self, url, uuid_var=None): - self.url = url - if uuid_var == None: - self.uuid_var = None - else: - self.uuid_var = self.__get_uuid(uuid_var) - - def __get_uuid(self, uuid_var): - return os.getenv(uuid_var) - - # Send a request to the Block engine url using the JSON RPC methods - def __send_request(self, endpoint, method, params=None): - if endpoint == None: - return "Error: Please enter a valid endpoint." - - if self.uuid_var == None: - headers = { - 'Content-Type': 'application/json', - "accept": "application/json" - } - else: - headers = { - 'Content-Type': 'application/json', - "accept": "application/json", - "x-jito-atuh": self.uuid_var - } - data = { - "id": 1, - "jsonrpc": "2.0", - "method": method, - "params": [params] - } - - print(data) - try: - resp = requests.post(self.url + endpoint, headers=headers, json=data) - resp.raise_for_status() - return {"success": True, "data": resp.json()} - except requests.exceptions.HTTPError as errh: - return {"success": False, "error": f"HTTP Error: {errh}"} - except requests.exceptions.ConnectionError as errc: - return {"success": False, "error": f"Error Connecting: {errc}"} - except requests.exceptions.Timeout as errt: - return {"success": False, "error": f"Timeout Error: {errt}"} - except requests.exceptions.InvalidHeader as err: - return {"success": False, "error": f"Invalid Header error: {err}"} - except requests.exceptions.InvalidURL as err: - return {"success": False, "error": f"InvalidURL error: {err}"} - except requests.exceptions.RequestException as err: - return {"success": False, "error": f"An error occurred: {err}"} - - #Bundle Endpoint - def get_tip_accounts(self, params=None): - if self.uuid_var == None: - return self.__send_request(endpoint="/bundles", method="getTipAccounts") - else: - return self.__send_request(endpoint="/bundles?uuid=" + self.uuid_var, method="getTipAccounts") - - def get_random_tip_account(self): - response = self.get_tip_accounts() - if not response['success']: - print(f"Error getting tip accounts: {response.get('error', 'Unknown error')}") - return None - - tip_accounts = response['data']['result'] - if not tip_accounts: - print("No tip accounts found.") - return None - - random_account = random.choice(tip_accounts) - return random_account - - - def get_bundle_statuses(self, bundle_uuids): - endpoint = "/bundles" - if self.uuid_var is not None: - endpoint += f"?uuid={self.uuid_var}" - - # Ensure bundle_uuids is a list - if not isinstance(bundle_uuids, list): - bundle_uuids = [bundle_uuids] - - # Correct format for the request - params = bundle_uuids - - return self.__send_request(endpoint=endpoint, method="getBundleStatuses", params=params) - - def send_bundle(self, params=None): - if self.uuid_var == None: - return self.__send_request(endpoint="/bundles",method="sendBundle", params=params) - else: - return self.__send_request(endpoint="/bundles?uuid=" + self.uuid_var, method="sendBundle", params=params) - - def get_inflight_bundle_statuses(self, bundle_uuids): - endpoint = "/bundles" - if self.uuid_var is not None: - endpoint += f"?uuid={self.uuid_var}" - - # Ensure bundle_uuids is a list - if not isinstance(bundle_uuids, list): - bundle_uuids = [bundle_uuids] - - # Correct format for the request - params = bundle_uuids - - return self.__send_request(endpoint=endpoint, method="getInflightBundleStatuses", params=params) - - # Transaction Endpoint - def send_txn(self, params=None, bundleOnly=False): - ep = "/transactions" - query_params = [] - - if bundleOnly: - query_params.append("bundleOnly=true") - - if self.uuid_var is not None: - query_params.append(f"uuid={self.uuid_var}") - - if query_params: - ep += "?" + "&".join(query_params) - - return self.__send_request(endpoint=ep, method="sendTransaction", params=params) + # Initialize a block engine URL + def __init__(self, url, uuid_var=None): + self.url = url + if uuid_var is None: + self.uuid_var = None + else: + self.uuid_var = self.__get_uuid(uuid_var) + + def __get_uuid(self, uuid_var): + return os.getenv(uuid_var) + + # Send a request to the Block engine url using the JSON RPC methods + def __send_request(self, endpoint, method, params=None): + if endpoint is None: + return "Error: Please enter a valid endpoint." + + if self.uuid_var is None: + headers = { + 'Content-Type': 'application/json', + "accept": "application/json" + } + else: + headers = { + 'Content-Type': 'application/json', + "accept": "application/json", + "x-jito-atuh": self.uuid_var + } + data = { + "id": 1, + "jsonrpc": "2.0", + "method": method, + "params": [params, { + "encoding": "base64" + }], + } + + # print(data) + try: + resp = requests.post(self.url + endpoint, headers=headers, json=data) + resp.raise_for_status() + return {"success": True, "data": resp.json()} + except requests.exceptions.HTTPError as errh: + return {"succes s": False, "error": f"HTTP Error: {errh}"} + except requests.exceptions.ConnectionError as errc: + return {"success": False, "error": f"Error Connecting: {errc}"} + except requests.exceptions.Timeout as errt: + return {"success": False, "error": f"Timeout Error: {errt}"} + except requests.exceptions.InvalidHeader as err: + return {"success": False, "error": f"Invalid Header error: {err}"} + except requests.exceptions.InvalidURL as err: + return {"success": False, "error": f"InvalidURL error: {err}"} + except requests.exceptions.RequestException as err: + return {"success": False, "error": f"An error occurred: {err}"} + + # Bundle Endpoint + def get_tip_accounts(self, params=None): + if self.uuid_var is None: + return self.__send_request(endpoint="/bundles", method="getTipAccounts") + else: + return self.__send_request(endpoint="/bundles?uuid=" + self.uuid_var, method="getTipAccounts") + + def get_random_tip_account(self): + response = self.get_tip_accounts() + if not response['success']: + print(f"Error getting tip accounts: {response.get('error', 'Unknown error')}") + return None + + tip_accounts = response['data']['result'] + if not tip_accounts: + print("No tip accounts found.") + return None + + random_account = random.choice(tip_accounts) + return random_account + + def get_bundle_statuses(self, bundle_uuids): + endpoint = "/bundles" + if self.uuid_var is not None: + endpoint += f"?uuid={self.uuid_var}" + + # Ensure bundle_uuids is a list + if not isinstance(bundle_uuids, list): + bundle_uuids = [bundle_uuids] + + # Correct format for the request + params = bundle_uuids + + return self.__send_request(endpoint=endpoint, method="getBundleStatuses", params=params) + + def send_bundle(self, params=None): + if self.uuid_var is None: + return self.__send_request( + endpoint="/bundles", + method="sendBundle", + params=params + ) + else: + return self.__send_request( + endpoint="/bundles?uuid=" + self.uuid_var, + method="sendBundle", + params=params + ) + + def get_inflight_bundle_statuses(self, bundle_uuids): + endpoint = "/bundles" + if self.uuid_var is not None: + endpoint += f"?uuid={self.uuid_var}" + + # Ensure bundle_uuids is a list + if not isinstance(bundle_uuids, list): + bundle_uuids = [bundle_uuids] + + # Correct format for the request + params = bundle_uuids + + return self.__send_request( + endpoint=endpoint, + method="getInflightBundleStatuses", + params=params + ) + + # Transaction Endpoint + def send_txn(self, params=None, bundleOnly=False): + ep = "/transactions" + query_params = [] + + if bundleOnly: + query_params.append("bundleOnly=true") + + if self.uuid_var is not None: + query_params.append(f"uuid={self.uuid_var}") + + if query_params: + ep += "?" + "&".join(query_params) + + return self.__send_request(endpoint=ep, method="sendTransaction", params=params) diff --git a/bot/libs/pump_buy.py b/bot/libs/pump_buy.py index b8aef99..e940eae 100644 --- a/bot/libs/pump_buy.py +++ b/bot/libs/pump_buy.py @@ -6,6 +6,8 @@ import time import websockets +from datetime import datetime + from construct import Struct, Int64ul, Flag from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed @@ -19,12 +21,15 @@ from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction, VersionedTransaction +from solders.system_program import transfer, TransferParams +from solders.signature import Signature from spl.token.instructions import get_associated_token_address import spl.token.instructions as spl_token from bot.config import appconfig -from bot.libs.utils import get_account_information, get_token_accounts_by_owner +from bot.libs.utils import get_account_information +from bot.domain.jito_rpc import JitoJsonRpcSDK EXPECTED_DISCRIMINATOR = struct.pack(" New token: {} at block: {}".format( - decoded_args["mint"], - decoded_args["block"] - )) + # print("\n> New token: {} at block: {}".format( + # decoded_args["mint"], + # decoded_args["block"] + # )) continue @@ -546,28 +582,28 @@ async def listen_for_create_transaction(websocket): developer = decoded_args.get("developer") trader = "developer" if developer == buyer else "sniper" - if buyer not in token_data[decoded_buy_args.get('mint')]["seen_buyers"]: + if buyer not in token_data[decoded_buy_args.get('mint')]["seen_buyers"]: # noqa: E501 # Add the buyer to the set - token_data[decoded_buy_args.get('mint')]["seen_buyers"].add(buyer) + token_data[decoded_buy_args.get('mint')]["seen_buyers"].add(buyer) # noqa: E501 token_data[decoded_buy_args.get('mint')]["buyers"].append({ 'buyer': buyer, 'trader': trader, 'tokens_bought': tokens_bought / 10**6, - 'sol_traded': (sol_traded / 10**9) / 1.01 # taking out Pump.fun fee + 'sol_traded': (sol_traded / 10**9) / 1.01 # taking out Pump.fun fee # noqa: E501 }) - print("Buyer: {}, Tokens Bought: {}, SOL Traded: {}".format( - buyer, - tokens_bought / 10**6, - (sol_traded / 10**9) / 1.01 - )) + # print("Buyer: {}, Tokens Bought: {}, SOL Traded: {}".format( + # buyer, + # tokens_bought / 10**6, + # (sol_traded / 10**9) / 1.01 + # )) continue # End of transaction loop # if token_data: # for token, data in token_data.items(): - # total_tokens_bought = sum(buyer["tokens_bought"] for buyer in data.get("buyers", [])) + # total_tokens_bought = sum(buyer["tokens_bought"] for buyer in data.get("buyers", [])) # noqa: E501 # if total_tokens_bought > threshold: - # print(f"Token {token} exceeds the threshold with {total_tokens_bought} tokens bought.") + # print(f"Token {token} exceeds the threshold with {total_tokens_bought} tokens bought.") # noqa: E501 # break # Ending block loop @@ -581,7 +617,9 @@ async def listen_for_create_transaction(websocket): ) if total_tokens_bought >= threshold: if (len(data.get("buyers", [])) > 5): - print(" Scam 'BIG' token found. Tokens bought {}".format( + start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() + print("{} Scam 'BIG' token found. Tokens bought {}".format( + start_time, total_tokens_bought )) tradable_tokens.append(data) @@ -590,7 +628,9 @@ async def listen_for_create_transaction(websocket): else: print(" Whale scam found. Discarting token...") else: - print("Discarting token...") + print("*** TESTING token...") + tradable_tokens.append(data) + if tradable_tokens: keep_loop = False break # Breaking transactions loop @@ -662,10 +702,10 @@ async def trade(websocket: websockets, trades: int): # )) # continue - # Calculate time delta: Get timestamp from buy block and timestamp from creation block time_delta = abs(confirmation_stamp - token_data["blockTime"]) sleep_time = 0 if appconfig.TRADING_TIME - time_delta < 0 else appconfig.TRADING_TIME - time_delta + sleep_time = 0 data = { "sleep_time": sleep_time, "mint": mint, @@ -678,18 +718,23 @@ async def trade(websocket: websockets, trades: int): return data -async def main(): +async def main(trades: int): # import hashlib # hash_bytes = hashlib.sha256(b'global:buy').digest() # # Extract the first 8 bytes and unpack them as a little-endian unsigned long long integer # buy_discriminator = struct.unpack_from(' float: :return: Balance in SOL as a float. """ async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - try: - # Fetch the balance (in lamports) - balance_response = await client.get_balance(public_key) - # Balance is returned in lamports (1 SOL = 10^9 lamports) - lamports = balance_response.value - sol_balance = lamports / 1e9 - return sol_balance - except Exception as e: - print(f"Error fetching balance: {e.error_msg}") - return 0.0 + try: + # Fetch the balance (in lamports) + balance_response = await client.get_balance(public_key) + # Balance is returned in lamports (1 SOL = 10^9 lamports) + lamports = balance_response.value + sol_balance = lamports / 1e9 + return sol_balance + except Exception as e: + print(f"Error fetching balance: {e.error_msg}") + return 0.0 def decode_pump_fun_token(token: str) -> Pubkey: """ Decodes a 44-character token from pump.fun to retrieve its mint address. - + :param token: 44-character token. :return: Token mint address (base58 encoded). """ @@ -139,7 +141,7 @@ def get_instructions_from_message(msg: Message): parsed_instructions.append( {"field_id": idx, "possible_price_impact": price_impact} ) - + # Decode the instruction data if necessary (e.g., Base64 decoding) try: decoded_data = base64.b64decode(data).decode("utf-8") @@ -149,7 +151,7 @@ def get_instructions_from_message(msg: Message): # Check if priceImpactPct is in the decoded data if "priceImpactPct" in decoded_data: print(f"Instruction {idx} contains priceImpactPct: {decoded_data}") - + return parsed_instructions @@ -165,7 +167,7 @@ def include_instruction(msg: MessageV0): compute_budget_ix = set_compute_unit_limit(1_000_000) # Set compute unit limit to 1M # Step 2: Get a fresh recent ∫blockhash - client = Client(appconfig.RPC_URL) # Assuming RpcClient is already imported and available + client = Client(appconfig.JITO_RPC_URL) # Assuming RpcClient is already imported and available recent_blockhash = client.get_latest_blockhash().value.blockhash accountKeys = list(msg.account_keys) # Convert to mutable list @@ -234,14 +236,14 @@ def get_solana_price() -> float: price_data = response.json() price = price_data["solana"]["usd"] break - except Exception as e: + except Exception: counter += 1 print("get_solana_price Error: Failed to fetch Solana price. retriying {} of {}".format( counter, retries )) time.sleep(counter) - + return price @@ -263,8 +265,7 @@ def get_token_mint_decimals(mint_address: str) -> int: response = client.get_account_info(mint_pubkey) if not response.value: raise ValueError(f"Mint account {mint_address} not found") - - from spl.token._layouts import MINT_LAYOUT, ACCOUNT_LAYOUT + mint_data = MINT_LAYOUT.parse(response.value.data) decimals = mint_data.decimals @@ -328,19 +329,18 @@ def get_token_metadata(token_address: str) -> dict: try: response = requests.post( - url=appconfig.RPC_URL_HELIUS, - json=data, - headers={"Content-Type": "application/json"} - ) + url=appconfig.RPC_URL_HELIUS, + json=data, + headers={"Content-Type": "application/json"} + ) if response.status_code != 200: counter += 1 print("get_metadata: Bad status code '{}' recevied for token {}. Retries {} of {}".format( - response.status_code, - token_address, - counter, - retries - ) - ) + response.status_code, + token_address, + counter, + retries + )) time.sleep(counter) continue @@ -362,14 +362,13 @@ def get_token_metadata(token_address: str) -> dict: metadata["insufficient_data"] = True break - except Exception as e: + except Exception: counter += 1 print("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( - token_address, - counter, - retries - ) - ) + token_address, + counter, + retries + )) time.sleep(counter) return metadata @@ -458,98 +457,97 @@ async def detect_dust_token_accounts( min_token_value = 1 account_samples = 5 working_balance = 0 - # Refactor this: remove this line of code and test as client is not being used - async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: - try: - # TODO: implement token's black list (like USDC, etc) - token_blacklist = [] - original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) - accounts = [ - account for account in original_accounts - if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist - ] - - if not accounts: - return [] + + try: + # TODO: implement token's black list (like USDC, etc) + token_blacklist = [] + original_accounts = await get_token_accounts_by_owner(wallet_address=str(wallet_pubkey)) + accounts = [ + account for account in original_accounts + if account["account"]["data"]["parsed"]["info"]["mint"] not in token_blacklist + ] + + if not accounts: + return [] + + # Fetch the current Solana price + sol_price = get_solana_price() + if sol_price == 0: + return [] + + if do_balance_aproximation: + account_sample_list = accounts + if len(accounts) > account_samples: + my_list = list(range(len(accounts))) + # Generate X random positions + random_positions = random.sample(range(len(my_list)), account_samples) + # Get the 5 random values from the list + account_sample_list = [my_list[pos] for pos in random_positions] + + sum_balance = 0 + for index in account_sample_list: + associated_tokan_account = accounts[index]["pubkey"] + sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) + + working_balance = sum_balance / len(account_sample_list) + + counter = 0 + account_output = [] + for account in accounts: + counter += 1 + + mint = account["account"]["data"]["parsed"]["info"]["mint"] + owner = account["account"]["data"]["parsed"]["info"]["owner"] + token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] + decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] + associated_token_account = account["pubkey"] + + metadata = get_token_metadata(token_address=mint) + + token_price = metadata["price_info"]["price_per_token"] + token_value = token_price * token_amount - # Fetch the current Solana price - sol_price = get_solana_price() - if sol_price == 0: - return [] - - if do_balance_aproximation: - account_sample_list = accounts - if len(accounts) > account_samples: - my_list = list(range(len(accounts))) - # Generate X random positions - random_positions = random.sample(range(len(my_list)), account_samples) - # Get the 5 random values from the list - account_sample_list = [my_list[pos] for pos in random_positions] - - sum_balance = 0 - for index in account_sample_list: - associated_tokan_account = accounts[index]["pubkey"] - sum_balance += await get_solana_balance(public_key=Pubkey.from_string(associated_tokan_account)) - - working_balance = sum_balance / len(account_sample_list) - - counter = 0 - account_output = [] - for account in accounts: - counter +=1 - - mint = account["account"]["data"]["parsed"]["info"]["mint"] - owner = account["account"]["data"]["parsed"]["info"]["owner"] - token_amount = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["uiAmount"] - decimals = account["account"]["data"]["parsed"]["info"]["tokenAmount"]["decimals"] - associated_token_account = account["pubkey"] - - metadata = get_token_metadata(token_address=mint) - - token_price = metadata["price_info"]["price_per_token"] - token_value = token_price * token_amount - - uri = metadata["uri"] - cdn_uri = metadata["cdn_uri"] - mime = metadata["mime"] - description = metadata["description"] if "description" in metadata else "" - name = metadata["name"].strip() - symbol = metadata["symbol"].strip() - authority = metadata["authority"] - supply = metadata["supply"] - token_program = metadata["token_program"] - insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price - - account_output.append( - { - "token_mint": mint, - "associated_token_account": associated_token_account, - "owner": owner, - "token_amount": token_amount, - "token_price": token_price, - "token_value": token_value, - "decimals": decimals, - "sol_balance": working_balance, - "sol_balance_usd": working_balance * sol_price, - "is_dust": token_value < min_token_value, - "uri": uri, - "cdn_uri": cdn_uri, - "mime": mime, - "description": description, - "name": name, - "symbol": symbol, - "authority": authority, - "supply": supply, - "token_program": token_program, - "insufficient_data": insufficient_data, - } - ) + uri = metadata["uri"] + cdn_uri = metadata["cdn_uri"] + mime = metadata["mime"] + description = metadata["description"] if "description" in metadata else "" + name = metadata["name"].strip() + symbol = metadata["symbol"].strip() + authority = metadata["authority"] + supply = metadata["supply"] + token_program = metadata["token_program"] + insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price + + account_output.append( + { + "token_mint": mint, + "associated_token_account": associated_token_account, + "owner": owner, + "token_amount": token_amount, + "token_price": token_price, + "token_value": token_value, + "decimals": decimals, + "sol_balance": working_balance, + "sol_balance_usd": working_balance * sol_price, + "is_dust": token_value < min_token_value, + "uri": uri, + "cdn_uri": cdn_uri, + "mime": mime, + "description": description, + "name": name, + "symbol": symbol, + "authority": authority, + "supply": supply, + "token_program": token_program, + "insufficient_data": insufficient_data, + } + ) - return account_output + return account_output - except Exception as e: - print(f"Error fetching token '{token_mint_address}' balance: {e}") - return account_output + except Exception as e: + print(f"Error fetching token '{token_mint_address}' balance: {e}") + return account_output async def burn_associated_token_account( @@ -629,15 +627,15 @@ async def burn_associated_token_account( # Create and sign transaction blockhash = await client.get_latest_blockhash() recent_blockhash = blockhash.value.blockhash - + msg = Message( instructions=[set_compute_unit_price(1_000), burn_ix], payer=keypair.pubkey() ) tx_signature = await client.send_transaction( - txn=Transaction([keypair], msg, recent_blockhash), - opts=TxOpts(preflight_commitment=Confirmed), - ) + txn=Transaction([keypair], msg, recent_blockhash), + opts=TxOpts(preflight_commitment=Confirmed), + ) current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( "Burn", @@ -647,32 +645,12 @@ async def burn_associated_token_account( await client.confirm_transaction(tx_signature.value, commitment="confirmed") print("Transaction confirmed") - # SECOND WAY: POSTING TRANSACTION - # config = RpcSendTransactionConfig( - # preflight_commitment=CommitmentLevel.Confirmed - # ) - # tx = SendLegacyTransaction(tx=Transaction([keypair], msg, recent_blockhash),config=config) - # response = requests.post( - # url=appconfig.RPC_URL_HELIUS, - # headers={"Content-Type": "application/json"}, - # data=tx.to_json() - # ) - # txSignature = response.json()['result'] - # current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() - # print("Trade->{} Transaction: https://solscan.io/tx/{} at {}".format( - # "Burn", - # txSignature, - # current_time - # )) - except Exception as e: print("burn_associated_token_account Error: {}".format(e)) return txn -from solders.pubkey import Pubkey -from spl.token.instructions import close_account, CloseAccountParams async def burn_and_close_associated_token_account( associated_token_account: Pubkey, token_mint: Pubkey, @@ -785,7 +763,7 @@ def generate_vanity_wallet(prefix: str, key_sensitive: bool = False) -> Keypair: # Generate a new keypair keypair = Keypair() public_key = str(keypair.pubkey()) - + # Check if the public key starts with the desired prefix validator = public_key.lower().startswith(prefix.lower()) if not key_sensitive else public_key.startswith(prefix) @@ -795,21 +773,20 @@ def generate_vanity_wallet(prefix: str, key_sensitive: bool = False) -> Keypair: pk = base58.b58encode(bytes(keypair)).decode() print("PK: {}".format(pk)) print("finishing generate_vanity_wallet at {} after {} iterations".format( - stop_time, - counter - ) - ) + stop_time, + counter + )) return keypair def get_account_information(account: Pubkey) -> GetAccountInfoResp: """ Fetch account information from Solana RPC and return it as a GetAccountInfoResp object. - + Args: rpc_url (str): The Solana RPC URL. account (str): The account public key in string format. - + Returns: GetAccountInfoResp: The account information as a GetAccountInfoResp object. """ @@ -894,7 +871,6 @@ def get_account_information(account: Pubkey) -> GetAccountInfoResp: return account_info_resp - async def test(): prefix = "Ghost" @@ -906,7 +882,7 @@ async def test(): return solana_address = "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb" - #solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" + # solana_address = "onK5ruraCpbvjzWjqvJ3uBXgXapNX6ddB799qsNipeR" token_address = "FFqR2bk3ULB1WuYECRbooxPpbYvvZBp3z9Uc94Pqpump" token_address = "7AeGaYhyhvDRdPkH7Wg7vup8D1SjNRZc1fXLZNcYpump" @@ -926,7 +902,7 @@ async def test(): associated_token_account=Pubkey.from_string(associated_token_account), token_mint=Pubkey.from_string(token_address), keypair=Keypair.from_base58_string(appconfig.PRIVKEY), - decimals=6 + decimals=6 ) account = "DnJaA2C7Ak93HGvACoCQ9ULacYuq7BuiYMWtWePekzJH" diff --git a/bot/module/jito.py b/bot/module/jito.py index 743a6a4..7882eef 100644 --- a/bot/module/jito.py +++ b/bot/module/jito.py @@ -20,13 +20,13 @@ async def check_transaction_status(client: AsyncClient, signature_str: str): print("Checking transaction status...") max_attempts = 60 # 60 seconds attempt = 0 - + signature = Signature.from_string(signature_str) - + while attempt < max_attempts: try: response = await client.get_signature_statuses([signature]) - + if response.value[0] is not None: status = response.value[0] slot = status.slot @@ -37,7 +37,7 @@ async def check_transaction_status(client: AsyncClient, signature_str: str): print(f"Slot: {slot}") print(f"Confirmations: {confirmations}") print(f"Confirmation status: {confirmation_status}") - + if err: print(f"Transaction failed with error: {err}") return False @@ -50,28 +50,38 @@ async def check_transaction_status(client: AsyncClient, signature_str: str): print("Transaction is processed but not yet confirmed or finalized.") else: print("Transaction status not available yet.") - + await asyncio.sleep(1) attempt += 1 except Exception as e: print(f"Error checking transaction status: {e}") await asyncio.sleep(1) attempt += 1 - + print(f"Transaction not finalized after {max_attempts} attempts.") return False -async def send_transaction_with_priority_fee(sdk, solana_client, sender, receiver, amount, jito_tip_amount, priority_fee, compute_unit_limit=100_000): + +async def send_transaction_with_priority_fee( + sdk, + solana_client, + sender, + receiver, + amount, + jito_tip_amount, + priority_fee, + compute_unit_limit=100_000 +): try: recent_blockhash = await solana_client.get_latest_blockhash() - + # Transfer to the known receiver transfer_ix = transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=receiver, lamports=amount)) - + # Jito tip transfer jito_tip_account = Pubkey.from_string(sdk.get_random_tip_account()) jito_tip_ix = transfer(TransferParams(from_pubkey=sender.pubkey(), to_pubkey=jito_tip_account, lamports=jito_tip_amount)) - + # Priority Fee priority_fee_ix = set_compute_unit_price(priority_fee) @@ -83,12 +93,12 @@ async def send_transaction_with_priority_fee(sdk, solana_client, sender, receive ) serialized_transaction = base64.b64encode(bytes(transaction)).decode('ascii') - + print(f"Sending transaction with priority fee: {priority_fee} micro-lamports per compute unit") print(f"Transfer amount: {amount} lamports to {receiver}") print(f"Jito tip amount: {jito_tip_amount} lamports to {jito_tip_account}") print(f"Serialized transaction: {serialized_transaction}") - + response = sdk.send_txn(params=serialized_transaction, bundleOnly=False) if response['success']: @@ -97,14 +107,14 @@ async def send_transaction_with_priority_fee(sdk, solana_client, sender, receive print(f"Transaction signature: {signature_str}") finalized = await check_transaction_status(solana_client, signature_str) - + if finalized: print("Transaction has been finalized.") solscan_url = f"https://solscan.io/tx/{signature_str}" print(f"View transaction details on Solscan: {solscan_url}") else: print("Transaction was not finalized within the expected time.") - + return signature_str else: print(f"Error sending transaction: {response['error']}") @@ -114,34 +124,35 @@ async def send_transaction_with_priority_fee(sdk, solana_client, sender, receive print(f"Exception occurred: {str(e)}") return None -### BUNDLE ### + +# ## BUNDLE ### async def confirm_landed_bundle(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempts: int = 60, delay: float = 2.0): for attempt in range(max_attempts): response = sdk.get_bundle_statuses([bundle_id]) - + if not response['success']: print(f"Error confirming bundle status: {response.get('error', 'Unknown error')}") await asyncio.sleep(delay) - + print(f"Confirmation attempt {attempt + 1}/{max_attempts}:") print(json.dumps(response, indent=2)) - + if 'result' not in response['data']: - print(f"Unexpected response structure. 'result' not found in response data.") + print("Unexpected response structure. 'result' not found in response data.") await asyncio.sleep(delay) - + result = response['data']['result'] if 'value' not in result or not result['value']: print(f"Bundle {bundle_id} not found in confirmation response") await asyncio.sleep(delay) - + bundle_status = result['value'][0] if bundle_status['bundle_id'] != bundle_id: print(f"Unexpected bundle ID in response: {bundle_status['bundle_id']}") await asyncio.sleep(delay) - + status = bundle_status.get('confirmation_status') - + if status == 'finalized': print(f"Bundle {bundle_id} has been finalized on-chain!") # Extract transaction ID and construct Solscan link @@ -158,45 +169,46 @@ async def confirm_landed_bundle(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempt print(f"Bundle {bundle_id} is processed but not yet confirmed. Checking again...") else: print(f"Unexpected status '{status}' during confirmation for bundle {bundle_id}") - + # Check for errors err = bundle_status.get('err', {}).get('Ok') if err is not None: print(f"Error in bundle {bundle_id}: {err}") return 'Failed' - + await asyncio.sleep(delay) - + print(f"Max confirmation attempts reached. Unable to confirm finalization of bundle {bundle_id}") return 'Landed' + async def check_bundle_status(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempts: int = 30, delay: float = 2.0): for attempt in range(max_attempts): response = sdk.get_inflight_bundle_statuses([bundle_id]) - + if not response['success']: print(f"Error checking bundle status: {response.get('error', 'Unknown error')}") await asyncio.sleep(delay) continue - + print(f"Raw response (Attempt {attempt + 1}/{max_attempts}):") print(json.dumps(response, indent=2)) - + if 'result' not in response['data']: - print(f"Unexpected response structure. 'result' not found in response data.") + print("Unexpected response structure. 'result' not found in response data.") await asyncio.sleep(delay) continue - + result = response['data']['result'] if 'value' not in result or not result['value']: print(f"Bundle {bundle_id} not found in response") await asyncio.sleep(delay) continue - + bundle_status = result['value'][0] status = bundle_status.get('status') print(f"Attempt {attempt + 1}/{max_attempts}: Bundle status - {status}") - + if status == 'Landed': print(f"Bundle {bundle_id} has landed on-chain! Performing additional confirmation...") final_status = await confirm_landed_bundle(sdk, bundle_id) @@ -214,20 +226,21 @@ async def check_bundle_status(sdk: JitoJsonRpcSDK, bundle_id: str, max_attempts: print(f"Bundle {bundle_id} is still pending. Checking again in {delay} seconds...") else: print(f"Unknown status '{status}' for bundle {bundle_id}") - + await asyncio.sleep(delay) - + print(f"Max attempts reached. Final status of bundle {bundle_id}: {status}") return status + async def confirm_bundle(jito_client, bundle_id, timeout_seconds=60): start_time = asyncio.get_event_loop().time() - + while asyncio.get_event_loop().time() - start_time < timeout_seconds: try: status = jito_client.get_bundle_statuses([[bundle_id]]) print('Bundle status:', status) - + if status['success'] and bundle_id in status['data']['result']: bundle_status = status['data']['result'][bundle_id] if bundle_status['status'] == 'finalized': @@ -246,18 +259,18 @@ async def confirm_bundle(jito_client, bundle_id, timeout_seconds=60): # Wait for a short time before checking again await asyncio.sleep(2) - + print(f"Bundle {bundle_id} has not finalized within {timeout_seconds}s, but it may still be in progress.") return jito_client.get_bundle_statuses([bundle_id]) async def basic_bundle(): # Initialize connection to Solana testnet - solana_client = AsyncClient(appconfig.RPC_URL) + solana_client = AsyncClient(appconfig.JITO_RPC_URL) wallet_keypair = Keypair.from_base58_string(appconfig.PRIVKEY) # Initialize JitoJsonRpcSDK - jito_client = JitoJsonRpcSDK(url="https://mainnet.block-engine.jito.wtf/api/v1") + jito_client = JitoJsonRpcSDK(url="{}/api/v1".format(appconfig.JITO_RPC_URL)) #Example using UUID #jito_client = JitoJsonRpcSDK(url="https://mainnet.block-engine.jito.wtf/api/v1", uuid_var="YOUR_UUID" ) @@ -308,29 +321,29 @@ async def basic_bundle(): serialized_transaction = base64.b64encode(bytes(transaction)).decode('ascii') try: - # Prepare the bundle request - bundle_request = [serialized_transaction] - print(f"Sending bundle request: {json.dumps(bundle_request, indent=2)}") - - # Send the bundle using sendBundle method - result = jito_client.send_bundle(bundle_request) - print('Raw API response:', json.dumps(result, indent=2)) - - if result['success']: - bundle_id = result['data']['result'] - print(f"Bundle sent successfully. Bundle ID: {bundle_id}") - - # Check the status of the bundle - final_status = await check_bundle_status(jito_client, bundle_id, max_attempts=30, delay=2.0) - - if final_status == 'Finalized': - print("Bundle has been confirmed and finalized on-chain.") - elif final_status == 'Landed': - print("Bundle has landed on-chain but could not be confirmed as finalized within the timeout period.") - else: - print(f"Bundle did not land on-chain. Final status: {final_status}") + # Prepare the bundle request + bundle_request = [serialized_transaction] + print(f"Sending bundle request: {json.dumps(bundle_request, indent=2)}") + + # Send the bundle using sendBundle method + result = jito_client.send_bundle(bundle_request) + print('Raw API response:', json.dumps(result, indent=2)) + + if result['success']: + bundle_id = result['data']['result'] + print(f"Bundle sent successfully. Bundle ID: {bundle_id}") + + # Check the status of the bundle + final_status = await check_bundle_status(jito_client, bundle_id, max_attempts=30, delay=2.0) + + if final_status == 'Finalized': + print("Bundle has been confirmed and finalized on-chain.") + elif final_status == 'Landed': + print("Bundle has landed on-chain but could not be confirmed as finalized within the timeout period.") else: - print(f"Failed to send bundle: {result.get('error', 'Unknown error')}") + print(f"Bundle did not land on-chain. Final status: {final_status}") + else: + print(f"Failed to send bundle: {result.get('error', 'Unknown error')}") except Exception as error: print('Error sending or confirming bundle:', str(error)) @@ -341,7 +354,7 @@ async def basic_bundle(): async def basic_txn(): solana_client = AsyncClient("https://api.mainnet-beta.solana.com") - sdk = JitoJsonRpcSDK(url=appconfig.RPC_URL) + sdk = JitoJsonRpcSDK(url=appconfig.JITO_RPC_URL) sender = Keypair.from_bytes(bytes(appconfig.PRIVKEY)) receiver = Pubkey.from_string() @@ -354,10 +367,10 @@ async def basic_txn(): jito_tip_amount = 1000 # Lamports for Jito tip signature = await send_transaction_with_priority_fee(sdk, solana_client, sender, receiver, amount, jito_tip_amount, priority_fee) - + if signature: print(f"Transaction process completed. Signature: {signature}") await solana_client.close() -asyncio.run(basic_bundle()) +# asyncio.run(basic_bundle()) From 823a5c2a64db85f1208d24d68a31de083c7d0c3c Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 9 Mar 2025 00:39:51 +0100 Subject: [PATCH 078/103] minor changes --- bot/app/AWS/steps.md | 2 +- bot/module/pump.py | 63 ++++++++++++++------------------------------ 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index f924d91..e19067c 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -76,4 +76,4 @@ aws lambda update-function-code \ --function-name ghostfunds-backend-fastapi-docker \ --image-uri 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest -aws lambda get-function-configuration --function-name ghostfunds-backend-fastapi-docker \ No newline at end of file +aws lambda get-function-configuration --function-name ghostfunds-backend-fastapi-docker diff --git a/bot/module/pump.py b/bot/module/pump.py index cdaf278..e50f255 100644 --- a/bot/module/pump.py +++ b/bot/module/pump.py @@ -26,7 +26,6 @@ from bot.libs import criterias as criteria_functions from bot.libs.utils import ( get_solana_balance, - get_own_token_balance, Trader, TxType, Celebrimborg, @@ -37,6 +36,7 @@ from datetime import datetime, timedelta from enum import Enum +from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction from solders.keypair import Keypair from solders.commitment_config import CommitmentLevel @@ -228,6 +228,7 @@ class TradeRoadmap: # Patch the running event loop. Required to get wallet's balance nest_asyncio.apply() + class Pump: sniping_token_list = [] @@ -248,7 +249,7 @@ def __init__( self.tokens_to_be_traded = appconfig.TRADING_TOKENS_AT_THE_SAME_TIME self.trading_amount = amount self.trading_sell_amount = amount * (1 + target) # TODO: apply this when we have the Bounding Courve - + self.keypair = Keypair.from_base58_string(appconfig.PRIVKEY) self.ssl_context = ssl.create_default_context(cafile=certifi.where()) self.balance = self.get_balance(public_key=self.keypair.pubkey()) @@ -259,7 +260,6 @@ def __init__( self.trade_fees = appconfig.FEES - def start_scanner(self): self.scanner_start_time = datetime.now() @@ -294,7 +294,6 @@ def decrease_fees(self): lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES )) self.trade_fees = lower_fees if lower_fees >= appconfig.FEES else appconfig.FEES - def reset_fees(self): self.trade_fees = appconfig.FEES @@ -370,7 +369,7 @@ async def subscribe(self, steps: list): roadmap_name = steps[step_index]["name"] if step_index == 0: print("subscribe -> restarting roadmap '{}'\n".format(roadmap_name)) - + print("Step {}: {}".format( step_index, steps[step_index]["name"] @@ -383,7 +382,7 @@ async def subscribe(self, steps: list): # Applying starting criterias if "scanner_activity_time" in step["criteria"]: self.scanner_activity_time = step["criteria"]["scanner_activity_time"] - + start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("Start at {} ".format( start_time @@ -757,18 +756,17 @@ async def subscribe(self, steps: list): except Exception as e: print(f"Unexpected error: {e}. Exiting program abruptly.") break # Exit on non-recoverable errors - def new_token_suscription(self, msg: str, step: Dict, redisdb=RedisDB) -> bool: move_to_next_step = False - + if self.scanner_start_time is None: self.start_scanner() # Check if scanner needs to be torned off. scanner_activity_time == -1 -> runs forever. if (datetime.now() - self.scanner_start_time).total_seconds() >= self.scanner_activity_time and \ - self.scanner_activity_time != -1: + self.scanner_activity_time != -1: move_to_next_step = True return move_to_next_step @@ -913,7 +911,7 @@ def validate_criteria(self, msg: Dict, amount_traded: float, criteria: Dict) -> break return is_valid, exit_criteria - + def prepare_data( self, keypair: Keypair, @@ -981,10 +979,9 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No if appconfig.APPMODE not in [AppMode.real.value]: current_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("simulate trade -> {} MODE: returning dummy transaction at {}".format( - appconfig.APPMODE, - current_time - ) - ) + appconfig.APPMODE, + current_time + )) txSignature = "txn_dummy_{}".format(txtype.value) break @@ -1007,7 +1004,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No response )) break - + retries += 1 print("Trade->{} Error: {} returned a status code {}. Retrying again {} times. Response: {}".format( @@ -1037,7 +1034,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No )) time.sleep(appconfig.RETRYING_SECONDS) continue - + vst = VersionedTransaction.from_bytes(response.content) msg = vst.message @@ -1054,7 +1051,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No try: response = requests.post( - url=appconfig.RPC_URL, + url=appconfig.JITO_RPC_URL, headers={"Content-Type": "application/json"}, data=txPayload.to_json() ) @@ -1068,9 +1065,8 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No current_time )) break - - except Exception as e: + except Exception: retries += 1 if txtype.value == TxType.buy.value: print("Trade->{} Transaction failed. Exiting trade function. Error: {}".format( @@ -1083,7 +1079,7 @@ def trade(self, txtype: TxType, token: str, keypair: Keypair, amount: float = No retries, response.json()["error"]["message"] )) - + time.sleep(appconfig.RETRYING_SECONDS) return txSignature @@ -1120,7 +1116,6 @@ def nuke(self, token: str, keypair: Keypair, amount: float = appconfig.TRADING_D def get_tokens(message): # Constants - from solders.pubkey import Pubkey TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") TRANSFER_INSTRUCTION_INDEX = 3 # Index for "transfer" instruction in SPL Token Program. @@ -1137,8 +1132,8 @@ def get_tokens(message): for index, encodedTransaction in enumerate(encodedTransactions): msg = VersionedTransaction.from_bytes( - base58.b58decode(encodedTransaction) - ).message + base58.b58decode(encodedTransaction) + ).message get_tokens(message=msg) signedTx = VersionedTransaction( msg, @@ -1148,7 +1143,7 @@ def get_tokens(message): txSignatures.append(str(signedTx.signatures[0])) jito_response = requests.post( - appconfig.RPC_URL + "/bundles", + appconfig.JITO_RPC_URL + "/bundles", headers={"Content-Type": "application/json"}, json={ "jsonrpc": "2.0", @@ -1167,25 +1162,9 @@ def get_tokens(message): except Exception as e: print(e) - - return txSignatures + return txSignatures -def study(): - from solders.pubkey import Pubkey - from solana.rpc.api import Client - import struct - program_id = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") - SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com" - client = Client(SOLANA_RPC_URL) - - - # Token Mint IDs - token_a = Pubkey.from_string("CXXsWzhbzNZLDosyebxqMtoJtnxYLRg4hTxudgoApump") - - seeds = [bytes(token_a)] - pool_address, _ = Pubkey.find_program_address(seeds, program_id) - print("Liquidity Pool Address:", pool_address) def test(): messages = [ @@ -1394,5 +1373,3 @@ async def subscribe_to_trades(): # Run the async function #asyncio.run(subscribe_to_trades()) - -#study() From b3abdb03dab00d67e648c033dfd6bfaecd88a91b Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Wed, 12 Mar 2025 13:38:30 +0100 Subject: [PATCH 079/103] returning a list of transactions is implemented --- bot/app/api/libs/utils.py | 212 ++++++++++-------- bot/app/api/models/outer_models.py | 4 +- .../api/routes/outer/request_transaction.py | 6 +- 3 files changed, 120 insertions(+), 102 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index a3c7f45..1b722c2 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -603,81 +603,78 @@ async def close_ata_transaction( tokens: list[dict], fee: float, encode_base64: bool = True -) -> str: +) -> list[str]: """ Burs all tokens from the associated token account :param associated_token_account[Pubkey]: associated token account :param token_mint[Pubkey]: tokens to get burned :param decimals[int]: The token's decimals (default 9 for SOL). - :return: [str] base64 transaction object by default. + :return: list[str] base64 transaction object by default. """ txn = None + txns = [] async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: # Will set both burn and close intructions for every ATA - burn_close_instructions = [] METRIC = {'ACCOUNTS': len(tokens), 'CLAIMED': sum(token.balance for token in tokens)} logging.info("close_ata_transaction-> METRIC: {}".format(METRIC)) + + # Prepare chunks to deal with many instructions: max 24 instructions per transaction + # Each chunk will become a transaction + burn_close_instructions = [] + chunk = [] + max_ixs_per_transaction = 5 + ghost_ixs = 4 + partners_fee_ixs = 0 + max_ixs_per_chunk = max_ixs_per_transaction - ghost_ixs - partners_fee_ixs + + burn_ix_counter = 0 + close_ix_counter = 0 + token_balance_sum = 0 for token in tokens: associated_token_account = get_associated_token_address( owner=owner, mint=Pubkey.from_string(token.token_mint) ) - # response = await client.get_account_info(associated_token_account) - # account_info = response.value - # if not account_info: - # logging.error("close_ata_transaction: Associated token account {} does not exist.".format( - # associated_token_account - # )) - # raise EntityNotFoundException( - # detail="Associated token account {} nor found.".format( - # str(associated_token_account) - # ) - # ) - - # BURN - # data = account_info.data - # if len(data) != 165: - # logging.error("close_burn_ata_instructions-> Error: Invalid data length for ATA {} for token {}".format( # noqa: 501 - # str(associated_token_account), - # token.token_mint - # )) - # raise ErrorProcessingData( - # detail="Unprocessed Token" - # ) - # ( - # mint, - # owner_data, - # amount_lamports, - # delegate_option, - # delegate, - # state, - # is_native_option, - # is_native, - # delegated_amount, - # close_authority_option, - # close_authority - # ) = struct.unpack("<32s32sQ4s32sB4sQ8s4s32s", data) amount = token.token_amount_lamports mint = token.token_mint decimals = token.decimals + token_balance_sum += token.balance - # Construct the burn instruction - params = BurnCheckedParams( - program_id=TOKEN_PROGRAM_ID, - mint=Pubkey.from_string(mint), - account=associated_token_account, - owner=owner, - amount=amount, - decimals=decimals, - signers=[owner] - ) - burn_ix = burn_checked(params=params) - # burn_ix_bytes = bytes(burn_ix) + # Construct the burn instruction only if there're tokens to burn + if amount > 0: + burn_ix_counter += 1 + params = BurnCheckedParams( + program_id=TOKEN_PROGRAM_ID, + mint=Pubkey.from_string(mint), + account=associated_token_account, + owner=owner, + amount=amount, + decimals=decimals, + signers=[owner] + ) + burn_ix = burn_checked(params=params) + + # Every Burn ix must be with the corresponding close ix in the same transaction + if chunk and len(chunk) + 1 >= max_ixs_per_chunk: + chunk_data = { + "ixs": [ix for ix in chunk], + "burned": burn_ix_counter, + "closed": close_ix_counter, + "balance": token_balance_sum + } + burn_close_instructions.append(chunk_data) + chunk = [] + burn_ix_counter = 0 + close_ix_counter = 0 + token_balance_sum = 0 + + chunk.append(burn_ix) # CLOSE # Create the close account instruction + close_ix_counter += 1 close_ix = close_account( CloseAccountParams( program_id=TOKEN_PROGRAM_ID, @@ -686,58 +683,78 @@ async def close_ata_transaction( owner=owner ) ) - # close_ix_bytes = bytes(close_ix) - - burn_close_instructions.append(burn_ix) - burn_close_instructions.append(close_ix) + chunk.append(close_ix) + + # Closing current chunk + if len(chunk) >= max_ixs_per_chunk: + chunk_data = { + "ixs": [ix for ix in chunk], + "burned": burn_ix_counter, + "closed": close_ix_counter, + "balance": token_balance_sum + } + burn_close_instructions.append(chunk_data) + chunk = [] + burn_ix_counter = 0 + close_ix_counter = 0 + token_balance_sum = 0 + + if chunk: + chunk_data = { + "ixs": [ix for ix in chunk], + "burned": burn_ix_counter, + "closed": close_ix_counter, + "balance": token_balance_sum + } + burn_close_instructions.append(chunk_data) + chunk = [] # Required instructions to set compute unit limit and price compute_unit_price_ix = set_compute_unit_price(5_000) - # compute_unit_price_ix_bytes = bytes(compute_unit_price_ix) - compute_units = calculate_compute_units(atas=len(tokens)) - compute_unit_limit_ix = set_compute_unit_limit(units=compute_units) - # compute_unit_limit_ix_bytes = bytes(compute_unit_limit_ix) + blockhash = await client.get_latest_blockhash() + recent_blockhash = blockhash.value.blockhash - # Variable fee - # Summing all ATAs balances to calculate variable fee - fee_ix_list = get_fee_instructions( - fee=fee, - balance=sum(token.balance for token in tokens), - owner=owner, - atas=len(tokens) - ) - # fee_ix_list_bytes = [bytes(fee_ix) for fee_ix in fee_ix_list] + # Got chink by chunk to create transactions + for chunk in burn_close_instructions: + compute_units = calculate_compute_units(closed=chunk["closed"], burned=chunk["burned"]) + compute_unit_limit_ix = set_compute_unit_limit(units=compute_units) - # Package all instructions as hex values from bytes into a single list - instructions = [ - compute_unit_price_ix, # Convert to hex for compatibility - compute_unit_limit_ix, - ] - instructions.extend(burn_close_instructions) - instructions.extend(fee_ix_list) + # Variable fee + # Summing all ATAs balances to calculate variable fee - blockhash = await client.get_latest_blockhash() - recent_blockhash = blockhash.value.blockhash - msg = Message.new_with_blockhash( - instructions=instructions, - payer=owner, - blockhash=recent_blockhash - ) + # TODO: include partners fee instructions + fee_ix_list = get_fee_instructions( + fee=fee, + balance=chunk["balance"], + owner=owner, + atas=chunk["closed"] + ) + # Package all instructions as hex values from bytes into a single list + instructions = [ + compute_unit_price_ix, # Convert to hex for compatibility + compute_unit_limit_ix, + ] + instructions.extend(chunk["ixs"]) + instructions.extend(fee_ix_list) + + msg = Message.new_with_blockhash( + instructions=instructions, + payer=owner, + blockhash=recent_blockhash + ) - tx = Transaction.new_unsigned(message=msg) - txn = bytes(tx) - if encode_base64: - txn = base64.b64encode(txn).decode('ascii') + tx = Transaction.new_unsigned(message=msg) + txn = { + "tx": bytes(tx), + "balance": chunk["balance"], + "tokens": chunk["closed"] + } - # from solders.message import MessageV0 - # from solders.transaction import VersionedTransaction + if encode_base64: + txn["tx"] = base64.b64encode(txn["tx"]).decode('ascii') - # tx = VersionedTransaction.populate(message=msg, signatures=[]) - # # tx = VersionedTransaction(message=msg, keypairs=[]) - # txn = bytes(tx) - # if encode_base64: - # txn = base64.b64encode(txn).decode('ascii') + txns.append(txn) except EntityNotFoundException as enfe: raise enfe @@ -745,7 +762,7 @@ async def close_ata_transaction( logging.error("request_close_ata_instruction Error: {}".format(e)) raise ErrorProcessingData(detail="Internal Server Error") - return txn + return txns def get_fee_instructions( @@ -773,18 +790,17 @@ def get_fee_instructions( return [fix_fees_ix, variable_fees_ix] -def calculate_compute_units(atas: int): +def calculate_compute_units(closed: int, burned: int): # Units consumed by each instruction burn_checked_units = 4742 close_account_units = 2916 - other_instruction_units = 1000 # Adjust based on your use case + other_instruction_units = 2000 # Adjust based on your use case # Total units required - total_units = atas * (burn_checked_units + close_account_units) + other_instruction_units + total_units = burned * burn_checked_units + closed * close_account_units + other_instruction_units # Add a buffer for safety - buffer_units = 1000 - return total_units + buffer_units + return total_units async def close_burn_ata_instructions( diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 30cf8c4..110a2b9 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -154,8 +154,8 @@ class RequestTransaction(BaseModel): class Quote(BaseModel): - quote: Optional[str] = Field( - None, description='A base64-encoded string representing the quote.' + quote: List[dict] = Field( + None, description='A list of base64-encoded strings representing the transactions.' ) diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 195cd39..6c352ac 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -23,6 +23,8 @@ async def request_close_ata_transaction( owner = body.owner fee = body.fee tokens = body.tokens + # TODO: implement partner fee and pubkey as optional + if not tokens: instructions = Quote( quote=None @@ -42,14 +44,14 @@ async def request_close_ata_transaction( )) fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] - transaction = await close_ata_transaction( + transactions = await close_ata_transaction( owner=Pubkey.from_string(owner), tokens=tokens, fee=fee ) quote = Quote( - quote=transaction + quote=transactions ) return quote except EntityNotFoundException as e: From 5caee0d8cfe180ac4743e35e9d8badbf830d9b2c Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sun, 16 Mar 2025 23:05:15 +0100 Subject: [PATCH 080/103] increasing max instructions per transaction --- bot/app/api/config/__init__.py | 2 +- bot/app/api/libs/utils.py | 2 +- bot/app/api/routes/outer/request_instructions.py | 4 ++-- bot/app/api/routes/outer/request_transaction.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 331ed1c..faeff3c 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -31,7 +31,7 @@ class AuthConfig: } GHOSTFUNDS_FIX_FEES_RECEIVER = "GhoStvfwEx5FYEX7jMEpsu6R13xJFJdTLs4BxEpB9qxQ" GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "Ghost5UYkXcgLdja6Uhyac3gTnuefrx7TuSFat5JUVdW" - BACKEND_MAX_INSTRUCTIONS = 10 + BACKEND_MAX_INSTRUCTIONS_PER_TRANSACTION = 24 MIN_TOKEN_VALUE = 0.0001 # Min value of a token to not be considered dust MAX_RETRIEVABLE_ACCOUNTS = 1100 # Safety. To avoid api server to crash diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 1b722c2..cf8a3fd 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -623,7 +623,7 @@ async def close_ata_transaction( # Each chunk will become a transaction burn_close_instructions = [] chunk = [] - max_ixs_per_transaction = 5 + max_ixs_per_transaction = appconfig.BACKEND_MAX_INSTRUCTIONS_PER_TRANSACTION ghost_ixs = 4 partners_fee_ixs = 0 max_ixs_per_chunk = max_ixs_per_transaction - ghost_ixs - partners_fee_ixs diff --git a/bot/app/api/routes/outer/request_instructions.py b/bot/app/api/routes/outer/request_instructions.py index 4d04740..868e0ee 100644 --- a/bot/app/api/routes/outer/request_instructions.py +++ b/bot/app/api/routes/outer/request_instructions.py @@ -31,8 +31,8 @@ async def request_close_ata_instructions( ) return instructions - if len(tokens) > appconfig.BACKEND_MAX_INSTRUCTIONS: - raise TooManyInstructionsException(detail="Too many instructions") + if len(tokens) > appconfig.MAX_RETRIEVABLE_ACCOUNTS: + raise TooManyInstructionsException(detail=appconfig.MAX_RETRIEVABLE_ACCOUNTS_MESSAGE) tokens = [token.model_dump() for token in tokens] diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 6c352ac..bc2e396 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -31,8 +31,8 @@ async def request_close_ata_transaction( ) return instructions - if len(tokens) > appconfig.BACKEND_MAX_INSTRUCTIONS: - raise TooManyInstructionsException(detail="Too many instructions") + if len(tokens) > appconfig.MAX_RETRIEVABLE_ACCOUNTS: + raise TooManyInstructionsException(detail=appconfig.MAX_RETRIEVABLE_ACCOUNTS_MESSAGE) last_fee = list(appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values())[-1] if fee not in appconfig.GHOSTFUNDS_FEES_PERCENTAGES.values() or fee < last_fee: From 9e038dd50137be5b12a7d40dfcafb6f6dbf6bdaf Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Sat, 29 Mar 2025 23:57:09 +0100 Subject: [PATCH 081/103] Include Strapi as a new middleware adapter. Adding referral fees --- bot/app/api/adapters/__init__.py | 0 bot/app/api/adapters/strapi_adapter.py | 37 ++++++++ bot/app/api/config/__init__.py | 4 + bot/app/api/libs/utils.py | 95 ++++++++++++++++--- .../api/routes/outer/request_transaction.py | 9 +- 5 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 bot/app/api/adapters/__init__.py create mode 100644 bot/app/api/adapters/strapi_adapter.py diff --git a/bot/app/api/adapters/__init__.py b/bot/app/api/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/app/api/adapters/strapi_adapter.py b/bot/app/api/adapters/strapi_adapter.py new file mode 100644 index 0000000..8da2c25 --- /dev/null +++ b/bot/app/api/adapters/strapi_adapter.py @@ -0,0 +1,37 @@ +import logging +import requests + +from typing import List +from api.config import appconfig + + +class Middelware: + def __init__(self): + self.base_url = appconfig.MIDDLEWARE_BASE_URL_STRAPI + self.referral_commissions_endpoint = "referral-commissions/" + + def get_remote_commissions(self, pubkey: str) -> List: + referrrals = [] + try: + response = requests.get( + url="{}{}{}".format( + self.base_url, + self.referral_commissions_endpoint, + pubkey + ), + headers={"Content-Type": "application/json"} + ) + response.raise_for_status() + referrrals = response.json() + + except Exception as e: + logging.error( + "Failed to fetch commissions from middleware {}{}{}. Error: {}".format( + self.base_url, + self.referral_commissions_endpoint, + pubkey, + str(e) + ) + ) + + return referrrals diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index faeff3c..fd84af5 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -60,5 +60,9 @@ class AuthConfig: REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = os.environ.get("REDIS_PORT", "6379") + # MIDDLEWARE + # MIDDLEWARE_BASE_URL_STRAPI = "http://localhost:1337/api/" + MIDDLEWARE_BASE_URL_STRAPI = "https://celebrinborg-86928ad1f22d.herokuapp.com/api/" + appconfig: AuthConfig = AuthConfig() diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index cf8a3fd..e5388f3 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -602,14 +602,27 @@ async def close_ata_transaction( owner: Pubkey, tokens: list[dict], fee: float, + referrals: list[dict], encode_base64: bool = True ) -> list[str]: """ - Burs all tokens from the associated token account - :param associated_token_account[Pubkey]: associated token account - :param token_mint[Pubkey]: tokens to get burned - :param decimals[int]: The token's decimals (default 9 for SOL). - :return: list[str] base64 transaction object by default. + Closes an Associated Token Account (ATA) transaction for a given owner. + + Args: + owner (Pubkey): The public key of the account owner. + tokens (list of dict): A list of dictionaries, each containing details about a token involved + in the transaction. + fee (float): The transaction fee to be applied. + referrals (list of dict): A list of dictionaries specifying commission for referrals. + encode_base64 (bool, optional): Whether to encode the transaction in base64 format. Defaults + to True. + + Returns: + list of str: A list of transaction signatures as strings. + + Raises: + ValueError: If any of the input parameters are invalid. + TransactionError: If the transaction fails to process. """ txn = None txns = [] @@ -625,9 +638,17 @@ async def close_ata_transaction( chunk = [] max_ixs_per_transaction = appconfig.BACKEND_MAX_INSTRUCTIONS_PER_TRANSACTION ghost_ixs = 4 - partners_fee_ixs = 0 + partners_fee_ixs = len(referrals) max_ixs_per_chunk = max_ixs_per_transaction - ghost_ixs - partners_fee_ixs + # Check if we're dealing with an Corporate referral if claimFunds is True + fund_claimer = [referral["pubKey"] for referral in referrals if referral["claimFunds"]] + if fund_claimer: + # The Corporate referral will receive all funds + fund_claimer = Pubkey.from_string(fund_claimer[0]) + else: + fund_claimer = owner + burn_ix_counter = 0 close_ix_counter = 0 token_balance_sum = 0 @@ -679,7 +700,7 @@ async def close_ata_transaction( CloseAccountParams( program_id=TOKEN_PROGRAM_ID, account=associated_token_account, - dest=owner, + dest=fund_claimer, owner=owner ) ) @@ -723,12 +744,12 @@ async def close_ata_transaction( # Variable fee # Summing all ATAs balances to calculate variable fee - # TODO: include partners fee instructions fee_ix_list = get_fee_instructions( fee=fee, balance=chunk["balance"], owner=owner, - atas=chunk["closed"] + atas=chunk["closed"], + referrals=referrals ) # Package all instructions as hex values from bytes into a single list instructions = [ @@ -769,9 +790,30 @@ def get_fee_instructions( fee: float, balance: float, owner: Pubkey, - atas: int + atas: int, + referrals: list[dict] ) -> list[Instruction]: - # Fix fee: we're charing a base fix fee for every ATA we're burning and closing. + """_summary_ + + Args: + fee (float): _description_ + balance (float): _description_ + owner (Pubkey): _description_ + atas (int): _description_ + referrals (list[dict]): example + [ + { + "slug": "saul", + "commission": 0.2, + "pubKey": "ECcPyowqkvKPnYTH4fnpE1sULKScbRAbw6UsU3w5Xgx", + "claimFunds": false + } + ] + + Returns: + list[Instruction]: _description_ + """ + # Fix fee: we're charging a base fix fee for every ATA we're burning and closing. lamports_to_charge = int(appconfig.GHOSTFUNDS_FIX_FEES * atas * 10**9) # Convert SOL to lamports fix_fees_params = TransferParams( from_pubkey=owner, @@ -779,15 +821,40 @@ def get_fee_instructions( lamports=lamports_to_charge ) fix_fees_ix = transfer(params=fix_fees_params) - # Variable fee - lamports_to_charge = int(balance * fee * 10**9) # Convert SOL to lamports + # Getting possible commisions to referrals + all_commissions = 0 + if referrals: + # Summing all referral commisions + all_commissions = sum(int(referral["commission"] * 10**9) for referral in referrals) / 10**9 + + # Variable fee: discounting possible referral commissions + lamports_to_charge = int(balance * fee * (1 - all_commissions) * 10**9) # Convert SOL to lamports variable_fees_params = TransferParams( from_pubkey=owner, to_pubkey=Pubkey.from_string(appconfig.GHOSTFUNDS_VARIABLE_FEES_RECEIVER), lamports=lamports_to_charge ) variable_fees_ix = transfer(params=variable_fees_params) - return [fix_fees_ix, variable_fees_ix] + fees = [fix_fees_ix, variable_fees_ix] + + # Adding transfer for referrals + referrals_commissions = [] + if referrals: + for referral in referrals: + lamports = int(balance * fee * referral["commission"] * 10**9) + # No transfer fee if commission is 0 + if lamports == 0: + continue + + fees_params = TransferParams( + from_pubkey=owner, + to_pubkey=Pubkey.from_string(referral["pubKey"]), + lamports=lamports + ) + referrals_commissions.append(transfer(params=fees_params)) + + fees.extend(referrals_commissions) + return fees def calculate_compute_units(closed: int, burned: int): diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index bc2e396..97f4407 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -3,6 +3,7 @@ from solders.pubkey import Pubkey +from api.adapters.strapi_adapter import Middelware from api.config import appconfig from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException, ErrorProcessingData from api.models.outer_models import Quote, RequestTransaction @@ -44,10 +45,16 @@ async def request_close_ata_transaction( )) fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] + # Retrieving possible referrals from middleware + middleware = Middelware() + + referrals = middleware.get_remote_commissions(pubkey=owner) + transactions = await close_ata_transaction( owner=Pubkey.from_string(owner), tokens=tokens, - fee=fee + fee=fee, + referrals=referrals ) quote = Quote( From 41246da4f5e023199702c367a6445e8d0ce81a12 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Tue, 8 Apr 2025 01:02:43 +0200 Subject: [PATCH 082/103] BUG fixed in detect_dust_token_accounts function when retrieving tokens that were missing basic information --- bot/app/api/libs/utils.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index e5388f3..b88a1f7 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -167,6 +167,7 @@ def get_token_metadata(token_address: str) -> dict: counter = 0 while True: if counter >= retries: + metadata = {} break try: @@ -187,11 +188,13 @@ def get_token_metadata(token_address: str) -> dict: continue content = response.json() + file_dict = {'uri': None, 'cdn_uri': None, 'mime': None} files = content["result"]["content"]["files"] metadata.update(files[0] if files else file_dict) metadata.update(content["result"]["content"]["metadata"]) - metadata["authority"] = content["result"]["authorities"][0]["address"] + authority = content["result"]["authorities"][0]["address"] if content["result"]["authorities"] else "" + metadata["authority"] = authority metadata["supply"] = content["result"]["token_info"]["supply"] metadata["decimals"] = content["result"]["token_info"]["decimals"] metadata["token_program"] = content["result"]["token_info"]["token_program"] @@ -206,12 +209,13 @@ def get_token_metadata(token_address: str) -> dict: metadata["insufficient_data"] = True break - except Exception: + except Exception as e: counter += 1 - logging.error("get_metadata: Error retrieving metadata for token {}. Retries {} of {}".format( + logging.error("get_metadata: Error retrieving metadata for token {}. Retries {} of {}. Exception: {}".format( token_address, counter, - retries + retries, + e )) time.sleep(counter) @@ -438,20 +442,25 @@ async def detect_dust_token_accounts( associated_token_account = account["pubkey"] metadata = get_token_metadata(token_address=mint) + if not metadata: + continue + + if "name" not in metadata: + continue token_price = metadata["price_info"]["price_per_token"] token_value = token_price * token_amount - uri = metadata["uri"] - cdn_uri = metadata["cdn_uri"] - mime = metadata["mime"] - description = metadata["description"] if "description" in metadata else "" - name = metadata["name"].strip() - symbol = metadata["symbol"].strip() - authority = metadata["authority"] - supply = metadata["supply"] - token_program = metadata["token_program"] - insufficient_data = metadata["insufficient_data"] # Non listed tokens returns a zero price + uri = metadata.get("uri", "") + cdn_uri = metadata.get("cdn_uri") + mime = metadata.get("mime") + description = metadata.get("description", "") + name = metadata.get("name", "").strip() + symbol = metadata.get("symbol", "").strip() + authority = metadata.get("authority", "") + supply = metadata.get("supply", "") + token_program = metadata.get("token_program", "") + insufficient_data = metadata.get("insufficient_data", "") # Non listed tokens returns a zero price account_output.append( { From b0ac323cd8d661fdd208ad44de0741c593b17ba8 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 01:40:50 +0200 Subject: [PATCH 083/103] MIN_TOKEN_VALUE is now lower: from 0.0001 to 0.000001 --- bot/app/api/config/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index fd84af5..56c34ee 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -33,7 +33,7 @@ class AuthConfig: GHOSTFUNDS_VARIABLE_FEES_RECEIVER = "Ghost5UYkXcgLdja6Uhyac3gTnuefrx7TuSFat5JUVdW" BACKEND_MAX_INSTRUCTIONS_PER_TRANSACTION = 24 - MIN_TOKEN_VALUE = 0.0001 # Min value of a token to not be considered dust + MIN_TOKEN_VALUE = 0.000001 # Min value of a token to not be considered dust MAX_RETRIEVABLE_ACCOUNTS = 1100 # Safety. To avoid api server to crash MAX_RETRIEVABLE_ACCOUNTS_MESSAGE = "TOO_MANY_ATAS" @@ -61,8 +61,8 @@ class AuthConfig: REDIS_PORT = os.environ.get("REDIS_PORT", "6379") # MIDDLEWARE - # MIDDLEWARE_BASE_URL_STRAPI = "http://localhost:1337/api/" - MIDDLEWARE_BASE_URL_STRAPI = "https://celebrinborg-86928ad1f22d.herokuapp.com/api/" + MIDDLEWARE_BASE_URL_STRAPI = "http://localhost:1337/api/" + # MIDDLEWARE_BASE_URL_STRAPI = "https://celebrinborg-86928ad1f22d.herokuapp.com/api/" appconfig: AuthConfig = AuthConfig() From 7986ea29ef095e516ee70f6ea7b633042fa4357f Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 01:58:57 +0200 Subject: [PATCH 084/103] deploy lambda function: now it can update with latest docker iamge and retrieve update status --- bot/app/AWS/steps.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/app/AWS/steps.md b/bot/app/AWS/steps.md index e19067c..1a1f2a7 100644 --- a/bot/app/AWS/steps.md +++ b/bot/app/AWS/steps.md @@ -73,7 +73,11 @@ docker tag ghostfunds-backend-fastapi:latest 237733826785.dkr.ecr.eu-west-1.amaz docker push 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest ## Updaing Lambda function docker image aws lambda update-function-code \ + --function-name arn:aws:lambda:eu-west-1:237733826785:function:ghostfunds-backend-fastapi-docker \ + --image-uri 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest --region eu-west-1 --output json --no-cli-pager +## Fetch update status +aws lambda get-function-configuration \ --function-name ghostfunds-backend-fastapi-docker \ - --image-uri 237733826785.dkr.ecr.eu-west-1.amazonaws.com/ghostfunds-backend-fastapi:latest - -aws lambda get-function-configuration --function-name ghostfunds-backend-fastapi-docker + --query '{LastModified: LastModified, State: State, LastUpdateStatus: LastUpdateStatus}' \ + --output json \ + --no-cli-pager From b98d7ff2134b01a7299d40b6f21c7478a7dd35cf Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 02:35:14 +0200 Subject: [PATCH 085/103] Middleware typo class and variable name solved. API points to production strapi server --- bot/app/api/adapters/strapi_adapter.py | 8 ++++---- bot/app/api/config/__init__.py | 4 ++-- bot/app/api/routes/outer/request_transaction.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/app/api/adapters/strapi_adapter.py b/bot/app/api/adapters/strapi_adapter.py index 8da2c25..c3d0689 100644 --- a/bot/app/api/adapters/strapi_adapter.py +++ b/bot/app/api/adapters/strapi_adapter.py @@ -5,13 +5,13 @@ from api.config import appconfig -class Middelware: +class Middleware: def __init__(self): self.base_url = appconfig.MIDDLEWARE_BASE_URL_STRAPI self.referral_commissions_endpoint = "referral-commissions/" def get_remote_commissions(self, pubkey: str) -> List: - referrrals = [] + referrals = [] try: response = requests.get( url="{}{}{}".format( @@ -22,7 +22,7 @@ def get_remote_commissions(self, pubkey: str) -> List: headers={"Content-Type": "application/json"} ) response.raise_for_status() - referrrals = response.json() + referrals = response.json() except Exception as e: logging.error( @@ -34,4 +34,4 @@ def get_remote_commissions(self, pubkey: str) -> List: ) ) - return referrrals + return referrals diff --git a/bot/app/api/config/__init__.py b/bot/app/api/config/__init__.py index 56c34ee..4e28270 100644 --- a/bot/app/api/config/__init__.py +++ b/bot/app/api/config/__init__.py @@ -61,8 +61,8 @@ class AuthConfig: REDIS_PORT = os.environ.get("REDIS_PORT", "6379") # MIDDLEWARE - MIDDLEWARE_BASE_URL_STRAPI = "http://localhost:1337/api/" - # MIDDLEWARE_BASE_URL_STRAPI = "https://celebrinborg-86928ad1f22d.herokuapp.com/api/" + # MIDDLEWARE_BASE_URL_STRAPI = "http://localhost:1337/api/" + MIDDLEWARE_BASE_URL_STRAPI = "https://celebrinborg-86928ad1f22d.herokuapp.com/api/" appconfig: AuthConfig = AuthConfig() diff --git a/bot/app/api/routes/outer/request_transaction.py b/bot/app/api/routes/outer/request_transaction.py index 97f4407..7e91eaa 100644 --- a/bot/app/api/routes/outer/request_transaction.py +++ b/bot/app/api/routes/outer/request_transaction.py @@ -3,7 +3,7 @@ from solders.pubkey import Pubkey -from api.adapters.strapi_adapter import Middelware +from api.adapters.strapi_adapter import Middleware from api.config import appconfig from api.handlers.exceptions import EntityNotFoundException, TooManyInstructionsException, ErrorProcessingData from api.models.outer_models import Quote, RequestTransaction @@ -46,7 +46,7 @@ async def request_close_ata_transaction( fee = appconfig.GHOSTFUNDS_FEES_PERCENTAGES[1] # Retrieving possible referrals from middleware - middleware = Middelware() + middleware = Middleware() referrals = middleware.get_remote_commissions(pubkey=owner) From 15e225135bdfa0fcce2037cfa029d9ca42027620 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 15:07:44 +0200 Subject: [PATCH 086/103] api: Adding busines logic to prevent closing and burning tokens with value if the are not dust tokens --- bot/app/api/libs/utils.py | 8 ++++++++ bot/app/api/models/outer_models.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index b88a1f7..6be5389 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -662,6 +662,14 @@ async def close_ata_transaction( close_ix_counter = 0 token_balance_sum = 0 for token in tokens: + # Business logic validation: discard tokens with value for being closed and burned. + if not token.is_dust: + logging.warning("wallet {}: token {} is marked as not being 'is_dust'. Bypassing this to preserv its value".format( + str(owner), + token.token_mint + )) + continue + associated_token_account = get_associated_token_address( owner=owner, mint=Pubkey.from_string(token.token_mint) diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 110a2b9..9b8ed36 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -132,6 +132,9 @@ class RequestTransactionToken(BaseModel): token_amount_lamports: int = Field( ..., description='Token amount in lamports' ) + is_dust: bool = Field( + False, description='States if the token is suitable for being closed and burned' + ) class RequestTransaction(BaseModel): From 70b441f776ae16e62646eaf2bb995ede9c986ee7 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 15:58:20 +0200 Subject: [PATCH 087/103] RequestTransactionToken.is_dust now is mandatory with no default value --- bot/app/api/models/outer_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/app/api/models/outer_models.py b/bot/app/api/models/outer_models.py index 9b8ed36..f3dbf4e 100644 --- a/bot/app/api/models/outer_models.py +++ b/bot/app/api/models/outer_models.py @@ -133,7 +133,7 @@ class RequestTransactionToken(BaseModel): ..., description='Token amount in lamports' ) is_dust: bool = Field( - False, description='States if the token is suitable for being closed and burned' + ..., description='States if the token is suitable for being closed and burned' ) From 6375b403cabd4aa743343729fa8d8b1538e95638 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 23:44:30 +0200 Subject: [PATCH 088/103] adding logger to utils --- bot/app/AWS/lambda_functions/Dockerfile | 31 +++++++++ .../AWS/lambda_functions/lambda_function.py | 63 +++++++++++++++++++ bot/app/AWS/lambda_functions/requirements.txt | 7 +++ bot/app/api/libs/utils.py | 12 +++- bot/config/__init__.py | 2 +- bot/main.py | 7 ++- package-lock.json | 4 +- package.json | 4 +- 8 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 bot/app/AWS/lambda_functions/Dockerfile create mode 100644 bot/app/AWS/lambda_functions/lambda_function.py create mode 100644 bot/app/AWS/lambda_functions/requirements.txt diff --git a/bot/app/AWS/lambda_functions/Dockerfile b/bot/app/AWS/lambda_functions/Dockerfile new file mode 100644 index 0000000..0c430af --- /dev/null +++ b/bot/app/AWS/lambda_functions/Dockerfile @@ -0,0 +1,31 @@ +FROM public.ecr.aws/lambda/python:3.12-arm64 + +# ENV LANG=en_US.UTF-8 +# ENV TZ=:/etc/localtime +# ENV PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin +# ENV LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib +# ENV LAMBDA_TASK_ROOT=/var/task +# ENV LAMBDA_RUNTIME_DIR=/var/runtime + +WORKDIR /var/task +# Install dependencies +RUN dnf update -y && dnf install -y zip gcc gcc-c++ make glibc-devel && dnf clean all + +COPY requirements.txt . +COPY lambda_function.py ${LAMBDA_TASK_ROOT} + +# Install Solders +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt +# RUN python -m venv .venv +# RUN source .venv/bin/activate +# RUN pip install --no-cache-dir -r requirements.txt --target python/lib/python3.12/site-packages/ +# RUN zip -r layer.zip python + +# Copy function code +# COPY lambda_function.py ${LAMBDA_TASK_ROOT} + + +# Command for Lambda +CMD ["lambda_function.lambda_handler"] +#ENTRYPOINT ["/lambda-entrypoint.sh"] \ No newline at end of file diff --git a/bot/app/AWS/lambda_functions/lambda_function.py b/bot/app/AWS/lambda_functions/lambda_function.py new file mode 100644 index 0000000..e0773e6 --- /dev/null +++ b/bot/app/AWS/lambda_functions/lambda_function.py @@ -0,0 +1,63 @@ +import asyncio +import logging + +from solders.pubkey import Pubkey +from solana.rpc.async_api import AsyncClient + + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +RPC_URL_HELIUS = "https://mainnet.helius-rpc.com/?api-key=f32b640c-6877-43e7-924b-2035b448d17e" + + +async def get_solana_balance(public_key: Pubkey) -> float: + """ + Fetches the SOL balance of a given Solana wallet address. + + :param wallet_address: The public address of the Solana wallet. + :return: Balance in SOL as a float. + """ + async with AsyncClient(RPC_URL_HELIUS) as client: + try: + # Fetch the balance (in lamports) + balance_response = await client.get_balance(public_key) + # Balance is returned in lamports (1 SOL = 10^9 lamports) + lamports = balance_response.value + sol_balance = lamports / 1e9 + return sol_balance + except Exception as e: + print(f"Error fetching balance: {e.error_msg}") + return 0.0 + + +def lambda_handler(event, context): + body = { + "owner": "4ajMNhqWCeDVJtddbNhD3ss5N6CFZ37nV9Mg7StvBHdb", + "fee": 0.045, + "tokens": [ + { + "token_mint": "G1jFTkA5XCw8b9Sut9PW6cUzaoTfhmW58fRXqa78pump", + "decimals": 6, + "balance": 0.002039 + } + ], + } + sol_balance = [] + + try: + logger.info("Running lambda function successfully!") + + public_key = Pubkey.from_string(body["owner"]) + + # Run async function synchronously + sol_balance = asyncio.run(get_solana_balance(public_key=public_key)) + + except Exception as e: + logger.error("Error starting instance: %s", e) + raise e + + return { + 'statusCode': 200, + 'body': {"sol_balance": sol_balance} + } diff --git a/bot/app/AWS/lambda_functions/requirements.txt b/bot/app/AWS/lambda_functions/requirements.txt new file mode 100644 index 0000000..5a52ce6 --- /dev/null +++ b/bot/app/AWS/lambda_functions/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.115.6 +uvicorn==0.34.0 +datamodel-code-generator==0.26.4 +solders==0.21.0 +solana==0.35.0 +mangum==0.19.0 +requests==2.32.3 \ No newline at end of file diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index 6be5389..ef27a99 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -635,11 +635,17 @@ async def close_ata_transaction( """ txn = None txns = [] + + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + console_handler = logging.StreamHandler() + logger.addHandler(console_handler) + async with AsyncClient(appconfig.RPC_URL_HELIUS) as client: try: # Will set both burn and close intructions for every ATA METRIC = {'ACCOUNTS': len(tokens), 'CLAIMED': sum(token.balance for token in tokens)} - logging.info("close_ata_transaction-> METRIC: {}".format(METRIC)) + logger.info("close_ata_transaction-> METRIC: {}".format(METRIC)) # Prepare chunks to deal with many instructions: max 24 instructions per transaction # Each chunk will become a transaction @@ -664,7 +670,7 @@ async def close_ata_transaction( for token in tokens: # Business logic validation: discard tokens with value for being closed and burned. if not token.is_dust: - logging.warning("wallet {}: token {} is marked as not being 'is_dust'. Bypassing this to preserv its value".format( + logger.warning("wallet {}: token {} is marked as not being 'is_dust'. Bypassing this to preserv its value".format( str(owner), token.token_mint )) @@ -797,7 +803,7 @@ async def close_ata_transaction( except EntityNotFoundException as enfe: raise enfe except Exception as e: - logging.error("request_close_ata_instruction Error: {}".format(e)) + logger.error("request_close_ata_instruction Error: {}".format(e)) raise ErrorProcessingData(detail="Internal Server Error") return txns diff --git a/bot/config/__init__.py b/bot/config/__init__.py index 52cee50..840cb19 100644 --- a/bot/config/__init__.py +++ b/bot/config/__init__.py @@ -29,7 +29,7 @@ class AuthConfig: FEES_BPS = float(os.environ.get("FEES", 0.0005)) * 10000 # Fees in BPS PRIVKEY = os.environ.get( "PRIVKEY", - "" + "fbsSfi48ed9fN2CYcbFB5gWZbLmgkVese85TDvcN5FzaFY5K8MKhp89exYEruGbyNGGVJqYectWocJcjFmUs9XX" ) MIN_SOL_TRADING_AMOUNT = float( os.environ.get("MIN_SOL_TRADING_AMOUNT", 0.5) diff --git a/bot/main.py b/bot/main.py index 589451a..1e989da 100644 --- a/bot/main.py +++ b/bot/main.py @@ -4,6 +4,7 @@ from module.pump import Pump, TradeRoadmap from bot.libs.utils import Trader from config import appconfig +from bot.libs.pump_buy import main as tax_collector_main async def main(): pump = Pump( @@ -17,14 +18,14 @@ async def main(): ) tasks = [ - pump.subscribe(steps=TradeRoadmap.sniper_2_detect_artifical_pump), - #scanner.subscribe(steps=TradeRoadmap.scanner) + # pump.subscribe(steps=TradeRoadmap.sniper_2_detect_artifical_pump), + scanner.subscribe(steps=TradeRoadmap.scanner), + # tax_collector_main(trades=1) ] start_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() # TODO: implement trader execution from input parameters print("Starting {} program at {}".format(Trader.sniper.value, start_time)) - await asyncio.gather(*tasks) stop_time = datetime.now().strftime(appconfig.TIME_FORMAT).lower() print("Stoping {} program at {}".format(Trader.scanner.value, stop_time)) diff --git a/package-lock.json b/package-lock.json index cc1f30b..1c7e54f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pumpdotfun-sdk", - "version": "1.3.1", + "version": "1.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pumpdotfun-sdk", - "version": "1.3.1", + "version": "1.3.2", "license": "ISC", "dependencies": { "@coral-xyz/anchor": "^0.30.1", diff --git a/package.json b/package.json index b820ef4..14d0cf4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "pumpdotfun-sdk", "version": "1.3.2", "description": "A simple SDK for interacting with pumpdotfun", - "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", "browser": "./dist/browser/index.js", @@ -48,5 +47,6 @@ "@rollup/plugin-json": "^6.1.0", "@solana/spl-token": "0.4.6", "@solana/web3.js": "^1.92.1" - } + }, + "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" } From c2d1b638a057470e0a44f3f6382f08a89b0ea017 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 23:45:42 +0200 Subject: [PATCH 089/103] adding logger to utils --- bot/app/api/libs/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/app/api/libs/utils.py b/bot/app/api/libs/utils.py index ef27a99..809175c 100644 --- a/bot/app/api/libs/utils.py +++ b/bot/app/api/libs/utils.py @@ -646,7 +646,6 @@ async def close_ata_transaction( # Will set both burn and close intructions for every ATA METRIC = {'ACCOUNTS': len(tokens), 'CLAIMED': sum(token.balance for token in tokens)} logger.info("close_ata_transaction-> METRIC: {}".format(METRIC)) - # Prepare chunks to deal with many instructions: max 24 instructions per transaction # Each chunk will become a transaction burn_close_instructions = [] From c65dc655a6657bf07079ae2fc0899ea91852f25c Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 10 Apr 2025 23:46:05 +0200 Subject: [PATCH 090/103] test From cf69466a31c392013d3688da870398c7423aa426 Mon Sep 17 00:00:00 2001 From: Saul Maldonado Date: Thu, 1 May 2025 17:16:30 +0200 Subject: [PATCH 091/103] adding telegram bot --- .vscode/launch.json | 26 ++++++ bot/news/__init__.py | 0 bot/news/check_and_run.sh | 50 +++++++++++ bot/news/main.py | 165 +++++++++++++++++++++++++++++++++++ bot/news/media/referrals.png | Bin 0 -> 1057445 bytes bot/news/os_installation.sh | 58 ++++++++++++ bot/news/requirements.txt | 2 + 7 files changed, 301 insertions(+) create mode 100644 bot/news/__init__.py create mode 100644 bot/news/check_and_run.sh create mode 100644 bot/news/main.py create mode 100644 bot/news/media/referrals.png create mode 100644 bot/news/os_installation.sh create mode 100644 bot/news/requirements.txt diff --git a/.vscode/launch.json b/.vscode/launch.json index cbcae98..f61f708 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -107,6 +107,32 @@ }, "justMyCode": false }, + { + "name": "Telegram bot", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/news/main.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, + { + "name": "X connector", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/bot/news/lib/x.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}", + "ENVIRONMENT": "DUMMY" + }, + "justMyCode": false + }, ] } \ No newline at end of file diff --git a/bot/news/__init__.py b/bot/news/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/news/check_and_run.sh b/bot/news/check_and_run.sh new file mode 100644 index 0000000..1fad225 --- /dev/null +++ b/bot/news/check_and_run.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Path to the Python script +SCRIPT_PATH="/home/ec2-user/umotc/Telegram_bot/main.py" +# Path to the Python interpreter +PYTHON_PATH="/usr/bin/python3" # Adjust if your Python interpreter is in a different location +# Process name or command to identify if the script is running +PROCESS_NAME="main.py" + +#### Log treatment +# Variables +LOG_DIR="/var/log" +LOG_FILE="check_and_run.log" +HISTORY_COUNT=4 +# Full path to the log file +LOG_PATH="$LOG_DIR/$LOG_FILE" + +# Get yesterday's date in format DDMMYYYY +YESTERDAY=$(date -d "yesterday" +"%d%m%Y") + +# Check if the log file exists +if [ -f "$LOG_PATH" ]; then + # Rename the log file with yesterday's date + sudo mv "$LOG_PATH" "$LOG_DIR/check_and_run.$YESTERDAY.log" + echo "Log file renamed to: check_and_run.$YESTERDAY.log" +else + echo "Log file $LOG_FILE does not exist." +fi + +sudo touch "$LOG_PATH" +sudo chown ec2-user:ec2-user "$LOG_PATH" +echo "New log file $LOG_FILE created" + +# Remove log files older than 4 days +LOGS_TO_DELETE=$(ls -t $LOG_DIR/check_and_run.*.log | tail -n +$(($HISTORY_COUNT + 1))) + +for log in $LOGS_TO_DELETE; do + sudo rm -f "$log" + echo "Deleted old log file: $log" +done + +echo "About to start main process" + +# Check if the script is running +if ! pgrep -f "$PROCESS_NAME" > /dev/null; then + echo "$(date): Starting $SCRIPT_PATH" >> $LOG_PATH + $PYTHON_PATH $SCRIPT_PATH >> $LOG_PATH 2>&1 & +else + echo "$(date): $SCRIPT_PATH is already running" >> $LOG_PATH +fi diff --git a/bot/news/main.py b/bot/news/main.py new file mode 100644 index 0000000..c6adf4f --- /dev/null +++ b/bot/news/main.py @@ -0,0 +1,165 @@ +from functools import partial + +import asyncio +import logging + +from telegram.ext import ( + Application, + filters, + MessageHandler +) + +from lib.chatgpt import ( + Language +) + +from lib.utils import ( + forward_message, + get_bot_channels +) + +environment = "dev" + +# Set up logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +# Telegram Bot Token +BOT_TOKEN = "7151643103:AAG2Y0bVZfMQlIYxfM8iziTwTSd7sfb1EW4" # user: @umotc_admin_bot +BOT_DEV_TOKEN = "7772271474:AAF1Ud0oZPVLFEfhNi6kmUlF34y6moscKC8" # user: @umotc_admin_dev_bot + +if environment == "dev": + BOT_TOKEN = BOT_DEV_TOKEN + +# List of Telegram channels +CHANNELS = [ + { + "source": -1002199109243, + "source_language": Language.Spanish, + "name": "UMOTC - Source", + "tag": "TEST CHANNEL", + "env": "dev", + "target_channels": [ + { + "target": -1002247367774, + "target_language": Language.English, + "with_delay": False, + "name": "OTC Financial Markets - Derniére Minute", + "enabled": True + } + ] + }, + # { + # "source": -1001615771467, + # "source_language": Language.Spanish, + # "name": "OTC Financial Markets - Último Minuto", + # "tag": "PRODUCTION CHANNEL", + # "env": "prod", + # "target_channels": [ + # { + # "target": -1002225333317, + # "target_language": Language.English, + # "with_delay": False, + # "name": "OTC Financial Markets - Last Minute", + # "enabled": True + # }, + # ] + # }, + # { + # "source": -1001791935140, + # "source_language": Language.Spanish, + # "name": "OTC Financial Markets - Pivot points", + # "tag": "PRODUCTION CHANNEL", + # "env": "prod", + # "target_channels": [ + # { + # "target": -1002196867876, + # "target_language": Language.English, + # "with_delay": False, + # "name": "OTC Financial Markets - Pivot points - English", + # "enabled": True + # }, + # ] + # }, + # { + # "source": -1001682562569, + # "source_language": Language.Spanish, + # "name": "OTC Financial Markets - Forex y Materias primas", + # "tag": "PRODUCTION CHANNEL", + # "env": "prod", + # "target_channels": [ + # { + # "target": -1002175825671, + # "target_language": Language.English, + # "with_delay": False, + # "name": "OTC Financial Markets - Forex and Commodities", + # "enabled": True + # }, + # ] + # }, + # { + # "source": -1001755306119, + # "source_language": Language.Spanish, + # "name": "OTC Financial Markets - Crypto", + # "tag": "PRODUCTION CHANNEL", + # "env": "prod", + # "target_channels": [ + # { + # "target": -1002167028599, + # "target_language": Language.English, + # "with_delay": False, + # "name": "OTC Financial Markets - Crypto - English", + # "enabled": True + # }, + # ] + # }, +] + +# Delay in seconds (15 minutes) +DELAY_SECONDS = 3 * 1 + + +async def list_chanels(): + application = Application.builder().token(BOT_TOKEN).build() + channels = await get_bot_channels( + application=application, + logger=logger + ) + print(channels) + + +def main(): + application = Application.builder().token(BOT_TOKEN).build() + + channel_list = [ch for ch in CHANNELS if ch["env"] == environment] + + for channel in channel_list: + forward_message_with_parameters = partial( + forward_message, + logger=logger, + channel=channel, + delay=DELAY_SECONDS + ) + + application.add_handler( + MessageHandler( + filters.Chat(channel['source']), + forward_message_with_parameters + ) + ) + + # application.initialize() + # application.start() + + # Start polling for updates + application.run_polling() + + print("Done") + + +if __name__ == '__main__': + # asyncio.run(list_chanels()) + asyncio.run(main()) diff --git a/bot/news/media/referrals.png b/bot/news/media/referrals.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b4f9cbeb3a229ea2543e5a6c42221f0dfcfcc7 GIT binary patch literal 1057445 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 z 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�xKj+mdAh*FDa|g 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 z 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

    D3)9GwM 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

    #j@t1^dx=CH*x`b$wn$tqGLj8Ai!o`z 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

    d42($8!W770%RwR1dz9hMBJbU8W3g^_KTXGF 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

    n(Cj=QIt7HxjYOlVD35H|%d^B3L0Nk9(Ub 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 z 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

    Ql+a2tQ%hEQR>GO4W?>_geQ-AZ=&*q1^z=g^J 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