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
+138 -18
View File
@@ -3,7 +3,7 @@ import os
import struct
import base58
from construct import Bytes, Flag, Int64ul, Struct
from construct import Flag, Int64ul, Struct
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
@@ -18,7 +18,7 @@ from spl.token.instructions import get_associated_token_address
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
TOKEN_DECIMALS = 6
TOKEN_MINT = Pubkey.from_string("...")
TOKEN_MINT = Pubkey.from_string("...") # Replace with actual token mint address
# Global constants
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
@@ -30,6 +30,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
@@ -43,27 +44,59 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState:
_STRUCT = Struct(
"""Bonding curve state parser with progressive field parsing.
Parses bonding curve account data progressively based on available bytes,
making it forward-compatible with future schema versions.
"""
# Base struct present in all versions
_BASE_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
)
def __init__(self, data: bytes) -> None:
"""Parse bonding curve data."""
"""Parse bonding curve data progressively based on available bytes.
Args:
data: Raw account data including discriminator
Raises:
ValueError: If discriminator is invalid or data is too short
"""
if len(data) < 8:
raise ValueError("Data too short to contain discriminator")
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
parsed = self._STRUCT.parse(data[8:])
# Parse base fields (always present)
offset = 8
base_data = data[offset:]
parsed = self._BASE_STRUCT.parse(base_data)
self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field
if hasattr(self, "creator") and isinstance(self.creator, bytes):
self.creator = Pubkey.from_bytes(self.creator)
# Calculate offset after base struct
offset += self._BASE_STRUCT.sizeof()
# Parse creator if bytes remaining (added in V2)
if len(data) >= offset + 32:
creator_bytes = data[offset : offset + 32]
self.creator = Pubkey.from_bytes(creator_bytes)
offset += 32
else:
self.creator = None
# Parse mayhem mode flag if bytes remaining (added in V3)
if len(data) >= offset + 1:
self.is_mayhem_mode = bool(data[offset])
else:
self.is_mayhem_mode = False
async def get_pump_curve_state(
@@ -84,11 +117,13 @@ def get_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]:
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
def find_associated_bonding_curve(
mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey
) -> Pubkey:
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(SYSTEM_TOKEN_PROGRAM),
bytes(token_program_id),
bytes(mint),
],
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
@@ -112,6 +147,53 @@ def _find_fee_config() -> Pubkey:
return derived_address
async def get_fee_recipient(
client: AsyncClient, curve_state: BondingCurveState
) -> Pubkey:
"""Determine the correct fee recipient based on mayhem mode.
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
instead of the standard fee recipient. This function checks the bonding curve state
and returns the appropriate fee recipient.
Args:
client: Solana RPC client to fetch Global account data
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
Returns:
Appropriate fee recipient pubkey (mayhem or standard)
"""
if not curve_state.is_mayhem_mode:
return PUMP_FEE
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
if not response.value or not response.value.data:
# Fallback to standard fee if Global account cannot be fetched
return PUMP_FEE
data = response.value.data
# Parse reserved_fee_recipient from Global account
# Offset calculation based on pump_fun_idl.json Global struct:
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
RESERVED_FEE_RECIPIENT_OFFSET = 483
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
# Fallback if account data is too short
return PUMP_FEE
reserved_fee_recipient_bytes = data[
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
]
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
@@ -128,11 +210,31 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
return 0
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
"""Determines if a mint uses TokenProgram or Token2022Program."""
mint_info = await client.get_account_info(mint_address)
if not mint_info.value:
raise ValueError(f"Could not fetch mint info for {mint_address}")
owner = mint_info.value.owner
if owner == SYSTEM_TOKEN_PROGRAM:
return SYSTEM_TOKEN_PROGRAM
elif owner == TOKEN_2022_PROGRAM:
return TOKEN_2022_PROGRAM
else:
raise ValueError(
f"Mint account {mint_address} is owned by an unknown program: {owner}"
)
async def sell_token(
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
token_program_id: Pubkey,
slippage: float = 0.25,
max_retries=5,
):
@@ -140,7 +242,9 @@ async def sell_token(
payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
associated_token_account = get_associated_token_address(
payer.pubkey(), mint, token_program_id
)
# Get token balance
token_balance = await get_token_balance(client, associated_token_account)
@@ -150,7 +254,7 @@ async def sell_token(
print("No tokens to sell.")
return
# Fetch the token price
# Fetch bonding curve state to calculate price and determine fee recipient
curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state)
print(f"Price per Token: {token_price_sol:.20f} SOL")
@@ -164,9 +268,12 @@ async def sell_token(
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
# Determine fee recipient based on whether token uses mayhem mode
fee_recipient = await get_fee_recipient(client, curve_state)
accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(
@@ -187,8 +294,8 @@ async def sell_token(
is_writable=True,
),
AccountMeta(
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
),
pubkey=token_program_id, is_signer=False, is_writable=False
), # Use dynamic token_program_id
AccountMeta(
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
),
@@ -208,10 +315,13 @@ async def sell_token(
]
discriminator = struct.pack("<Q", 12502976635542562355)
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
data = (
discriminator
+ struct.pack("<Q", amount)
+ struct.pack("<Q", min_sol_output)
+ track_volume_bytes
)
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
@@ -248,8 +358,13 @@ async def sell_token(
async def main():
# Replace these with the actual values for the token you want to sell
async with AsyncClient(RPC_ENDPOINT) as client:
token_program_id = await get_token_program_id(client, TOKEN_MINT)
bonding_curve, _ = get_bonding_curve_address(TOKEN_MINT)
associated_bonding_curve = find_associated_bonding_curve(TOKEN_MINT, bonding_curve)
associated_bonding_curve = find_associated_bonding_curve(
TOKEN_MINT, bonding_curve, token_program_id
)
async with AsyncClient(RPC_ENDPOINT) as client:
curve_state = await get_pump_curve_state(client, bonding_curve)
@@ -261,7 +376,12 @@ async def main():
print(f"Bonding curve address: {bonding_curve}")
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
await sell_token(
TOKEN_MINT, bonding_curve, associated_bonding_curve, creator_vault, slippage
TOKEN_MINT,
bonding_curve,
associated_bonding_curve,
creator_vault,
token_program_id,
slippage,
)