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
+119 -17
View File
@@ -6,7 +6,7 @@ import sys
import base58
import grpc
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
@@ -58,30 +58,63 @@ GEYSER_API_TOKEN = os.environ.get("GEYSER_API_TOKEN")
AUTH_TYPE = os.environ.get("GEYSER_AUTH_TYPE", "x-token") # Default to x-token
PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_DISCRIMINATOR = bytes([214, 144, 76, 236, 95, 139, 49, 180])
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(
@@ -139,6 +172,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)
async def create_geyser_connection():
"""Establish a secure connection to the Geyser endpoint using the configured auth type."""
if AUTH_TYPE == "x-token":
@@ -232,7 +312,13 @@ async def listen_for_create_transaction_geyser():
# Check each instruction in the transaction
for ix in msg.instructions:
if not ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
# Check which create instruction was used
token_program = None
if ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
token_program = SYSTEM_TOKEN_PROGRAM
elif ix.data.startswith(PUMP_CREATE_V2_DISCRIMINATOR):
token_program = SYSTEM_TOKEN_2022_PROGRAM
else:
continue
# Found a create instruction
@@ -240,6 +326,10 @@ async def listen_for_create_transaction_geyser():
ix.data, msg.account_keys, ix.accounts
)
# Add token program info to decoded args
token_data["token_program"] = str(token_program)
token_data["is_token_2022"] = (token_program == SYSTEM_TOKEN_2022_PROGRAM)
signature = base58.b58encode(
bytes(update.transaction.transaction.signature)
).decode()
@@ -253,6 +343,7 @@ async def buy_token(
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
token_program: Pubkey,
amount: float,
slippage: float = 0.25,
max_retries=5,
@@ -262,22 +353,30 @@ async def buy_token(
async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address(
payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
payer.pubkey(), mint, token_program
)
amount_lamports = int(amount * LAMPORTS_PER_SOL)
# Fetch the token price
# Fetch bonding curve state to calculate price and determine fee recipient
# NOTE: Price calculation is commented out to speed up testing - using fixed values
# For production, uncomment the lines below to:
# 1. Calculate proper token amounts based on current price
# 2. Detect mayhem mode and use correct fee recipient
# curve_state = await get_pump_curve_state(client, bonding_curve)
# token_price_sol = calculate_pump_curve_price(curve_state)
# token_amount = amount / token_price_sol
token_amount = 100
# fee_recipient = await get_fee_recipient(client, curve_state)
# Testing values - replace with code above for production
token_amount = 100 # Fixed token amount
fee_recipient = PUMP_FEE # Standard fee recipient (doesn't detect mayhem mode)
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + slippage))
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(
@@ -293,7 +392,7 @@ async def buy_token(
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
pubkey=token_program, is_signer=False, is_writable=False
),
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
AccountMeta(
@@ -303,7 +402,7 @@ async def buy_token(
AccountMeta(
pubkey=_find_global_volume_accumulator(),
is_signer=False,
is_writable=True,
is_writable=False,
),
AccountMeta(
pubkey=_find_user_volume_accumulator(payer.pubkey()),
@@ -329,10 +428,11 @@ async def buy_token(
discriminator
+ struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports)
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
)
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
idempotent_ata_ix = create_idempotent_associated_token_account(
payer.pubkey(), payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
payer.pubkey(), payer.pubkey(), mint, token_program
)
msg = Message(
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
@@ -396,6 +496,7 @@ async def main():
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
token_program = Pubkey.from_string(token_data["token_program"])
# Fetch the token price
# async with AsyncClient(RPC_ENDPOINT) as client:
@@ -407,12 +508,13 @@ async def main():
slippage = 0.3 # 30% slippage tolerance
print(f"Bonding curve address: {bonding_curve}")
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
# print(f"Token price: {token_price_sol:.10f} SOL")
print(
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
)
await buy_token(
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
)