Add mayhem mode support and Token2022 integration (#149)

* feat: mayhem update in idl

* feat(examples): update bonding curve scripts

* feat(example): update listen_blocksubscribe

* feat(examples): update geyser listener

* feat(examples): update all new token listeners

* feat(examples): add comments, fix printing, formatting

* feat(examples): pumpswap buy and sell update with mayhem mode

* fix(examples): sell pump amm fee recipient

* feat(examples): update decode scripts

* feat(examples): update fetch price

* feat(examples): buy and sell bonding curve scripts

* feat(examples): add mint with mayhem mode enabled

* feat(examples): improve listening to wallet txs

* feat(examples): migration listener improvements

* feat(examples): global vol accumulator is not writable

* feat(examples): support token/token2022 programs in buy instructions

* feat(examples): token/token2022 for pumpswap buy

* feat(examples): token/token2022 supprot for sell instructions

* feat(bot): support create_v2 with token2022, mayhem mode, other fixes

* fix(bot): support only token2022 in logs and pumportal listeners

* feat(bot): token2022 support in cleanup flow

* fix(bot): update token program handling and improve price validation in trading logic

* feat(bot): enhance token program handling for LetsBonk integration
This commit is contained in:
Anton Sauchyk
2025-11-18 13:09:37 +01:00
committed by GitHub
parent 7ae8b560bd
commit 03a4e7bcbc
42 changed files with 4593 additions and 885 deletions
+11 -4
View File
@@ -34,12 +34,19 @@ class AccountCleanupManager:
self.use_priority_fee = use_priority_fee
self.close_with_force_burn = force_burn
async def cleanup_ata(self, mint: Pubkey) -> None:
async def cleanup_ata(self, mint: Pubkey, token_program_id: Pubkey | None = None) -> None:
"""
Attempt to burn any remaining tokens and close the ATA.
Skips if account doesn't exist or is already empty/closed.
Args:
mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
"""
ata = self.wallet.get_associated_token_address(mint)
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
ata = self.wallet.get_associated_token_address(mint, token_program_id)
solana_client = await self.client.get_client()
priority_fee = (
@@ -70,7 +77,7 @@ class AccountCleanupManager:
mint=mint,
owner=self.wallet.pubkey,
amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM,
program_id=token_program_id,
)
)
instructions.append(burn_ix)
@@ -89,7 +96,7 @@ class AccountCleanupManager:
account=ata,
dest=self.wallet.pubkey,
owner=self.wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM,
program_id=token_program_id,
)
)
instructions.append(close_ix)
+7 -4
View File
@@ -20,6 +20,7 @@ async def handle_cleanup_after_failure(
client,
wallet,
mint,
token_program_id,
priority_fee_manager,
cleanup_mode,
cleanup_with_prior_fee,
@@ -30,13 +31,14 @@ async def handle_cleanup_after_failure(
manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
)
await manager.cleanup_ata(mint)
await manager.cleanup_ata(mint, token_program_id)
async def handle_cleanup_after_sell(
client,
wallet,
mint,
token_program_id,
priority_fee_manager,
cleanup_mode,
cleanup_with_prior_fee,
@@ -47,13 +49,14 @@ async def handle_cleanup_after_sell(
manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
)
await manager.cleanup_ata(mint)
await manager.cleanup_ata(mint, token_program_id)
async def handle_cleanup_post_session(
client,
wallet,
mints,
token_program_ids,
priority_fee_manager,
cleanup_mode,
cleanup_with_prior_fee,
@@ -64,5 +67,5 @@ async def handle_cleanup_post_session(
manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
)
for mint in mints:
await manager.cleanup_ata(mint)
for mint, token_program_id in zip(mints, token_program_ids):
await manager.cleanup_ata(mint, token_program_id)
+5
View File
@@ -23,6 +23,9 @@ SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("1111111111111111111111111111
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
)
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
@@ -42,6 +45,7 @@ class SystemAddresses:
# Reference the module-level constants
SYSTEM_PROGRAM = SYSTEM_PROGRAM
TOKEN_PROGRAM = TOKEN_PROGRAM
TOKEN_2022_PROGRAM = TOKEN_2022_PROGRAM
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
RENT = RENT
SOL_MINT = SOL_MINT
@@ -56,6 +60,7 @@ class SystemAddresses:
return {
"system_program": cls.SYSTEM_PROGRAM,
"token_program": cls.TOKEN_PROGRAM,
"token_2022_program": cls.TOKEN_2022_PROGRAM,
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
"rent": cls.RENT,
"sol_mint": cls.SOL_MINT,
+9 -2
View File
@@ -7,6 +7,8 @@ from solders.keypair import Keypair
from solders.pubkey import Pubkey
from spl.token.instructions import get_associated_token_address
from core.pubkeys import SystemAddresses
class Wallet:
"""Manages a Solana wallet for trading operations."""
@@ -30,16 +32,21 @@ class Wallet:
"""Get the keypair for signing transactions."""
return self._keypair
def get_associated_token_address(self, mint: Pubkey) -> Pubkey:
def get_associated_token_address(
self, mint: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey:
"""Get the associated token account address for a mint.
Args:
mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns:
Associated token account address
"""
return get_associated_token_address(self.pubkey, mint)
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
return get_associated_token_address(self.pubkey, mint, token_program_id)
@staticmethod
def _load_keypair(private_key: str) -> Keypair:
+2
View File
@@ -45,6 +45,8 @@ class TokenInfo:
user: Pubkey | None = None
creator: Pubkey | None = None
creator_vault: Pubkey | None = None
token_program_id: Pubkey | None = None # Token or Token2022 program
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
# Metadata
creation_timestamp: float | None = None
+25 -6
View File
@@ -147,17 +147,22 @@ class LetsBonkAddressProvider(AddressProvider):
)
return quote_vault
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
def derive_user_token_account(
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey:
"""Derive user's associated token account address.
Args:
user: User's wallet address
mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns:
User's associated token account address
"""
return get_associated_token_address(user, mint)
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
return get_associated_token_address(user, mint, token_program_id)
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
"""Get LetsBonk-specific additional accounts needed for trading.
@@ -296,6 +301,13 @@ class LetsBonkAddressProvider(AddressProvider):
"""
additional_accounts = self.get_additional_accounts(token_info)
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_2022_PROGRAM
)
# Use global_config from TokenInfo if available, otherwise use default
global_config = (
token_info.global_config
@@ -316,12 +328,12 @@ class LetsBonkAddressProvider(AddressProvider):
"global_config": global_config,
"platform_config": platform_config,
"pool_state": additional_accounts["pool_state"],
"user_base_token": self.derive_user_token_account(user, token_info.mint),
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
"base_vault": additional_accounts["base_vault"],
"quote_vault": additional_accounts["quote_vault"],
"base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_MINT,
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
"base_token_program": token_program_id,
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
@@ -351,6 +363,13 @@ class LetsBonkAddressProvider(AddressProvider):
"""
additional_accounts = self.get_additional_accounts(token_info)
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_2022_PROGRAM
)
# Use global_config from TokenInfo if available, otherwise use default
global_config = (
token_info.global_config
@@ -371,12 +390,12 @@ class LetsBonkAddressProvider(AddressProvider):
"global_config": global_config,
"platform_config": platform_config,
"pool_state": additional_accounts["pool_state"],
"user_base_token": self.derive_user_token_account(user, token_info.mint),
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
"base_vault": additional_accounts["base_vault"],
"quote_vault": additional_accounts["quote_vault"],
"base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_MINT,
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
"base_token_program": token_program_id,
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
+14 -7
View File
@@ -194,14 +194,21 @@ class LetsBonkCurveManager(CurveManager):
}
# Calculate additional metrics
if pool_data["virtual_base"] > 0:
pool_data["price_per_token"] = (
(pool_data["virtual_quote"] / pool_data["virtual_base"])
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
# Validate reserves are positive before calculating price
if pool_data["virtual_base"] <= 0:
raise ValueError(
f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price"
)
else:
pool_data["price_per_token"] = 0
if pool_data["virtual_quote"] <= 0:
raise ValueError(
f"Invalid virtual_quote: {pool_data['virtual_quote']} - cannot calculate price"
)
pool_data["price_per_token"] = (
(pool_data["virtual_quote"] / pool_data["virtual_base"])
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
)
logger.debug(
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
+11
View File
@@ -13,6 +13,7 @@ from typing import Any
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from core.pubkeys import SystemAddresses
from interfaces.core import EventParser, Platform, TokenInfo
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
from utils.idl_parser import IDLParser
@@ -114,6 +115,15 @@ class LetsBonkEventParser(EventParser):
}:
return None
# Determine token program based on instruction variant
instruction_name = decoded["instruction_name"]
is_token_2022 = instruction_name == "initialize_with_token_2022"
token_program_id = (
SystemAddresses.TOKEN_2022_PROGRAM
if is_token_2022
else SystemAddresses.TOKEN_PROGRAM
)
args = decoded.get("args", {})
# Extract MintParams from the decoded arguments
@@ -174,6 +184,7 @@ class LetsBonkEventParser(EventParser):
platform_config=platform_config,
user=creator,
creator=creator,
token_program_id=token_program_id,
creation_timestamp=monotonic(),
)
+12 -5
View File
@@ -75,12 +75,19 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
# Get all required accounts
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_2022_PROGRAM
)
# 1. Create idempotent ATA for base token
ata_instruction = create_idempotent_associated_token_account(
user, # payer
user, # owner
token_info.mint, # mint
SystemAddresses.TOKEN_PROGRAM, # token program
token_program_id, # token program (dynamic for token2022 support)
)
instructions.append(ata_instruction)
@@ -151,10 +158,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=accounts_info["event_authority"],
@@ -306,10 +313,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
), # base_token_program
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
), # quote_token_program
AccountMeta(
pubkey=accounts_info["event_authority"],
+63 -11
View File
@@ -31,6 +31,12 @@ class PumpFunAddresses:
FEE: Final[Pubkey] = Pubkey.from_string(
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
)
# Mayhem mode fee recipient (hardcoded to avoid RPC calls)
# To check if this address is up-to-date, fetch Global account data at offset 483
# from the pump.fun Global account: 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf
MAYHEM_FEE: Final[Pubkey] = Pubkey.from_string(
"GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"
)
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
)
@@ -139,17 +145,22 @@ class PumpFunAddressProvider(AddressProvider):
)
return bonding_curve
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
def derive_user_token_account(
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey:
"""Derive user's associated token account address.
Args:
user: User's wallet address
mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns:
User's associated token account address
"""
return get_associated_token_address(user, mint)
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
return get_associated_token_address(user, mint, token_program_id)
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
"""Get pump.fun-specific additional accounts needed for trading.
@@ -177,7 +188,7 @@ class PumpFunAddressProvider(AddressProvider):
# Derive associated bonding curve if not provided
if not token_info.associated_bonding_curve and token_info.bonding_curve:
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
token_info.mint, token_info.bonding_curve
token_info.mint, token_info.bonding_curve, token_info.token_program_id
)
# Derive creator vault if not provided but creator is available
@@ -187,21 +198,25 @@ class PumpFunAddressProvider(AddressProvider):
return accounts
def derive_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey:
"""Derive the associated bonding curve (ATA of bonding curve for the token).
Args:
mint: Token mint address
bonding_curve: Bonding curve address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns:
Associated bonding curve address
"""
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM),
bytes(token_program_id),
bytes(mint),
],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
@@ -249,6 +264,19 @@ class PumpFunAddressProvider(AddressProvider):
"""
return PumpFunAddresses.find_fee_config()
def get_fee_recipient(self, token_info: TokenInfo) -> Pubkey:
"""Get the correct fee recipient based on mayhem mode.
Args:
token_info: Token information with is_mayhem_mode flag
Returns:
Fee recipient address (mayhem or standard)
"""
if token_info.is_mayhem_mode:
return PumpFunAddresses.MAYHEM_FEE
return PumpFunAddresses.FEE
def get_buy_instruction_accounts(
self, token_info: TokenInfo, user: Pubkey
) -> dict[str, Pubkey]:
@@ -263,9 +291,19 @@ class PumpFunAddressProvider(AddressProvider):
"""
additional_accounts = self.get_additional_accounts(token_info)
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_PROGRAM
)
# Determine fee recipient based on mayhem mode
fee_recipient = self.get_fee_recipient(token_info)
return {
"global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE,
"fee": fee_recipient,
"mint": token_info.mint,
"bonding_curve": additional_accounts.get(
"bonding_curve", token_info.bonding_curve
@@ -273,10 +311,12 @@ class PumpFunAddressProvider(AddressProvider):
"associated_bonding_curve": additional_accounts.get(
"associated_bonding_curve", token_info.associated_bonding_curve
),
"user_token_account": self.derive_user_token_account(user, token_info.mint),
"user_token_account": self.derive_user_token_account(
user, token_info.mint, token_program_id
),
"user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"token_program": SystemAddresses.TOKEN_PROGRAM,
"token_program": token_program_id,
"creator_vault": additional_accounts.get(
"creator_vault", token_info.creator_vault
),
@@ -302,9 +342,19 @@ class PumpFunAddressProvider(AddressProvider):
"""
additional_accounts = self.get_additional_accounts(token_info)
# Determine token program to use
token_program_id = (
token_info.token_program_id
if token_info.token_program_id
else SystemAddresses.TOKEN_PROGRAM
)
# Determine fee recipient based on mayhem mode
fee_recipient = self.get_fee_recipient(token_info)
return {
"global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE,
"fee": fee_recipient,
"mint": token_info.mint,
"bonding_curve": additional_accounts.get(
"bonding_curve", token_info.bonding_curve
@@ -312,13 +362,15 @@ class PumpFunAddressProvider(AddressProvider):
"associated_bonding_curve": additional_accounts.get(
"associated_bonding_curve", token_info.associated_bonding_curve
),
"user_token_account": self.derive_user_token_account(user, token_info.mint),
"user_token_account": self.derive_user_token_account(
user, token_info.mint, token_program_id
),
"user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"creator_vault": additional_accounts.get(
"creator_vault", token_info.creator_vault
),
"token_program": SystemAddresses.TOKEN_PROGRAM,
"token_program": token_program_id,
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
"program": PumpFunAddresses.PROGRAM,
"fee_config": self.derive_fee_config(),
+18 -10
View File
@@ -190,20 +190,28 @@ class PumpFunCurveManager(CurveManager):
"token_total_supply": decoded_curve_state.get("token_total_supply", 0),
"complete": decoded_curve_state.get("complete", False),
"creator": decoded_curve_state.get("creator", ""),
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
}
# Calculate additional metrics
if curve_data["virtual_token_reserves"] > 0:
curve_data["price_per_token"] = (
(
curve_data["virtual_sol_reserves"]
/ curve_data["virtual_token_reserves"]
)
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
# Validate reserves are positive before calculating price
if curve_data["virtual_token_reserves"] <= 0:
raise ValueError(
f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price"
)
else:
curve_data["price_per_token"] = 0
if curve_data["virtual_sol_reserves"] <= 0:
raise ValueError(
f"Invalid virtual_sol_reserves: {curve_data['virtual_sol_reserves']} - cannot calculate price"
)
curve_data["price_per_token"] = (
(
curve_data["virtual_sol_reserves"]
/ curve_data["virtual_token_reserves"]
)
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
)
# Add convenience decimal fields
curve_data["token_reserves_decimal"] = (
+114 -21
View File
@@ -47,6 +47,16 @@ class PumpFunEventParser(EventParser):
"<Q", self._create_instruction_discriminator_bytes
)[0]
# Support for token2022 (create_v2 instruction)
self._create_v2_instruction_discriminator_bytes = instruction_discriminators.get(
"create_v2"
)
self._create_v2_instruction_discriminator = (
struct.unpack("<Q", self._create_v2_instruction_discriminator_bytes)[0]
if self._create_v2_instruction_discriminator_bytes
else None
)
logger.info(
"Pump.Fun event parser initialized with IDL-based event and instruction parsing"
)
@@ -56,6 +66,10 @@ class PumpFunEventParser(EventParser):
logger.info(
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}"
)
if self._create_v2_instruction_discriminator_bytes:
logger.info(
f"create_v2 instruction discriminator: {self._create_v2_instruction_discriminator_bytes.hex()}"
)
@property
def platform(self) -> Platform:
@@ -74,8 +88,12 @@ class PumpFunEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
# Check if this is a token creation transaction
if not any("Program log: Instruction: Create" in log for log in logs):
# Check if this is a token creation transaction (create or create_v2 for token2022)
if not any(
"Program log: Instruction: Create" in log
or "Program log: Instruction: Create_v2" in log
for log in logs
):
return None
# Skip swaps as the first condition may pass them
@@ -92,9 +110,10 @@ class PumpFunEventParser(EventParser):
# First, collect all Program data entries and note when Create instruction happens
for i, log in enumerate(logs):
if "Program log: Instruction: Create" in log:
if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log:
create_instruction_found = True
logger.info(f"📝 Found Create instruction at log index {i}")
instruction_type = "Create_v2" if "Create_v2" in log else "Create"
logger.info(f"📝 Found {instruction_type} instruction at log index {i}")
elif "Program data:" in log:
# Extract base64 encoded event data
encoded_data = log.split("Program data: ")[1].strip()
@@ -104,7 +123,7 @@ class PumpFunEventParser(EventParser):
)
if not create_instruction_found:
logger.info("❌ No Create instruction found in logs")
logger.info("❌ No Create or Create_v2 instruction found in logs")
return None
if not program_data_entries:
@@ -224,9 +243,13 @@ class PumpFunEventParser(EventParser):
logger.info(f"❌ Failed to convert pubkey fields: {e}")
continue
# Derive additional addresses
# Derive additional addresses (default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2)
# Note: As of recent pump.fun updates, all tokens are created via create_v2 instruction
# This is a technical limitation of logs listener - cannot distinguish create vs create_v2
# Risk is low since pump.fun now defaults to Token2022 for all new tokens
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
associated_bonding_curve = self._derive_associated_bonding_curve(
mint, bonding_curve
mint, bonding_curve, token_program_id
)
creator_vault = self._derive_creator_vault(creator)
@@ -245,6 +268,7 @@ class PumpFunEventParser(EventParser):
user=user,
creator=creator,
creator_vault=creator_vault,
token_program_id=token_program_id,
creation_timestamp=monotonic(),
)
@@ -274,9 +298,16 @@ class PumpFunEventParser(EventParser):
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(
self._create_instruction_discriminator_bytes
# Determine which create instruction (standard or v2 for token2022)
is_create_v2 = False
if instruction_data.startswith(self._create_instruction_discriminator_bytes):
is_create_v2 = False
elif (
self._create_v2_instruction_discriminator_bytes
and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes)
):
is_create_v2 = True
else:
return None
try:
@@ -293,7 +324,8 @@ class PumpFunEventParser(EventParser):
decoded = self._idl_parser.decode_instruction(
instruction_data, account_keys, accounts
)
if not decoded or decoded["instruction_name"] != "create":
expected_instruction_name = "create_v2" if is_create_v2 else "create"
if not decoded or decoded["instruction_name"] != expected_instruction_name:
return None
args = decoded.get("args", {})
@@ -315,6 +347,13 @@ class PumpFunEventParser(EventParser):
)
creator_vault = self._derive_creator_vault(creator)
# Determine token program based on instruction type
token_program_id = (
SystemAddresses.TOKEN_2022_PROGRAM
if is_create_v2
else SystemAddresses.TOKEN_PROGRAM
)
return TokenInfo(
name=args.get("name", ""),
symbol=args.get("symbol", ""),
@@ -326,6 +365,7 @@ class PumpFunEventParser(EventParser):
user=user,
creator=creator,
creator_vault=creator_vault,
token_program_id=token_program_id,
creation_timestamp=monotonic(),
)
@@ -435,14 +475,19 @@ class PumpFunEventParser(EventParser):
ix_data = bytes(ix.data)
# Check for create discriminator
# Check for create or create_v2 discriminator
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if (
discriminator
== self._create_instruction_discriminator
):
is_create = (
discriminator == self._create_instruction_discriminator
)
is_create_v2 = (
self._create_v2_instruction_discriminator
and discriminator == self._create_v2_instruction_discriminator
)
if is_create or is_create_v2:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue
@@ -497,10 +542,15 @@ class PumpFunEventParser(EventParser):
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if (
discriminator
== self._create_instruction_discriminator
):
is_create = (
discriminator == self._create_instruction_discriminator
)
is_create_v2 = (
self._create_v2_instruction_discriminator
and discriminator == self._create_v2_instruction_discriminator
)
if is_create or is_create_v2:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
@@ -544,27 +594,70 @@ class PumpFunEventParser(EventParser):
return derived_address
def _derive_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey:
"""Derive the associated bonding curve (ATA of bonding curve for the token).
Args:
mint: Token mint address
bonding_curve: Bonding curve address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_PROGRAM
Returns:
Associated bonding curve address
"""
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_PROGRAM
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM),
bytes(token_program_id),
bytes(mint),
],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
)
return derived_address
def _parse_bonding_curve_state(self, data: bytes) -> dict[str, Any] | None:
"""Parse bonding curve state from raw account data using IDL parser.
Args:
data: Raw bonding curve account data
Returns:
Dictionary with parsed bonding curve state or None if parsing fails
"""
try:
decoded = self._idl_parser.decode_account_data(
data, "BondingCurve", skip_discriminator=True
)
if not decoded:
return None
return decoded
except Exception as e:
logger.debug(f"Failed to parse bonding curve state: {e}")
return None
def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool:
"""Determine if a token is in mayhem mode based on bonding curve state.
Note: This would require an RPC call to fetch the bonding curve account.
For now, we return False as a default since the event parser doesn't have
access to an RPC client. The mayhem mode flag will be set by traders
when they fetch bonding curve state for other operations.
Args:
bonding_curve_address: Address of the bonding curve
Returns:
True if mayhem mode, False otherwise (or if parsing fails)
"""
# Since event parser doesn't have RPC client access, we cannot fetch
# and parse bonding curve state here. Mayhem mode will be set later
# when traders fetch the bonding curve state.
return False
@property
def verbose(self) -> bool:
"""Check if verbose logging is enabled."""
+13 -6
View File
@@ -64,15 +64,16 @@ class PumpFunInstructionBuilder(InstructionBuilder):
"""
instructions = []
# Get all required accounts
# Get all required accounts (includes mayhem-mode-aware fee recipient)
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
# Use token_program from accounts_info to ensure AddressProvider controls program selection
ata_instruction = create_idempotent_associated_token_account(
user, # payer
user, # owner
token_info.mint, # mint
SystemAddresses.TOKEN_PROGRAM, # token program
accounts_info["token_program"], # token program from AddressProvider
)
instructions.append(ata_instruction)
@@ -123,7 +124,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
AccountMeta(
pubkey=accounts_info["global_volume_accumulator"],
is_signer=False,
is_writable=True,
is_writable=False,
),
AccountMeta(
pubkey=accounts_info["user_volume_accumulator"],
@@ -144,11 +145,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
),
]
# Build instruction data: discriminator + token_amount + max_sol_cost
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
instruction_data = (
self._buy_discriminator
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units
+ struct.pack("<Q", amount_in) # max SOL cost in lamports
+ track_volume_bytes # enable volume tracking
)
buy_instruction = Instruction(
@@ -182,7 +186,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
"""
instructions = []
# Get all required accounts
# Get all required accounts (includes mayhem-mode-aware fee recipient)
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
# Build sell instruction accounts
@@ -243,11 +247,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
),
]
# Build instruction data: discriminator + token_amount + min_sol_output
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
instruction_data = (
self._sell_discriminator
+ struct.pack("<Q", amount_in) # token amount in raw units
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
+ track_volume_bytes # enable volume tracking
)
sell_instruction = Instruction(
@@ -5,6 +5,7 @@ File: src/platforms/pumpfun/pumpportal_processor.py
from solders.pubkey import Pubkey
from core.pubkeys import SystemAddresses
from interfaces.core import Platform, TokenInfo
from platforms.pumpfun.address_provider import PumpFunAddressProvider
from utils.logger import get_logger
@@ -81,9 +82,15 @@ class PumpFunPumpPortalProcessor:
creator = user
# Derive additional addresses using platform provider
# PumpPortal doesn't distinguish between Token and Token2022.
# Default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2.
# Technical limitation: Cannot distinguish from pre-parsed data, but risk is low
# since pump.fun now defaults to Token2022 for all new tokens.
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
associated_bonding_curve = (
self.address_provider.derive_associated_bonding_curve(
mint, bonding_curve
mint, bonding_curve, token_program_id
)
)
creator_vault = self.address_provider.derive_creator_vault(creator)
@@ -99,6 +106,7 @@ class PumpFunPumpPortalProcessor:
user=user,
creator=creator,
creator_vault=creator_vault,
token_program_id=token_program_id,
)
except Exception:
+27 -5
View File
@@ -66,10 +66,20 @@ class PlatformAwareBuyer(Trader):
pool_address = self._get_pool_address(token_info, address_provider)
# Regular behavior with RPC call
token_price_sol = await curve_manager.calculate_price(pool_address)
token_amount = (
self.amount / token_price_sol if token_price_sol > 0 else 0
)
# Fetch pool state to get price and mayhem mode status
pool_state = await curve_manager.get_pool_state(pool_address)
token_price_sol = pool_state.get("price_per_token")
# Validate price_per_token is present and positive
if token_price_sol is None or token_price_sol <= 0:
raise ValueError(
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price"
)
# Set is_mayhem_mode from bonding curve state
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
token_amount = self.amount / token_price_sol
# Calculate minimum token amount with slippage
minimum_token_amount = token_amount * (1 - self.slippage)
@@ -225,7 +235,19 @@ class PlatformAwareSeller(Trader):
# Get pool address and current price using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
token_price_sol = await curve_manager.calculate_price(pool_address)
# Fetch pool state to get price and mayhem mode status
pool_state = await curve_manager.get_pool_state(pool_address)
token_price_sol = pool_state.get("price_per_token")
# Validate price_per_token is present and positive
if token_price_sol is None or token_price_sol <= 0:
raise ValueError(
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price"
)
# Set is_mayhem_mode from bonding curve state
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
+16 -1
View File
@@ -193,6 +193,7 @@ class UniversalTrader:
# State tracking
self.traded_mints: set[Pubkey] = set()
self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id
self.token_queue: asyncio.Queue = asyncio.Queue()
self.processing: bool = False
self.processed_tokens: set[str] = set()
@@ -325,10 +326,17 @@ class UniversalTrader:
if self.traded_mints:
try:
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
# Build parallel lists of mints and token_program_ids
mints_list = list(self.traded_mints)
token_program_ids = [
self.traded_token_programs.get(str(mint))
for mint in mints_list
]
await handle_cleanup_post_session(
self.solana_client,
self.wallet,
list(self.traded_mints),
mints_list,
token_program_ids,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
@@ -447,6 +455,10 @@ class UniversalTrader:
buy_result.tx_signature,
)
self.traded_mints.add(token_info.mint)
# Track token program for cleanup
mint_str = str(token_info.mint)
if token_info.token_program_id:
self.traded_token_programs[mint_str] = token_info.token_program_id
# Choose exit strategy
if not self.marry_mode:
@@ -469,6 +481,7 @@ class UniversalTrader:
self.solana_client,
self.wallet,
token_info.mint,
token_info.token_program_id,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
@@ -521,6 +534,7 @@ class UniversalTrader:
self.solana_client,
self.wallet,
token_info.mint,
token_info.token_program_id,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
@@ -590,6 +604,7 @@ class UniversalTrader:
self.solana_client,
self.wallet,
token_info.mint,
token_info.token_program_id,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,