diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index 4b9865d..2cb825d 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -6,7 +6,7 @@ by implementing the AddressProvider interface. """ from dataclasses import dataclass -from typing import Final +from typing import ClassVar, Final from solders.pubkey import Pubkey from spl.token.instructions import get_associated_token_address @@ -43,6 +43,29 @@ class PumpFunAddresses: FEE_PROGRAM: Final[Pubkey] = Pubkey.from_string( "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 def find_global_volume_accumulator() -> Pubkey: @@ -354,6 +377,7 @@ class PumpFunAddressProvider(AddressProvider): "fee_config": self.derive_fee_config(), "fee_program": PumpFunAddresses.FEE_PROGRAM, "bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint), + "breaking_fee_recipient": PumpFunAddresses.pick_breaking_fee_recipient(), } def get_sell_instruction_accounts( @@ -405,4 +429,5 @@ class PumpFunAddressProvider(AddressProvider): "fee_program": PumpFunAddresses.FEE_PROGRAM, "bonding_curve_v2": self.derive_bonding_curve_v2(token_info.mint), "user_volume_accumulator": self.derive_user_volume_accumulator(user), + "breaking_fee_recipient": PumpFunAddresses.pick_breaking_fee_recipient(), } diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index a52f0e3..0753efc 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -274,6 +274,7 @@ class PumpFunEventParser(EventParser): creator=creator, creator_vault=creator_vault, token_program_id=token_program_id, + is_mayhem_mode=fields.get("is_mayhem_mode", False), is_cashback_coin=fields.get("is_cashback_enabled", False), creation_timestamp=monotonic(), ) @@ -384,6 +385,7 @@ class PumpFunEventParser(EventParser): creator=creator, creator_vault=creator_vault, token_program_id=token_program_id, + is_mayhem_mode=bool(args.get("is_mayhem_mode", False)), is_cashback_coin=is_cashback, creation_timestamp=monotonic(), ) diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index 31a578c..a18ca9d 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -149,6 +149,12 @@ class PumpFunInstructionBuilder(InstructionBuilder): is_signer=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 @@ -271,6 +277,14 @@ class PumpFunInstructionBuilder(InstructionBuilder): 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 # Encode OptionBool for track_volume: [1, 1] = Some(true) @@ -319,6 +333,7 @@ class PumpFunInstructionBuilder(InstructionBuilder): accounts_info["fee_config"], accounts_info["fee_program"], accounts_info["bonding_curve_v2"], + accounts_info["breaking_fee_recipient"], ] def get_required_accounts_for_sell( @@ -347,6 +362,7 @@ class PumpFunInstructionBuilder(InstructionBuilder): accounts_info["fee_config"], accounts_info["fee_program"], accounts_info["bonding_curve_v2"], + accounts_info["breaking_fee_recipient"], ] def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 58350c8..2ab08f2 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -77,8 +77,13 @@ class PlatformAwareBuyer(Trader): 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_cashback_coin = pool_state.get( + "is_cashback_coin", token_info.is_cashback_coin + ) token_amount = self.amount / token_price_sol # Calculate minimum token amount with slippage @@ -304,6 +309,28 @@ class PlatformAwareSeller(Trader): ) address_provider = implementations.address_provider 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) token_balance_decimal = token_amount