mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-24 20:58:05 +00:00
Extreme-fast mode submits with no RPC calls between detection and buy when the listener parsed the on-chain CreateEvent (geyser/logs/blocks): TokenInfo.state_from_event carries the canonical creator, mayhem/cashback flags and quote_mint, so the pre-buy curve refresh is skipped. The geyser listener now delegates to the event-first parser instead of inlining instruction decoding, which also catches create_v2 transactions that omit the trailing is_cashback_enabled byte. Listeners without event data (pumpportal) keep the refresh, hardened per issue #170: bonding curve derived from the mint instead of the payload's bondingCurveKey, curve and mint read in one slot-consistent getMultipleAccounts (correcting the guessed token program), and an unreadable curve now skips the token within trade.curve_refresh_budget instead of submitting a buy built from guessed accounts. Machine-checked by verify_extreme_fast_zero_rpc.py and verify_pumpportal_buy_path.py; validated live via simulate_bot_buy_path on both geyser and pumpportal paths. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bc59a2257d
commit
7bc6f25508
@@ -98,6 +98,8 @@ async def start_bot(config_path: str):
|
||||
# Extreme fast mode settings
|
||||
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
|
||||
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
|
||||
curve_refresh_budget=cfg["trade"].get("curve_refresh_budget", 2.0),
|
||||
trust_create_event=cfg["trade"].get("trust_create_event", True),
|
||||
# Quote asset configuration (pump.fun non-SOL pairs)
|
||||
quote_amounts=cfg["trade"].get("quote_amounts"),
|
||||
allowed_quote_mints=cfg["filters"].get("allowed_quote_mints"),
|
||||
|
||||
+25
-1
@@ -175,6 +175,30 @@ class SolanaClient:
|
||||
raise ValueError(f"Account {pubkey} not found")
|
||||
return response.value
|
||||
|
||||
async def get_multiple_accounts(
|
||||
self, pubkeys: list[Pubkey], commitment: str | None = None
|
||||
) -> list[Any]:
|
||||
"""Get several accounts in one slot-consistent RPC round trip.
|
||||
|
||||
A single getMultipleAccounts response is served by one node at one
|
||||
slot, unlike back-to-back get_account_info calls which a load-balanced
|
||||
endpoint may serve from nodes seconds apart (issue #170).
|
||||
|
||||
Args:
|
||||
pubkeys: Public keys of the accounts
|
||||
commitment: Optional commitment override (default "confirmed")
|
||||
|
||||
Returns:
|
||||
One entry per pubkey, in order; None for accounts that don't exist
|
||||
"""
|
||||
await self._rate_limiter.acquire()
|
||||
client = await self.get_client()
|
||||
kwargs: dict[str, Any] = {"encoding": "base64"}
|
||||
if commitment is not None:
|
||||
kwargs["commitment"] = commitment
|
||||
response = await client.get_multiple_accounts(pubkeys, **kwargs)
|
||||
return list(response.value)
|
||||
|
||||
async def get_token_account_balance(
|
||||
self, token_account: Pubkey, commitment: str = "confirmed"
|
||||
) -> int:
|
||||
@@ -630,7 +654,7 @@ class SolanaClient:
|
||||
# timeout fires, and it is not an aiohttp.ClientError — without it
|
||||
# here every RPC timeout propagated out of post_rpc unretried and
|
||||
# crashed the caller with an exception whose str() is empty.
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
error_attempts += 1
|
||||
if error_attempts >= max_retries:
|
||||
logger.exception(
|
||||
|
||||
@@ -56,6 +56,14 @@ class TokenInfo:
|
||||
quote_token_program_id: Pubkey | None = None
|
||||
virtual_quote_reserves: int | None = None
|
||||
|
||||
# True when creator, mayhem/cashback flags and quote_mint were read from
|
||||
# the on-chain CreateEvent (canonical at create time), letting
|
||||
# extreme_fast_mode skip the pre-buy curve refresh entirely — zero RPC
|
||||
# calls between detection and submission. Listeners that guess any of
|
||||
# these (pumpportal) or read them from user-supplied instruction args
|
||||
# must leave it False.
|
||||
state_from_event: bool = False
|
||||
|
||||
# Metadata
|
||||
creation_timestamp: float | None = None
|
||||
additional_data: dict[str, Any] | None = None
|
||||
|
||||
@@ -181,35 +181,23 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def _process_update(self, update) -> TokenInfo | None:
|
||||
"""Process a Geyser update and extract token creation info."""
|
||||
"""Process a Geyser update and extract token creation info.
|
||||
|
||||
Delegates to each platform parser's geyser method rather than decoding
|
||||
instructions here: the parser prefers the CreateEvent from
|
||||
meta.log_messages, which carries the canonical creator (instruction
|
||||
args.creator is user-supplied) and marks the TokenInfo
|
||||
state_from_event so extreme_fast_mode can buy with zero RPC calls.
|
||||
Each parser filters on its own program id internally.
|
||||
"""
|
||||
try:
|
||||
if not update.HasField("transaction"):
|
||||
return None
|
||||
|
||||
tx = update.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Check which platform this instruction belongs to
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = Pubkey.from_bytes(msg.account_keys[program_idx])
|
||||
|
||||
# Find the matching platform parser
|
||||
for platform, parser in self.platform_parsers.items():
|
||||
if program_id == parser.get_program_id():
|
||||
# Use the platform's event parser
|
||||
token_info = parser.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
for parser in self.platform_parsers.values():
|
||||
token_info = parser.parse_token_creation_from_geyser(update)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -74,6 +74,43 @@ class PumpFunCurveManager(CurveManager):
|
||||
logger.exception("Failed to get curve state")
|
||||
raise ValueError(f"Invalid bonding curve state: {e!s}")
|
||||
|
||||
async def get_pool_state_and_token_program(
|
||||
self, pool_address: Pubkey, mint: Pubkey, commitment: str | None = None
|
||||
) -> tuple[dict[str, Any], Pubkey | None]:
|
||||
"""Read curve state and the mint's owning token program together.
|
||||
|
||||
One getMultipleAccounts round trip, so both values come from the same
|
||||
node and slot. Listeners that don't carry the token program (pumpportal
|
||||
guesses Token-2022) can be corrected from the mint account's owner
|
||||
without a second, possibly inconsistent read (issue #170).
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
mint: Token mint whose owner identifies the token program
|
||||
commitment: Optional commitment override (see get_pool_state)
|
||||
|
||||
Returns:
|
||||
Tuple of (decoded curve state, token program id or None if the
|
||||
mint account was not readable)
|
||||
|
||||
Raises:
|
||||
ValueError: If the bonding curve account is missing or undecodable
|
||||
"""
|
||||
try:
|
||||
curve_account, mint_account = await self.client.get_multiple_accounts(
|
||||
[pool_address, mint], commitment=commitment
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to read curve and mint accounts")
|
||||
raise ValueError(f"Invalid bonding curve state: {e!s}") from e # noqa: TRY003
|
||||
|
||||
if curve_account is None or not curve_account.data:
|
||||
raise ValueError(f"No data in bonding curve account {pool_address}") # noqa: TRY003
|
||||
|
||||
curve_state_data = self._decode_curve_state_with_idl(curve_account.data)
|
||||
token_program = mint_account.owner if mint_account is not None else None
|
||||
return curve_state_data, token_program
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from bonding curve state.
|
||||
|
||||
|
||||
@@ -306,6 +306,15 @@ class PumpFunEventParser(EventParser):
|
||||
_coerce_pubkey(fields.get("quote_mint"))
|
||||
)
|
||||
|
||||
# The event's creator is canonical (unlike instruction
|
||||
# args.creator) and the flags/quote_mint are authoritative
|
||||
# at create time, so extreme_fast_mode can trade on them
|
||||
# without a curve read. Old-format events missing the
|
||||
# trailing fields stay conservative.
|
||||
state_from_event = (
|
||||
"quote_mint" in fields and "is_mayhem_mode" in fields
|
||||
)
|
||||
|
||||
return TokenInfo(
|
||||
name=fields["name"],
|
||||
symbol=fields["symbol"],
|
||||
@@ -323,6 +332,7 @@ class PumpFunEventParser(EventParser):
|
||||
quote_mint=quote_mint,
|
||||
quote_token_program_id=quote_token_program(quote_mint),
|
||||
virtual_quote_reserves=fields.get("virtual_quote_reserves"),
|
||||
state_from_event=state_from_event,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
@@ -466,7 +476,23 @@ class PumpFunEventParser(EventParser):
|
||||
if not hasattr(transaction_info, "transaction"):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
# Prefer the CreateEvent from meta.log_messages, same as the block
|
||||
# parser: the event carries the canonical creator (instruction
|
||||
# args.creator is user-supplied and may differ post-2026-04-28)
|
||||
# plus mayhem/cashback/quote_mint, which marks the TokenInfo
|
||||
# state_from_event so extreme_fast_mode can buy with zero RPC
|
||||
# calls. Fall back to instruction decoding when logs are absent.
|
||||
tx_info = transaction_info.transaction.transaction
|
||||
meta = getattr(tx_info, "meta", None)
|
||||
log_messages = list(getattr(meta, "log_messages", []) or [])
|
||||
if log_messages:
|
||||
token_info = self.parse_token_creation_from_logs(
|
||||
log_messages, signature=""
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
tx = tx_info.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
@@ -74,9 +74,20 @@ class PumpFunPumpPortalProcessor:
|
||||
|
||||
# Convert string addresses to Pubkey objects
|
||||
mint = Pubkey.from_string(mint_str)
|
||||
bonding_curve = Pubkey.from_string(bonding_curve_str)
|
||||
user = Pubkey.from_string(creator_str)
|
||||
|
||||
# Derive the bonding curve from the mint rather than trusting the
|
||||
# payload: PumpPortal's bondingCurveKey was observed pointing at a
|
||||
# different mint's curve (issue #170), and the PDA derivation is
|
||||
# free. A mismatch is logged as a data-quality signal only.
|
||||
bonding_curve = self.address_provider.derive_pool_address(mint)
|
||||
if str(bonding_curve) != bonding_curve_str:
|
||||
logger.warning(
|
||||
f"PumpPortal bondingCurveKey {bonding_curve_str} does not "
|
||||
f"match curve {bonding_curve} derived from mint {mint}; "
|
||||
f"using the derived address"
|
||||
)
|
||||
|
||||
# For PumpPortal, we assume the creator is the same as the user
|
||||
# since PumpPortal doesn't distinguish between them
|
||||
creator = user
|
||||
|
||||
+189
-57
@@ -4,6 +4,7 @@ Final cleanup removing all platform-specific hardcoding.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from time import monotonic
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
@@ -12,6 +13,7 @@ from core.priority_fee.manager import PriorityFeeManager
|
||||
from core.pubkeys import (
|
||||
TOKEN_DECIMALS,
|
||||
WSOL_MINT,
|
||||
SystemAddresses,
|
||||
is_sol_paired,
|
||||
normalize_quote_mint,
|
||||
quote_units_per_token,
|
||||
@@ -43,37 +45,60 @@ def _quote_symbol(quote_mint: Pubkey) -> str:
|
||||
async def _read_pool_state_with_retry(
|
||||
curve_manager: object,
|
||||
pool_address: Pubkey,
|
||||
attempts: int = 4,
|
||||
mint: Pubkey | None = None,
|
||||
budget_seconds: float = 2.0,
|
||||
delay_seconds: float = 0.15,
|
||||
) -> dict:
|
||||
"""Read bonding curve state, retrying briefly on a lagging RPC node.
|
||||
) -> tuple[dict, Pubkey | None]:
|
||||
"""Read bonding curve state, retrying within a time budget on a lagging node.
|
||||
|
||||
A freshly created curve may not be visible at `confirmed` yet, and a node
|
||||
can momentarily serve a slot that predates it — both surface as "account
|
||||
not found". Reading at `processed` and retrying a few times costs a handful
|
||||
of RPC calls, which is far cheaper than trading on stale account data.
|
||||
not found". Reading at `processed` and retrying costs a handful of RPC
|
||||
calls, which is far cheaper than trading on stale account data. Issue #170
|
||||
measured individual reads on a load-balanced endpoint lagging several
|
||||
seconds behind a fast listener, hence a time budget rather than a fixed
|
||||
attempt count.
|
||||
|
||||
When `mint` is given and the curve manager supports it, the curve and the
|
||||
mint are read in one slot-consistent batch so the mint's owning token
|
||||
program comes back for free (pumpportal listeners can only guess it).
|
||||
|
||||
Args:
|
||||
curve_manager: Platform curve manager
|
||||
pool_address: Bonding curve / pool address
|
||||
attempts: How many reads to try before giving up
|
||||
mint: Optional token mint to read alongside the curve
|
||||
budget_seconds: Total time to keep retrying before giving up
|
||||
delay_seconds: Pause between attempts
|
||||
|
||||
Returns:
|
||||
Decoded pool state
|
||||
Tuple of (decoded pool state, token program id or None if unknown)
|
||||
|
||||
Raises:
|
||||
Exception: The last read error if every attempt fails
|
||||
"""
|
||||
batch_read = mint is not None and hasattr(
|
||||
curve_manager, "get_pool_state_and_token_program"
|
||||
)
|
||||
deadline = monotonic() + budget_seconds
|
||||
last_error: Exception | None = None
|
||||
for _ in range(attempts):
|
||||
while True:
|
||||
try:
|
||||
return await curve_manager.get_pool_state(
|
||||
pool_address, commitment="processed"
|
||||
)
|
||||
if batch_read:
|
||||
result = await curve_manager.get_pool_state_and_token_program(
|
||||
pool_address, mint, commitment="processed"
|
||||
)
|
||||
else:
|
||||
state = await curve_manager.get_pool_state(
|
||||
pool_address, commitment="processed"
|
||||
)
|
||||
result = (state, None)
|
||||
except Exception as error: # noqa: BLE001
|
||||
last_error = error
|
||||
if monotonic() + delay_seconds > deadline:
|
||||
break
|
||||
await asyncio.sleep(delay_seconds)
|
||||
else:
|
||||
return result
|
||||
|
||||
raise last_error or RuntimeError("pool_state unavailable after retries")
|
||||
|
||||
@@ -113,6 +138,9 @@ class PlatformAwareBuyer(Trader):
|
||||
extreme_fast_mode: bool = False,
|
||||
compute_units: dict | None = None,
|
||||
quote_amounts: dict[Pubkey, float] | None = None,
|
||||
curve_refresh_budget: float = 2.0,
|
||||
*,
|
||||
trust_create_event: bool = True,
|
||||
):
|
||||
"""Initialize platform-aware token buyer.
|
||||
|
||||
@@ -130,6 +158,15 @@ class PlatformAwareBuyer(Trader):
|
||||
for coins paired against something other than SOL. A coin whose
|
||||
quote mint is absent from this map is skipped rather than
|
||||
traded with a SOL-denominated amount.
|
||||
curve_refresh_budget: Seconds to keep retrying the pre-buy curve
|
||||
read before skipping the token. A buy built without fresh curve
|
||||
state guesses fee_recipient/creator_vault and tends to revert
|
||||
on-chain (issue #170), so skipping beats racing.
|
||||
trust_create_event: Skip the pre-buy curve read entirely for
|
||||
TokenInfo marked state_from_event (creator/flags/quote_mint
|
||||
read from the on-chain CreateEvent) — extreme_fast_mode then
|
||||
makes zero RPC calls between detection and submission. Set
|
||||
False to force the refresh for every listener.
|
||||
"""
|
||||
self.client = client
|
||||
self.wallet = wallet
|
||||
@@ -140,6 +177,8 @@ class PlatformAwareBuyer(Trader):
|
||||
self.extreme_fast_mode = extreme_fast_mode
|
||||
self.extreme_fast_token_amount = extreme_fast_token_amount
|
||||
self.compute_units = compute_units or {}
|
||||
self.curve_refresh_budget = curve_refresh_budget
|
||||
self.trust_create_event = trust_create_event
|
||||
# SOL-paired coins always use `amount`; other quotes need an explicit
|
||||
# per-mint amount because 0.0001 USDC and 0.0001 SOL are not comparable.
|
||||
self.quote_amounts: dict[Pubkey, float] = {
|
||||
@@ -174,53 +213,22 @@ class PlatformAwareBuyer(Trader):
|
||||
quote_mint = normalize_quote_mint(token_info.quote_mint)
|
||||
|
||||
if self.extreme_fast_mode:
|
||||
# Skip the price check; the token count is fixed by config and
|
||||
# sizing is finished once the quote amount is resolved below.
|
||||
# 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)
|
||||
# Geyser/logs fire on processed, so the BC is typically
|
||||
# readable in the same slot; pumpportal occasionally races
|
||||
# the on-chain commit, hence the retries.
|
||||
pool_state = await _read_pool_state_with_retry(
|
||||
curve_manager, pool_address
|
||||
# Zero-RPC hot path — the point of extreme_fast_mode. When the
|
||||
# CreateEvent already carried the canonical creator, the
|
||||
# mayhem/cashback flags and quote_mint, nothing sits between
|
||||
# detection and submission. Otherwise (pumpportal, old-format
|
||||
# events) refresh from chain or skip.
|
||||
if not self._can_skip_refresh(token_info):
|
||||
skip_reason = await self._refresh_curve_state(
|
||||
token_info, address_provider, curve_manager
|
||||
)
|
||||
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
|
||||
)
|
||||
# The quote asset decides which balance we spend and how
|
||||
# amounts are scaled, so it must come from the curve rather
|
||||
# than a listener guess.
|
||||
quote_mint = _refresh_quote_mint(token_info, pool_state)
|
||||
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
|
||||
if skip_reason is not None:
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=skip_reason,
|
||||
)
|
||||
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"
|
||||
)
|
||||
quote_mint = normalize_quote_mint(token_info.quote_mint)
|
||||
else:
|
||||
# Get pool address based on platform using platform-agnostic method
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
@@ -396,6 +404,130 @@ class PlatformAwareBuyer(Trader):
|
||||
# Fallback to deriving the address using platform provider
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
|
||||
def _can_skip_refresh(self, token_info: TokenInfo) -> bool:
|
||||
"""Whether the pre-buy curve read can be skipped entirely.
|
||||
|
||||
True when the listener read creator, mayhem/cashback and quote_mint
|
||||
from the on-chain CreateEvent (canonical at create time), keeping
|
||||
extreme_fast_mode at zero RPC calls between detection and submission.
|
||||
|
||||
Args:
|
||||
token_info: Token information from the listener
|
||||
|
||||
Returns:
|
||||
True if the buy can be built from token_info as-is
|
||||
"""
|
||||
return (
|
||||
self.trust_create_event
|
||||
and token_info.state_from_event
|
||||
and token_info.quote_mint is not None
|
||||
)
|
||||
|
||||
async def _refresh_curve_state(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
address_provider: AddressProvider,
|
||||
curve_manager: object,
|
||||
) -> str | None:
|
||||
"""Refresh mayhem/cashback/creator/quote_mint/token program from chain.
|
||||
|
||||
Listeners that guess these (pumpportal carries none of them) produce
|
||||
buys the program rejects with NotAuthorized (0x1770) / ConstraintSeeds
|
||||
(0x7d6) when fee_recipient or creator_vault is wrong. PumpPortal also
|
||||
notifies before the BC account is readable on a lagging node, so the
|
||||
read retries within curve_refresh_budget.
|
||||
|
||||
Args:
|
||||
token_info: Token information, mutated in place on success
|
||||
address_provider: Platform address provider
|
||||
curve_manager: Platform curve manager
|
||||
|
||||
Returns:
|
||||
None on success; on failure a reason to skip the buy — a buy built
|
||||
from listener-guessed defaults tends to revert on-chain
|
||||
(issue #170: 0x1770 / 0x7d6 / pool 3012), which still costs the fee
|
||||
"""
|
||||
try:
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
# Geyser/logs fire on processed, so the BC is typically readable in
|
||||
# the same slot; pumpportal occasionally races the on-chain commit,
|
||||
# hence the retries.
|
||||
pool_state, fresh_token_program = await _read_pool_state_with_retry(
|
||||
curve_manager,
|
||||
pool_address,
|
||||
mint=token_info.mint,
|
||||
budget_seconds=self.curve_refresh_budget,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
return (
|
||||
f"Curve state unreadable within {self.curve_refresh_budget:.1f}s "
|
||||
f"({e}); skipping buy rather than submitting with guessed accounts"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
# The quote asset decides which balance we spend and how amounts are
|
||||
# scaled, so it must come from the curve rather than a listener guess.
|
||||
_refresh_quote_mint(token_info, pool_state)
|
||||
fresh_creator = pool_state.get("creator")
|
||||
if fresh_creator and hasattr(address_provider, "derive_creator_vault"):
|
||||
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
|
||||
)
|
||||
self._apply_token_program(token_info, fresh_token_program, address_provider)
|
||||
return None
|
||||
|
||||
def _apply_token_program(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
token_program: Pubkey | None,
|
||||
address_provider: AddressProvider,
|
||||
) -> None:
|
||||
"""Correct a listener-guessed token program from the mint's real owner.
|
||||
|
||||
PumpPortal payloads carry no token program, so the processor defaults
|
||||
to Token-2022; a legacy-`create` coin is SPL Token and the ATA-create
|
||||
instruction then fails with IncorrectProgramId. The associated bonding
|
||||
curve is an ordinary ATA, so it must be re-derived under the corrected
|
||||
program too.
|
||||
|
||||
Args:
|
||||
token_info: Token information, mutated in place
|
||||
token_program: Owner of the mint account, or None if unknown
|
||||
address_provider: Platform address provider for ATA derivation
|
||||
"""
|
||||
known_programs = (
|
||||
SystemAddresses.TOKEN_PROGRAM,
|
||||
SystemAddresses.TOKEN_2022_PROGRAM,
|
||||
)
|
||||
if token_program is None or token_program not in known_programs:
|
||||
return
|
||||
if token_info.token_program_id == token_program:
|
||||
return
|
||||
logger.info(
|
||||
f"Correcting token program for {token_info.mint}: "
|
||||
f"{token_info.token_program_id} -> {token_program}"
|
||||
)
|
||||
token_info.token_program_id = token_program
|
||||
if token_info.bonding_curve and hasattr(
|
||||
address_provider, "derive_associated_bonding_curve"
|
||||
):
|
||||
token_info.associated_bonding_curve = (
|
||||
address_provider.derive_associated_bonding_curve(
|
||||
token_info.mint, token_info.bonding_curve, token_program
|
||||
)
|
||||
)
|
||||
|
||||
def _get_sol_destination(
|
||||
self, token_info: TokenInfo, address_provider: AddressProvider
|
||||
) -> Pubkey:
|
||||
@@ -521,7 +653,7 @@ class PlatformAwareSeller(Trader):
|
||||
# slightly stale slot reports the curve as missing, and silently
|
||||
# falling back to create-time values risks a wrong creator_vault
|
||||
# (ConstraintSeeds 0x7d6) or wrong mayhem fee_recipient.
|
||||
pool_state = await _read_pool_state_with_retry(
|
||||
pool_state, _ = await _read_pool_state_with_retry(
|
||||
curve_manager, pool_address
|
||||
)
|
||||
token_info.is_mayhem_mode = pool_state.get(
|
||||
|
||||
@@ -102,6 +102,9 @@ class UniversalTrader:
|
||||
# Trading configuration
|
||||
extreme_fast_mode: bool = False,
|
||||
extreme_fast_token_amount: int = 30,
|
||||
curve_refresh_budget: float = 2.0,
|
||||
*,
|
||||
trust_create_event: bool = True,
|
||||
# Quote asset configuration (pump.fun non-SOL pairs)
|
||||
quote_amounts: dict[str, float] | None = None,
|
||||
allowed_quote_mints: list[str] | None = None,
|
||||
@@ -193,6 +196,8 @@ class UniversalTrader:
|
||||
extreme_fast_mode,
|
||||
compute_units=self.compute_units,
|
||||
quote_amounts=self.quote_amounts,
|
||||
curve_refresh_budget=curve_refresh_budget,
|
||||
trust_create_event=trust_create_event,
|
||||
),
|
||||
PlatformAwareSeller(
|
||||
self.solana_client,
|
||||
|
||||
Reference in New Issue
Block a user