mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-16 16:58:04 +00:00
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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user