fix(trading): refresh BC state on extreme_fast_mode buy + letsbonk pumpportal token-program default

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) <noreply@anthropic.com>
This commit is contained in:
Anton Sauchyk
2026-04-28 19:12:37 +02:00
parent 2ece1c9a2d
commit 20e3e163d8
2 changed files with 64 additions and 0 deletions
@@ -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:
+57
View File
@@ -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)