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
+825 -150
View File
File diff suppressed because it is too large Load Diff
+241 -2
View File
@@ -543,7 +543,6 @@
},
{
"name": "global_volume_accumulator",
"writable": true,
"pda": {
"seeds": [
{
@@ -972,7 +971,6 @@
},
{
"name": "global_volume_accumulator",
"writable": true,
"pda": {
"seeds": [
{
@@ -2085,6 +2083,10 @@
{
"name": "coin_creator",
"type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
}
]
},
@@ -3006,6 +3008,92 @@
],
"args": []
},
{
"name": "set_reserved_fee_recipients",
"discriminator": [
111,
172,
162,
232,
114,
89,
213,
142
],
"accounts": [
{
"name": "global_config",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
103,
108,
111,
98,
97,
108,
95,
99,
111,
110,
102,
105,
103
]
}
]
}
},
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": [
{
"name": "whitelist_pda",
"type": "pubkey"
}
]
},
{
"name": "sync_user_volume_accumulator",
"discriminator": [
@@ -3134,6 +3222,70 @@
],
"args": []
},
{
"name": "toggle_mayhem_mode",
"discriminator": [
1,
9,
111,
208,
100,
31,
255,
163
],
"accounts": [
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": [
{
"name": "enabled",
"type": "bool"
}
]
},
{
"name": "update_admin",
"discriminator": [
@@ -3644,6 +3796,19 @@
216
]
},
{
"name": "ReservedFeeRecipientsEvent",
"discriminator": [
43,
188,
250,
18,
221,
75,
187,
95
]
},
{
"name": "SellEvent",
"discriminator": [
@@ -3902,6 +4067,22 @@
"code": 6040,
"name": "BuySlippageBelowMinBaseAmountOut",
"msg": "buy: slippage - would buy less tokens than expected min_base_amount_out"
},
{
"code": 6041,
"name": "MayhemModeDisabled"
},
{
"code": 6042,
"name": "OnlyPumpPoolsMayhemMode"
},
{
"code": 6043,
"name": "MayhemModeInDesiredState"
},
{
"code": 6044,
"name": "NotEnoughRemainingAccounts"
}
],
"types": [
@@ -4005,6 +4186,10 @@
{
"name": "creator",
"type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
}
]
}
@@ -4357,6 +4542,10 @@
{
"name": "coin_creator",
"type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
}
]
}
@@ -4625,6 +4814,27 @@
"The admin authority for setting coin creators"
],
"type": "pubkey"
},
{
"name": "whitelist_pda",
"type": "pubkey"
},
{
"name": "reserved_fee_recipient",
"type": "pubkey"
},
{
"name": "mayhem_mode_enabled",
"type": "bool"
},
{
"name": "reserved_fee_recipients",
"type": {
"array": [
"pubkey",
7
]
}
}
]
}
@@ -4747,6 +4957,35 @@
{
"name": "coin_creator",
"type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
}
]
}
},
{
"name": "ReservedFeeRecipientsEvent",
"type": {
"kind": "struct",
"fields": [
{
"name": "timestamp",
"type": "i64"
},
{
"name": "reserved_fee_recipient",
"type": "pubkey"
},
{
"name": "reserved_fee_recipients",
"type": {
"array": [
"pubkey",
7
]
}
}
]
}
@@ -1,9 +1,6 @@
"""
Module for checking the status of a token's bonding curve on the Solana network using
the Pump.fun program. It allows querying the bonding curve state and completion status.
Note: creator fee upgrade introduced updates in bonding curve structure.
https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
"""
import argparse
@@ -42,20 +39,11 @@ class BondingCurveState:
real_sol_reserves: Real SOL reserves in the curve
token_total_supply: Total token supply in the curve
complete: Whether the curve has completed and liquidity migrated
is_mayhem_mode: Whether the curve is in mayhem mode
"""
_STRUCT_1 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
)
# Struct after creator fee update has been introduced
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
_STRUCT_2 = Struct(
# V2: Struct with creator field (81 bytes total: 8 discriminator + 73 data)
_STRUCT_V2 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
@@ -65,26 +53,43 @@ class BondingCurveState:
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
)
# V3: Struct with creator + mayhem mode (82 bytes total: 8 discriminator + 74 data)
_STRUCT_V3 = 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),
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
)
def __init__(self, data: bytes) -> None:
"""Parse bonding curve data."""
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
if len(data) < 150:
parsed = self._STRUCT_1.parse(data[8:])
self.__dict__.update(parsed)
total_length = len(data)
else:
parsed = self._STRUCT_2.parse(data[8:])
if total_length == 81: # V2: Creator only
parsed = self._STRUCT_V2.parse(data[8:])
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)
self.creator = Pubkey.from_bytes(self.creator)
self.is_mayhem_mode = False
elif total_length >= 82: # V3: Creator + mayhem mode
parsed = self._STRUCT_V3.parse(data[8:])
self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field
self.creator = Pubkey.from_bytes(self.creator)
else:
raise ValueError(f"Unexpected bonding curve size: {total_length} bytes")
def get_associated_bonding_curve_address(
mint: Pubkey, program_id: Pubkey
) -> tuple[Pubkey, int]:
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
"""
Derives the associated bonding curve address for a given mint.
@@ -134,17 +139,14 @@ async def check_token_status(mint_address: str) -> None:
"""
try:
mint = Pubkey.from_string(mint_address)
# Get the associated bonding curve address
bonding_curve_address, bump = get_associated_bonding_curve_address(
mint, PUMP_PROGRAM_ID
)
bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM_ID)
print("\nToken status:")
print("-" * 50)
print(f"Token mint: {mint}")
print(f"Associated bonding curve: {bonding_curve_address}")
print(f"Bump seed: {bump}")
print(f"Bonding curve: {bonding_curve_address}")
if bump is not None:
print(f"Bump seed: {bump}")
print("-" * 50)
# Check completion status
@@ -156,9 +158,25 @@ async def check_token_status(mint_address: str) -> None:
print("\nBonding curve status:")
print("-" * 50)
print(f"Creator: {curve_state.creator}")
print(
f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}"
f"Mayhem Mode: {'✅ Enabled' if curve_state.is_mayhem_mode else '❌ Disabled'}"
)
print(
f"Completed: {'✅ Migrated' if curve_state.complete else '❌ Bonding curve'}"
)
print("\nBonding curve reserves:")
print(f"Virtual Token: {curve_state.virtual_token_reserves:,}")
print(
f"Virtual SOL: {curve_state.virtual_sol_reserves:,} lamports"
)
print(f"Real Token: {curve_state.real_token_reserves:,}")
print(
f"Real SOL: {curve_state.real_sol_reserves:,} lamports"
)
print(f"Total Supply: {curve_state.token_total_supply:,}")
if curve_state.complete:
print(
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
@@ -17,7 +17,7 @@ load_dotenv()
# Constants
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT: Final[str] = (
"YOUR_TOKEN_MINT_ADDRESS_HERE" # Replace with actual token mint address
"5ZHx2GGGj87xpidVJpBqadMUutqBirhL2TqUR9T9taKc" # Replace with actual token mint address
)
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
@@ -30,7 +30,7 @@ EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check
def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
"""
Derive the bonding curve PDA address from a mint address.
@@ -81,8 +81,9 @@ def parse_curve_state(data: bytes) -> dict:
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid discriminator for bonding curve")
# Parse common fields (present in all versions)
fields = struct.unpack_from("<QQQQQ?", data, 8)
return {
result = {
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
@@ -91,6 +92,20 @@ def parse_curve_state(data: bytes) -> dict:
"complete": fields[5],
}
# Parse creator field if present
data_length = len(data) - 8
if data_length >= 73: # Has creator field
creator_bytes = data[49:81] # 8 (discriminator) + 41 (base fields) = 49
result["creator"] = Pubkey.from_bytes(creator_bytes)
# Parse is_mayhem_mode if present
if data_length >= 74: # Has mayhem mode field
result["is_mayhem_mode"] = bool(data[81])
else:
result["is_mayhem_mode"] = False
return result
def print_curve_status(state: dict) -> None:
"""
@@ -130,9 +145,7 @@ async def track_curve() -> None:
return
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
curve_pubkey: Pubkey = get_associated_bonding_curve_address(
mint_pubkey, PUMP_PROGRAM_ID
)
curve_pubkey: Pubkey = get_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID)
print("Tracking bonding curve for:", mint_pubkey)
print("Curve address:", curve_pubkey, "\n")
+7 -3
View File
@@ -19,6 +19,10 @@ PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY")
# Update this address to MINT address of a token you want to close
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump")
# Token program for the mint - use TOKEN_PROGRAM for legacy SPL tokens, TOKEN_2022_PROGRAM for Token-2022
# This must match the actual token's program to derive the correct ATA address
TOKEN_PROGRAM = SystemAddresses.TOKEN_PROGRAM
async def close_account_if_exists(
client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey
@@ -41,7 +45,7 @@ async def close_account_if_exists(
mint=mint,
owner=wallet.pubkey,
amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM,
program_id=TOKEN_PROGRAM,
)
)
await client.build_and_send_transaction([burn_ix], wallet.keypair)
@@ -54,7 +58,7 @@ async def close_account_if_exists(
account=account,
dest=wallet.pubkey,
owner=wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM,
program_id=TOKEN_PROGRAM,
)
ix = close_account(close_params)
@@ -78,7 +82,7 @@ async def main():
wallet = Wallet(PRIVATE_KEY)
# Get user's ATA for the token
ata = wallet.get_associated_token_address(MINT_ADDRESS)
ata = wallet.get_associated_token_address(MINT_ADDRESS, TOKEN_PROGRAM)
await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
except Exception as e:
@@ -121,13 +121,27 @@ def parse_trade_event(logs):
def decode_trade_event(data):
"""Decode TradeEvent structure from raw bytes."""
if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check
"""Decode TradeEvent structure from raw bytes with progressive parsing.
Supports both pre-mayhem and post-mayhem IDL versions by parsing fields
progressively based on available bytes. This ensures backward compatibility
with older transaction logs.
Core fields (always present): mint, sol_amount, token_amount, is_buy, user,
timestamp, virtual_sol_reserves, virtual_token_reserves
Extended fields (added later): real_sol_reserves, real_token_reserves,
fee_recipient, fee_basis_points, fee, creator, creator_fee_basis_points,
creator_fee, track_volume, total_unclaimed_tokens, total_claimed_tokens,
current_sol_volume, last_update_timestamp, ix_name
"""
# Minimum size for core fields: 32+8+8+1+32+8+8+8 = 105 bytes
if len(data) < 105:
return None
offset = 0
# Parse fields according to TradeEvent structure
# Parse core fields (always present in all versions)
mint = data[offset : offset + 32]
offset += 32
@@ -152,6 +166,73 @@ def decode_trade_event(data):
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
# Parse extended fields if bytes remaining (added in later versions)
# Real reserves (8+8 = 16 bytes)
if len(data) >= offset + 16:
real_sol_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
real_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
else:
real_sol_reserves = 0
real_token_reserves = 0
# Fee recipient and fee details (32+8+8 = 48 bytes)
if len(data) >= offset + 48:
fee_recipient = data[offset : offset + 32]
offset += 32
fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
fee = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
else:
fee_recipient = b'\x00' * 32
fee_basis_points = 0
fee = 0
# Creator and creator fee details (32+8+8 = 48 bytes)
if len(data) >= offset + 48:
creator = data[offset : offset + 32]
offset += 32
creator_fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
creator_fee = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
else:
creator = b'\x00' * 32
creator_fee_basis_points = 0
creator_fee = 0
# Volume tracking fields (1+8+8+8+8 = 33 bytes)
if len(data) >= offset + 33:
track_volume = bool(data[offset])
offset += 1
total_unclaimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
total_claimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
current_sol_volume = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8
last_update_timestamp = struct.unpack("<q", data[offset : offset + 8])[0]
offset += 8
else:
track_volume = False
total_unclaimed_tokens = 0
total_claimed_tokens = 0
current_sol_volume = 0
last_update_timestamp = 0
# Parse string field (ix_name): 4 bytes for length + string data
if len(data) >= offset + 4:
string_length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if len(data) >= offset + string_length:
ix_name = data[offset : offset + string_length].decode("utf-8")
else:
ix_name = ""
else:
ix_name = ""
return {
"mint": base58.b58encode(mint).decode(),
"sol_amount": sol_amount,
@@ -161,6 +242,20 @@ def decode_trade_event(data):
"timestamp": timestamp,
"virtual_sol_reserves": virtual_sol_reserves,
"virtual_token_reserves": virtual_token_reserves,
"real_sol_reserves": real_sol_reserves,
"real_token_reserves": real_token_reserves,
"fee_recipient": base58.b58encode(fee_recipient).decode() if fee_recipient != b'\x00' * 32 else None,
"fee_basis_points": fee_basis_points,
"fee": fee,
"creator": base58.b58encode(creator).decode() if creator != b'\x00' * 32 else None,
"creator_fee_basis_points": creator_fee_basis_points,
"creator_fee": creator_fee,
"track_volume": track_volume,
"total_unclaimed_tokens": total_unclaimed_tokens,
"total_claimed_tokens": total_claimed_tokens,
"current_sol_volume": current_sol_volume,
"last_update_timestamp": last_update_timestamp,
"ix_name": ix_name,
"price_per_token": (sol_amount * 1_000_000) / token_amount
if token_amount > 0
else 0,
@@ -178,7 +273,10 @@ def display_transaction_info(signature, logs):
# Parse trade event data
trade_data = parse_trade_event(logs)
if trade_data:
print(f" Type: {'BUY' if trade_data['is_buy'] else 'SELL'}")
# Core transaction info (always present)
ix_name = trade_data.get('ix_name', '')
trade_type = 'BUY' if trade_data['is_buy'] else 'SELL'
print(f" Type: {trade_type}{f' ({ix_name})' if ix_name else ''}")
print(f" Token: {trade_data['mint']}")
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
print(f" Token Amount: {trade_data['token_amount']:,}")
@@ -187,6 +285,25 @@ def display_transaction_info(signature, logs):
)
print(f" Trader: {trade_data['user']}")
# Fee info (may not be present in older transactions)
if trade_data['fee'] > 0 or trade_data['fee_basis_points'] > 0:
print(f" Fee: {trade_data['fee'] / 1_000_000_000:.6f} SOL ({trade_data['fee_basis_points']} bps)")
if trade_data['creator_fee'] > 0 or trade_data['creator_fee_basis_points'] > 0:
print(f" Creator Fee: {trade_data['creator_fee'] / 1_000_000_000:.6f} SOL ({trade_data['creator_fee_basis_points']} bps)")
if trade_data['creator']:
print(f" Creator: {trade_data['creator']}")
if trade_data['fee_recipient']:
print(f" Fee Recipient: {trade_data['fee_recipient']}")
# Reserve info
print(f" Virtual Reserves: {trade_data['virtual_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['virtual_token_reserves']:,} tokens")
if trade_data['real_sol_reserves'] > 0 or trade_data['real_token_reserves'] > 0:
print(f" Real Reserves: {trade_data['real_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['real_token_reserves']:,} tokens")
# Extract and display program info
display_program_info(logs)
+30 -21
View File
@@ -11,18 +11,7 @@ EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
class BondingCurveState:
_STRUCT_1 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
)
# Struct after creator fee update has been introduced
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
_STRUCT_2 = Struct(
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
@@ -30,23 +19,43 @@ class BondingCurveState:
"token_total_supply" / Int64ul,
"complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
)
def __init__(self, data: bytes) -> None:
"""Parse bonding curve data."""
"""Parse bonding curve data - supports all versions."""
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
if len(data) < 150:
parsed = self._STRUCT_1.parse(data[8:])
self.__dict__.update(parsed)
# Required fields (always present)
offset = 8
self.virtual_token_reserves = int.from_bytes(
data[offset : offset + 8], "little"
)
offset += 8
self.virtual_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
offset += 8
self.real_token_reserves = int.from_bytes(data[offset : offset + 8], "little")
offset += 8
self.real_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
offset += 8
self.token_total_supply = int.from_bytes(data[offset : offset + 8], "little")
offset += 8
self.complete = bool(data[offset])
offset += 1
# Optional fields (may not be present in older versions)
if len(data) >= offset + 32:
self.creator = Pubkey.from_bytes(data[offset : offset + 32])
offset += 32
if len(data) > offset:
self.is_mayhem_mode = bool(data[offset])
else:
self.is_mayhem_mode = None
else:
parsed = self._STRUCT_2.parse(data[8:])
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)
self.creator = None
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
@@ -27,6 +27,7 @@ print(json.dumps(tx_data, indent=2))
def decode_create_instruction(data):
"""Decode legacy Create instruction (Metaplex tokens)."""
# The Create instruction has 3 string arguments: name, symbol, uri
offset = 8 # Skip the 8-byte discriminator
results = []
@@ -36,7 +37,42 @@ def decode_create_instruction(data):
string_data = data[offset : offset + length].decode("utf-8")
results.append(string_data)
offset += length
return {"name": results[0], "symbol": results[1], "uri": results[2]}
return {
"name": results[0],
"symbol": results[1],
"uri": results[2],
"token_standard": "legacy",
"is_mayhem_mode": False,
}
def decode_create_v2_instruction(data):
"""Decode CreateV2 instruction (Token2022 tokens)."""
# The CreateV2 instruction has 3 string arguments: name, symbol, uri + is_mayhem_mode
offset = 8 # Skip the 8-byte discriminator
results = []
for _ in range(3):
length = struct.unpack_from("<I", data, offset)[0]
offset += 4
string_data = data[offset : offset + length].decode("utf-8")
results.append(string_data)
offset += length
# Skip creator pubkey (32 bytes)
offset += 32
# Parse is_mayhem_mode (OptionBool at the end)
is_mayhem_mode = False
if offset < len(data):
is_mayhem_mode = bool(data[offset])
return {
"name": results[0],
"symbol": results[1],
"uri": results[2],
"token_standard": "token2022",
"is_mayhem_mode": is_mayhem_mode,
}
def decode_buy_instruction(data):
@@ -48,6 +84,8 @@ def decode_buy_instruction(data):
def decode_instruction_data(instruction, accounts, data):
if instruction["name"] == "create":
return decode_create_instruction(data)
elif instruction["name"] == "createV2":
return decode_create_v2_instruction(data)
elif instruction["name"] == "buy":
return decode_buy_instruction(data)
else:
+45 -10
View File
@@ -1,20 +1,15 @@
import asyncio
import os
import struct
import sys
from typing import Final
from construct import Flag, Int64ul, Struct
from construct import Bytes, Flag, Int64ul, Struct
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6
CURVE_ADDRESS: Final[str] = (
"YOUR_BONDING_CURVE_ADDRESS_HERE" # Replace with actual bonding curve address
)
CURVE_ADDRESS: Final[str] = "..." # Replace with actual bonding curve address
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
@@ -23,7 +18,9 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState:
_STRUCT = Struct(
"""Parse bonding curve account data - supports all versions."""
_STRUCT_V1 = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
@@ -32,9 +29,47 @@ class BondingCurveState:
"complete" / Flag,
)
_STRUCT_V2 = 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),
)
_STRUCT_V3 = 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),
"is_mayhem_mode" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
"""Parse bonding curve data - auto-detects version."""
data_length = len(data) - 8
if data_length < 73: # V1: without creator and mayhem mode
parsed = self._STRUCT_V1.parse(data[8:])
self.__dict__.update(parsed)
self.creator = None
self.is_mayhem_mode = False
elif data_length == 73: # V2: with creator, without mayhem mode
parsed = self._STRUCT_V2.parse(data[8:])
self.__dict__.update(parsed)
if isinstance(self.creator, bytes):
self.creator = Pubkey.from_bytes(self.creator)
self.is_mayhem_mode = False
else: # V3: with creator and mayhem mode
parsed = self._STRUCT_V3.parse(data[8:])
self.__dict__.update(parsed)
if isinstance(self.creator, bytes):
self.creator = Pubkey.from_bytes(self.creator)
async def get_bonding_curve_state(
@@ -1,11 +1,19 @@
"""
This script compares two methods of detecting migrations:
1. Migration program listener (listens Migration program) - detects markets via successful migration transactions
2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription
The script tracks which method detects new markets first and provides detailed performance statistics.
1. Migration Program Listener - Listens to migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
which emits detailed migration events via logsSubscribe
Note: multiple endpoints available. Scroll down to change providers which you want to test.
2. Direct Pool Account Listener - Listens to pump_amm program (pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA)
for new Pool account creations via programSubscribe
Note: The migration wrapper program emits a different event structure than
CompletePumpAmmMigrationEvent in pump_fun_idl.json.
The script tracks which method detects new migrations first and provides detailed performance
statistics including message counts, detection timing, provider latency comparison.
Configure multiple RPC endpoints in .env file to test provider performance.
"""
import asyncio
@@ -33,7 +41,7 @@ QUOTE_MINT_SOL = base58.b58encode(
).decode()
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # Pool account with is_mayhem_mode = 244 bytes
class DetectionTracker:
@@ -306,9 +314,9 @@ async def fetch_existing_market_pubkeys():
def parse_market_account_data(data):
"""
Parse binary market account data into a structured format
Parse binary Pool account data according to pump_swap_idl.json structure
This function matches the parser from the market listener script
Total 11 fields including is_mayhem_mode field added with mayhem update
"""
parsed_data = {}
offset = 8 # Skip discriminator
@@ -323,6 +331,8 @@ def parse_market_account_data(data):
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
]
try:
@@ -347,6 +357,10 @@ def parse_market_account_data(data):
value = data[offset]
parsed_data[field_name] = value
offset += 1
elif field_type == "bool":
value = bool(data[offset])
parsed_data[field_name] = value
offset += 1
except Exception as e:
print(f"[ERROR] Failed to parse market data: {e}")
@@ -355,9 +369,11 @@ def parse_market_account_data(data):
def parse_migrate_instruction(data):
"""
Parse binary migration instruction data into a structured format
Parse migration event from the migration wrapper program
This function matches the parser from the migration listener script
Note: This parses the event emitted by the migration wrapper program
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
"""
if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes")
@@ -1,8 +1,11 @@
"""
Listens for 'Migrate' instructions from a Solana migration program via WebSocket.
Listens for 'Migrate' instructions from Solana migration program via WebSocket.
Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations.
Note: skips transactions with truncated logs (no Program data in the logs -> no parsed data).
Note: This uses a migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
that emits a different event structure than the CompletePumpAmmMigrationEvent in pump_fun_idl.json.
Skips transactions with truncated logs (no Program data in the logs -> no parsed data).
To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs.
"""
@@ -26,6 +29,12 @@ MIGRATION_PROGRAM_ID = Pubkey.from_string(
def parse_migrate_instruction(data):
"""Parse migration event from the migration wrapper program.
Note: This parses the event emitted by the migration wrapper program
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
"""
if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes")
return None
@@ -24,7 +24,7 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # discriminator + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode = 244 bytes
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
QUOTE_MINT_SOL = base58.b58encode(
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
@@ -58,6 +58,10 @@ async def fetch_existing_market_pubkeys():
def parse_market_account_data(data):
"""Parse Pool account data according to pump_swap_idl.json structure.
Total 11 fields including the new is_mayhem_mode field added with mayhem update.
"""
parsed_data = {}
offset = 8 # Discriminator
@@ -72,6 +76,7 @@ def parse_market_account_data(data):
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
]
try:
@@ -96,6 +101,10 @@ def parse_market_account_data(data):
value = data[offset]
parsed_data[field_name] = value
offset += 1
elif field_type == "bool":
value = bool(data[offset])
parsed_data[field_name] = value
offset += 1
except Exception as e:
print(f"[ERROR] Failed to parse market data: {e}")
@@ -1,13 +1,36 @@
"""
This script compares four methods of detecting new Pump.fun tokens:
1. Block subscription listener - listens for blocks containing Pump.fun program
2. Geyser gRPC listener - uses Geyser gRPC API to get transactions containing Pump.fun program
3. Logs subscription listener - listens for logs containing Pump.fun program
4. PumpPortal WebSocket listener - connects to PumpPortal WebSocket and listens for token events
Performance Comparison Tool for Pump.fun Token Detection Methods
The script tracks which method detects new tokens first and provides detailed performance statistics.
This script compares four methods of detecting new Pump.fun tokens in real-time:
Note: multiple endpoints available. Scroll down to change providers which you want to test.
1. Block Subscription (blockSubscribe)
- Method: WebSocket subscription to blocks mentioning Pump.fun program
- Speed: Slowest (processes entire blocks)
- Reference: https://solana.com/docs/rpc/websocket/blocksubscribe
2. Logs Subscription (logsSubscribe)
- Method: WebSocket subscription to program logs
- Speed: Fast (event data includes all fields)
- Reference: https://solana.com/docs/rpc/websocket/logssubscribe
3. Geyser gRPC
- Method: Yellowstone Dragon's Mouth gRPC streaming
- Speed: Fastest (optimized streaming protocol)
- Reference: https://docs.triton.one/rpc-pool/grpc-subscriptions
4. PumpPortal WebSocket
- Method: Third-party aggregated WebSocket feed
- Speed: Fast (pre-processed data)
- Note: Requires trust in third-party provider
The script tracks which method detects each token first and provides detailed
performance statistics including:
- First detection counts per method
- Average latency between methods
- Message counts per provider
- Token detection coverage
Configuration: Set provider endpoints in .env or modify the providers dict at the bottom.
"""
import asyncio
@@ -26,13 +49,31 @@ from solders.transaction import VersionedTransaction
load_dotenv(override=True)
# Constants
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
TEST_DURATION = 30 # seconds
# ============ CONSTANTS ============
GEYSER_AUTH_TYPE = "x-token" # or "basic"
# Pump.fun program ID
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
# Instruction discriminators (8-byte identifiers for instruction types)
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
# and sha256("global:createV2") for Token2022 CreateV2
# See: learning-examples/calculate_discriminator.py
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
# Event discriminator for CreateEvent (8-byte identifier)
# This is emitted by both Create and CreateV2 instructions
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
# PumpPortal WebSocket endpoint (third-party service)
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
# Test duration in seconds
TEST_DURATION = 30
# Geyser authentication type: "x-token" or "basic"
GEYSER_AUTH_TYPE = "x-token"
class DetectionTracker:
@@ -217,44 +258,198 @@ async def fetch_existing_token_mints():
return set()
def parse_create_instruction(data):
"""
Parse binary create instruction data into a structured format
"""
if len(data) < 8:
def decode_create_instruction(ix_data, account_keys):
"""Decode legacy Create instruction (Metaplex tokens) from instruction data."""
if len(ix_data) < 8:
return None
offset = 8 # Skip discriminator
parsed_data = {}
try:
# Parse name (string)
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
parsed_data["name"] = data[offset : offset + length].decode("utf-8")
offset += length
# Read string fields from instruction data
def read_string():
nonlocal offset
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
return value
# Parse symbol (string)
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
parsed_data["symbol"] = data[offset : offset + length].decode("utf-8")
offset += length
def read_pubkey():
nonlocal offset
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
return value
# Parse uri (string)
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
parsed_data["uri"] = data[offset : offset + length].decode("utf-8")
offset += length
# Parse instruction arguments
parsed_data["name"] = read_string()
parsed_data["symbol"] = read_string()
parsed_data["uri"] = read_string()
parsed_data["creator"] = read_pubkey()
# Parse mint (pubkey)
parsed_data["mint"] = base58.b58encode(data[offset : offset + 32]).decode(
"utf-8"
)
offset += 32
# Extract accounts from account_keys array
if len(account_keys) >= 8:
parsed_data["mint"] = account_keys[0]
parsed_data["bondingCurve"] = account_keys[2]
parsed_data["user"] = account_keys[7]
elif len(account_keys) > 0:
parsed_data["mint"] = account_keys[0]
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse create instruction: {e}")
print(f"[ERROR] Failed to decode create instruction: {e}")
return None
def decode_create_v2_instruction(ix_data, account_keys):
"""Decode CreateV2 instruction (Token2022 tokens) from instruction data."""
if len(ix_data) < 8:
return None
offset = 8 # Skip discriminator
parsed_data = {}
try:
# Read string fields from instruction data
def read_string():
nonlocal offset
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
return value
def read_pubkey():
nonlocal offset
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
return value
# Parse instruction arguments
parsed_data["name"] = read_string()
parsed_data["symbol"] = read_string()
parsed_data["uri"] = read_string()
parsed_data["creator"] = read_pubkey()
# Parse is_mayhem_mode (OptionBool at the end)
if offset < len(ix_data):
parsed_data["is_mayhem_mode"] = bool(ix_data[offset])
else:
parsed_data["is_mayhem_mode"] = False
# Extract accounts from account_keys array
if len(account_keys) >= 6:
parsed_data["mint"] = account_keys[0]
parsed_data["bondingCurve"] = account_keys[2]
parsed_data["user"] = account_keys[5]
elif len(account_keys) > 0:
parsed_data["mint"] = account_keys[0]
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to decode create v2 instruction: {e}")
return None
def parse_create_event(data):
"""Parse legacy Create event from logs (event data includes all fields)."""
if len(data) < 8:
return None
offset = 8 # Skip discriminator
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse create event: {e}")
return None
def parse_create_v2_event(data):
"""Parse CreateV2 event from logs (event data includes all fields)."""
if len(data) < 8:
return None
offset = 8 # Skip discriminator
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse create v2 event: {e}")
return None
@@ -269,6 +464,51 @@ def is_transaction_successful(logs):
# ============ WEBSOCKET LISTENERS ============
def get_account_keys(transaction, instruction, loaded_addresses=None):
"""
Safely extract account keys for an instruction from a versioned transaction.
Handles both static account keys and loaded addresses from lookup tables.
Args:
transaction: VersionedTransaction object
instruction: Instruction object
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
Returns:
List of account keys as strings, or None if unable to resolve
"""
account_keys = []
static_keys = transaction.message.account_keys
# Combine all available account keys: static + loaded
all_keys = list(static_keys)
if loaded_addresses:
# Add loaded writable addresses
if "writable" in loaded_addresses:
for addr in loaded_addresses["writable"]:
all_keys.append(Pubkey.from_string(addr))
# Add loaded readonly addresses
if "readonly" in loaded_addresses:
for addr in loaded_addresses["readonly"]:
all_keys.append(Pubkey.from_string(addr))
# Now resolve account indices
for index in instruction.accounts:
try:
if index < len(all_keys):
account_keys.append(str(all_keys[index]))
else:
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
return None
except (IndexError, Exception) as e:
print(f"Error resolving account at index {index}: {e}")
return None
return account_keys
async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
"""
Listen for new tokens via block subscription
@@ -330,6 +570,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
try:
transaction = VersionedTransaction.from_bytes(tx_data)
# Extract loaded addresses from transaction metadata
loaded_addresses = None
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
loaded_addresses = tx["meta"]["loadedAddresses"]
for ix in transaction.message.instructions:
if (
transaction.message.account_keys[
@@ -339,39 +585,53 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
):
data_bytes = bytes(ix.data)
if not data_bytes.startswith(
PUMP_CREATE_PREFIX
):
# Check for both Create and CreateV2 instructions
is_create = data_bytes.startswith(PUMP_CREATE_PREFIX)
is_create_v2 = data_bytes.startswith(PUMP_CREATE_V2_PREFIX)
if not (is_create or is_create_v2):
continue
parsed = parse_create_instruction(data_bytes)
if not parsed:
# Get account keys with ALT support
account_keys = get_account_keys(
transaction, ix, loaded_addresses
)
if account_keys is None:
print("Skipping transaction due to unresolved accounts")
continue
if len(ix.accounts) > 0:
try:
mint = str(
transaction.message.account_keys[
ix.accounts[0]
]
) # First account is usually the mint
# Decode based on instruction type
if is_create_v2:
print(f"[{provider_name}_block] Detected: CreateV2 instruction (Token2022)")
decoded = decode_create_v2_instruction(data_bytes, account_keys)
else:
print(f"[{provider_name}_block] Detected: Create instruction (Legacy/Metaplex)")
decoded = decode_create_instruction(data_bytes, account_keys)
if mint in known_tokens:
continue
if not decoded:
continue
ts = time.time()
tracker.add_token(
mint,
parsed["name"],
parsed["symbol"],
f"{provider_name}_block",
ts,
)
known_tokens.add(mint)
except Exception as e:
print(
f"[ERROR] Failed to process block instruction: {e}"
)
mint = decoded.get("mint")
if not mint:
continue
if mint in known_tokens:
continue
try:
ts = time.time()
tracker.add_token(
mint,
decoded["name"],
decoded["symbol"],
f"{provider_name}_block",
ts,
)
known_tokens.add(mint)
except Exception as e:
print(
f"[ERROR] Failed to process block instruction: {e}"
)
except Exception as e:
print(f"[ERROR] Failed to process transaction: {e}")
@@ -424,9 +684,17 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
if not any(
"Program log: Instruction: Create" in log for log in logs
):
# Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log
for log in logs
)
is_create_v2 = any(
"Program log: Instruction: CreateV2" in log
for log in logs
)
if not (is_create or is_create_v2):
continue
for log in logs:
@@ -435,7 +703,23 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
encoded_data = log.split(": ")[1]
data_bytes = base64.b64decode(encoded_data)
parsed = parse_create_instruction(data_bytes)
# Check if this is a CreateEvent by validating discriminator
if len(data_bytes) < 8:
continue
event_discriminator = data_bytes[:8]
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
continue
# Parse based on instruction type
if is_create_v2:
print(f"[{provider_name}_logs] Detected: CreateV2 instruction (Token2022)")
parsed = parse_create_v2_event(data_bytes)
else:
print(f"[{provider_name}_logs] Detected: Create instruction (Legacy/Metaplex)")
parsed = parse_create_event(data_bytes)
if not parsed:
continue
@@ -492,11 +776,11 @@ async def listen_geyser_grpc(
if GEYSER_AUTH_TYPE == "x-token":
auth = grpc.metadata_call_credentials(
lambda context, callback: callback((("x-token", api_token),), None)
lambda _context, callback: callback((("x-token", api_token),), None)
)
else:
auth = grpc.metadata_call_credentials(
lambda context, callback: callback(
lambda _context, callback: callback(
(("authorization", f"Basic {api_token}"),), None
)
)
@@ -529,28 +813,44 @@ async def listen_geyser_grpc(
continue
for ix in msg.instructions:
if not ix.data.startswith(PUMP_CREATE_PREFIX):
# Check for both Create and CreateV2 instructions
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
if not (is_create or is_create_v2):
continue
parsed = parse_create_instruction(ix.data)
if not parsed:
# Convert account keys to string format
account_keys = []
for account_idx in ix.accounts:
if account_idx < len(msg.account_keys):
account_keys.append(
base58.b58encode(bytes(msg.account_keys[account_idx])).decode()
)
if len(account_keys) == 0:
continue
if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys):
continue
mint = base58.b58encode(
bytes(msg.account_keys[ix.accounts[0]])
).decode()
mint = account_keys[0]
if mint in known_tokens:
continue
# Decode based on instruction type
if is_create_v2:
print(f"[{provider_name}_geyser] Detected: CreateV2 instruction (Token2022)")
decoded = decode_create_v2_instruction(ix.data, account_keys)
else:
print(f"[{provider_name}_geyser] Detected: Create instruction (Legacy/Metaplex)")
decoded = decode_create_instruction(ix.data, account_keys)
if not decoded:
continue
ts = time.time()
tracker.add_token(
mint,
parsed["name"],
parsed["symbol"],
decoded["name"],
decoded["symbol"],
f"{provider_name}_geyser",
ts,
)
@@ -2,7 +2,17 @@
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
Decodes transaction data to extract mint, bonding curve, and user details.
It is usually slower than other listeners.
Performance: Usually slower than other listeners due to block-level processing.
This script uses blockSubscribe which receives entire blocks containing transactions
that mention the Pump.fun program. It then decodes the instruction data from each
transaction to extract token creation details.
WebSocket API Reference:
https://solana.com/docs/rpc/websocket/blocksubscribe
Address Lookup Tables (ALT) Support:
https://solana.com/docs/advanced/lookup-tables
"""
import asyncio
@@ -22,6 +32,93 @@ load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
# Instruction discriminators (8-byte identifiers for instruction types)
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
# and sha256("global:createV2") for Token2022 CreateV2
# See: learning-examples/calculate_discriminator.py
CREATE_DISCRIMINATOR = 8576854823835016728
CREATE_V2_DISCRIMINATOR = struct.unpack("<Q", bytes([214, 144, 76, 236, 95, 139, 49, 180]))[0]
def print_token_info(token_data, signature=None):
"""
Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields
signature: Optional transaction signature
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
if "bondingCurve" in token_data:
print(f"Bonding Curve: {token_data['bondingCurve']}")
if "associatedBondingCurve" in token_data:
print(f"Associated BC: {token_data['associatedBondingCurve']}")
if "user" in token_data:
print(f"User: {token_data['user']}")
if "creator" in token_data:
print(f"Creator: {token_data['creator']}")
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
if "uri" in token_data:
print(f"URI: {token_data['uri']}")
if signature:
print(f"Signature: {signature}")
print("=" * 80 + "\n")
def get_account_keys(transaction, instruction, loaded_addresses=None):
"""
Safely extract account keys for an instruction from a versioned transaction.
Handles both static account keys and loaded addresses from lookup tables.
Args:
transaction: VersionedTransaction object
instruction: Instruction object
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
Returns:
List of account keys as strings, or None if unable to resolve
"""
account_keys = []
static_keys = transaction.message.account_keys
# Combine all available account keys: static + loaded
all_keys = list(static_keys)
if loaded_addresses:
# Add loaded writable addresses
if "writable" in loaded_addresses:
for addr in loaded_addresses["writable"]:
all_keys.append(Pubkey.from_string(addr))
# Add loaded readonly addresses
if "readonly" in loaded_addresses:
for addr in loaded_addresses["readonly"]:
all_keys.append(Pubkey.from_string(addr))
# Now resolve account indices
for index in instruction.accounts:
try:
if index < len(all_keys):
account_keys.append(str(all_keys[index]))
else:
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
return None
except (IndexError, Exception) as e:
print(f"Error resolving account at index {index}: {e}")
return None
return account_keys
def load_idl(file_path):
with open(file_path) as f:
@@ -29,16 +126,34 @@ def load_idl(file_path):
def decode_create_instruction(ix_data, ix_def, accounts):
"""
Decode legacy Create instruction (Metaplex tokens).
The Create instruction creates tokens using the Metaplex Token Metadata standard.
Instruction data contains: name, symbol, uri, and additional creator pubkey.
Account references are extracted from the accounts array.
Args:
ix_data: Raw instruction data bytes
ix_def: Instruction definition from IDL
accounts: List of account pubkeys involved in the instruction
Returns:
Dictionary containing decoded token information
"""
args = {}
offset = 8 # Skip 8-byte discriminator
# Parse instruction arguments according to IDL definition
for arg in ix_def["args"]:
if arg["type"] == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "pubkey":
# Pubkey is 32 bytes, encoded as base58
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
@@ -46,19 +161,90 @@ def decode_create_instruction(ix_data, ix_def, accounts):
args[arg["name"]] = value
# Add accounts
# Extract account addresses from the accounts array
# Account layout for Create instruction:
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
# 4: tokenProgram, 5: systemProgram, 6: rent, 7: user
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
args["token_standard"] = "legacy"
args["is_mayhem_mode"] = False
return args
def decode_create_v2_instruction(ix_data, ix_def, accounts):
"""
Decode CreateV2 instruction (Token2022 tokens).
The CreateV2 instruction creates tokens using the Token-2022 standard, which supports
additional features like transfer fees, interest-bearing tokens, and more.
This instruction includes an optional is_mayhem_mode flag.
Token-2022 Reference:
https://spl.solana.com/token-2022
Args:
ix_data: Raw instruction data bytes
ix_def: Instruction definition from IDL
accounts: List of account pubkeys involved in the instruction
Returns:
Dictionary containing decoded token information
"""
args = {}
offset = 8 # Skip 8-byte discriminator
# Parse instruction arguments according to IDL definition
for arg in ix_def["args"]:
if arg["type"] == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "pubkey":
# Pubkey is 32 bytes, encoded as base58
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg["name"]] = value
# Parse is_mayhem_mode (OptionBool at the end)
# Format: 1 byte (0 = false/None, 1 = true)
is_mayhem_mode = False
if offset < len(ix_data):
is_mayhem_mode = bool(ix_data[offset])
# Extract account addresses from the accounts array
# Account layout for CreateV2 instruction:
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
# 4: tokenProgram (Token2022), 5: user, 6: systemProgram, 7: rent
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[5])
args["token_standard"] = "token2022"
args["is_mayhem_mode"] = is_mayhem_mode
return args
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
async def listen_and_decode_create():
"""
Main listener function that subscribes to Solana blocks and decodes Pump.fun token creations.
This function:
1. Loads the Pump.fun IDL for instruction parsing
2. Subscribes to blocks mentioning the Pump.fun program
3. Decodes transactions to extract Create/CreateV2 instructions
4. Handles Address Lookup Tables (ALTs) for account resolution
"""
idl = load_idl("idl/pump_fun_idl.json")
create_discriminator = 8576854823835016728
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
@@ -101,6 +287,11 @@ async def listen_and_decode_create():
tx_data_decoded
)
# Extract loaded addresses from transaction metadata
loaded_addresses = None
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
loaded_addresses = tx["meta"]["loadedAddresses"]
for ix in transaction.message.instructions:
if str(
transaction.message.account_keys[
@@ -112,36 +303,68 @@ async def listen_and_decode_create():
"<Q", ix_data[:8]
)[0]
if (
discriminator
== create_discriminator
):
if discriminator == CREATE_DISCRIMINATOR:
# Legacy Create instruction (Metaplex tokens)
create_ix = next(
instr
for instr in idl["instructions"]
if instr["name"] == "create"
)
account_keys = [
str(
transaction.message.account_keys[
index
]
)
for index in ix.accounts
]
decoded_args = (
decode_create_instruction(
ix_data,
create_ix,
account_keys,
)
account_keys = get_account_keys(
transaction, ix, loaded_addresses
)
print(
json.dumps(
decoded_args, indent=2
)
if account_keys is None:
print("⚠️ Skipping transaction due to unresolved accounts")
continue
# Decode instruction data
decoded_args = decode_create_instruction(
ix_data,
create_ix,
account_keys,
)
print("--------------------")
# Print token information
print_token_info(decoded_args)
# Note if using Address Lookup Tables
if loaded_addresses:
writable_count = len(loaded_addresses.get("writable", []))
readonly_count = len(loaded_addresses.get("readonly", []))
if writable_count > 0 or readonly_count > 0:
print(f"️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
elif discriminator == CREATE_V2_DISCRIMINATOR:
# CreateV2 instruction (Token2022 tokens)
create_v2_ix = next(
(instr for instr in idl["instructions"]
if instr["name"] == "createV2"),
next(instr for instr in idl["instructions"]
if instr["name"] == "create")
)
account_keys = get_account_keys(
transaction, ix, loaded_addresses
)
if account_keys is None:
print("⚠️ Skipping transaction due to unresolved accounts")
continue
# Decode instruction data
decoded_args = decode_create_v2_instruction(
ix_data,
create_v2_ix,
account_keys,
)
# Print token information
print_token_info(decoded_args)
# Note if using Address Lookup Tables
if loaded_addresses:
writable_count = len(loaded_addresses.get("writable", []))
readonly_count = len(loaded_addresses.get("readonly", []))
if writable_count > 0 or readonly_count > 0:
print(f"️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
elif "result" in data:
print("Subscription confirmed")
else:
@@ -1,10 +1,18 @@
"""
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
Requires a Geyser API token for access.
Supports both Basic and X-Token authentication methods.
It is proven to be the fastest listener.
Performance: Proven to be the fastest listener method available.
This script uses Yellowstone Dragon's Mouth Geyser gRPC interface, which provides
real-time streaming of Solana blockchain data with lower latency than WebSocket methods.
Requires a Geyser API token for access.
Geyser gRPC Reference:
https://docs.triton.one/rpc-pool/grpc-subscriptions
Authentication: Supports both Basic and X-Token authentication methods.
Configure via GEYSER_ENDPOINT, GEYSER_API_TOKEN, and AUTH_TYPE variables.
"""
import asyncio
@@ -22,11 +30,53 @@ load_dotenv()
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
# Default to x-token auth, can be set to "basic"
# Authentication type: "x-token" or "basic"
AUTH_TYPE = "x-token"
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
# Instruction discriminators (8-byte identifiers for instruction types)
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
# and sha256("global:createV2") for Token2022 CreateV2
# See: learning-examples/calculate_discriminator.py
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
def print_token_info(token_data, signature=None):
"""
Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields
signature: Optional transaction signature
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
if "bonding_curve" in token_data:
print(f"Bonding Curve: {token_data['bonding_curve']}")
if "associated_bonding_curve" in token_data:
print(f"Associated BC: {token_data['associated_bonding_curve']}")
if "user" in token_data:
print(f"User: {token_data['user']}")
if "creator" in token_data:
print(f"Creator: {token_data['creator']}")
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
if "uri" in token_data:
print(f"URI: {token_data['uri']}")
if signature:
print(f"Signature: {signature}")
print("=" * 80 + "\n")
async def create_geyser_connection():
@@ -57,7 +107,7 @@ def create_subscription_request():
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
"""Decode a create instruction from transaction data."""
"""Decode a legacy create instruction (Metaplex) from transaction data."""
# Skip past the 8-byte discriminator prefix
offset = 8
@@ -103,20 +153,68 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
"system_program": get_account_key(5),
"rent": get_account_key(6),
"user": get_account_key(7),
"token_standard": "legacy",
"is_mayhem_mode": False,
}
return token_info
def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict:
"""Decode a CreateV2 instruction (Token2022) from transaction data."""
# Skip past the 8-byte discriminator prefix
offset = 8
# Extract account keys in base58 format
def get_account_key(index):
if index >= len(accounts):
return "N/A"
account_index = accounts[index]
return base58.b58encode(keys[account_index]).decode()
# Read string fields (prefixed with length)
def read_string():
nonlocal offset
# Get string length (4-byte uint)
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
# Extract and decode the string
value = ix_data[offset : offset + length].decode()
offset += length
return value
def read_pubkey():
nonlocal offset
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
return value
name = read_string()
symbol = read_string()
uri = read_string()
creator = read_pubkey()
# Parse is_mayhem_mode (OptionBool at the end)
is_mayhem_mode = False
if offset < len(ix_data):
is_mayhem_mode = bool(ix_data[offset])
token_info = {
"name": name,
"symbol": symbol,
"uri": uri,
"creator": creator,
"mint": get_account_key(0),
"bonding_curve": get_account_key(2),
"associated_bonding_curve": get_account_key(3),
"user": get_account_key(5),
"token_standard": "token2022",
"is_mayhem_mode": is_mayhem_mode,
}
return token_info
def print_token_info(info, signature):
"""Print formatted token information."""
print("\n🎯 New Pump.fun token detected!")
print(f"Name: {info['name']} | Symbol: {info['symbol']}")
print(f"Mint: {info['mint']}")
print(f"Bonding curve: {info['bonding_curve']}")
print(f"Associated bonding curve: {info['associated_bonding_curve']}")
print(f"Creator: {info['creator']}")
print(f"Signature: {signature}")
async def monitor_pump():
@@ -137,14 +235,28 @@ async def monitor_pump():
# Check each instruction in the transaction
for ix in msg.instructions:
if not ix.data.startswith(PUMP_CREATE_PREFIX):
# Check for both Create and CreateV2 instructions
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
if not (is_create or is_create_v2):
continue
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
# Decode based on instruction type
if is_create_v2:
info = decode_create_v2_instruction(
ix.data, msg.account_keys, ix.accounts
)
else:
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
# Extract transaction signature
signature = base58.b58encode(
bytes(update.transaction.transaction.signature)
).decode()
print_token_info(info, signature)
# Print token information in consistent format
print_token_info(info, signature=signature)
if __name__ == "__main__":
@@ -2,7 +2,17 @@
Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
It is usually faster than blockSubscribe, but slower than Geyser.
Performance: Usually faster than blockSubscribe, but slower than Geyser.
This script uses logsSubscribe which receives program logs containing event data.
Event logs include all token fields directly, making parsing simpler and faster than
decoding full transactions.
WebSocket API Reference:
https://solana.com/docs/rpc/websocket/logssubscribe
Program Logs and Events:
https://solana.com/docs/programs/debugging#logging
"""
import asyncio
@@ -21,11 +31,71 @@ load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
# Event discriminator for CreateEvent (8-byte identifier)
# This is emitted by both Create and CreateV2 instructions
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
def print_token_info(token_data, signature=None):
"""
Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields
signature: Optional transaction signature
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
if "bondingCurve" in token_data:
print(f"Bonding Curve: {token_data['bondingCurve']}")
if "user" in token_data:
print(f"User: {token_data['user']}")
if "creator" in token_data:
print(f"Creator: {token_data['creator']}")
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
if "uri" in token_data:
print(f"URI: {token_data['uri']}")
if signature:
print(f"Signature: {signature}")
print("=" * 80 + "\n")
def parse_create_instruction(data):
"""
Parse CreateEvent data from legacy Create instruction (Metaplex tokens).
Event logs contain all fields directly embedded in the event data, unlike
instruction data which requires account lookup. Event format:
- 8 bytes: event discriminator
- Variable: name (4-byte length + UTF-8 string)
- Variable: symbol (4-byte length + UTF-8 string)
- Variable: uri (4-byte length + UTF-8 string)
- 32 bytes: mint pubkey
- 32 bytes: bondingCurve pubkey
- 32 bytes: user pubkey
- 32 bytes: creator pubkey
Args:
data: Raw event data bytes from program logs
Returns:
Dictionary containing decoded token information, or None if parsing fails
"""
if len(data) < 8:
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
return None
offset = 8
offset = 8 # Skip event discriminator
parsed_data = {}
# Parse fields based on CreateEvent structure
@@ -42,18 +112,102 @@ def parse_create_instruction(data):
try:
for field_name, field_type in fields:
if field_type == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
# Pubkey is 32 bytes, encoded as base58
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data
except:
except Exception as e:
print(f"❌ Parse Create error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
print(f" Data hex: {data.hex()[:200]}...")
return None
def parse_create_v2_instruction(data):
"""
Parse CreateEvent data from CreateV2 instruction (Token2022 tokens).
CreateV2 uses Token-2022 standard with additional features. The event format
is identical to Create, with an additional optional is_mayhem_mode flag at the end.
Token-2022 Reference:
https://spl.solana.com/token-2022
Args:
data: Raw event data bytes from program logs
Returns:
Dictionary containing decoded token information, or None if parsing fails
"""
if len(data) < 8:
print(f"⚠️ Data too short for CreateV2 event: {len(data)} bytes")
return None
offset = 8 # Skip event discriminator
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
# Pubkey is 32 bytes, encoded as base58
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
# Format: 1 byte (0 = false/None, 1 = true)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"❌ Parse CreateV2 error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
print(f" Data hex: {data.hex()[:200]}...")
return None
@@ -90,10 +244,17 @@ async def listen_for_new_tokens():
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
if any(
# Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log
for log in logs
):
)
is_create_v2 = any(
"Program log: Instruction: CreateV2" in log
for log in logs
)
if is_create or is_create_v2:
for log in logs:
if "Program data:" in log:
try:
@@ -101,22 +262,40 @@ async def listen_for_new_tokens():
decoded_data = base64.b64decode(
encoded_data
)
parsed_data = parse_create_instruction(
decoded_data
)
# Check if this is a CreateEvent by validating discriminator
if len(decoded_data) < 8:
continue
event_discriminator = decoded_data[:8]
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
continue
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
print(f" Signature: {log_data.get('signature')}")
# Both create and create_v2 emit the same CreateEvent
# The difference is in the optional is_mayhem_mode field
if is_create_v2:
parsed_data = parse_create_v2_instruction(
decoded_data
)
else:
parsed_data = parse_create_instruction(
decoded_data
)
if parsed_data and "name" in parsed_data:
print(
"Signature:",
log_data.get("signature"),
)
for key, value in parsed_data.items():
print(f"{key}: {value}")
print(
"##########################################################################################"
# Print token information in consistent format
print_token_info(
parsed_data,
signature=log_data.get("signature")
)
else:
print(f"⚠️ Parsing failed for CreateEvent")
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {e!s}")
print(f"❌ Error processing log: {e!s}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
@@ -1,9 +1,19 @@
"""
Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
Additionally, calculates an associated bonding curve address for each token.
Additionally, calculates the associated bonding curve address for each token using PDA derivation.
It is usually faster than blockSubscribe, but slower than Geyser.
Performance: Usually faster than blockSubscribe, but slower than Geyser.
This script demonstrates Program Derived Address (PDA) calculation for the associated
bonding curve, which is the token account owned by the bonding curve that holds
the minted tokens.
WebSocket API Reference:
https://solana.com/docs/rpc/websocket/logssubscribe
Program Derived Addresses:
https://solana.com/docs/core/pda
"""
import asyncio
@@ -26,11 +36,67 @@ ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
# Event discriminator for CreateEvent (8-byte identifier)
# This is emitted by both Create and CreateV2 instructions
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
def print_token_info(token_data, signature=None, associated_bonding_curve=None):
"""
Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields
signature: Optional transaction signature
associated_bonding_curve: Optional associated bonding curve address
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
if "bondingCurve" in token_data:
print(f"Bonding Curve: {token_data['bondingCurve']}")
if associated_bonding_curve:
print(f"Associated BC: {associated_bonding_curve}")
if "user" in token_data:
print(f"User: {token_data['user']}")
if "creator" in token_data:
print(f"Creator: {token_data['creator']}")
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
if "uri" in token_data:
print(f"URI: {token_data['uri']}")
if signature:
print(f"Signature: {signature}")
print("=" * 80 + "\n")
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
Calculate the associated token account (ATA) address for the bonding curve.
The associated bonding curve is a Program Derived Address (PDA) that holds
the token supply controlled by the bonding curve. It's derived using the
standard Associated Token Account (ATA) derivation.
ATA Derivation: find_program_address(
[bonding_curve_pubkey, token_program_id, mint_pubkey],
associated_token_program_id
)
Args:
mint: The token mint pubkey
bonding_curve: The bonding curve pubkey
Returns:
The derived associated bonding curve address
"""
derived_address, _ = Pubkey.find_program_address(
[
@@ -44,7 +110,9 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey
def parse_create_instruction(data):
"""Parse legacy Create instruction (Metaplex tokens)."""
if len(data) < 8:
print(f"⚠️ Data too short for Create instruction: {len(data)} bytes")
return None
offset = 8
parsed_data = {}
@@ -63,18 +131,81 @@ def parse_create_instruction(data):
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data
except:
except Exception as e:
print(f"❌ Parse Create error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
return None
def parse_create_v2_instruction(data):
"""Parse CreateV2 instruction (Token2022 tokens)."""
if len(data) < 8:
print(f"⚠️ Data too short for CreateV2 instruction: {len(data)} bytes")
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateV2Event structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
if offset + 4 > len(data):
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
if offset + length > len(data):
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
if offset + 32 > len(data):
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
# Parse is_mayhem_mode (OptionBool at the end)
if offset < len(data):
is_mayhem_mode = bool(data[offset])
parsed_data["is_mayhem_mode"] = is_mayhem_mode
else:
parsed_data["is_mayhem_mode"] = False
parsed_data["token_standard"] = "token2022"
return parsed_data
except Exception as e:
print(f"❌ Parse CreateV2 error: {e}")
print(f" Data length: {len(data)} bytes, offset: {offset}")
return None
@@ -111,10 +242,17 @@ async def listen_for_new_tokens():
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
if any(
# Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log
for log in logs
):
)
is_create_v2 = any(
"Program log: Instruction: CreateV2" in log
for log in logs
)
if is_create or is_create_v2:
for log in logs:
if "Program data:" in log:
try:
@@ -122,38 +260,56 @@ async def listen_for_new_tokens():
decoded_data = base64.b64decode(
encoded_data
)
parsed_data = parse_create_instruction(
decoded_data
)
if parsed_data and "name" in parsed_data:
print(
"Signature:",
log_data.get("signature"),
)
for key, value in parsed_data.items():
print(f"{key}: {value}")
# Calculate associated bonding curve
# Check if this is a CreateEvent by validating discriminator
if len(decoded_data) < 8:
continue
event_discriminator = decoded_data[:8]
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
continue
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
print(f" Signature: {log_data.get('signature')}")
# Both create and create_v2 emit the same CreateEvent
# The difference is in the optional is_mayhem_mode field
if is_create_v2:
print("📝 Instruction: CreateV2 (Token2022)")
parsed_data = (
parse_create_v2_instruction(
decoded_data
)
)
else:
print("📝 Instruction: Create (Legacy/Metaplex)")
parsed_data = parse_create_instruction(
decoded_data
)
if parsed_data and "name" in parsed_data:
# Calculate associated bonding curve using PDA derivation
mint = Pubkey.from_string(
parsed_data["mint"]
)
bonding_curve = Pubkey.from_string(
parsed_data["bondingCurve"]
)
associated_curve = (
find_associated_bonding_curve(
mint, bonding_curve
)
associated_curve = find_associated_bonding_curve(
mint, bonding_curve
)
print(
f"Associated Bonding Curve: {associated_curve}"
)
print(
"##########################################################################################"
# Print token information in consistent format
print_token_info(
parsed_data,
signature=log_data.get("signature"),
associated_bonding_curve=str(associated_curve)
)
else:
print(f"⚠️ Parsing failed for CreateEvent")
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {e!s}")
print(f"❌ Error processing log: {e!s}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
@@ -1,5 +1,17 @@
"""
Listens for new Pump.fun token creations via PumpPortal WebSocket.
Performance: Fast, real-time data via third-party API.
This script uses PumpPortal's WebSocket API, a third-party service that aggregates
and provides real-time Pump.fun token creation events. This provides additional
market data like initial buy amounts and market cap that aren't available in
raw blockchain data.
PumpPortal API: https://pumpportal.fun/
Note: This is a third-party service and requires trust in the data provider.
For trustless monitoring, use the direct blockchain listeners (logs, block, geyser).
"""
import asyncio
@@ -12,12 +24,52 @@ import websockets
WS_URL = "wss://pumpportal.fun/api/data"
def format_sol(value):
return f"{value:.6f} SOL"
def print_token_info(token_data):
"""
Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields from PumpPortal
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED (via PumpPortal)")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
# PumpPortal-specific fields
if "initialBuy" in token_data:
initial_buy_sol = token_data['initialBuy']
print(f"Initial Buy: {initial_buy_sol:.6f} SOL")
if "marketCapSol" in token_data:
market_cap_sol = token_data['marketCapSol']
print(f"Market Cap: {market_cap_sol:.6f} SOL")
if "bondingCurveKey" in token_data:
print(f"Bonding Curve: {token_data['bondingCurveKey']}")
if "traderPublicKey" in token_data:
print(f"Creator: {token_data['traderPublicKey']}")
# Virtual reserves
if "vSolInBondingCurve" in token_data:
v_sol = token_data['vSolInBondingCurve']
print(f"Virtual SOL: {v_sol:.6f} SOL")
if "vTokensInBondingCurve" in token_data:
v_tokens = token_data['vTokensInBondingCurve']
print(f"Virtual Tokens: {v_tokens:,.0f}")
if "uri" in token_data:
print(f"URI: {token_data['uri']}")
if "signature" in token_data:
print(f"Signature: {token_data['signature']}")
print("=" * 80 + "\n")
def format_timestamp(timestamp):
return datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S")
async def listen_for_new_tokens():
@@ -39,27 +91,8 @@ async def listen_for_new_tokens():
else:
continue
print("\n" + "=" * 50)
print(
f"New token created: {token_info.get('name')} ({token_info.get('symbol')})"
)
print("=" * 50)
print(f"Address: {token_info.get('mint')}")
print(f"Creator: {token_info.get('traderPublicKey')}")
print(f"Initial Buy: {format_sol(token_info.get('initialBuy', 0))}")
print(
f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}"
)
print(f"Bonding Curve: {token_info.get('bondingCurveKey')}")
print(
f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}"
)
print(
f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}"
)
print(f"Metadata URI: {token_info.get('uri')}")
print(f"Signature: {token_info.get('signature')}")
print("=" * 50)
# Print token information in consistent format
print_token_info(token_info)
except websockets.exceptions.ConnectionClosed:
print("\nWebSocket connection closed. Reconnecting...")
break
+122 -16
View File
@@ -7,7 +7,7 @@ import struct
import base58
import websockets
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
@@ -36,6 +36,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"
)
@@ -48,27 +49,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_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(
@@ -126,11 +159,59 @@ 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 buy_token(
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
creator_vault: Pubkey,
token_program: Pubkey,
amount: float,
slippage: float = 0.25,
max_retries=5,
@@ -139,10 +220,12 @@ async def buy_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=token_program
)
amount_lamports = int(amount * LAMPORTS_PER_SOL)
# 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)
token_amount = amount / token_price_sol
@@ -150,9 +233,12 @@ async def buy_token(
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + slippage))
# 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(
@@ -168,7 +254,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(
@@ -178,7 +264,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()),
@@ -200,14 +286,17 @@ async def buy_token(
]
discriminator = struct.pack("<Q", 16927863322537952870)
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
data = (
discriminator
+ struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports)
+ track_volume_bytes
)
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
idempotent_ata_ix = create_idempotent_associated_token_account(
payer.pubkey(), payer.pubkey(), mint
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
)
msg = Message(
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
@@ -284,6 +373,7 @@ async def listen_for_create_transaction():
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
idl = load_idl(idl_path)
create_discriminator = calculate_discriminator("global:create")
create_v2_discriminator = calculate_discriminator("global:create_v2")
async with websockets.connect(RPC_WEBSOCKET) as websocket:
subscription_message = json.dumps(
@@ -336,11 +426,22 @@ async def listen_for_create_transaction():
"<Q", ix_data[:8]
)[0]
# Check which create instruction was used
instruction_name = None
token_program = None
if discriminator == create_discriminator:
instruction_name = "create"
token_program = SYSTEM_TOKEN_PROGRAM
elif discriminator == create_v2_discriminator:
instruction_name = "create_v2"
token_program = TOKEN_2022_PROGRAM
if instruction_name:
create_ix = next(
instr
for instr in idl["instructions"]
if instr["name"] == "create"
if instr["name"] == instruction_name
)
account_keys = [
str(
@@ -355,6 +456,9 @@ async def listen_for_create_transaction():
ix_data, create_ix, account_keys
)
)
# Add token program info to decoded args
decoded_args["token_program"] = str(token_program)
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
return decoded_args
@@ -372,6 +476,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:
@@ -383,12 +488,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
)
+120 -16
View File
@@ -26,7 +26,7 @@ import struct
import base58
import websockets
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
@@ -55,6 +55,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"
)
@@ -69,27 +70,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_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),
)
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(
@@ -147,6 +180,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 set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
"""
Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption.
@@ -169,6 +249,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,
@@ -177,10 +258,12 @@ async def buy_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=token_program
)
amount_lamports = int(amount * LAMPORTS_PER_SOL)
# 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)
token_amount = amount / token_price_sol
@@ -188,9 +271,12 @@ async def buy_token(
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + slippage))
# 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(
@@ -206,7 +292,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(
@@ -216,7 +302,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()),
@@ -242,10 +328,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
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
)
# CU OPTIMIZATION: Limit account data to 512KB (down from 64MB default)
@@ -328,6 +415,7 @@ async def listen_for_create_transaction():
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
idl = load_idl(idl_path)
create_discriminator = calculate_discriminator("global:create")
create_v2_discriminator = calculate_discriminator("global:create_v2")
async with websockets.connect(RPC_WEBSOCKET) as websocket:
subscription_message = json.dumps(
@@ -380,11 +468,22 @@ async def listen_for_create_transaction():
"<Q", ix_data[:8]
)[0]
# Check which create instruction was used
instruction_name = None
token_program = None
if discriminator == create_discriminator:
instruction_name = "create"
token_program = SYSTEM_TOKEN_PROGRAM
elif discriminator == create_v2_discriminator:
instruction_name = "create_v2"
token_program = TOKEN_2022_PROGRAM
if instruction_name:
create_ix = next(
instr
for instr in idl["instructions"]
if instr["name"] == "create"
if instr["name"] == instruction_name
)
account_keys = [
str(
@@ -399,6 +498,9 @@ async def listen_for_create_transaction():
ix_data, create_ix, account_keys
)
)
# Add token program info to decoded args
decoded_args["token_program"] = str(token_program)
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
return decoded_args
@@ -415,6 +517,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:
@@ -426,13 +529,14 @@ 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..."
)
print("CU Optimization: Enabled (512KB account data limit)")
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
)
+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
)
+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,
)
+1 -1
View File
@@ -244,7 +244,7 @@ def create_buy_instruction(
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
),
AccountMeta(
pubkey=_find_user_volume_accumulator(user),
+492
View File
@@ -0,0 +1,492 @@
import asyncio
import os
import struct
from typing import Final
import base58
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import Transaction
from spl.token.instructions import (
create_idempotent_associated_token_account,
get_associated_token_address,
)
# Configuration for the token to be created
TOKEN_NAME = "Test Token V2"
TOKEN_SYMBOL = "TEST2"
TOKEN_URI = "https://example.com/token-v2.json"
BUY_AMOUNT_SOL = 0.0001 # Amount of SOL to spend on buying
MAX_SLIPPAGE = 0.3 # 30% slippage
PRIORITY_FEE_MICROLAMPORTS = 37_037 # Priority fee in microlamports
COMPUTE_UNIT_LIMIT = 350_000 # Compute unit limit for the transaction
ENABLE_MAYHEM_MODE = True # Set to True to enable mayhem mode
load_dotenv()
# Global constants from existing codebase
PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
)
PUMP_GLOBAL: Final[Pubkey] = Pubkey.from_string(
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
)
PUMP_EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
)
PUMP_FEE: Final[Pubkey] = Pubkey.from_string(
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
)
PUMP_FEE_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
)
PUMP_MINT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
"TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"
)
# Token2022 and Mayhem constants
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
)
MAYHEM_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
"MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e"
)
GLOBAL_PARAMS: Final[Pubkey] = Pubkey.from_string(
"13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ"
)
SOL_VAULT: Final[Pubkey] = Pubkey.from_string(
"BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s"
)
SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6
# Discriminators
CREATE_V2_DISCRIMINATOR: Final[bytes] = bytes([214, 144, 76, 236, 95, 139, 49, 180])
BUY_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
EXTEND_ACCOUNT_DISCRIMINATOR: Final[bytes] = bytes(
[234, 102, 194, 203, 150, 72, 62, 229]
)
# From environment
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = os.environ.get("SOLANA_PRIVATE_KEY")
def find_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]:
"""Find the bonding curve PDA for a mint."""
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
"""Find the associated bonding curve token account."""
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(TOKEN_2022_PROGRAM),
bytes(mint),
],
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
)
return derived_address
def find_creator_vault(creator: Pubkey) -> Pubkey:
"""Find the creator vault PDA."""
derived_address, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PUMP_PROGRAM,
)
return derived_address
def find_mayhem_state(mint: Pubkey) -> Pubkey:
"""Find the mayhem state PDA for a mint.
Seeds: ["mayhem-state", mint] (note: hyphen, not underscore)
"""
derived_address, _ = Pubkey.find_program_address(
[b"mayhem-state", bytes(mint)],
MAYHEM_PROGRAM_ID,
)
return derived_address
def find_mayhem_token_vault(mint: Pubkey) -> Pubkey:
"""Find the mayhem token vault - this is an ATA for sol_vault.
This is derived as an Associated Token Account with:
- Owner: SOL_VAULT
- Mint: mint
- Token Program: TOKEN_2022_PROGRAM
"""
return get_associated_token_address(SOL_VAULT, mint, TOKEN_2022_PROGRAM)
def _find_global_volume_accumulator() -> Pubkey:
derived_address, _ = Pubkey.find_program_address(
[b"global_volume_accumulator"],
PUMP_PROGRAM,
)
return derived_address
def _find_user_volume_accumulator(user: Pubkey) -> Pubkey:
derived_address, _ = Pubkey.find_program_address(
[b"user_volume_accumulator", bytes(user)],
PUMP_PROGRAM,
)
return derived_address
def _find_fee_config() -> Pubkey:
derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_PROGRAM)],
PUMP_FEE_PROGRAM,
)
return derived_address
def create_pump_create_v2_instruction(
mint: Pubkey,
mint_authority: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
global_state: Pubkey,
user: Pubkey,
creator: Pubkey,
name: str,
symbol: str,
uri: str,
is_mayhem_mode: bool = False,
) -> Instruction:
"""Create the pump.fun create_v2 instruction for Token2022.
Account order matches pump_fun_idl.json create_v2 instruction.
"""
accounts = [
AccountMeta(pubkey=mint, is_signer=True, is_writable=True),
AccountMeta(pubkey=mint_authority, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=global_state, is_signer=False, is_writable=False),
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
is_signer=False,
is_writable=False,
),
]
# Add mayhem accounts if enabled (must come before event_authority and program)
if is_mayhem_mode:
mayhem_state = find_mayhem_state(mint)
mayhem_token_vault = find_mayhem_token_vault(mint)
accounts.extend(
[
AccountMeta(
pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True
),
AccountMeta(pubkey=GLOBAL_PARAMS, is_signer=False, is_writable=False),
AccountMeta(pubkey=SOL_VAULT, is_signer=False, is_writable=True),
AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True),
AccountMeta(
pubkey=mayhem_token_vault, is_signer=False, is_writable=True
),
]
)
# Event authority and program come last
accounts.extend(
[
AccountMeta(
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
]
)
# Encode string as length-prefixed
def encode_string(s: str) -> bytes:
encoded = s.encode("utf-8")
return struct.pack("<I", len(encoded)) + encoded
def encode_pubkey(pubkey: Pubkey) -> bytes:
return bytes(pubkey)
data = (
CREATE_V2_DISCRIMINATOR
+ encode_string(name)
+ encode_string(symbol)
+ encode_string(uri)
+ encode_pubkey(creator)
+ struct.pack("<?", is_mayhem_mode) # OptionBool for is_mayhem_mode
)
return Instruction(PUMP_PROGRAM, data, accounts)
def create_extend_account_instruction(
bonding_curve: Pubkey,
user: Pubkey,
) -> Instruction:
"""Create the extend_account instruction to expand bonding curve account size."""
accounts = [
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
]
# No arguments for extend_account instruction
data = EXTEND_ACCOUNT_DISCRIMINATOR
return Instruction(PUMP_PROGRAM, data, accounts)
def create_buy_instruction(
global_state: Pubkey,
fee_recipient: Pubkey,
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
associated_user: Pubkey,
user: Pubkey,
creator_vault: Pubkey,
token_amount: int,
max_sol_cost: int,
track_volume: bool = True,
) -> Instruction:
"""Create the buy instruction."""
accounts = [
AccountMeta(pubkey=global_state, is_signer=False, is_writable=False),
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(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_user, is_signer=False, is_writable=True),
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
),
AccountMeta(
pubkey=_find_user_volume_accumulator(user),
is_signer=False,
is_writable=True,
),
# Index 14: fee_config (readonly)
AccountMeta(
pubkey=_find_fee_config(),
is_signer=False,
is_writable=False,
),
# Index 15: fee_program (readonly)
AccountMeta(
pubkey=PUMP_FEE_PROGRAM,
is_signer=False,
is_writable=False,
),
]
# Encode OptionBool for track_volume
# OptionBool: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1 if track_volume else 0])
data = (
BUY_DISCRIMINATOR
+ struct.pack("<Q", token_amount)
+ struct.pack("<Q", max_sol_cost)
+ track_volume_bytes
)
return Instruction(PUMP_PROGRAM, data, accounts)
async def get_fee_recipient_for_mayhem(client: AsyncClient, is_mayhem: bool) -> Pubkey:
"""Get the appropriate fee recipient based on mayhem mode.
For mayhem tokens, we need to use reserved_fee_recipient from Global account.
For standard tokens, we use the standard PUMP_FEE.
"""
if not is_mayhem:
return PUMP_FEE
# Fetch Global account to get reserved_fee_recipient for mayhem mode
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
if not response.value or not response.value.data:
print("Warning: Could not fetch Global account, using standard fee recipient")
return PUMP_FEE
data = response.value.data
# Parse reserved_fee_recipient from Global account at offset 483
RESERVED_FEE_RECIPIENT_OFFSET = 483
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
print("Warning: Global account data too short, using standard fee recipient")
return PUMP_FEE
reserved_fee_recipient_bytes = data[
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
]
reserved_fee_recipient = Pubkey.from_bytes(reserved_fee_recipient_bytes)
print(f"Using mayhem mode fee recipient: {reserved_fee_recipient}")
return reserved_fee_recipient
async def main():
"""Create and buy pump.fun token (Token2022) in a single transaction."""
private_key_bytes = base58.b58decode(PRIVATE_KEY)
payer = Keypair.from_bytes(private_key_bytes)
mint_keypair = Keypair()
print("Creating Token2022 token with:")
print(f" Name: {TOKEN_NAME}")
print(f" Symbol: {TOKEN_SYMBOL}")
print(f" Mint: {mint_keypair.pubkey()}")
print(f" Creator: {payer.pubkey()}")
print(f" Mayhem mode: {'Enabled' if ENABLE_MAYHEM_MODE else 'Disabled'}")
# Derive PDAs
bonding_curve, _ = find_bonding_curve_address(mint_keypair.pubkey())
associated_bonding_curve = find_associated_bonding_curve(
mint_keypair.pubkey(), bonding_curve
)
user_ata = get_associated_token_address(
payer.pubkey(), mint_keypair.pubkey(), TOKEN_2022_PROGRAM
)
creator_vault = find_creator_vault(payer.pubkey())
print("\nDerived addresses:")
print(f" Bonding curve: {bonding_curve}")
print(f" Associated bonding curve: {associated_bonding_curve}")
print(f" User ATA: {user_ata}")
print(f" Creator vault: {creator_vault}")
if ENABLE_MAYHEM_MODE:
mayhem_state = find_mayhem_state(mint_keypair.pubkey())
mayhem_token_vault = find_mayhem_token_vault(mint_keypair.pubkey())
print(f" Mayhem state: {mayhem_state}")
print(f" Mayhem token vault: {mayhem_token_vault}")
# Calculate buy parameters
# For pump.fun, we need to calculate expected tokens based on initial curve state
# Initial virtual reserves (from pump.fun constants)
initial_virtual_token_reserves = 1_073_000_000 * 10**TOKEN_DECIMALS
initial_virtual_sol_reserves = 30 * LAMPORTS_PER_SOL
initial_real_token_reserves = 793_100_000 * 10**TOKEN_DECIMALS
initial_price = initial_virtual_sol_reserves / initial_virtual_token_reserves
buy_amount_lamports = int(BUY_AMOUNT_SOL * LAMPORTS_PER_SOL)
expected_tokens = int(
(buy_amount_lamports * 0.99) / initial_price
) # 1% buffer for fees
max_sol_cost = int(buy_amount_lamports * (1 + MAX_SLIPPAGE))
print("\nBuy parameters:")
print(f" Buy amount: {BUY_AMOUNT_SOL} SOL")
print(f" Expected tokens: {expected_tokens / 10**TOKEN_DECIMALS:.6f}")
print(f" Max SOL cost: {max_sol_cost / LAMPORTS_PER_SOL:.6f} SOL")
# Send transaction
async with AsyncClient(RPC_ENDPOINT) as client:
# Get correct fee recipient based on mayhem mode
fee_recipient = await get_fee_recipient_for_mayhem(client, ENABLE_MAYHEM_MODE)
instructions = [
# Priority fee instructions
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
set_compute_unit_price(PRIORITY_FEE_MICROLAMPORTS),
# Create token with pump.fun create_v2 (Token2022)
create_pump_create_v2_instruction(
mint=mint_keypair.pubkey(),
mint_authority=PUMP_MINT_AUTHORITY,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
global_state=PUMP_GLOBAL,
user=payer.pubkey(),
creator=payer.pubkey(),
name=TOKEN_NAME,
symbol=TOKEN_SYMBOL,
uri=TOKEN_URI,
is_mayhem_mode=ENABLE_MAYHEM_MODE,
),
# Extend bonding curve account (required for frontend visibility)
create_extend_account_instruction(
bonding_curve=bonding_curve,
user=payer.pubkey(),
),
# Create user ATA
create_idempotent_associated_token_account(
payer.pubkey(),
payer.pubkey(),
mint_keypair.pubkey(),
TOKEN_2022_PROGRAM,
),
# Buy tokens
create_buy_instruction(
global_state=PUMP_GLOBAL,
fee_recipient=fee_recipient,
mint=mint_keypair.pubkey(),
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
associated_user=user_ata,
user=payer.pubkey(),
creator_vault=creator_vault,
token_amount=expected_tokens,
max_sol_cost=max_sol_cost,
track_volume=True,
),
]
recent_blockhash = await client.get_latest_blockhash()
message = Message(instructions, payer.pubkey())
transaction = Transaction(
[payer, mint_keypair], message, recent_blockhash.value.blockhash
)
print("\nSending transaction...")
opts = TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
try:
response = await client.send_transaction(transaction, opts)
tx_hash = response.value
print(f"Transaction sent: https://solscan.io/tx/{tx_hash}")
print("Waiting for confirmation...")
await client.confirm_transaction(tx_hash, commitment="confirmed")
print("Transaction confirmed!")
return tx_hash
except Exception as e:
print(f"Transaction failed: {e}")
raise
if __name__ == "__main__":
asyncio.run(main())
+324 -155
View File
@@ -1,12 +1,16 @@
"""
Solana PUMP AMM Interface
This standalone script demonstrates how to buy tokens on the PUMP AMM (pAMM) protocol.
It covers the complete flow from finding markets to executing buys with mayhem mode support.
This module provides functionality to interact with the PUMP AMM program on Solana, enabling:
- Finding market addresses by token mint
- Fetching and parsing market data from PUMP AMM pools
- Calculating token prices in AMM pools
- Creating associated token accounts (ATAs) idempotently
- Buying tokens on the PUMP AMM with slippage protection
Key concepts demonstrated:
- Finding AMM pool addresses by token mint
- Parsing binary account data structures
- Dynamic fee recipient calculation (mayhem mode vs standard)
- Program Derived Address (PDA) derivation
- WSOL wrapping (converting SOL to wrapped SOL for SPL token operations)
- Volume tracking incentives integration
- Transaction simulation before sending
- Slippage protection mechanisms
"""
import asyncio
@@ -34,31 +38,33 @@ from spl.token.instructions import (
load_dotenv()
# Configuration constants
# ============================================================================
# Configuration
# ============================================================================
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT = Pubkey.from_string("...")
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept
SLIPPAGE = 0.3 # 30% - maximum acceptable price movement during trade
TOKEN_DECIMALS = 6
BUY_DISCRIMINATOR = bytes.fromhex(
"66063d1201daebea"
) # Program instruction identifier for the buy function
# Token configuration
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
# Program instruction discriminators (first 8 bytes identify the instruction)
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
# ============================================================================
# Solana Program IDs and System Accounts
# ============================================================================
# Solana system addresses and program IDs
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
)
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
)
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
)
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
@@ -67,61 +73,91 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
)
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
# ============================================================================
# Constants for Account Structure Parsing
# ============================================================================
# Pool account structure offsets
POOL_DISCRIMINATOR_SIZE = 8
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
# GlobalConfig structure offsets
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
GLOBALCONFIG_ADMIN_SIZE = 32
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
GLOBALCONFIG_DISCRIMINATOR_SIZE
+ GLOBALCONFIG_ADMIN_SIZE
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
)
# Fee recipients
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
)
# Solana constants
LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 200_000 # Max compute units for transaction
# Buy-specific constants
PROTOCOL_FEE_BUFFER = 0.1 # 10% buffer for protocol fees when wrapping SOL
VOLUME_TRACKING_ENABLED = 1 # 1 = true, 0 = false
# ============================================================================
# Market Discovery
# ============================================================================
async def get_market_address_by_base_mint(
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
) -> Pubkey:
"""Find the market address for a given token mint.
"""Find the AMM pool address for a specific token.
Searches for the AMM pool that contains the specified token mint as its base token
by querying program accounts with a filter for the base_mint field.
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
that matches the given token mint address.
Args:
client: Solana RPC client instance
base_mint_address: Address of the token mint you want to find the market for
amm_program_id: Address of the AMM program
client: Solana RPC client
base_mint_address: Token mint to find the pool for
amm_program_id: PUMP AMM program address
Returns:
The Pubkey of the market (AMM pool) for the token
Address of the AMM pool (market) for the token
"""
base_mint_bytes = bytes(base_mint_address)
offset = (
43 # Offset where the base_mint field is stored in the account data structure
)
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
response = await client.get_program_accounts(
amm_program_id, encoding="base64", filters=filters
)
market_address = [account.pubkey for account in response.value][0]
return market_address
return response.value[0].pubkey
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
"""Fetch and parse market data from the blockchain.
"""Parse binary pool account data into a structured dictionary.
Retrieves and deserializes the binary data stored in the market account
into a structured dictionary containing key market information.
The pool account stores data in a specific binary format. This function
deserializes that data based on the known structure.
Args:
client: Solana RPC client instance
market_address: Address of the market (AMM pool) to fetch data for
client: Solana RPC client
market_address: Address of the pool account
Returns:
Dictionary containing the parsed market data
Dictionary with parsed pool data fields
"""
response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data
parsed_data: dict = {}
# Start after the 8-byte discriminator
offset = 8
# Define the structure of the market account data
offset = POOL_DISCRIMINATOR_SIZE
# Field definitions: (name, type)
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
fields = [
("pool_bump", "u8"),
("index", "u16"),
@@ -141,39 +177,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = (
struct.unpack("<Q", data[offset : offset + 8])[0]
if field_type == "u64"
else struct.unpack("<q", data[offset : offset + 8])[0]
)
parsed_data[field_name] = value
format_char = "<Q" if field_type == "u64" else "<q"
parsed_data[field_name] = struct.unpack(
format_char, data[offset : offset + 8]
)[0]
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset : offset + 2])[0]
parsed_data[field_name] = value
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
parsed_data[field_name] = data[offset]
offset += 1
return parsed_data
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
# ============================================================================
# Program Derived Address (PDA) Derivation
# ============================================================================
# PDAs are deterministic addresses derived from seeds and a program ID.
# They allow programs to own accounts without needing a private key.
Calculates the deterministic PDA that serves as the vault authority
for a specific coin creator in the PUMP AMM protocol.
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the PDA for the coin creator's fee vault.
The creator vault collects fees on behalf of the token creator.
This is a deterministic address that can be recalculated by anyone.
Args:
coin_creator: Pubkey of the coin creator account
coin_creator: Public key of the token creator
Returns:
Pubkey of the derived coin creator vault authority
Note:
This vault is used to collect creator fees from token transactions
PDA of the creator's vault authority
"""
derived_address, _ = Pubkey.find_program_address(
[b"creator_vault", bytes(coin_creator)],
@@ -183,13 +219,13 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
def find_global_volume_accumulator() -> Pubkey:
"""Derive the Program Derived Address (PDA) for the global volume accumulator.
"""Derive the PDA for the global volume accumulator.
Calculates the deterministic PDA that tracks global trading volume
across all pools in the PUMP AMM protocol.
This account tracks total trading volume across all pools.
Volume tracking is used for incentive programs and analytics.
Returns:
Pubkey of the derived global volume accumulator account
PDA of the global volume accumulator
"""
derived_address, _ = Pubkey.find_program_address(
[b"global_volume_accumulator"],
@@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey:
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
"""Derive the Program Derived Address (PDA) for a user's volume accumulator.
"""Derive the PDA for a user's volume accumulator.
Calculates the deterministic PDA that tracks trading volume
for a specific user in the PUMP AMM protocol.
Tracks individual user's trading volume, which may qualify them
for incentives or rewards based on trading activity.
Args:
user: Pubkey of the user account
user: Public key of the user
Returns:
Pubkey of the derived user volume accumulator account
PDA of the user's volume accumulator
"""
derived_address, _ = Pubkey.find_program_address(
[b"user_volume_accumulator", bytes(user)],
@@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
def find_fee_config() -> Pubkey:
"""Derive the Program Derived Address (PDA) for the fee config.
"""Derive the PDA for the fee configuration account.
Returns:
Pubkey of the derived fee config account
This account stores fee-related configuration for the AMM.
"""
derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
@@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey:
return derived_address
# ============================================================================
# Mayhem Mode Fee Handling
# ============================================================================
# Mayhem mode is a special fee structure where fees go to a different recipient.
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
"""Fetch the mayhem mode fee recipient from GlobalConfig.
When mayhem mode is active, fees are redirected to a special recipient
stored in the GlobalConfig account.
Args:
client: Solana RPC client
Returns:
Public key of the mayhem mode fee recipient
"""
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
if not response.value or not response.value.data:
msg = "Cannot fetch GlobalConfig account"
raise ValueError(msg)
data = response.value.data
recipient_bytes = data[
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
]
return Pubkey.from_bytes(recipient_bytes)
async def get_pumpswap_fee_recipients(
client: AsyncClient, pool: Pubkey
) -> tuple[Pubkey, Pubkey]:
"""Determine the correct fee recipient based on pool's mayhem mode status.
This function checks if mayhem mode is enabled for the pool and returns
the appropriate fee recipient and their WSOL token account.
Args:
client: Solana RPC client
pool: Address of the AMM pool
Returns:
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
"""
response = await client.get_account_info(pool, encoding="base64")
if not response.value or not response.value.data:
msg = "Cannot fetch pool account"
raise ValueError(msg)
pool_data = response.value.data
# Check if mayhem mode flag exists and is enabled
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
pool_data[POOL_MAYHEM_MODE_OFFSET]
)
# Select appropriate fee recipient
if is_mayhem_mode:
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
else:
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
# Get the fee recipient's WSOL token account
fee_recipient_token_account = get_associated_token_address(
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
return (fee_recipient, fee_recipient_token_account)
# ============================================================================
# Price Calculation
# ============================================================================
async def calculate_token_pool_price(
client: AsyncClient,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
) -> float:
"""Calculate the current price of tokens in an AMM pool.
"""Calculate current token price from AMM pool balances.
Fetches the balance of tokens in the pool and calculates the price ratio
between the base token and quote token (typically SOL).
AMM price is determined by the ratio of tokens in the pool:
price = quote_balance / base_balance
Args:
client: Solana RPC client instance
pool_base_token_account: Address of the pool's base token account (your token)
pool_quote_token_account: Address of the pool's quote token account (SOL)
client: Solana RPC client
pool_base_token_account: Pool's token account (the token being priced)
pool_quote_token_account: Pool's SOL account (the quote currency)
Returns:
The price of the base token in terms of the quote token (SOL per token)
Price in SOL per token
"""
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
quote_balance_resp = await client.get_token_account_balance(
pool_quote_token_account
)
# Extract the UI amounts (human-readable with decimals)
base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_balance_resp.value.ui_amount)
token_price = quote_amount / base_amount
return quote_amount / base_amount
return token_price
# ============================================================================
# Token Buying
# ============================================================================
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 buy_pump_swap(
client: AsyncClient,
pump_fun_amm_market: Pubkey,
market: Pubkey,
payer: Keypair,
base_mint: Pubkey,
user_base_token_account: Pubkey,
@@ -273,52 +406,64 @@ async def buy_pump_swap(
pool_quote_token_account: Pubkey,
coin_creator_vault_authority: Pubkey,
coin_creator_vault_ata: Pubkey,
sol_amount_to_spend: int,
sol_amount_to_spend: float,
slippage: float = 0.25,
) -> str | None:
"""Buy tokens on the PUMP AMM with slippage protection.
"""Execute a token buy on the PUMP AMM with slippage protection.
Executes a token purchase on the PUMP AMM protocol, calculating the expected
token amount based on the current pool price and applying slippage protection.
This function:
1. Calculates expected token output based on current price
2. Wraps SOL into WSOL (required for SPL token operations)
3. Constructs and simulates the transaction
4. Sends the buy transaction if simulation succeeds
Why WSOL wrapping is needed:
SPL tokens can only interact with other SPL tokens. Native SOL must be
wrapped into WSOL (an SPL token representation of SOL) before trading.
Args:
client: Solana RPC client instance
pump_fun_amm_market: Address of the AMM market
payer: Keypair of the transaction signer and token buyer
base_mint: Address of the token mint being purchased
user_base_token_account: Address of the user's token account for receiving purchased tokens
user_quote_token_account: Address of the user's SOL token account
pool_base_token_account: Address of the pool's token account for the token being purchased
pool_quote_token_account: Address of the pool's SOL token account
coin_creator_vault_authority: Address of the coin creator's vault authority
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports)
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
client: Solana RPC client
market: AMM pool address
payer: Wallet keypair for signing
base_mint: Token mint address
user_base_token_account: User's token account (for receiving tokens)
user_quote_token_account: User's WSOL account
pool_base_token_account: Pool's token account
pool_quote_token_account: Pool's WSOL account
coin_creator_vault_authority: Creator vault PDA
coin_creator_vault_ata: Creator's WSOL account
sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports)
slippage: Maximum acceptable slippage (0.25 = 25%)
Returns:
Transaction signature if successful, None otherwise
"""
# Calculate token price
token_program_id = await get_token_program_id(client, base_mint)
token_price_sol = await calculate_token_pool_price(
client, pool_base_token_account, pool_quote_token_account
)
print(f"Token price in SOL: {token_price_sol:.10f} SOL")
# Calculate maximum SOL input with slippage protection
# Calculate expected token amount and maximum SOL we're willing to spend
base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
slippage_factor = 1 + slippage
max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
max_sol_input = int((sol_amount_to_spend * (1 + slippage)) * LAMPORTS_PER_SOL)
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
# Calculate required PDAs for volume tracking
# Derive volume accumulator PDAs for incentive tracking
global_volume_accumulator = find_global_volume_accumulator()
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
# Define all accounts needed for the buy instruction
# Get fee recipient based on mayhem mode
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
client, market
)
# Build account list for buy instruction
# Order matters! Must match the program's expected account layout
accounts = [
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
@@ -327,15 +472,13 @@ async def buy_pump_swap(
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
is_signer=False,
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=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
@@ -352,34 +495,43 @@ async def buy_pump_swap(
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=global_volume_accumulator, is_signer=False, is_writable=True
pubkey=global_volume_accumulator, is_signer=False, is_writable=False
),
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
# Index 21: fee_config (readonly)
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
# Index 22: fee_program (readonly)
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
]
# Instruction data format:
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
# All integers are little-endian (<)
data = (
BUY_DISCRIMINATOR
+ struct.pack("<Q", base_amount_out)
+ struct.pack("<Q", max_sol_input)
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
+ struct.pack("<Q", base_amount_out) # Expected token amount
+ struct.pack("<Q", max_sol_input) # Maximum SOL to spend
+ struct.pack("<B", VOLUME_TRACKING_ENABLED) # Enable volume tracking
)
# Set compute budget to avoid transaction failures
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
# Wrapping SOL
# Create WSOL account if it doesn't exist
# Note: WSOL always uses the standard Token program, never Token2022
create_wsol_ata_ix = create_idempotent_associated_token_account(
payer.pubkey(), payer.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM
payer.pubkey(),
payer.pubkey(),
SOL,
SYSTEM_TOKEN_PROGRAM, # WSOL always uses standard Token program
)
# Calculate the amount of SOL to wrap(can also use `max_sol_input`), can get the SOL back by closing the WSOL account while selling
pump_protocol_fees = sol_amount_to_spend * 0.1 # adding some buffer fees
wrap_amount = int((sol_amount_to_spend + pump_protocol_fees) * LAMPORTS_PER_SOL)
# Calculate amount to wrap (includes buffer for fees)
wrap_amount = int(
(sol_amount_to_spend * (1 + PROTOCOL_FEE_BUFFER)) * LAMPORTS_PER_SOL
)
# Transfer SOL to WSOL account and sync
# This converts native SOL to the SPL token version (WSOL)
transfer_sol_ix = transfer(
TransferParams(
from_pubkey=payer.pubkey(),
@@ -387,20 +539,24 @@ async def buy_pump_swap(
lamports=wrap_amount,
)
)
sync_native_ix = sync_native(
SyncNativeParams(SYSTEM_TOKEN_PROGRAM, user_quote_token_account)
SyncNativeParams(
SYSTEM_TOKEN_PROGRAM, user_quote_token_account
) # WSOL always uses standard Token program
)
idempotent_ata_ix = create_idempotent_associated_token_account(
payer.pubkey(), payer.pubkey(), base_mint, SYSTEM_TOKEN_PROGRAM
# Create token account for receiving purchased tokens
create_token_ata_ix = create_idempotent_associated_token_account(
payer.pubkey(),
payer.pubkey(),
base_mint,
token_program_id, # Use dynamic token_program_id
)
buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
# Build and sign transaction
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash(
[
compute_limit_ix,
@@ -408,64 +564,77 @@ async def buy_pump_swap(
create_wsol_ata_ix,
transfer_sol_ix,
sync_native_ix,
idempotent_ata_ix,
create_token_ata_ix,
buy_ix,
],
payer.pubkey(),
recent_blockhash,
blockhash_resp.value.blockhash,
)
tx = VersionedTransaction(message=msg, keypairs=[payer])
tx_buy = VersionedTransaction(message=msg, keypairs=[payer])
# Optionally, you can simulate the transaction first and check for errors and get the compute units used
simulation = await client.simulate_transaction(tx_buy)
# Simulate first to catch errors before sending
simulation = await client.simulate_transaction(tx)
if simulation.value.err:
print(f"Simulation error: {simulation.value.err}")
return None
compute_units_used = simulation.value.units_consumed
print(f"Simulation successful, compute units used: {compute_units_used}")
print(
f"Simulation successful, compute units used: {simulation.value.units_consumed}"
)
try:
# Skip preflight since we already simulated (faster execution)
tx_sig = await client.send_transaction(
tx_buy,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
)
tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
await client.confirm_transaction(tx_hash, commitment="confirmed")
await client.confirm_transaction(tx_hash, commitment="confirmed")
print("Transaction confirmed")
return tx_hash
except Exception as e:
print(f"Error sending transaction: {e!s}")
print(f"Error: {e!s}")
return None
async def main():
"""Main function to execute the token buying process."""
sol_amount_to_spend = 0.000001
# ============================================================================
# Main Execution
# ============================================================================
async def main() -> None:
"""Execute the complete buy flow."""
sol_amount_to_spend = 0.000001 # Amount of SOL to spend on the purchase
async with AsyncClient(RPC_ENDPOINT) as client:
# Step 1: Find the pool address for our token
market_address = await get_market_address_by_base_mint(
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
)
# Step 2: Parse pool data to get necessary accounts
market_data = await get_market_data(client, market_address)
# Determine token program ID for the base mint
token_program_id = await get_token_program_id(client, TOKEN_MINT)
# Step 3: Derive PDAs needed for the transaction
coin_creator_vault_authority = find_coin_creator_vault(
Pubkey.from_string(market_data["coin_creator"])
)
coin_creator_vault_ata = get_associated_token_address(
coin_creator_vault_authority, SOL
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
)
# Step 4: Execute the buy
await buy_pump_swap(
client,
market_address,
PAYER,
TOKEN_MINT,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
get_associated_token_address(PAYER.pubkey(), SOL),
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
Pubkey.from_string(market_data["pool_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority,
+294 -137
View File
@@ -1,10 +1,14 @@
"""
This module provides functionality to:
- Find market addresses by token mint.
- Fetch and parse market data from PUMP AMM pools.
- Calculate token prices in AMM pools.
- Create associated token accounts (ATAs) idempotently.
- Sell tokens on the PUMP AMM with slippage protection.
This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol.
It covers the complete flow from finding markets to executing sells with mayhem mode support.
Key concepts demonstrated:
- Finding AMM pool addresses by token mint
- Parsing binary account data structures
- Dynamic fee recipient calculation (mayhem mode vs standard)
- Program Derived Address (PDA) derivation
- Transaction construction with compute budgets
- Slippage protection mechanisms
"""
import asyncio
@@ -26,31 +30,33 @@ from spl.token.instructions import get_associated_token_address
load_dotenv()
# Configuration constants
# ============================================================================
# Configuration
# ============================================================================
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT = Pubkey.from_string("...")
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade
TOKEN_DECIMALS = 6
SELL_DISCRIMINATOR = bytes.fromhex(
"33e685a4017f83ad"
) # Program instruction identifier for the sell function
# Token configuration
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
# Program instruction discriminators (first 8 bytes identify the instruction)
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
# ============================================================================
# Solana Program IDs and System Accounts
# ============================================================================
# Solana system addresses and program IDs
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
)
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
)
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
)
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
@@ -59,59 +65,87 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
)
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
# ============================================================================
# Constants for Account Structure Parsing
# ============================================================================
# Pool account structure offsets
POOL_DISCRIMINATOR_SIZE = 8
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
# GlobalConfig structure offsets
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
GLOBALCONFIG_ADMIN_SIZE = 32
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
GLOBALCONFIG_DISCRIMINATOR_SIZE
+ GLOBALCONFIG_ADMIN_SIZE
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
)
# Fee recipients
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
)
# Solana constants
LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
# ============================================================================
# Market Discovery
# ============================================================================
async def get_market_address_by_base_mint(
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
) -> Pubkey:
"""Find the market address for a given token mint.
"""Find the AMM pool address for a specific token.
Searches for the AMM pool that contains the specified token as its base token.
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
that matches the given token mint address.
Args:
client: Solana RPC client instance
base_mint_address: Address of the token mint you want to find the market for
amm_program_id: Address of the AMM program
client: Solana RPC client
base_mint_address: Token mint to find the pool for
amm_program_id: PUMP AMM program address
Returns:
The Pubkey of the market (AMM pool) for the token
Address of the AMM pool (market) for the token
"""
base_mint_bytes = bytes(base_mint_address)
offset = (
43 # Offset where the base_mint field is stored in the account data structure
)
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
response = await client.get_program_accounts(
amm_program_id, encoding="base64", filters=filters
)
market_address = [account.pubkey for account in response.value][0]
return market_address
return response.value[0].pubkey
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
"""Fetch and parse market data from the blockchain.
"""Parse binary pool account data into a structured dictionary.
Retrieves and deserializes the data stored in the market account.
The pool account stores data in a specific binary format. This function
deserializes that data based on the known structure.
Args:
client: Solana RPC client instance
market_address: Address of the market (AMM pool) to fetch data for
client: Solana RPC client
market_address: Address of the pool account
Returns:
Dictionary containing the parsed market data
Dictionary with parsed pool data fields
"""
response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data
parsed_data: dict = {}
# Start after the 8-byte discriminator
offset = 8
# Define the structure of the market account data
offset = POOL_DISCRIMINATOR_SIZE
# Field definitions: (name, type)
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
fields = [
("pool_bump", "u8"),
("index", "u16"),
@@ -131,39 +165,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = (
struct.unpack("<Q", data[offset : offset + 8])[0]
if field_type == "u64"
else struct.unpack("<q", data[offset : offset + 8])[0]
)
parsed_data[field_name] = value
format_char = "<Q" if field_type == "u64" else "<q"
parsed_data[field_name] = struct.unpack(
format_char, data[offset : offset + 8]
)[0]
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset : offset + 2])[0]
parsed_data[field_name] = value
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
parsed_data[field_name] = data[offset]
offset += 1
return parsed_data
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
# ============================================================================
# Program Derived Address (PDA) Derivation
# ============================================================================
# PDAs are deterministic addresses derived from seeds and a program ID.
# They allow programs to own accounts without needing a private key.
Calculates the deterministic PDA that serves as the vault authority
for a specific coin creator in the PUMP AMM protocol.
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the PDA for the coin creator's fee vault.
The creator vault collects fees on behalf of the token creator.
This is a deterministic address that can be recalculated by anyone.
Args:
coin_creator: Pubkey of the coin creator account
coin_creator: Public key of the token creator
Returns:
Pubkey of the derived coin creator vault authority
Note:
This vault is used to collect creator fees from token transactions
PDA of the creator's vault authority
"""
derived_address, _ = Pubkey.find_program_address(
[b"creator_vault", bytes(coin_creator)],
@@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
def find_fee_config() -> Pubkey:
"""Derive the Program Derived Address (PDA) for the fee config.
"""Derive the PDA for the fee configuration account.
Returns:
Pubkey of the derived fee config account
This account stores fee-related configuration for the AMM.
"""
derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
@@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey:
return derived_address
# ============================================================================
# Mayhem Mode Fee Handling
# ============================================================================
# Mayhem mode is a special fee structure where fees go to a different recipient.
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
"""Fetch the mayhem mode fee recipient from GlobalConfig.
When mayhem mode is active, fees are redirected to a special recipient
stored in the GlobalConfig account.
Args:
client: Solana RPC client
Returns:
Public key of the mayhem mode fee recipient
"""
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
if not response.value or not response.value.data:
msg = "Cannot fetch GlobalConfig account"
raise ValueError(msg)
data = response.value.data
recipient_bytes = data[
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
]
return Pubkey.from_bytes(recipient_bytes)
async def get_pumpswap_fee_recipients(
client: AsyncClient, pool: Pubkey
) -> tuple[Pubkey, Pubkey]:
"""Determine the correct fee recipient based on pool's mayhem mode status.
This function checks if mayhem mode is enabled for the pool and returns
the appropriate fee recipient and their WSOL token account.
Args:
client: Solana RPC client
pool: Address of the AMM pool
Returns:
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
"""
response = await client.get_account_info(pool, encoding="base64")
if not response.value or not response.value.data:
msg = "Cannot fetch pool account"
raise ValueError(msg)
pool_data = response.value.data
# Check if mayhem mode flag exists and is enabled
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
pool_data[POOL_MAYHEM_MODE_OFFSET]
)
# Select appropriate fee recipient
if is_mayhem_mode:
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
else:
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
# Get the fee recipient's WSOL token account
fee_recipient_token_account = get_associated_token_address(
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
return (fee_recipient, fee_recipient_token_account)
# ============================================================================
# Price Calculation
# ============================================================================
async def calculate_token_pool_price(
client: AsyncClient,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
) -> float:
"""Calculate the price of tokens in the pool.
"""Calculate current token price from AMM pool balances.
Fetches the balance of tokens in the pool and calculates the price ratio.
AMM price is determined by the ratio of tokens in the pool:
price = quote_balance / base_balance
Args:
client: Solana RPC client instance
pool_base_token_account: Address of the pool's base token account (your token)
pool_quote_token_account: Address of the pool's quote token account (SOL)
client: Solana RPC client
pool_base_token_account: Pool's token account (the token being priced)
pool_quote_token_account: Pool's SOL account (the quote currency)
Returns:
The price of the base token in terms of the quote token (usually SOL)
Price in SOL per token
"""
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
quote_balance_resp = await client.get_token_account_balance(
pool_quote_token_account
)
# Extract the UI amounts (human-readable with decimals)
base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_balance_resp.value.ui_amount)
token_price = quote_amount / base_amount
return quote_amount / base_amount
return token_price
# ============================================================================
# Token Program Determination
# ============================================================================
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}"
)
# ============================================================================
# Associated Token Account (ATA) Creation
# ============================================================================
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
"""Create an instruction to create an Associated Token Account (ATA) if it doesn't exist.
"""Create instruction to initialize a WSOL ATA if it doesn't exist.
This creates an instruction that will create an Associated Token Account for SOL
if it doesn't already exist.
Idempotent means this instruction won't fail if the ATA already exists.
See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
Args:
payer_pubkey: The public key of the account that will pay for the creation
payer_pubkey: Account that will pay for ATA creation
Returns:
An instruction to create the ATA
Instruction to create the ATA
"""
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
@@ -239,20 +376,23 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
]
# The data for creating an ATA idempotently is just a single byte with value 1
# Check the details here:
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
data = bytes([1])
# Instruction data: single byte with value 1 = CreateIdempotent
return Instruction(
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts
)
# ============================================================================
# Token Selling
# ============================================================================
async def sell_pump_swap(
client: AsyncClient,
pump_fun_amm_market: Pubkey,
market: Pubkey,
payer: Keypair,
base_mint: Pubkey,
token_program_id: Pubkey,
user_base_token_account: Pubkey,
user_quote_token_account: Pubkey,
pool_base_token_account: Pubkey,
@@ -261,54 +401,61 @@ async def sell_pump_swap(
coin_creator_vault_ata: Pubkey,
slippage: float = 0.25,
) -> str | None:
"""Sell tokens on the PUMP AMM.
"""Execute a token sell on the PUMP AMM with slippage protection.
This function sells all tokens in the user's token account with the specified slippage tolerance.
This function:
1. Fetches current token balance and pool price
2. Calculates minimum SOL output with slippage tolerance
3. Constructs and sends the sell transaction
Args:
client: Solana RPC client instance
pump_fun_amm_market: Address of the AMM market
payer: Keypair of the transaction signer and token seller
base_mint: Address of the token mint being sold
user_base_token_account: Address of the user's token account for the token being sold
user_quote_token_account: Address of the user's SOL token account
pool_base_token_account: Address of the pool's token account for the token being sold
pool_quote_token_account: Address of the pool's SOL token account
coin_creator_vault_authority: Address of the coin creator's vault authority
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
client: Solana RPC client
market: AMM pool address
payer: Wallet keypair for signing
base_mint: Token mint address
user_base_token_account: User's token account
user_quote_token_account: User's WSOL account
pool_base_token_account: Pool's token account
pool_quote_token_account: Pool's WSOL account
coin_creator_vault_authority: Creator vault PDA
coin_creator_vault_ata: Creator's WSOL account
slippage: Maximum acceptable slippage (0.25 = 25%)
Returns:
Transaction signature if successful, None otherwise
"""
# Get token balance
token_balance = int(
(await client.get_token_account_balance(user_base_token_account)).value.amount
)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
print(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
print("No tokens to sell.")
return None
# Calculate token price
token_price_sol = await calculate_token_pool_price(
client, pool_base_token_account, pool_quote_token_account
)
print(f"Price per Token: {token_price_sol:.20f} SOL")
# Calculate minimum SOL output with slippage protection
amount = token_balance
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - slippage
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
# Calculate minimum SOL we're willing to receive (slippage protection)
expected_sol_output = token_balance_decimal * token_price_sol
min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
# Define all accounts needed for the sell instruction
# Get fee recipient based on mayhem mode
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
client, market
)
# Build account list for sell instruction
# Order matters! Must match the program's expected account layout
accounts = [
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
@@ -317,15 +464,13 @@ async def sell_pump_swap(
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
is_signer=False,
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=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
@@ -341,76 +486,88 @@ async def sell_pump_swap(
AccountMeta(
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
),
# Index 19: fee_config (readonly)
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
# Index 20: fee_program (readonly)
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
]
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
# All integers are little-endian (<)
data = (
SELL_DISCRIMINATOR
+ struct.pack("<Q", amount)
+ struct.pack("<Q", min_sol_output)
+ struct.pack("<Q", token_balance) # Amount to sell
+ struct.pack("<Q", min_sol_output) # Minimum SOL to receive
)
# Set compute budget to avoid transaction failures
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
create_ata_ix = create_ata_idempotent_ix(
payer_pubkey=payer.pubkey(),
)
# Ensure WSOL ATA exists (needed to receive SOL from sell)
create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
# Build and sign transaction
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash(
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
payer.pubkey(),
recent_blockhash,
blockhash_resp.value.blockhash,
)
tx_sell = VersionedTransaction(message=msg, keypairs=[payer])
tx = VersionedTransaction(message=msg, keypairs=[payer])
try:
# Skip preflight to send transaction faster (useful in competitive scenarios)
tx_sig = await client.send_transaction(
tx_sell,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
)
tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
await client.confirm_transaction(tx_hash, commitment="confirmed")
await client.confirm_transaction(tx_hash, commitment="confirmed")
print("Transaction confirmed")
return tx_hash
except Exception as e:
print(f"Error sending transaction: {e!s}")
print(f"Error: {e!s}")
return None
async def main():
"""Main function to execute the token selling process."""
# ============================================================================
# Main Execution
# ============================================================================
async def main() -> None:
"""Execute the complete sell flow."""
async with AsyncClient(RPC_ENDPOINT) as client:
# Step 1: Find the pool address for our token
market_address = await get_market_address_by_base_mint(
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
)
# Step 2: Parse pool data to get necessary accounts
market_data = await get_market_data(client, market_address)
# Determine token program ID for the base mint
token_program_id = await get_token_program_id(client, TOKEN_MINT)
# Step 3: Derive PDAs needed for the transaction
coin_creator_vault_authority = find_coin_creator_vault(
Pubkey.from_string(market_data["coin_creator"])
)
coin_creator_vault_ata = get_associated_token_address(
coin_creator_vault_authority, SOL
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
)
# Step 4: Execute the sell
await sell_pump_swap(
client,
market_address,
PAYER,
TOKEN_MINT,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
get_associated_token_address(PAYER.pubkey(), SOL),
token_program_id,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
Pubkey.from_string(market_data["pool_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority,
+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,