From 339ae4b47f1dc08aeff364f612432fa35eaa9e8b Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Mon, 1 Sep 2025 07:54:48 +0700 Subject: [PATCH 1/8] fix(common): Use free Jupiter API (previous endpoint is now paid) --- libs/common/solbot_common/utils/jupiter.py | 6 ++- tests/trading/conftest.py | 59 +++++++++++++++++----- tests/trading/test_executor.py | 10 ++++ 3 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 tests/trading/test_executor.py diff --git a/libs/common/solbot_common/utils/jupiter.py b/libs/common/solbot_common/utils/jupiter.py index c1133f9..0b9073b 100644 --- a/libs/common/solbot_common/utils/jupiter.py +++ b/libs/common/solbot_common/utils/jupiter.py @@ -2,10 +2,14 @@ import httpx +#TODO: Set this up in the config +# api.jup.ag in not free anymore +JUPITER_API_URL = "https://lite-api.jup.ag" + class JupiterAPI: def __init__(self): - self.client = httpx.AsyncClient(base_url="https://api.jup.ag") + self.client = httpx.AsyncClient(base_url=JUPITER_API_URL) async def get_quote( self, diff --git a/tests/trading/conftest.py b/tests/trading/conftest.py index 1497faa..554f743 100644 --- a/tests/trading/conftest.py +++ b/tests/trading/conftest.py @@ -1,12 +1,11 @@ -from types import SimpleNamespace - import pytest +import pytest_asyncio from solbot_common.types.swap import SwapEvent from solbot_common.types.tx import TxEvent, TxType from solbot_common.utils import get_async_client -@pytest.fixture +@pytest_asyncio.fixture async def rpc_client(): """Real Async Solana RPC client shared across trading tests.""" client = get_async_client() @@ -33,6 +32,12 @@ def executor(rpc_client): #decimals=9, to_amount=4181819987502, to_decimals=6, mint='8qAbzjWBxD2kxnNwE9voR9Xkr2zT8mg1aM6ri34Jpump', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=17564489 #61, pre_token_amount=25274744460671, post_token_amount=29456564448173, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P') +#SwapEvent #2 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So1111111111111111111111111111 +#1111111111112' output_mint='6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk' amount=50000000 ui_amount=0.05 timestamp=1756531032 amount_pct=None swap_in_type='qty' priority_fee=0.0001 slippage_bps=250 by='copytrade' dynamic_slippage=False min_sl +#ippage_bps=None max_slippage_bps=None program_id=None tx_event=TxEvent(signature='3gqWLDzno5LKU1asduxDdACMbQmQFdvfwU6WJarnWT3Mcrwb73oYbmfaFaDVLFtyZhwToWi4WZEawL9y9JzdP1RA', from_amount=1090005000, from_decimals=9, to_amount=1037510604527, to_ +#decimals=6, mint='6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=1756531032, pre_token_amount=19836071332282, pos +#t_token_amount=20873581936809, program_id=None) @pytest.fixture def tx_event_from_logs() -> TxEvent: @@ -82,20 +87,46 @@ def swap_event_from_logs(tx_event_from_logs) -> SwapEvent: @pytest.fixture -def swap_event_from_logs_second(): - """DEX-aggregator style event (no specific program id).""" - return SimpleNamespace( +def tx_event_from_logs_second() -> TxEvent: + """Second TxEvent instance derived from logged example (#2).""" + return TxEvent( + signature="3gqWLDzno5LKU1asduxDdACMbQmQFdvfwU6WJarnWT3Mcrwb73oYbmfaFaDVLFtyZhwToWi4WZEawL9y9JzdP1RA", + from_amount=1090005000, + from_decimals=9, + to_amount=1037510604527, + to_decimals=6, + mint="6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk", + who="DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj", + tx_type=TxType.ADD_POSITION, + tx_direction="buy", + timestamp=1756531032, + pre_token_amount=19836071332282, + post_token_amount=20873581936809, + program_id=None, + ) + + +@pytest.fixture +def swap_event_from_logs_second(tx_event_from_logs_second) -> SwapEvent: + """Second SwapEvent matching the commented example (#2).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", swap_mode="ExactIn", input_mint="So11111111111111111111111111111111111111112", - output_mint="G1WcqfZxkZGHvPLRvbSpVDgzsWxuWxbi1GcufwcHpump", + output_mint="6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk", + amount=50000000, + ui_amount=0.05, + timestamp=1756531032, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.0001, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, program_id=None, - user_pubkey="HyygEkpVJJpuUyUYdAWQTZMwX4Ee1BS9ND7Yd2YdkWQg", - ui_amount=0.0001, - slippage_bps=100, - swap_in_type="Pct", - priority_fee=None, - amount=1000, - timestamp=0, + tx_event=tx_event_from_logs_second, ) diff --git a/tests/trading/test_executor.py b/tests/trading/test_executor.py new file mode 100644 index 0000000..549c3d6 --- /dev/null +++ b/tests/trading/test_executor.py @@ -0,0 +1,10 @@ +""" Test TradingExecutor class. This uses actual RPC client (and connects to the network)""" + +import pytest + + +@pytest.mark.asyncio +async def test_exec(executor, swap_event_from_logs_second): + sig = await executor.exec(swap_event_from_logs_second) + assert sig is not None + From ede267da30cf889a56bb81b9ef5f79c6e1f00cc7 Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Fri, 5 Sep 2025 12:40:32 +0700 Subject: [PATCH 2/8] feature(common,tests): Added is_pumpfun_token --- libs/common/solbot_common/utils/pump.py | 24 ++++++++++ tests/common/utils/test_pump_integration.py | 29 ++++++++++++ tests/common/utils/test_utils.py | 13 +++--- tests/trading/conftest.py | 49 +++++++++++++++++++++ tests/trading/test_route.py | 1 + 5 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 libs/common/solbot_common/utils/pump.py create mode 100644 tests/common/utils/test_pump_integration.py diff --git a/libs/common/solbot_common/utils/pump.py b/libs/common/solbot_common/utils/pump.py new file mode 100644 index 0000000..ec4f41f --- /dev/null +++ b/libs/common/solbot_common/utils/pump.py @@ -0,0 +1,24 @@ +import requests +from solbot_common.config import settings +from solbot_common.utils.shyft import ShyftAPI + + +async def is_pumpfun_token(mint_address): + shyft_api = ShyftAPI(settings.api.shyft_api_key) + resp = await shyft_api.get_token_info(mint_address) + metadata_uri = resp.get('metadata_uri') + if not metadata_uri: + return False + + # 2. Fetch the off-chain metadata JSON + meta_resp = requests.get(metadata_uri, timeout=10) + if meta_resp.status_code != 200: + meta_resp.raise_for_status() + + metadata = meta_resp.json() + created_on = metadata.get("createdOn") or metadata.get("created_on") + + # 3. Check if it originated from pump.fun + return isinstance(created_on, str) and "pump.fun" in created_on.lower() + + \ No newline at end of file diff --git a/tests/common/utils/test_pump_integration.py b/tests/common/utils/test_pump_integration.py new file mode 100644 index 0000000..e8b396f --- /dev/null +++ b/tests/common/utils/test_pump_integration.py @@ -0,0 +1,29 @@ +"""Test Pump.fun token detection. Integration test connects to the network.""" +import pytest +from solbot_common.config import settings +from solbot_common.utils.pump import is_pumpfun_token + +pytestmark = [pytest.mark.asyncio] #, pytest.mark.integration] + + +@pytest.fixture(autouse=True) +def _require_shyft_api_key(): + if not settings.api.shyft_api_key: + pytest.skip("Missing SHYFT API key (settings.api.shyft_api_key)") + + +@pytest.mark.parametrize( + "mint_address,expected", + [ + # Likely Pump.fun token examples + ("GFUgXbMeDnLkhZaJS3nYFqunqkFNMRo9ukhyajeXpump", True), + # Well-known non-pump tokens + ("So11111111111111111111111111111111111111112", False), # WSOL + ("7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve", False), # Meteora Token + ], +) +async def test_is_pumpfun_token_integration(mint_address, expected): + assert await is_pumpfun_token(mint_address) is expected + + + diff --git a/tests/common/utils/test_utils.py b/tests/common/utils/test_utils.py index 2c8cc18..71c6e9b 100644 --- a/tests/common/utils/test_utils.py +++ b/tests/common/utils/test_utils.py @@ -1,13 +1,11 @@ import pytest from solbot_common.constants import PUMP_FUN_PROGRAM from solbot_common.utils import validate_transaction -from solbot_common.utils.utils import ( - get_associated_bonding_curve, - get_async_client, - get_bonding_curve_account, - get_bonding_curve_pda, - get_global_account, -) +from solbot_common.utils.utils import (get_associated_bonding_curve, + get_async_client, + get_bonding_curve_account, + get_bonding_curve_pda, + get_global_account) from solders.pubkey import Pubkey @@ -155,3 +153,4 @@ async def test_validate_transaction_fail_tx_hash(): ) result = await validate_transaction(tx_hash) assert result is False + diff --git a/tests/trading/conftest.py b/tests/trading/conftest.py index 1497faa..a0266ea 100644 --- a/tests/trading/conftest.py +++ b/tests/trading/conftest.py @@ -34,6 +34,12 @@ def executor(rpc_client): #61, pre_token_amount=25274744460671, post_token_amount=29456564448173, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P') +#SwapEvent #3 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='7i +#CcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve' amount=50000000 ui_amount=0.05 timestamp=1756687287 amount_pct=None swap_in_type='qty' priority_fee=0.0001 slippage_bps=250 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='cpamdpZCGK +#Uy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG' tx_event=TxEvent(signature='4CL5kwExbe7JkFFjmGvTqdrpKhV8kjWkpZKy7ovR2GWU2dFBMJEXaBAfVBApzgAxNVKbSaNzuk7SZjnQbvwSyfZf', from_amount=5092044280, from_decimals=9, to_amount=8883051365766, to_decimals=6, mint='7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwD +#jKRMPcNdei3ve', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=1756687287, pre_token_amount=0, post_token_amount=8883051365766, program_id='cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG') + @pytest.fixture def tx_event_from_logs() -> TxEvent: """TxEvent instance based on logged output.""" @@ -99,3 +105,46 @@ def swap_event_from_logs_second(): ) +@pytest.fixture +def tx_event_from_logs_third() -> TxEvent: + """Third TxEvent instance derived from logged example (#3).""" + return TxEvent( + signature="4CL5kwExbe7JkFFjmGvTqdrpKhV8kjWkpZKy7ovR2GWU2dFBMJEXaBAfVBApzgAxNVKbSaNzuk7SZjnQbvwSyfZf", + from_amount=5092044280, + from_decimals=9, + to_amount=8883051365766, + to_decimals=6, + mint="7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve", + who="DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1756687287, + pre_token_amount=0, + post_token_amount=8883051365766, + program_id="cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", + ) + + +@pytest.fixture +def swap_event_from_logs_third(tx_event_from_logs_third) -> SwapEvent: + """Third SwapEvent matching the commented example (#3).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve", + amount=50000000, + ui_amount=0.05, + timestamp=1756687287, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.0001, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", + tx_event=tx_event_from_logs_third, + ) + diff --git a/tests/trading/test_route.py b/tests/trading/test_route.py index 6689e2d..cd1e9cc 100644 --- a/tests/trading/test_route.py +++ b/tests/trading/test_route.py @@ -13,6 +13,7 @@ @pytest.mark.parametrize("swap_event_fixture,expected_route", [ ("swap_event_from_logs", TradingRoute.PUMP), ("swap_event_from_logs_second", TradingRoute.DEX), + ("swap_event_from_logs_third", TradingRoute.DEX), ]) async def test_find_route(executor, request, swap_event_fixture, expected_route): """Test the find_trading_route method with both log examples""" From 3ba23a89a32b76daff103fd9003549f3164797de Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Sat, 6 Sep 2025 13:16:15 +0700 Subject: [PATCH 3/8] fix(trading): Use AsyncClient not requests --- app/trading/trading/executor.py | 41 ++++++++++++------------- libs/common/solbot_common/utils/pump.py | 13 ++++---- tests/trading/test_route.py | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/trading/trading/executor.py b/app/trading/trading/executor.py index eb14d60..b4930b2 100644 --- a/app/trading/trading/executor.py +++ b/app/trading/trading/executor.py @@ -6,11 +6,11 @@ from solbot_common.log import logger from solbot_common.models.tg_bot.user import User from solbot_common.types.swap import SwapEvent +from solbot_common.utils.pump import is_pumpfun_token from solbot_db.session import NEW_ASYNC_SESSION, provide_session from solders.keypair import Keypair # type: ignore from solders.signature import Signature # type: ignore from sqlmodel import select - from trading.swap import SwapDirection, SwapInType from trading.transaction import TradingRoute, TradingService @@ -50,28 +50,27 @@ async def find_route(self, swap_event: SwapEvent) -> TradingRoute: _, token_address = self._get_direction_address(swap_event) try: - is_pump_token_graduated = await self._launch_cache.is_pump_token_graduated(token_address) - logger.info(f"Pump token {token_address} is graduated: {is_pump_token_graduated}") - if program_id == PUMP_FUN_PROGRAM_ID or ( - token_address.endswith("pump") and not is_pump_token_graduated - ): - should_use_pump = True - logger.info( - f"Token {token_address} has not graduated, using Pump protocol to trade" - ) - else: - # Check if the token is launched on Raydium - pool_data = await get_preferred_pool(token_address) - if pool_data is not None: + if await is_pumpfun_token(token_address): + is_pump_token_graduated = await self._launch_cache.is_pump_token_graduated(token_address) + logger.info(f"Pump token {token_address} is graduated: {is_pump_token_graduated}") + if not is_pump_token_graduated: + should_use_pump = True logger.info( - f"Token {token_address} is trading on Raydium, using Raydium protocol to trade" + f"Token {token_address} has not graduated, using Pump protocol to trade" ) - # Program ID should be Raydium V4 when the token is launched on Raydium - if program_id != RAY_V4_PROGRAM_ID: - logger.warning("Original transaction was on a different protocol than Raydium") - # assert program_id == RAY_V4_PROGRAM_ID, "Program ID must be Raydium V4" - # 如果 token 在 Raydium 上启动,则使用 Raydium 协议进行交易 - #swap_event.program_id = RAY_V4_PROGRAM_ID + else: + # Check if the token is launched on Raydium + pool_data = await get_preferred_pool(token_address) + if pool_data is not None: + logger.info( + f"Token {token_address} is trading on Raydium, using Raydium protocol to trade" + ) + # Program ID should be Raydium V4 when the token is launched on Raydium + if program_id != RAY_V4_PROGRAM_ID: + logger.warning("Original transaction was on a different protocol than Raydium") + # assert program_id == RAY_V4_PROGRAM_ID, "Program ID must be Raydium V4" + # 如果 token 在 Raydium 上启动,则使用 Raydium 协议进行交易 + #swap_event.program_id = RAY_V4_PROGRAM_ID except Exception as e: logger.error(f"Failed graduation status check on PUMP, cause: {e}") diff --git a/libs/common/solbot_common/utils/pump.py b/libs/common/solbot_common/utils/pump.py index ec4f41f..38e77d1 100644 --- a/libs/common/solbot_common/utils/pump.py +++ b/libs/common/solbot_common/utils/pump.py @@ -1,8 +1,10 @@ -import requests +import httpx +from solbot_cache.cached import cached from solbot_common.config import settings from solbot_common.utils.shyft import ShyftAPI +@cached(ttl=None, noself=True) async def is_pumpfun_token(mint_address): shyft_api = ShyftAPI(settings.api.shyft_api_key) resp = await shyft_api.get_token_info(mint_address) @@ -10,12 +12,11 @@ async def is_pumpfun_token(mint_address): if not metadata_uri: return False - # 2. Fetch the off-chain metadata JSON - meta_resp = requests.get(metadata_uri, timeout=10) - if meta_resp.status_code != 200: + # 2. Fetch the off-chain metadata JSON asynchronously + async with httpx.AsyncClient(timeout=10.0) as client: + meta_resp = await client.get(metadata_uri) meta_resp.raise_for_status() - - metadata = meta_resp.json() + metadata = meta_resp.json() created_on = metadata.get("createdOn") or metadata.get("created_on") # 3. Check if it originated from pump.fun diff --git a/tests/trading/test_route.py b/tests/trading/test_route.py index cd1e9cc..29acdd9 100644 --- a/tests/trading/test_route.py +++ b/tests/trading/test_route.py @@ -12,7 +12,7 @@ @pytest.mark.asyncio @pytest.mark.parametrize("swap_event_fixture,expected_route", [ ("swap_event_from_logs", TradingRoute.PUMP), - ("swap_event_from_logs_second", TradingRoute.DEX), + ("swap_event_from_logs_second", TradingRoute.PUMP), ("swap_event_from_logs_third", TradingRoute.DEX), ]) async def test_find_route(executor, request, swap_event_fixture, expected_route): From c2d5c6ccc15d422be0f5e2ffed464c55dada9f3b Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Fri, 12 Sep 2025 12:05:32 +0700 Subject: [PATCH 4/8] fix(trading,tests): If program ID is known (ie not None), use Jupiter DEX --- app/trading/trading/executor.py | 13 ++++- tests/trading/conftest.py | 100 ++++++++++++++++++++++++++++++++ tests/trading/test_route.py | 2 + 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app/trading/trading/executor.py b/app/trading/trading/executor.py index b4930b2..8bd6035 100644 --- a/app/trading/trading/executor.py +++ b/app/trading/trading/executor.py @@ -50,7 +50,14 @@ async def find_route(self, swap_event: SwapEvent) -> TradingRoute: _, token_address = self._get_direction_address(swap_event) try: - if await is_pumpfun_token(token_address): + is_pump = await is_pumpfun_token(token_address) + except ValueError as e: + logger.warning(f"Failed to check if token is pumpfun token, cause: {e}. Falling back to guess.") + is_pump = token_address.endswith("pump") + + + try: + if is_pump: is_pump_token_graduated = await self._launch_cache.is_pump_token_graduated(token_address) logger.info(f"Pump token {token_address} is graduated: {is_pump_token_graduated}") if not is_pump_token_graduated: @@ -85,7 +92,9 @@ async def find_route(self, swap_event: SwapEvent) -> TradingRoute: logger.warning("Program ID is Unknown. Using Jupiter DEX aggregator to trade") trade_route = TradingRoute.DEX else: - raise ValueError(f"Program ID is not supported, {swap_event.program_id}") + logger.warning("Program ID: {}, is unkown to the bot. Using Jupiter DEX aggregator to trade", swap_event.program_id) + trade_route = TradingRoute.DEX + #raise ValueError(f"Program ID is not supported, {swap_event.program_id}") return trade_route diff --git a/tests/trading/conftest.py b/tests/trading/conftest.py index a0266ea..ab1df7e 100644 --- a/tests/trading/conftest.py +++ b/tests/trading/conftest.py @@ -40,6 +40,19 @@ def executor(rpc_client): #Uy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG' tx_event=TxEvent(signature='4CL5kwExbe7JkFFjmGvTqdrpKhV8kjWkpZKy7ovR2GWU2dFBMJEXaBAfVBApzgAxNVKbSaNzuk7SZjnQbvwSyfZf', from_amount=5092044280, from_decimals=9, to_amount=8883051365766, to_decimals=6, mint='7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwD #jKRMPcNdei3ve', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=1756687287, pre_token_amount=0, post_token_amount=8883051365766, program_id='cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG') +#SwapEvent #4 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='6n +#AvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump' amount=50000000 ui_amount=0.05 timestamp=1757642156 amount_pct=None swap_in_type='qty' priority_fee=0.002 slippage_bps=3000 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='6EF8rrecth +#R5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P' tx_event=TxEvent(signature='37URoS3xVL5BYgH7SYb7AdQz5gzAPph366ZKFP779CeiQRKbbzZnuYm1MgFaJ1ZmbPSWxLoo36USSr4hY8QSZWpX', from_amount=1424953722, from_decimals=9, to_amount=42923062985000, to_decimals=6, mint='6nAvJcCLUJKffEXDRZ4SzgHcssgZJY +#xJ6j5xbj4qpump', who='suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK', tx_type=, tx_direction='buy', timestamp=1757642156, pre_token_amount=0, post_token_amount=42923062985000, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P') + + +#SwapEvent #5 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='Co +#novv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j' amount=50000000 ui_amount=0.05 timestamp=1757651257 amount_pct=None swap_in_type='qty' priority_fee=0.002 slippage_bps=250 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='6EF8rrecthR +#5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P' tx_event=TxEvent(signature='3AA8G5B5KQa2MBHbPYcWNwzK76gdAVb2De5ooiXtKA6y2jSkFjApNHQMGoRfsHX7btrZ1g82FjxQa9nFqpcMVzN8', from_amount=95827897, from_decimals=9, to_amount=679708083051, to_decimals=6, mint='Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1 +#JGY7oEj2j', who='suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK', tx_type=, tx_direction='buy', timestamp=1757651257, pre_token_amount=30666413513140, post_token_amount=31346121596191, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF +#6P') @pytest.fixture def tx_event_from_logs() -> TxEvent: """TxEvent instance based on logged output.""" @@ -148,3 +161,90 @@ def swap_event_from_logs_third(tx_event_from_logs_third) -> SwapEvent: tx_event=tx_event_from_logs_third, ) + +@pytest.fixture +def tx_event_from_logs_fourth() -> TxEvent: + """Fourth TxEvent instance derived from logged example (#4).""" + return TxEvent( + signature="37URoS3xVL5BYgH7SYb7AdQz5gzAPph366ZKFP779CeiQRKbbzZnuYm1MgFaJ1ZmbPSWxLoo36USSr4hY8QSZWpX", + from_amount=1424953722, + from_decimals=9, + to_amount=42923062985000, + to_decimals=6, + mint="6nAvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump", + who="suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1757642156, + pre_token_amount=0, + post_token_amount=42923062985000, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + ) + + +@pytest.fixture +def swap_event_from_logs_fourth(tx_event_from_logs_fourth) -> SwapEvent: + """Fourth SwapEvent matching the commented example (#4).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="6nAvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump", + amount=50000000, + ui_amount=0.05, + timestamp=1757642156, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=3000, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + tx_event=tx_event_from_logs_fourth, + ) + + +@pytest.fixture +def tx_event_from_logs_fifth() -> TxEvent: + """Fifth TxEvent instance derived from updated logged example (#5).""" + return TxEvent( + signature="3AA8G5B5KQa2MBHbPYcWNwzK76gdAVb2De5ooiXtKA6y2jSkFjApNHQMGoRfsHX7btrZ1g82FjxQa9nFqpcMVzN8", + from_amount=95827897, + from_decimals=9, + to_amount=679708083051, + to_decimals=6, + mint="Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j", + who="suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK", + tx_type=TxType.ADD_POSITION, + tx_direction="buy", + timestamp=1757651257, + pre_token_amount=30666413513140, + post_token_amount=31346121596191, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + ) + + +@pytest.fixture +def swap_event_from_logs_fifth(tx_event_from_logs_fifth) -> SwapEvent: + """Fifth SwapEvent matching the updated commented example (#5).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j", + amount=50000000, + ui_amount=0.05, + timestamp=1757651257, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + tx_event=tx_event_from_logs_fifth, + ) \ No newline at end of file diff --git a/tests/trading/test_route.py b/tests/trading/test_route.py index 29acdd9..b8021ca 100644 --- a/tests/trading/test_route.py +++ b/tests/trading/test_route.py @@ -14,6 +14,8 @@ ("swap_event_from_logs", TradingRoute.PUMP), ("swap_event_from_logs_second", TradingRoute.PUMP), ("swap_event_from_logs_third", TradingRoute.DEX), + ("swap_event_from_logs_fourth", TradingRoute.PUMP), + ("swap_event_from_logs_fifth", TradingRoute.DEX), ]) async def test_find_route(executor, request, swap_event_fixture, expected_route): """Test the find_trading_route method with both log examples""" From 8d99cf32856bc24fa269fe687f3b0608dab98a1d Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Tue, 23 Sep 2025 09:46:36 +0700 Subject: [PATCH 5/8] chore(tests): add new test for div / bug --- tests/trading/conftest.py | 229 ++++++++++++++++++++++++++++++++- tests/trading/test_executor.py | 14 ++ 2 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 tests/trading/test_executor.py diff --git a/tests/trading/conftest.py b/tests/trading/conftest.py index 1497faa..2cae252 100644 --- a/tests/trading/conftest.py +++ b/tests/trading/conftest.py @@ -33,6 +33,38 @@ def executor(rpc_client): #decimals=9, to_amount=4181819987502, to_decimals=6, mint='8qAbzjWBxD2kxnNwE9voR9Xkr2zT8mg1aM6ri34Jpump', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=17564489 #61, pre_token_amount=25274744460671, post_token_amount=29456564448173, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P') +#SwapEvent #2 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So1111111111111111111111111111 +#1111111111112' output_mint='6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk' amount=50000000 ui_amount=0.05 timestamp=1756531032 amount_pct=None swap_in_type='qty' priority_fee=0.0001 slippage_bps=250 by='copytrade' dynamic_slippage=False min_sl +#ippage_bps=None max_slippage_bps=None program_id=None tx_event=TxEvent(signature='3gqWLDzno5LKU1asduxDdACMbQmQFdvfwU6WJarnWT3Mcrwb73oYbmfaFaDVLFtyZhwToWi4WZEawL9y9JzdP1RA', from_amount=1090005000, from_decimals=9, to_amount=1037510604527, to_ +#decimals=6, mint='6NqXBdXA38ZXEGU7nGfp9Fr4JSTKgtfZDqZit91Dbonk', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=1756531032, pre_token_amount=19836071332282, pos +#t_token_amount=20873581936809, program_id=None) + +#SwapEvent #3 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='7i +#CcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve' amount=50000000 ui_amount=0.05 timestamp=1756687287 amount_pct=None swap_in_type='qty' priority_fee=0.0001 slippage_bps=250 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='cpamdpZCGK +#Uy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG' tx_event=TxEvent(signature='4CL5kwExbe7JkFFjmGvTqdrpKhV8kjWkpZKy7ovR2GWU2dFBMJEXaBAfVBApzgAxNVKbSaNzuk7SZjnQbvwSyfZf', from_amount=5092044280, from_decimals=9, to_amount=8883051365766, to_decimals=6, mint='7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwD +#jKRMPcNdei3ve', who='DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj', tx_type=, tx_direction='buy', timestamp=1756687287, pre_token_amount=0, post_token_amount=8883051365766, program_id='cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG') + +#SwapEvent #4 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='6n +#AvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump' amount=50000000 ui_amount=0.05 timestamp=1757642156 amount_pct=None swap_in_type='qty' priority_fee=0.002 slippage_bps=3000 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='6EF8rrecth +#R5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P' tx_event=TxEvent(signature='37URoS3xVL5BYgH7SYb7AdQz5gzAPph366ZKFP779CeiQRKbbzZnuYm1MgFaJ1ZmbPSWxLoo36USSr4hY8QSZWpX', from_amount=1424953722, from_decimals=9, to_amount=42923062985000, to_decimals=6, mint='6nAvJcCLUJKffEXDRZ4SzgHcssgZJY +#xJ6j5xbj4qpump', who='suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK', tx_type=, tx_direction='buy', timestamp=1757642156, pre_token_amount=0, post_token_amount=42923062985000, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P') + +#SwapEvent #5 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So11111111111111111111111111111111111111112' output_mint='Co +#novv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j' amount=50000000 ui_amount=0.05 timestamp=1757651257 amount_pct=None swap_in_type='qty' priority_fee=0.002 slippage_bps=250 by='copytrade' dynamic_slippage=False min_slippage_bps=None max_slippage_bps=None program_id='6EF8rrecthR +#5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P' tx_event=TxEvent(signature='3AA8G5B5KQa2MBHbPYcWNwzK76gdAVb2De5ooiXtKA6y2jSkFjApNHQMGoRfsHX7btrZ1g82FjxQa9nFqpcMVzN8', from_amount=95827897, from_decimals=9, to_amount=679708083051, to_decimals=6, mint='Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1 +#JGY7oEj2j', who='suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK', tx_type=, tx_direction='buy', timestamp=1757651257, pre_token_amount=30666413513140, post_token_amount=31346121596191, program_id='6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF +#6P') + +#SwapEvent #6 +#user_pubkey='5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa' swap_mode='ExactIn' input_mint='So1111111111111111111111111111 +#1111111111112' output_mint='2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump' amount=50000000 ui_amount=0.05 timestamp=1758522230 amount_pct=None swap_in_type='qty' priority_fee=0.002 slippage_bps=250 by='copytrade' dynamic_slippage=False min_sli +#ppage_bps=None max_slippage_bps=None program_id=None tx_event=TxEvent(signature='2cbwMNFKr5zgNMJAWx1PczcurPZpP2aMT2urDzJVJJhznNy68jjGmca8GE8EVQQ5HU8xSkCFtjud3MJUBiojwpZZ', from_amount=3000025000, from_decimals=9, to_amount=29165361138622, to_ +#decimals=6, mint='2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump', who='8rvAsDKeAcEjEkiZMug9k8v1y8mW6gQQiMobd89Uy7qR', tx_type=, tx_direction='buy', timestamp=1758522230, pre_token_amount=0, post_token_amo +#unt=29165361138622, program_id=None) @pytest.fixture def tx_event_from_logs() -> TxEvent: @@ -89,13 +121,196 @@ def swap_event_from_logs_second(): input_mint="So11111111111111111111111111111111111111112", output_mint="G1WcqfZxkZGHvPLRvbSpVDgzsWxuWxbi1GcufwcHpump", program_id=None, - user_pubkey="HyygEkpVJJpuUyUYdAWQTZMwX4Ee1BS9ND7Yd2YdkWQg", - ui_amount=0.0001, - slippage_bps=100, - swap_in_type="Pct", - priority_fee=None, - amount=1000, - timestamp=0, + tx_event=tx_event_from_logs_second, + ) + + +@pytest.fixture +def tx_event_from_logs_third() -> TxEvent: + """Third TxEvent instance derived from logged example (#3).""" + return TxEvent( + signature="4CL5kwExbe7JkFFjmGvTqdrpKhV8kjWkpZKy7ovR2GWU2dFBMJEXaBAfVBApzgAxNVKbSaNzuk7SZjnQbvwSyfZf", + from_amount=5092044280, + from_decimals=9, + to_amount=8883051365766, + to_decimals=6, + mint="7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve", + who="DfMxre4cKmvogbLrPigxmibVTTQDuzjdXojWzjCXXhzj", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1756687287, + pre_token_amount=0, + post_token_amount=8883051365766, + program_id="cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", + ) + + +@pytest.fixture +def swap_event_from_logs_third(tx_event_from_logs_third) -> SwapEvent: + """Third SwapEvent matching the commented example (#3).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="7iCcjyC8NWooMmpjwaAuFPG7ZeGvSwDjKRMPcNdei3ve", + amount=50000000, + ui_amount=0.05, + timestamp=1756687287, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.0001, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", + tx_event=tx_event_from_logs_third, + ) + + +@pytest.fixture +def tx_event_from_logs_fourth() -> TxEvent: + """Fourth TxEvent instance derived from logged example (#4).""" + return TxEvent( + signature="37URoS3xVL5BYgH7SYb7AdQz5gzAPph366ZKFP779CeiQRKbbzZnuYm1MgFaJ1ZmbPSWxLoo36USSr4hY8QSZWpX", + from_amount=1424953722, + from_decimals=9, + to_amount=42923062985000, + to_decimals=6, + mint="6nAvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump", + who="suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1757642156, + pre_token_amount=0, + post_token_amount=42923062985000, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + ) + + +@pytest.fixture +def swap_event_from_logs_fourth(tx_event_from_logs_fourth) -> SwapEvent: + """Fourth SwapEvent matching the commented example (#4).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="6nAvJcCLUJKffEXDRZ4SzgHcssgZJYxJ6j5xbj4qpump", + amount=50000000, + ui_amount=0.05, + timestamp=1757642156, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=3000, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + tx_event=tx_event_from_logs_fourth, + ) + + +@pytest.fixture +def tx_event_from_logs_fifth() -> TxEvent: + """Fifth TxEvent instance derived from updated logged example (#5).""" + return TxEvent( + signature="3AA8G5B5KQa2MBHbPYcWNwzK76gdAVb2De5ooiXtKA6y2jSkFjApNHQMGoRfsHX7btrZ1g82FjxQa9nFqpcMVzN8", + from_amount=95827897, + from_decimals=9, + to_amount=679708083051, + to_decimals=6, + mint="Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j", + who="suqh5sHtr8HyJ7q8scBimULPkPpA557prMG47xCHQfK", + tx_type=TxType.ADD_POSITION, + tx_direction="buy", + timestamp=1757651257, + pre_token_amount=30666413513140, + post_token_amount=31346121596191, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + ) + + +@pytest.fixture +def swap_event_from_logs_fifth(tx_event_from_logs_fifth) -> SwapEvent: + """Fifth SwapEvent matching the updated commented example (#5).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="Conovv7mKcmj1UnPSccVoUVqPZMZqXDATJ1JGY7oEj2j", + amount=50000000, + ui_amount=0.05, + timestamp=1757651257, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", + tx_event=tx_event_from_logs_fifth, + ) + + + +@pytest.fixture +def tx_event_from_logs_sixth() -> TxEvent: + """Sixth TxEvent instance derived from updated logged example (#6).""" + return TxEvent( + signature="2cbwMNFKr5zgNMJAWx1PczcurPZpP2aMT2urDzJVJJhznNy68jjGmca8GE8EVQQ5HU8xSkCFtjud3MJUBiojwpZZ", + from_amount=3000025000, + from_decimals=9, + to_amount=29165361138622, + to_decimals=6, + mint="2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump", + who="8rvAsDKeAcEjEkiZMug9k8v1y8mW6gQQiMobd89Uy7qR", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1758522230, + pre_token_amount=0, + post_token_amount=29165361138622, + program_id=None, + ) + + + +@pytest.fixture +def swap_event_from_logs_sixth(tx_event_from_logs_sixth) -> SwapEvent: + """Sixth SwapEvent matching the updated commented example (#6).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump", + amount=50000000, + ui_amount=0.05, + timestamp=1758522230, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id=None, + tx_event=tx_event_from_logs_sixth, ) +@pytest.fixture +def swapper_for_sixth(rpc_client): + """Swapper instance for SwapEvent #6 via TradingService.use_route.""" + from app.trading.trading.transaction import TradingRoute, TradingService + + service = TradingService(rpc_client) + # For #6, route via Pump (token ends with 'pump' and program_id is None) + return service.use_route(TradingRoute.PUMP) + + + \ No newline at end of file diff --git a/tests/trading/test_executor.py b/tests/trading/test_executor.py new file mode 100644 index 0000000..1ab0932 --- /dev/null +++ b/tests/trading/test_executor.py @@ -0,0 +1,14 @@ +""" Test TradingExecutor class. This uses actual RPC client (and connects to the network)""" + +import pytest + + +@pytest.mark.asyncio +async def test_exec_second(executor, swap_event_from_logs_second): + sig = await executor.exec(swap_event_from_logs_second) + assert sig is not None + +@pytest.mark.asyncio +async def test_exec_sixth(executor, swap_event_from_logs_sixth): + sig = await executor.exec(swap_event_from_logs_sixth) + assert sig is not None \ No newline at end of file From 02cdc234a4f19727c6c2cd205065b672e2b595f0 Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Sun, 12 Oct 2025 07:51:09 +0700 Subject: [PATCH 6/8] fix(tests): recovered lost changes (git mishaps) --- tests/trading/conftest.py | 58 +++++++++++++++++++++++++++++++++- tests/trading/test_executor.py | 6 +++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/tests/trading/conftest.py b/tests/trading/conftest.py index 2a1b92f..2d515b2 100644 --- a/tests/trading/conftest.py +++ b/tests/trading/conftest.py @@ -278,4 +278,60 @@ def swap_event_from_logs_fifth(tx_event_from_logs_fifth) -> SwapEvent: max_slippage_bps=None, program_id="6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", tx_event=tx_event_from_logs_fifth, - ) \ No newline at end of file + ) + + +@pytest.fixture +def tx_event_from_logs_sixth() -> TxEvent: + """Sixth TxEvent instance derived from updated logged example (#6).""" + return TxEvent( + signature="2cbwMNFKr5zgNMJAWx1PczcurPZpP2aMT2urDzJVJJhznNy68jjGmca8GE8EVQQ5HU8xSkCFtjud3MJUBiojwpZZ", + from_amount=3000025000, + from_decimals=9, + to_amount=29165361138622, + to_decimals=6, + mint="2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump", + who="8rvAsDKeAcEjEkiZMug9k8v1y8mW6gQQiMobd89Uy7qR", + tx_type=TxType.OPEN_POSITION, + tx_direction="buy", + timestamp=1758522230, + pre_token_amount=0, + post_token_amount=29165361138622, + program_id=None, + ) + + + +@pytest.fixture +def swap_event_from_logs_sixth(tx_event_from_logs_sixth) -> SwapEvent: + """Sixth SwapEvent matching the updated commented example (#6).""" + return SwapEvent( + user_pubkey="5b9tuvErmHAXpfGNv4wyRDQx6mLhYp4tKry52gxhToBa", + swap_mode="ExactIn", + input_mint="So11111111111111111111111111111111111111112", + output_mint="2vdx1WyotkxA3UKc6VWnEAYobhL6iWpqj55N6g4hpump", + amount=50000000, + ui_amount=0.05, + timestamp=1758522230, + amount_pct=None, + swap_in_type="qty", + priority_fee=0.002, + slippage_bps=250, + by="copytrade", + dynamic_slippage=False, + min_slippage_bps=None, + max_slippage_bps=None, + program_id=None, + tx_event=tx_event_from_logs_sixth, + ) + + +@pytest.fixture +def swapper_for_sixth(rpc_client): + """Swapper instance for SwapEvent #6 via TradingService.use_route.""" + from app.trading.trading.transaction import TradingRoute, TradingService + + service = TradingService(rpc_client) + # For #6, route via Pump (token ends with 'pump' and program_id is None) + return service.use_route(TradingRoute.PUMP) + diff --git a/tests/trading/test_executor.py b/tests/trading/test_executor.py index 549c3d6..1ab0932 100644 --- a/tests/trading/test_executor.py +++ b/tests/trading/test_executor.py @@ -4,7 +4,11 @@ @pytest.mark.asyncio -async def test_exec(executor, swap_event_from_logs_second): +async def test_exec_second(executor, swap_event_from_logs_second): sig = await executor.exec(swap_event_from_logs_second) assert sig is not None +@pytest.mark.asyncio +async def test_exec_sixth(executor, swap_event_from_logs_sixth): + sig = await executor.exec(swap_event_from_logs_sixth) + assert sig is not None \ No newline at end of file From 75cbb15780fe54bf52951c37b7d756bafb52e443 Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Tue, 14 Oct 2025 11:29:24 +0700 Subject: [PATCH 7/8] fix(cache,tests): is_pump_token_graduated: only cache True values which will never change --- libs/cache/solbot_cache/launch.py | 7 +++++-- tests/trading/test_route.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/cache/solbot_cache/launch.py b/libs/cache/solbot_cache/launch.py index 230e920..1db711f 100644 --- a/libs/cache/solbot_cache/launch.py +++ b/libs/cache/solbot_cache/launch.py @@ -1,6 +1,7 @@ from solbot_common.constants import PUMP_FUN_PROGRAM from solbot_common.log import logger -from solbot_common.utils.utils import get_async_client, get_bonding_curve_account +from solbot_common.utils.utils import (get_async_client, + get_bonding_curve_account) from solders.pubkey import Pubkey from .cached import cached @@ -20,12 +21,14 @@ def __init__(self) -> None: def __repr__(self) -> str: return "LaunchCache()" - @cached(ttl=None, noself=True) + @cached(ttl=None, noself=True, skip_cache_func=lambda result: not result) async def is_pump_token_graduated(self, mint: str | Pubkey) -> bool: """Examine if a Pump.fun token has graduated. By checking if the token has completed the bonding curve. + Only cache the result upon graduation. + Args: mint (str): 代币的 mint 地址 diff --git a/tests/trading/test_route.py b/tests/trading/test_route.py index d4c1844..af8a5b5 100644 --- a/tests/trading/test_route.py +++ b/tests/trading/test_route.py @@ -12,10 +12,11 @@ @pytest.mark.asyncio @pytest.mark.parametrize("swap_event_fixture,expected_route", [ ("swap_event_from_logs", TradingRoute.PUMP), - ("swap_event_from_logs_second", TradingRoute.PUMP), + ("swap_event_from_logs_second", TradingRoute.DEX), ("swap_event_from_logs_third", TradingRoute.DEX), ("swap_event_from_logs_fourth", TradingRoute.PUMP), ("swap_event_from_logs_fifth", TradingRoute.DEX), + ("swap_event_from_logs_sixth", TradingRoute.PUMP), ]) async def test_find_route(executor, request, swap_event_fixture, expected_route): """Test the find_trading_route method with both log examples""" From c604e39c852adc034f01171a86cdfe33b908499c Mon Sep 17 00:00:00 2001 From: James Hirschorn Date: Tue, 14 Oct 2025 11:30:06 +0700 Subject: [PATCH 8/8] chore(common): remove irrelevant noself arg --- libs/common/solbot_common/utils/pump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/solbot_common/utils/pump.py b/libs/common/solbot_common/utils/pump.py index 38e77d1..da88481 100644 --- a/libs/common/solbot_common/utils/pump.py +++ b/libs/common/solbot_common/utils/pump.py @@ -4,7 +4,7 @@ from solbot_common.utils.shyft import ShyftAPI -@cached(ttl=None, noself=True) +@cached(ttl=None) async def is_pumpfun_token(mint_address): shyft_api = ShyftAPI(settings.api.shyft_api_key) resp = await shyft_api.get_token_info(mint_address)