From 20e3e163d80dc52422bf12451c0b9a454c4e6dfe Mon Sep 17 00:00:00 2001 From: Anton Sauchyk Date: Tue, 28 Apr 2026 19:12:37 +0200 Subject: [PATCH] fix(trading): refresh BC state on extreme_fast_mode buy + letsbonk pumpportal token-program default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes from mainnet validation of geyser/logs/blocks/pumpportal: 1. extreme_fast_mode buys previously skipped the BC fetch entirely. After the 2026-04-28 cutover, fee_recipient (mayhem mode) and creator_vault (PFEE-delegated BC.creator) cannot be derived from listener-side data alone — the program rejects with NotAuthorized (0x1770) / ConstraintSeeds (0x7d6). Add a short retry loop that fetches the curve state once before submitting the buy, so the instruction builder picks the correct fee_recipient and creator_vault. The ~150ms cost is small relative to a failed tx and 5s sell wait. 2. LetsBonkPumpPortalProcessor returned TokenInfo without token_program_id. The universal builder then defaulted to Token-2022 and the buy failed with IncorrectProgramId at GetAccountDataSize on the user-ATA create ix. LetsBonk tokens are predominantly regular SPL Token, so default to TOKEN_PROGRAM in the processor. Note: the pumpportal listener still has unrelated data-race issues (the PumpPortal stream sometimes fires before the on-chain BC is RPC-readable, and bondingCurveKey occasionally references a different mint than the detected token). These predate the 2026-04-28 cutover and need a separate fix (e.g., verifying mint+BC consistency on the tx side before submit). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../letsbonk/pumpportal_processor.py | 7 +++ src/trading/platform_aware.py | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/platforms/letsbonk/pumpportal_processor.py b/src/platforms/letsbonk/pumpportal_processor.py index f29d9aa..f5e23c8 100644 --- a/src/platforms/letsbonk/pumpportal_processor.py +++ b/src/platforms/letsbonk/pumpportal_processor.py @@ -5,6 +5,7 @@ File: src/platforms/letsbonk/pumpportal_processor.py from solders.pubkey import Pubkey +from core.pubkeys import SystemAddresses from interfaces.core import Platform, TokenInfo from platforms.letsbonk.address_provider import LetsBonkAddressProvider from utils.logger import get_logger @@ -113,6 +114,12 @@ class LetsBonkPumpPortalProcessor: quote_vault=quote_vault, user=user, creator=creator, + # PumpPortal data does not carry the token program. LetsBonk + # tokens are predominantly regular SPL (Token), not Token-2022. + # Default here so the ATA-create ix uses the correct program; + # without this the universal builder defaults to Token-2022 and + # the buy fails with IncorrectProgramId on GetAccountDataSize. + token_program_id=SystemAddresses.TOKEN_PROGRAM, ) except Exception: diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 162dc77..bb8df2c 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -3,6 +3,8 @@ Platform-aware trader implementations that use the interface system. Final cleanup removing all platform-specific hardcoding. """ +import asyncio + from solders.pubkey import Pubkey from core.client import SolanaClient @@ -61,6 +63,61 @@ class PlatformAwareBuyer(Trader): # Skip the wait and directly calculate the amount token_amount = self.extreme_fast_token_amount token_price_sol = self.amount / token_amount if token_amount > 0 else 0 + # Even in extreme_fast_mode, refresh mayhem/cashback/creator from + # chain — listeners (especially pumpportal) often don't carry + # these, and the program rejects with NotAuthorized (0x1770) / + # ConstraintSeeds (0x7d6) when fee_recipient or creator_vault + # is wrong. PumpPortal often notifies before the BC account is + # readable, so retry briefly. One handful of RPC calls is cheap + # relative to a failed buy. + try: + pool_address = self._get_pool_address( + token_info, address_provider + ) + pool_state = None + last_err: Exception | None = None + # Up to ~6s of retries — pumpportal frequently fires before + # the BC account is RPC-readable. Without a settled BC the + # bot can't pick the right fee_recipient or creator_vault. + for attempt in range(10): + try: + pool_state = await curve_manager.get_pool_state( + pool_address + ) + break + except Exception as inner: # noqa: BLE001 + last_err = inner + await asyncio.sleep(min(0.3 + 0.3 * attempt, 1.5)) + if pool_state is None: + raise last_err or RuntimeError( + "pool_state unavailable after retries" + ) + token_info.is_mayhem_mode = pool_state.get( + "is_mayhem_mode", token_info.is_mayhem_mode + ) + token_info.is_cashback_coin = pool_state.get( + "is_cashback_coin", token_info.is_cashback_coin + ) + fresh_creator = pool_state.get("creator") + if fresh_creator and hasattr( + address_provider, "derive_creator_vault" + ): + from solders.pubkey import Pubkey as _Pubkey + + new_creator = ( + _Pubkey.from_string(fresh_creator) + if isinstance(fresh_creator, str) + else fresh_creator + ) + token_info.creator = new_creator + token_info.creator_vault = ( + address_provider.derive_creator_vault(new_creator) + ) + except Exception as e: # noqa: BLE001 + logger.warning( + f"extreme_fast_mode buy: could not refresh curve flags " + f"({e}); proceeding with token_info defaults" + ) else: # Get pool address based on platform using platform-agnostic method pool_address = self._get_pool_address(token_info, address_provider)