feat(pumpfun): align src/ with 2026-04-28 program upgrade + cashback path (#167)

Mirrors the four learning-examples commits (9b48418, c25e2ed, 57ffe8b,
ad6d2cc) into the production code paths under src/. Affects only pump_fun;
lets_bonk untouched. PumpSwap is not implemented under src/ so the
pool-v2 + breaking-fee-recipient pumpswap accounts are out of scope here.

Changes
-------
src/platforms/pumpfun/address_provider.py
  - Adds BREAKING_FEE_RECIPIENTS: ClassVar[list[Pubkey]] with the 8
    addresses pump.fun published for the 2026-04-28 upgrade.
  - Adds pick_breaking_fee_recipient() (random.choice across the 8 to
    spread program-tx throughput per pump.fun's recommendation).
  - get_buy_instruction_accounts and get_sell_instruction_accounts now
    expose "breaking_fee_recipient" alongside the existing keys.

src/platforms/pumpfun/instruction_builder.py
  - BC buy ix: appends breaking_fee_recipient (mutable) AFTER
    bonding_curve_v2 → 18 accounts total (was 17).
  - BC sell ix: appends breaking_fee_recipient (mutable) AFTER
    bonding_curve_v2 for both cashback and non-cashback paths
    → 16 accounts non-cashback / 17 cashback (was 15 / 16).
  - get_required_accounts_for_{buy,sell} updated to include the new
    account so priority-fee scraping covers it.

src/platforms/pumpfun/event_parser.py
  - is_mayhem_mode now extracted into TokenInfo on both paths
    (CreateEvent logs path and create / create_v2 instruction path);
    previously only is_cashback_coin was carried forward, leaving
    extreme_fast_mode buys to use the wrong fee_recipient on
    Mayhem-Mode tokens.

src/trading/platform_aware.py
  - Buy non-extreme path now also refreshes is_cashback_coin from
    pool_state (was only refreshing is_mayhem_mode).
  - Sell path now refreshes both flags from curve state before building
    the sell ix, so a coin that flips cashback after buy still gets the
    correct 16-vs-17-account layout. Wrapped in try/except so a transient
    RPC failure logs a warning and falls back to token_info defaults
    rather than failing the sell outright.

Live-validation (mainnet, all four bots/*.yaml configs)
-------------------------------------------------------
  bot-sniper-2-logs.yaml      buy 4GVJJwYtGixSPAQimgUhNYjEUBG6TB9XeQjKxs9EWpw2QfWuq7LHmUivPrsjhESgaPsPJQ9N6CCxECqxm9csqrH9 (18 accts) → sell 2ijgoPgxPmcSX4mFqz1PVkZ3yAvu36vj3Jy1yWZofxrK7jR92j2DWP3tRNy9PmUd1kiTaoYzVsrpGr6286KCnTp7 (16 accts) ✓
  bot-sniper-3-blocks.yaml    buy 4siRLG7tYk9iHyXAvnp3DH54vQXVEDHmCqZRmFgYhnRB9mVGjhh43ywohKjg8CpWctt5TyCtDkLRTRWT6VDwVyZi → sell 46jShWJ3sfuCP4YgTcpXWZdyqVmw4ULQSTC3Kwvbc9hE2y4yDosBww8Un8anXZpR1Xf96rN9QusV1G92oNcbp1o3 ✓
  bot-sniper-1-geyser.yaml    buy 4UEZy7LPm7Rxgw22d1bduQHTWHHHYJSpuaoq2iCyPFpnBPGt4C3QUCu9x77vwWugXnyGv4pQQeTbEZEjvyxxiDH2 → sell 21hSHVPKtqse25XybxhdGrpqK1fSxKKeNZGDCD5j568mzRPfyYR1TQs6mAc2moHKT7kaAAfdMC8DPt8VXmu2rviF ✓
  bot-sniper-4-pp.yaml        buy 4P91y4iPK6y1gPjz65VL6XAcVkT4DbqjHE5vHfX8PLnzqViCy1qZQ44eWiNDUPzoWCejibB7UJPKYAEj4K3Z3c3S ✓ ; sell hit slippage protection (Custom 6003 — program-side, the ix passed deserialization)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anton Sauchyk
2026-04-27 20:39:17 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent bda9099275
commit 22a0c23450
4 changed files with 72 additions and 2 deletions
+26 -1
View File
@@ -6,7 +6,7 @@ by implementing the AddressProvider interface.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Final from typing import ClassVar, Final
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from spl.token.instructions import get_associated_token_address from spl.token.instructions import get_associated_token_address
@@ -43,6 +43,29 @@ class PumpFunAddresses:
FEE_PROGRAM: Final[Pubkey] = Pubkey.from_string( FEE_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ" "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
) )
# 8 breaking-upgrade fee recipients (pump.fun program upgrade 2026-04-28).
# One must be appended (mutable) AFTER bonding-curve-v2 on every buy/sell.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
BREAKING_FEE_RECIPIENTS: ClassVar[list[Pubkey]] = [
Pubkey.from_string("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
Pubkey.from_string("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
Pubkey.from_string("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
Pubkey.from_string("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
Pubkey.from_string("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
Pubkey.from_string("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
Pubkey.from_string("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
Pubkey.from_string("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
]
@staticmethod
def pick_breaking_fee_recipient() -> Pubkey:
"""Pick one of the 8 breaking-upgrade fee recipients at random.
Spreads load across recipients per pump.fun's recommendation.
"""
import random
return random.choice(PumpFunAddresses.BREAKING_FEE_RECIPIENTS)
@staticmethod @staticmethod
def find_global_volume_accumulator() -> Pubkey: def find_global_volume_accumulator() -> Pubkey:
@@ -354,6 +377,7 @@ class PumpFunAddressProvider(AddressProvider):
"fee_config": self.derive_fee_config(), "fee_config": self.derive_fee_config(),
"fee_program": PumpFunAddresses.FEE_PROGRAM, "fee_program": PumpFunAddresses.FEE_PROGRAM,
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint), "bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
"breaking_fee_recipient": PumpFunAddresses.pick_breaking_fee_recipient(),
} }
def get_sell_instruction_accounts( def get_sell_instruction_accounts(
@@ -405,4 +429,5 @@ class PumpFunAddressProvider(AddressProvider):
"fee_program": PumpFunAddresses.FEE_PROGRAM, "fee_program": PumpFunAddresses.FEE_PROGRAM,
"bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint), "bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint),
"user_volume_accumulator": self.derive_user_volume_accumulator(user), "user_volume_accumulator": self.derive_user_volume_accumulator(user),
"breaking_fee_recipient": PumpFunAddresses.pick_breaking_fee_recipient(),
} }
+2
View File
@@ -274,6 +274,7 @@ class PumpFunEventParser(EventParser):
creator=creator, creator=creator,
creator_vault=creator_vault, creator_vault=creator_vault,
token_program_id=token_program_id, token_program_id=token_program_id,
is_mayhem_mode=fields.get("is_mayhem_mode", False),
is_cashback_coin=fields.get("is_cashback_enabled", False), is_cashback_coin=fields.get("is_cashback_enabled", False),
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
@@ -384,6 +385,7 @@ class PumpFunEventParser(EventParser):
creator=creator, creator=creator,
creator_vault=creator_vault, creator_vault=creator_vault,
token_program_id=token_program_id, token_program_id=token_program_id,
is_mayhem_mode=bool(args.get("is_mayhem_mode", False)),
is_cashback_coin=is_cashback, is_cashback_coin=is_cashback,
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
@@ -149,6 +149,12 @@ class PumpFunInstructionBuilder(InstructionBuilder):
is_signer=False, is_signer=False,
is_writable=False, is_writable=False,
), ),
# 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28
AccountMeta(
pubkey=accounts_info["breaking_fee_recipient"],
is_signer=False,
is_writable=True,
),
] ]
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume # Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
@@ -271,6 +277,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
is_writable=False, is_writable=False,
) )
) )
# 16/17th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28
sell_accounts.append(
AccountMeta(
pubkey=accounts_info["breaking_fee_recipient"],
is_signer=False,
is_writable=True,
)
)
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume # Build instruction data: discriminator + token_amount + min_sol_output + track_volume
# Encode OptionBool for track_volume: [1, 1] = Some(true) # Encode OptionBool for track_volume: [1, 1] = Some(true)
@@ -319,6 +333,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info["fee_config"], accounts_info["fee_config"],
accounts_info["fee_program"], accounts_info["fee_program"],
accounts_info["bonding_curve_v2"], accounts_info["bonding_curve_v2"],
accounts_info["breaking_fee_recipient"],
] ]
def get_required_accounts_for_sell( def get_required_accounts_for_sell(
@@ -347,6 +362,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
accounts_info["fee_config"], accounts_info["fee_config"],
accounts_info["fee_program"], accounts_info["fee_program"],
accounts_info["bonding_curve_v2"], accounts_info["bonding_curve_v2"],
accounts_info["breaking_fee_recipient"],
] ]
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
+28 -1
View File
@@ -77,8 +77,13 @@ class PlatformAwareBuyer(Trader):
f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price" f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price"
) )
# Set is_mayhem_mode from bonding curve state # Set mayhem-mode and cashback flags from bonding-curve state
# so the instruction builder picks the correct fee_recipient and
# account-list shape (cashback sells use 17 accounts, non-cashback 16).
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False) token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
token_info.is_cashback_coin = pool_state.get(
"is_cashback_coin", token_info.is_cashback_coin
)
token_amount = self.amount / token_price_sol token_amount = self.amount / token_price_sol
# Calculate minimum token amount with slippage # Calculate minimum token amount with slippage
@@ -304,6 +309,28 @@ class PlatformAwareSeller(Trader):
) )
address_provider = implementations.address_provider address_provider = implementations.address_provider
instruction_builder = implementations.instruction_builder instruction_builder = implementations.instruction_builder
curve_manager = implementations.curve_manager
# Refresh mayhem-mode and cashback flags from curve state.
# The sell account list is 16 (non-cashback) vs 17 (cashback), and
# fee_recipient differs in mayhem mode — both can change between
# buy and sell, so re-read from chain instead of trusting create-time
# flags carried in token_info.
try:
pool_address = self._get_pool_address(token_info, address_provider)
pool_state = await curve_manager.get_pool_state(pool_address)
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
)
except Exception as e: # noqa: BLE001
logger.warning(
f"Could not refresh curve flags before sell ({e}); "
f"using token_info values is_mayhem_mode={token_info.is_mayhem_mode}, "
f"is_cashback_coin={token_info.is_cashback_coin}"
)
# Use pre-known amount and price (no RPC delay) # Use pre-known amount and price (no RPC delay)
token_balance_decimal = token_amount token_balance_decimal = token_amount