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", "name": "global_volume_accumulator",
"writable": true,
"pda": { "pda": {
"seeds": [ "seeds": [
{ {
@@ -972,7 +971,6 @@
}, },
{ {
"name": "global_volume_accumulator", "name": "global_volume_accumulator",
"writable": true,
"pda": { "pda": {
"seeds": [ "seeds": [
{ {
@@ -2085,6 +2083,10 @@
{ {
"name": "coin_creator", "name": "coin_creator",
"type": "pubkey" "type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
} }
] ]
}, },
@@ -3006,6 +3008,92 @@
], ],
"args": [] "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", "name": "sync_user_volume_accumulator",
"discriminator": [ "discriminator": [
@@ -3134,6 +3222,70 @@
], ],
"args": [] "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", "name": "update_admin",
"discriminator": [ "discriminator": [
@@ -3644,6 +3796,19 @@
216 216
] ]
}, },
{
"name": "ReservedFeeRecipientsEvent",
"discriminator": [
43,
188,
250,
18,
221,
75,
187,
95
]
},
{ {
"name": "SellEvent", "name": "SellEvent",
"discriminator": [ "discriminator": [
@@ -3902,6 +4067,22 @@
"code": 6040, "code": 6040,
"name": "BuySlippageBelowMinBaseAmountOut", "name": "BuySlippageBelowMinBaseAmountOut",
"msg": "buy: slippage - would buy less tokens than expected min_base_amount_out" "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": [ "types": [
@@ -4005,6 +4186,10 @@
{ {
"name": "creator", "name": "creator",
"type": "pubkey" "type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
} }
] ]
} }
@@ -4357,6 +4542,10 @@
{ {
"name": "coin_creator", "name": "coin_creator",
"type": "pubkey" "type": "pubkey"
},
{
"name": "is_mayhem_mode",
"type": "bool"
} }
] ]
} }
@@ -4625,6 +4814,27 @@
"The admin authority for setting coin creators" "The admin authority for setting coin creators"
], ],
"type": "pubkey" "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", "name": "coin_creator",
"type": "pubkey" "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 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. 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 import argparse
@@ -42,20 +39,11 @@ class BondingCurveState:
real_sol_reserves: Real SOL reserves in the curve real_sol_reserves: Real SOL reserves in the curve
token_total_supply: Total token supply in the curve token_total_supply: Total token supply in the curve
complete: Whether the curve has completed and liquidity migrated complete: Whether the curve has completed and liquidity migrated
is_mayhem_mode: Whether the curve is in mayhem mode
""" """
_STRUCT_1 = Struct( # V2: Struct with creator field (81 bytes total: 8 discriminator + 73 data)
"virtual_token_reserves" / Int64ul, _STRUCT_V2 = Struct(
"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(
"virtual_token_reserves" / Int64ul, "virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
@@ -65,26 +53,43 @@ class BondingCurveState:
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey "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: def __init__(self, data: bytes) -> None:
"""Parse bonding curve data.""" """Parse bonding curve data."""
if data[:8] != EXPECTED_DISCRIMINATOR: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator") raise ValueError("Invalid curve state discriminator")
if len(data) < 150: total_length = len(data)
parsed = self._STRUCT_1.parse(data[8:])
self.__dict__.update(parsed)
else: if total_length == 81: # V2: Creator only
parsed = self._STRUCT_2.parse(data[8:]) parsed = self._STRUCT_V2.parse(data[8:])
self.__dict__.update(parsed) self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field # 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( def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
mint: Pubkey, program_id: Pubkey
) -> tuple[Pubkey, int]:
""" """
Derives the associated bonding curve address for a given mint. Derives the associated bonding curve address for a given mint.
@@ -134,17 +139,14 @@ async def check_token_status(mint_address: str) -> None:
""" """
try: try:
mint = Pubkey.from_string(mint_address) mint = Pubkey.from_string(mint_address)
bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM_ID)
# Get the associated bonding curve address
bonding_curve_address, bump = get_associated_bonding_curve_address(
mint, PUMP_PROGRAM_ID
)
print("\nToken status:") print("\nToken status:")
print("-" * 50) print("-" * 50)
print(f"Token mint: {mint}") print(f"Token mint: {mint}")
print(f"Associated bonding curve: {bonding_curve_address}") print(f"Bonding curve: {bonding_curve_address}")
print(f"Bump seed: {bump}") if bump is not None:
print(f"Bump seed: {bump}")
print("-" * 50) print("-" * 50)
# Check completion status # Check completion status
@@ -156,9 +158,25 @@ async def check_token_status(mint_address: str) -> None:
print("\nBonding curve status:") print("\nBonding curve status:")
print("-" * 50) print("-" * 50)
print(f"Creator: {curve_state.creator}")
print( 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: if curve_state.complete:
print( print(
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap." "\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
@@ -17,7 +17,7 @@ load_dotenv()
# Constants # Constants
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT") RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT: Final[str] = ( 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( PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
@@ -30,7 +30,7 @@ EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check 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. 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: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid discriminator for bonding curve") raise ValueError("Invalid discriminator for bonding curve")
# Parse common fields (present in all versions)
fields = struct.unpack_from("<QQQQQ?", data, 8) fields = struct.unpack_from("<QQQQQ?", data, 8)
return { result = {
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS, "virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL, "virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS, "real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
@@ -91,6 +92,20 @@ def parse_curve_state(data: bytes) -> dict:
"complete": fields[5], "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: def print_curve_status(state: dict) -> None:
""" """
@@ -130,9 +145,7 @@ async def track_curve() -> None:
return return
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT) mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
curve_pubkey: Pubkey = get_associated_bonding_curve_address( curve_pubkey: Pubkey = get_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID)
mint_pubkey, PUMP_PROGRAM_ID
)
print("Tracking bonding curve for:", mint_pubkey) print("Tracking bonding curve for:", mint_pubkey)
print("Curve address:", curve_pubkey, "\n") 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 # Update this address to MINT address of a token you want to close
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump") 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( async def close_account_if_exists(
client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey
@@ -41,7 +45,7 @@ async def close_account_if_exists(
mint=mint, mint=mint,
owner=wallet.pubkey, owner=wallet.pubkey,
amount=balance, amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM, program_id=TOKEN_PROGRAM,
) )
) )
await client.build_and_send_transaction([burn_ix], wallet.keypair) await client.build_and_send_transaction([burn_ix], wallet.keypair)
@@ -54,7 +58,7 @@ async def close_account_if_exists(
account=account, account=account,
dest=wallet.pubkey, dest=wallet.pubkey,
owner=wallet.pubkey, owner=wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM, program_id=TOKEN_PROGRAM,
) )
ix = close_account(close_params) ix = close_account(close_params)
@@ -78,7 +82,7 @@ async def main():
wallet = Wallet(PRIVATE_KEY) wallet = Wallet(PRIVATE_KEY)
# Get user's ATA for the token # 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) await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
except Exception as e: except Exception as e:
@@ -121,13 +121,27 @@ def parse_trade_event(logs):
def decode_trade_event(data): def decode_trade_event(data):
"""Decode TradeEvent structure from raw bytes.""" """Decode TradeEvent structure from raw bytes with progressive parsing.
if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check
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 return None
offset = 0 offset = 0
# Parse fields according to TradeEvent structure # Parse core fields (always present in all versions)
mint = data[offset : offset + 32] mint = data[offset : offset + 32]
offset += 32 offset += 32
@@ -152,6 +166,73 @@ def decode_trade_event(data):
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0] virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
offset += 8 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 { return {
"mint": base58.b58encode(mint).decode(), "mint": base58.b58encode(mint).decode(),
"sol_amount": sol_amount, "sol_amount": sol_amount,
@@ -161,6 +242,20 @@ def decode_trade_event(data):
"timestamp": timestamp, "timestamp": timestamp,
"virtual_sol_reserves": virtual_sol_reserves, "virtual_sol_reserves": virtual_sol_reserves,
"virtual_token_reserves": virtual_token_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 "price_per_token": (sol_amount * 1_000_000) / token_amount
if token_amount > 0 if token_amount > 0
else 0, else 0,
@@ -178,7 +273,10 @@ def display_transaction_info(signature, logs):
# Parse trade event data # Parse trade event data
trade_data = parse_trade_event(logs) trade_data = parse_trade_event(logs)
if trade_data: 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" Token: {trade_data['mint']}")
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL") print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
print(f" Token Amount: {trade_data['token_amount']:,}") print(f" Token Amount: {trade_data['token_amount']:,}")
@@ -187,6 +285,25 @@ def display_transaction_info(signature, logs):
) )
print(f" Trader: {trade_data['user']}") 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 # Extract and display program info
display_program_info(logs) display_program_info(logs)
+30 -21
View File
@@ -11,18 +11,7 @@ EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
class BondingCurveState: class BondingCurveState:
_STRUCT_1 = Struct( _STRUCT = 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(
"virtual_token_reserves" / Int64ul, "virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
@@ -30,23 +19,43 @@ class BondingCurveState:
"token_total_supply" / Int64ul, "token_total_supply" / Int64ul,
"complete" / Flag, "complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey "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: def __init__(self, data: bytes) -> None:
"""Parse bonding curve data.""" """Parse bonding curve data - supports all versions."""
if data[:8] != EXPECTED_DISCRIMINATOR: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator") raise ValueError("Invalid curve state discriminator")
if len(data) < 150: # Required fields (always present)
parsed = self._STRUCT_1.parse(data[8:]) offset = 8
self.__dict__.update(parsed) 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: else:
parsed = self._STRUCT_2.parse(data[8:]) self.creator = None
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)
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float: 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): def decode_create_instruction(data):
"""Decode legacy Create instruction (Metaplex tokens)."""
# The Create instruction has 3 string arguments: name, symbol, uri # The Create instruction has 3 string arguments: name, symbol, uri
offset = 8 # Skip the 8-byte discriminator offset = 8 # Skip the 8-byte discriminator
results = [] results = []
@@ -36,7 +37,42 @@ def decode_create_instruction(data):
string_data = data[offset : offset + length].decode("utf-8") string_data = data[offset : offset + length].decode("utf-8")
results.append(string_data) results.append(string_data)
offset += length 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): def decode_buy_instruction(data):
@@ -48,6 +84,8 @@ def decode_buy_instruction(data):
def decode_instruction_data(instruction, accounts, data): def decode_instruction_data(instruction, accounts, data):
if instruction["name"] == "create": if instruction["name"] == "create":
return decode_create_instruction(data) return decode_create_instruction(data)
elif instruction["name"] == "createV2":
return decode_create_v2_instruction(data)
elif instruction["name"] == "buy": elif instruction["name"] == "buy":
return decode_buy_instruction(data) return decode_buy_instruction(data)
else: else:
+45 -10
View File
@@ -1,20 +1,15 @@
import asyncio import asyncio
import os import os
import struct import struct
import sys
from typing import Final 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 solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey 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 LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6 TOKEN_DECIMALS: Final[int] = 6
CURVE_ADDRESS: Final[str] = ( CURVE_ADDRESS: Final[str] = "..." # Replace with actual bonding curve address
"YOUR_BONDING_CURVE_ADDRESS_HERE" # Replace with actual bonding curve address
)
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py # Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399) EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
@@ -23,7 +18,9 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState: class BondingCurveState:
_STRUCT = Struct( """Parse bonding curve account data - supports all versions."""
_STRUCT_V1 = Struct(
"virtual_token_reserves" / Int64ul, "virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
@@ -32,9 +29,47 @@ class BondingCurveState:
"complete" / Flag, "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: def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:]) """Parse bonding curve data - auto-detects version."""
self.__dict__.update(parsed) 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( async def get_bonding_curve_state(
@@ -1,11 +1,19 @@
""" """
This script compares two methods of detecting migrations: 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 import asyncio
@@ -33,7 +41,7 @@ QUOTE_MINT_SOL = base58.b58encode(
).decode() ).decode()
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").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: class DetectionTracker:
@@ -306,9 +314,9 @@ async def fetch_existing_market_pubkeys():
def parse_market_account_data(data): 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 = {} parsed_data = {}
offset = 8 # Skip discriminator offset = 8 # Skip discriminator
@@ -323,6 +331,8 @@ def parse_market_account_data(data):
("pool_base_token_account", "pubkey"), ("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"), ("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"), ("lp_supply", "u64"),
("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
] ]
try: try:
@@ -347,6 +357,10 @@ def parse_market_account_data(data):
value = data[offset] value = data[offset]
parsed_data[field_name] = value parsed_data[field_name] = value
offset += 1 offset += 1
elif field_type == "bool":
value = bool(data[offset])
parsed_data[field_name] = value
offset += 1
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to parse market data: {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): 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: if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes") 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. 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. 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): 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: if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes") print(f"[ERROR] Data length too short: {len(data)} bytes")
return None return None
@@ -24,7 +24,7 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") 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() MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
QUOTE_MINT_SOL = base58.b58encode( QUOTE_MINT_SOL = base58.b58encode(
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112")) bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
@@ -58,6 +58,10 @@ async def fetch_existing_market_pubkeys():
def parse_market_account_data(data): 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 = {} parsed_data = {}
offset = 8 # Discriminator offset = 8 # Discriminator
@@ -72,6 +76,7 @@ def parse_market_account_data(data):
("pool_quote_token_account", "pubkey"), ("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"), ("lp_supply", "u64"),
("coin_creator", "pubkey"), ("coin_creator", "pubkey"),
("is_mayhem_mode", "bool"),
] ]
try: try:
@@ -96,6 +101,10 @@ def parse_market_account_data(data):
value = data[offset] value = data[offset]
parsed_data[field_name] = value parsed_data[field_name] = value
offset += 1 offset += 1
elif field_type == "bool":
value = bool(data[offset])
parsed_data[field_name] = value
offset += 1
except Exception as e: except Exception as e:
print(f"[ERROR] Failed to parse market data: {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: Performance Comparison Tool for Pump.fun Token Detection Methods
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
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 import asyncio
@@ -26,13 +49,31 @@ from solders.transaction import VersionedTransaction
load_dotenv(override=True) load_dotenv(override=True)
# Constants # ============ 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
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: class DetectionTracker:
@@ -217,44 +258,198 @@ async def fetch_existing_token_mints():
return set() return set()
def parse_create_instruction(data): def decode_create_instruction(ix_data, account_keys):
""" """Decode legacy Create instruction (Metaplex tokens) from instruction data."""
Parse binary create instruction data into a structured format if len(ix_data) < 8:
"""
if len(data) < 8:
return None return None
offset = 8 # Skip discriminator offset = 8 # Skip discriminator
parsed_data = {} parsed_data = {}
try: try:
# Parse name (string) # Read string fields from instruction data
length = struct.unpack("<I", data[offset : offset + 4])[0] def read_string():
offset += 4 nonlocal offset
parsed_data["name"] = data[offset : offset + length].decode("utf-8") length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
offset += length offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
return value
# Parse symbol (string) def read_pubkey():
length = struct.unpack("<I", data[offset : offset + 4])[0] nonlocal offset
offset += 4 value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
parsed_data["symbol"] = data[offset : offset + length].decode("utf-8") offset += 32
offset += length return value
# Parse uri (string) # Parse instruction arguments
length = struct.unpack("<I", data[offset : offset + 4])[0] parsed_data["name"] = read_string()
offset += 4 parsed_data["symbol"] = read_string()
parsed_data["uri"] = data[offset : offset + length].decode("utf-8") parsed_data["uri"] = read_string()
offset += length parsed_data["creator"] = read_pubkey()
# Parse mint (pubkey) # Extract accounts from account_keys array
parsed_data["mint"] = base58.b58encode(data[offset : offset + 32]).decode( if len(account_keys) >= 8:
"utf-8" parsed_data["mint"] = account_keys[0]
) parsed_data["bondingCurve"] = account_keys[2]
offset += 32 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 return parsed_data
except Exception as e: 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 return None
@@ -269,6 +464,51 @@ def is_transaction_successful(logs):
# ============ WEBSOCKET LISTENERS ============ # ============ 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): async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
""" """
Listen for new tokens via block subscription Listen for new tokens via block subscription
@@ -330,6 +570,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
try: try:
transaction = VersionedTransaction.from_bytes(tx_data) 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: for ix in transaction.message.instructions:
if ( if (
transaction.message.account_keys[ 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) data_bytes = bytes(ix.data)
if not data_bytes.startswith( # Check for both Create and CreateV2 instructions
PUMP_CREATE_PREFIX 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 continue
parsed = parse_create_instruction(data_bytes) # Get account keys with ALT support
if not parsed: account_keys = get_account_keys(
transaction, ix, loaded_addresses
)
if account_keys is None:
print("Skipping transaction due to unresolved accounts")
continue continue
if len(ix.accounts) > 0: # Decode based on instruction type
try: if is_create_v2:
mint = str( print(f"[{provider_name}_block] Detected: CreateV2 instruction (Token2022)")
transaction.message.account_keys[ decoded = decode_create_v2_instruction(data_bytes, account_keys)
ix.accounts[0] else:
] print(f"[{provider_name}_block] Detected: Create instruction (Legacy/Metaplex)")
) # First account is usually the mint decoded = decode_create_instruction(data_bytes, account_keys)
if mint in known_tokens: if not decoded:
continue continue
ts = time.time() mint = decoded.get("mint")
tracker.add_token( if not mint:
mint, continue
parsed["name"],
parsed["symbol"], if mint in known_tokens:
f"{provider_name}_block", continue
ts,
) try:
known_tokens.add(mint) ts = time.time()
except Exception as e: tracker.add_token(
print( mint,
f"[ERROR] Failed to process block instruction: {e}" 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: except Exception as e:
print(f"[ERROR] Failed to process transaction: {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"] log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", []) logs = log_data.get("logs", [])
if not any( # Detect both Create and CreateV2 instructions
"Program log: Instruction: Create" in log for log in logs 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 continue
for log in logs: 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] encoded_data = log.split(": ")[1]
data_bytes = base64.b64decode(encoded_data) 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: if not parsed:
continue continue
@@ -492,11 +776,11 @@ async def listen_geyser_grpc(
if GEYSER_AUTH_TYPE == "x-token": if GEYSER_AUTH_TYPE == "x-token":
auth = grpc.metadata_call_credentials( auth = grpc.metadata_call_credentials(
lambda context, callback: callback((("x-token", api_token),), None) lambda _context, callback: callback((("x-token", api_token),), None)
) )
else: else:
auth = grpc.metadata_call_credentials( auth = grpc.metadata_call_credentials(
lambda context, callback: callback( lambda _context, callback: callback(
(("authorization", f"Basic {api_token}"),), None (("authorization", f"Basic {api_token}"),), None
) )
) )
@@ -529,28 +813,44 @@ async def listen_geyser_grpc(
continue continue
for ix in msg.instructions: 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 continue
parsed = parse_create_instruction(ix.data) # Convert account keys to string format
if not parsed: 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 continue
if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys): mint = account_keys[0]
continue
mint = base58.b58encode(
bytes(msg.account_keys[ix.accounts[0]])
).decode()
if mint in known_tokens: if mint in known_tokens:
continue 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() ts = time.time()
tracker.add_token( tracker.add_token(
mint, mint,
parsed["name"], decoded["name"],
parsed["symbol"], decoded["symbol"],
f"{provider_name}_geyser", f"{provider_name}_geyser",
ts, ts,
) )
@@ -2,7 +2,17 @@
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket. Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
Decodes transaction data to extract mint, bonding curve, and user details. 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 import asyncio
@@ -22,6 +32,93 @@ load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") 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): def load_idl(file_path):
with open(file_path) as f: with open(file_path) as f:
@@ -29,16 +126,34 @@ def load_idl(file_path):
def decode_create_instruction(ix_data, ix_def, accounts): 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 = {} args = {}
offset = 8 # Skip 8-byte discriminator offset = 8 # Skip 8-byte discriminator
# Parse instruction arguments according to IDL definition
for arg in ix_def["args"]: for arg in ix_def["args"]:
if arg["type"] == "string": if arg["type"] == "string":
# String format: 4-byte length prefix + UTF-8 encoded string
length = struct.unpack_from("<I", ix_data, offset)[0] length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4 offset += 4
value = ix_data[offset : offset + length].decode("utf-8") value = ix_data[offset : offset + length].decode("utf-8")
offset += length offset += length
elif arg["type"] == "pubkey": elif arg["type"] == "pubkey":
# Pubkey is 32 bytes, encoded as base58
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8") value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32 offset += 32
else: else:
@@ -46,19 +161,90 @@ def decode_create_instruction(ix_data, ix_def, accounts):
args[arg["name"]] = value 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["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2]) args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3]) args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7]) 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 return args
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
async def listen_and_decode_create(): 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") idl = load_idl("idl/pump_fun_idl.json")
create_discriminator = 8576854823835016728
async with websockets.connect(WSS_ENDPOINT) as websocket: async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps( subscription_message = json.dumps(
@@ -101,6 +287,11 @@ async def listen_and_decode_create():
tx_data_decoded 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: for ix in transaction.message.instructions:
if str( if str(
transaction.message.account_keys[ transaction.message.account_keys[
@@ -112,36 +303,68 @@ async def listen_and_decode_create():
"<Q", ix_data[:8] "<Q", ix_data[:8]
)[0] )[0]
if ( if discriminator == CREATE_DISCRIMINATOR:
discriminator # Legacy Create instruction (Metaplex tokens)
== create_discriminator
):
create_ix = next( create_ix = next(
instr instr
for instr in idl["instructions"] for instr in idl["instructions"]
if instr["name"] == "create" if instr["name"] == "create"
) )
account_keys = [ account_keys = get_account_keys(
str( transaction, ix, loaded_addresses
transaction.message.account_keys[
index
]
)
for index in ix.accounts
]
decoded_args = (
decode_create_instruction(
ix_data,
create_ix,
account_keys,
)
) )
print( if account_keys is None:
json.dumps( print("⚠️ Skipping transaction due to unresolved accounts")
decoded_args, indent=2 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: elif "result" in data:
print("Subscription confirmed") print("Subscription confirmed")
else: else:
@@ -1,10 +1,18 @@
""" """
Monitors Solana for new Pump.fun token creations using Geyser gRPC. 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). 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 import asyncio
@@ -22,11 +30,53 @@ load_dotenv()
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT") GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN") 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" AUTH_TYPE = "x-token"
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") 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_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(): async def create_geyser_connection():
@@ -57,7 +107,7 @@ def create_subscription_request():
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict: 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 # Skip past the 8-byte discriminator prefix
offset = 8 offset = 8
@@ -103,20 +153,68 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
"system_program": get_account_key(5), "system_program": get_account_key(5),
"rent": get_account_key(6), "rent": get_account_key(6),
"user": get_account_key(7), "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 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(): async def monitor_pump():
@@ -137,14 +235,28 @@ async def monitor_pump():
# Check each instruction in the transaction # Check each instruction in the transaction
for ix in msg.instructions: 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 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( signature = base58.b58encode(
bytes(update.transaction.transaction.signature) bytes(update.transaction.transaction.signature)
).decode() ).decode()
print_token_info(info, signature)
# Print token information in consistent format
print_token_info(info, signature=signature)
if __name__ == "__main__": if __name__ == "__main__":
@@ -2,7 +2,17 @@
Listens for new Pump.fun token creations via Solana WebSocket. Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.). 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 import asyncio
@@ -21,11 +31,71 @@ load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") 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): 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: if len(data) < 8:
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
return None return None
offset = 8 offset = 8 # Skip event discriminator
parsed_data = {} parsed_data = {}
# Parse fields based on CreateEvent structure # Parse fields based on CreateEvent structure
@@ -42,18 +112,102 @@ def parse_create_instruction(data):
try: try:
for field_name, field_type in fields: for field_name, field_type in fields:
if field_type == "string": 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] length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4 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") value = data[offset : offset + length].decode("utf-8")
offset += length offset += length
elif field_type == "publicKey": 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") value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32 offset += 32
parsed_data[field_name] = value parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data 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 return None
@@ -90,10 +244,17 @@ async def listen_for_new_tokens():
log_data = data["params"]["result"]["value"] log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", []) logs = log_data.get("logs", [])
if any( # Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log "Program log: Instruction: Create" in log
for log in logs 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: for log in logs:
if "Program data:" in log: if "Program data:" in log:
try: try:
@@ -101,22 +262,40 @@ async def listen_for_new_tokens():
decoded_data = base64.b64decode( decoded_data = base64.b64decode(
encoded_data 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: if parsed_data and "name" in parsed_data:
print( # Print token information in consistent format
"Signature:", print_token_info(
log_data.get("signature"), parsed_data,
) signature=log_data.get("signature")
for key, value in parsed_data.items():
print(f"{key}: {value}")
print(
"##########################################################################################"
) )
else:
print(f"⚠️ Parsing failed for CreateEvent")
except Exception as e: except Exception as e:
print(f"Failed to decode: {log}") print(f"❌ Error processing log: {e!s}")
print(f"Error: {e!s}")
except Exception as e: except Exception as e:
print(f"An error occurred while processing message: {e}") print(f"An error occurred while processing message: {e}")
@@ -1,9 +1,19 @@
""" """
Listens for new Pump.fun token creations via Solana WebSocket. Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.). 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 import asyncio
@@ -26,11 +36,67 @@ ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "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: def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
""" """
Find the associated bonding curve for a given mint and bonding curve. Calculate the associated token account (ATA) address for the bonding curve.
This uses the standard ATA derivation.
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( 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): def parse_create_instruction(data):
"""Parse legacy Create instruction (Metaplex tokens)."""
if len(data) < 8: if len(data) < 8:
print(f"⚠️ Data too short for Create instruction: {len(data)} bytes")
return None return None
offset = 8 offset = 8
parsed_data = {} parsed_data = {}
@@ -63,18 +131,81 @@ def parse_create_instruction(data):
try: try:
for field_name, field_type in fields: for field_name, field_type in fields:
if field_type == "string": 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] length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4 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") value = data[offset : offset + length].decode("utf-8")
offset += length offset += length
elif field_type == "publicKey": 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") value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32 offset += 32
parsed_data[field_name] = value parsed_data[field_name] = value
parsed_data["token_standard"] = "legacy"
parsed_data["is_mayhem_mode"] = False
return parsed_data 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 return None
@@ -111,10 +242,17 @@ async def listen_for_new_tokens():
log_data = data["params"]["result"]["value"] log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", []) logs = log_data.get("logs", [])
if any( # Detect both Create and CreateV2 instructions
is_create = any(
"Program log: Instruction: Create" in log "Program log: Instruction: Create" in log
for log in logs 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: for log in logs:
if "Program data:" in log: if "Program data:" in log:
try: try:
@@ -122,38 +260,56 @@ async def listen_for_new_tokens():
decoded_data = base64.b64decode( decoded_data = base64.b64decode(
encoded_data 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( mint = Pubkey.from_string(
parsed_data["mint"] parsed_data["mint"]
) )
bonding_curve = Pubkey.from_string( bonding_curve = Pubkey.from_string(
parsed_data["bondingCurve"] parsed_data["bondingCurve"]
) )
associated_curve = ( associated_curve = find_associated_bonding_curve(
find_associated_bonding_curve( mint, bonding_curve
mint, bonding_curve
)
) )
print(
f"Associated Bonding Curve: {associated_curve}" # Print token information in consistent format
) print_token_info(
print( parsed_data,
"##########################################################################################" signature=log_data.get("signature"),
associated_bonding_curve=str(associated_curve)
) )
else:
print(f"⚠️ Parsing failed for CreateEvent")
except Exception as e: except Exception as e:
print(f"Failed to decode: {log}") print(f"❌ Error processing log: {e!s}")
print(f"Error: {e!s}")
except Exception as e: except Exception as e:
print(f"An error occurred while processing message: {e}") print(f"An error occurred while processing message: {e}")
@@ -1,5 +1,17 @@
""" """
Listens for new Pump.fun token creations via PumpPortal WebSocket. 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 import asyncio
@@ -12,12 +24,52 @@ import websockets
WS_URL = "wss://pumpportal.fun/api/data" WS_URL = "wss://pumpportal.fun/api/data"
def format_sol(value): def print_token_info(token_data):
return f"{value:.6f} SOL" """
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(): async def listen_for_new_tokens():
@@ -39,27 +91,8 @@ async def listen_for_new_tokens():
else: else:
continue continue
print("\n" + "=" * 50) # Print token information in consistent format
print( print_token_info(token_info)
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)
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
print("\nWebSocket connection closed. Reconnecting...") print("\nWebSocket connection closed. Reconnecting...")
break break
+122 -16
View File
@@ -7,7 +7,7 @@ import struct
import base58 import base58
import websockets 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.async_api import AsyncClient
from solana.rpc.commitment import Confirmed from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts from solana.rpc.types import TxOpts
@@ -36,6 +36,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
) )
@@ -48,27 +49,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
class BondingCurveState: 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_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul, "real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul, "token_total_supply" / Int64ul,
"complete" / Flag, "complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
) )
def __init__(self, data: bytes) -> None: 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: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state 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) self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field # Calculate offset after base struct
if hasattr(self, "creator") and isinstance(self.creator, bytes): offset += self._BASE_STRUCT.sizeof()
self.creator = Pubkey.from_bytes(self.creator)
# 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( async def get_pump_curve_state(
@@ -126,11 +159,59 @@ def _find_fee_config() -> Pubkey:
return derived_address 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( async def buy_token(
mint: Pubkey, mint: Pubkey,
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
creator_vault: Pubkey, creator_vault: Pubkey,
token_program: Pubkey,
amount: float, amount: float,
slippage: float = 0.25, slippage: float = 0.25,
max_retries=5, max_retries=5,
@@ -139,10 +220,12 @@ async def buy_token(
payer = Keypair.from_bytes(private_key) payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client: 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) 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) curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state) token_price_sol = calculate_pump_curve_price(curve_state)
token_amount = amount / token_price_sol token_amount = amount / token_price_sol
@@ -150,9 +233,12 @@ async def buy_token(
# Calculate maximum SOL to spend with slippage # Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + 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 = [ accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), 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=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -168,7 +254,7 @@ async def buy_token(
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( 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(pubkey=creator_vault, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -178,7 +264,7 @@ async def buy_token(
AccountMeta( AccountMeta(
pubkey=_find_global_volume_accumulator(), pubkey=_find_global_volume_accumulator(),
is_signer=False, is_signer=False,
is_writable=True, is_writable=False,
), ),
AccountMeta( AccountMeta(
pubkey=_find_user_volume_accumulator(payer.pubkey()), pubkey=_find_user_volume_accumulator(payer.pubkey()),
@@ -200,14 +286,17 @@ async def buy_token(
] ]
discriminator = struct.pack("<Q", 16927863322537952870) discriminator = struct.pack("<Q", 16927863322537952870)
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
data = ( data = (
discriminator discriminator
+ struct.pack("<Q", int(token_amount * 10**6)) + struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports) + struct.pack("<Q", max_amount_lamports)
+ track_volume_bytes
) )
buy_ix = Instruction(PUMP_PROGRAM, data, accounts) buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
idempotent_ata_ix = create_idempotent_associated_token_account( 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( msg = Message(
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey() [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_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
idl = load_idl(idl_path) idl = load_idl(idl_path)
create_discriminator = calculate_discriminator("global:create") create_discriminator = calculate_discriminator("global:create")
create_v2_discriminator = calculate_discriminator("global:create_v2")
async with websockets.connect(RPC_WEBSOCKET) as websocket: async with websockets.connect(RPC_WEBSOCKET) as websocket:
subscription_message = json.dumps( subscription_message = json.dumps(
@@ -336,11 +426,22 @@ async def listen_for_create_transaction():
"<Q", ix_data[:8] "<Q", ix_data[:8]
)[0] )[0]
# Check which create instruction was used
instruction_name = None
token_program = None
if discriminator == create_discriminator: 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( create_ix = next(
instr instr
for instr in idl["instructions"] for instr in idl["instructions"]
if instr["name"] == "create" if instr["name"] == instruction_name
) )
account_keys = [ account_keys = [
str( str(
@@ -355,6 +456,9 @@ async def listen_for_create_transaction():
ix_data, create_ix, account_keys 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 return decoded_args
@@ -372,6 +476,7 @@ async def main():
bonding_curve = Pubkey.from_string(token_data["bondingCurve"]) bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"]) associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"])) creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
token_program = Pubkey.from_string(token_data["token_program"])
# Fetch the token price # Fetch the token price
async with AsyncClient(RPC_ENDPOINT) as client: async with AsyncClient(RPC_ENDPOINT) as client:
@@ -383,12 +488,13 @@ async def main():
slippage = 0.3 # 30% slippage tolerance slippage = 0.3 # 30% slippage tolerance
print(f"Bonding curve address: {bonding_curve}") 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"Token price: {token_price_sol:.10f} SOL")
print( print(
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..." f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
) )
await buy_token( 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 base58
import websockets 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.async_api import AsyncClient
from solana.rpc.commitment import Confirmed from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts from solana.rpc.types import TxOpts
@@ -55,6 +55,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
) )
@@ -69,27 +70,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
class BondingCurveState: 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_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul, "real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul, "token_total_supply" / Int64ul,
"complete" / Flag, "complete" / Flag,
"creator" / Bytes(32),
) )
def __init__(self, data: bytes) -> None: 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: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state 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) self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field # Calculate offset after base struct
if hasattr(self, "creator") and isinstance(self.creator, bytes): offset += self._BASE_STRUCT.sizeof()
self.creator = Pubkey.from_bytes(self.creator)
# 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( async def get_pump_curve_state(
@@ -147,6 +180,53 @@ def _find_fee_config() -> Pubkey:
return derived_address 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: def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
""" """
Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption. Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption.
@@ -169,6 +249,7 @@ async def buy_token(
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
creator_vault: Pubkey, creator_vault: Pubkey,
token_program: Pubkey,
amount: float, amount: float,
slippage: float = 0.25, slippage: float = 0.25,
max_retries=5, max_retries=5,
@@ -177,10 +258,12 @@ async def buy_token(
payer = Keypair.from_bytes(private_key) payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client: 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) 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) curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state) token_price_sol = calculate_pump_curve_price(curve_state)
token_amount = amount / token_price_sol token_amount = amount / token_price_sol
@@ -188,9 +271,12 @@ async def buy_token(
# Calculate maximum SOL to spend with slippage # Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + 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 = [ accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), 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=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -206,7 +292,7 @@ async def buy_token(
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( 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(pubkey=creator_vault, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -216,7 +302,7 @@ async def buy_token(
AccountMeta( AccountMeta(
pubkey=_find_global_volume_accumulator(), pubkey=_find_global_volume_accumulator(),
is_signer=False, is_signer=False,
is_writable=True, is_writable=False,
), ),
AccountMeta( AccountMeta(
pubkey=_find_user_volume_accumulator(payer.pubkey()), pubkey=_find_user_volume_accumulator(payer.pubkey()),
@@ -242,10 +328,11 @@ async def buy_token(
discriminator discriminator
+ struct.pack("<Q", int(token_amount * 10**6)) + struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports) + struct.pack("<Q", max_amount_lamports)
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
) )
buy_ix = Instruction(PUMP_PROGRAM, data, accounts) buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
idempotent_ata_ix = create_idempotent_associated_token_account( 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) # 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_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
idl = load_idl(idl_path) idl = load_idl(idl_path)
create_discriminator = calculate_discriminator("global:create") create_discriminator = calculate_discriminator("global:create")
create_v2_discriminator = calculate_discriminator("global:create_v2")
async with websockets.connect(RPC_WEBSOCKET) as websocket: async with websockets.connect(RPC_WEBSOCKET) as websocket:
subscription_message = json.dumps( subscription_message = json.dumps(
@@ -380,11 +468,22 @@ async def listen_for_create_transaction():
"<Q", ix_data[:8] "<Q", ix_data[:8]
)[0] )[0]
# Check which create instruction was used
instruction_name = None
token_program = None
if discriminator == create_discriminator: 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( create_ix = next(
instr instr
for instr in idl["instructions"] for instr in idl["instructions"]
if instr["name"] == "create" if instr["name"] == instruction_name
) )
account_keys = [ account_keys = [
str( str(
@@ -399,6 +498,9 @@ async def listen_for_create_transaction():
ix_data, create_ix, account_keys 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 return decoded_args
@@ -415,6 +517,7 @@ async def main():
bonding_curve = Pubkey.from_string(token_data["bondingCurve"]) bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"]) associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"])) creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
token_program = Pubkey.from_string(token_data["token_program"])
# Fetch the token price # Fetch the token price
async with AsyncClient(RPC_ENDPOINT) as client: async with AsyncClient(RPC_ENDPOINT) as client:
@@ -426,13 +529,14 @@ async def main():
slippage = 0.3 # 30% slippage tolerance slippage = 0.3 # 30% slippage tolerance
print(f"Bonding curve address: {bonding_curve}") 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"Token price: {token_price_sol:.10f} SOL")
print( print(
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..." f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
) )
print("CU Optimization: Enabled (512KB account data limit)") print("CU Optimization: Enabled (512KB account data limit)")
await buy_token( 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 base58
import grpc 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.async_api import AsyncClient
from solana.rpc.commitment import Confirmed from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts 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 AUTH_TYPE = os.environ.get("GEYSER_AUTH_TYPE", "x-token") # Default to x-token
PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728) PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_DISCRIMINATOR = bytes([214, 144, 76, 236, 95, 139, 49, 180])
class BondingCurveState: 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_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul, "real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul, "token_total_supply" / Int64ul,
"complete" / Flag, "complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
) )
def __init__(self, data: bytes) -> None: 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: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state 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) self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field # Calculate offset after base struct
if hasattr(self, "creator") and isinstance(self.creator, bytes): offset += self._BASE_STRUCT.sizeof()
self.creator = Pubkey.from_bytes(self.creator)
# 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( async def get_pump_curve_state(
@@ -139,6 +172,53 @@ def _find_fee_config() -> Pubkey:
return derived_address 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(): async def create_geyser_connection():
"""Establish a secure connection to the Geyser endpoint using the configured auth type.""" """Establish a secure connection to the Geyser endpoint using the configured auth type."""
if AUTH_TYPE == "x-token": if AUTH_TYPE == "x-token":
@@ -232,7 +312,13 @@ async def listen_for_create_transaction_geyser():
# Check each instruction in the transaction # Check each instruction in the transaction
for ix in msg.instructions: 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 continue
# Found a create instruction # Found a create instruction
@@ -240,6 +326,10 @@ async def listen_for_create_transaction_geyser():
ix.data, msg.account_keys, ix.accounts 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( signature = base58.b58encode(
bytes(update.transaction.transaction.signature) bytes(update.transaction.transaction.signature)
).decode() ).decode()
@@ -253,6 +343,7 @@ async def buy_token(
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
creator_vault: Pubkey, creator_vault: Pubkey,
token_program: Pubkey,
amount: float, amount: float,
slippage: float = 0.25, slippage: float = 0.25,
max_retries=5, max_retries=5,
@@ -262,22 +353,30 @@ async def buy_token(
async with AsyncClient(RPC_ENDPOINT) as client: async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address( 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) 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) # curve_state = await get_pump_curve_state(client, bonding_curve)
# token_price_sol = calculate_pump_curve_price(curve_state) # token_price_sol = calculate_pump_curve_price(curve_state)
# token_amount = amount / token_price_sol # 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 # Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + slippage)) max_amount_lamports = int(amount_lamports * (1 + slippage))
accounts = [ accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), 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=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -293,7 +392,7 @@ async def buy_token(
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( 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(pubkey=creator_vault, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -303,7 +402,7 @@ async def buy_token(
AccountMeta( AccountMeta(
pubkey=_find_global_volume_accumulator(), pubkey=_find_global_volume_accumulator(),
is_signer=False, is_signer=False,
is_writable=True, is_writable=False,
), ),
AccountMeta( AccountMeta(
pubkey=_find_user_volume_accumulator(payer.pubkey()), pubkey=_find_user_volume_accumulator(payer.pubkey()),
@@ -329,10 +428,11 @@ async def buy_token(
discriminator discriminator
+ struct.pack("<Q", int(token_amount * 10**6)) + struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports) + struct.pack("<Q", max_amount_lamports)
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
) )
buy_ix = Instruction(PUMP_PROGRAM, data, accounts) buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
idempotent_ata_ix = create_idempotent_associated_token_account( 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( msg = Message(
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey() [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"]) bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"]) associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"])) creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
token_program = Pubkey.from_string(token_data["token_program"])
# Fetch the token price # Fetch the token price
# async with AsyncClient(RPC_ENDPOINT) as client: # async with AsyncClient(RPC_ENDPOINT) as client:
@@ -407,12 +508,13 @@ async def main():
slippage = 0.3 # 30% slippage tolerance slippage = 0.3 # 30% slippage tolerance
print(f"Bonding curve address: {bonding_curve}") 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"Token price: {token_price_sol:.10f} SOL")
print( print(
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..." f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
) )
await buy_token( 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 struct
import base58 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.async_api import AsyncClient
from solana.rpc.commitment import Confirmed from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts 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 # Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399) EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
TOKEN_DECIMALS = 6 TOKEN_DECIMALS = 6
TOKEN_MINT = Pubkey.from_string("...") TOKEN_MINT = Pubkey.from_string("...") # Replace with actual token mint address
# Global constants # Global constants
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
@@ -30,6 +30,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
) )
@@ -43,27 +44,59 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState: 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_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul, "virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul, "real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul, "real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul, "token_total_supply" / Int64ul,
"complete" / Flag, "complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
) )
def __init__(self, data: bytes) -> None: 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: if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state 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) self.__dict__.update(parsed)
# Convert raw bytes to Pubkey for creator field # Calculate offset after base struct
if hasattr(self, "creator") and isinstance(self.creator, bytes): offset += self._BASE_STRUCT.sizeof()
self.creator = Pubkey.from_bytes(self.creator)
# 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( 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) 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( derived_address, _ = Pubkey.find_program_address(
[ [
bytes(bonding_curve), bytes(bonding_curve),
bytes(SYSTEM_TOKEN_PROGRAM), bytes(token_program_id),
bytes(mint), bytes(mint),
], ],
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
@@ -112,6 +147,53 @@ def _find_fee_config() -> Pubkey:
return derived_address 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: def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state") raise ValueError("Invalid reserve state")
@@ -128,11 +210,31 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
return 0 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( async def sell_token(
mint: Pubkey, mint: Pubkey,
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
creator_vault: Pubkey, creator_vault: Pubkey,
token_program_id: Pubkey,
slippage: float = 0.25, slippage: float = 0.25,
max_retries=5, max_retries=5,
): ):
@@ -140,7 +242,9 @@ async def sell_token(
payer = Keypair.from_bytes(private_key) payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client: 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 # Get token balance
token_balance = await get_token_balance(client, associated_token_account) token_balance = await get_token_balance(client, associated_token_account)
@@ -150,7 +254,7 @@ async def sell_token(
print("No tokens to sell.") print("No tokens to sell.")
return 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) curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state) token_price_sol = calculate_pump_curve_price(curve_state)
print(f"Price per Token: {token_price_sol:.20f} SOL") 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"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") 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 = [ accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), 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=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta( AccountMeta(
@@ -187,8 +294,8 @@ async def sell_token(
is_writable=True, is_writable=True,
), ),
AccountMeta( 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( AccountMeta(
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
), ),
@@ -208,10 +315,13 @@ async def sell_token(
] ]
discriminator = struct.pack("<Q", 12502976635542562355) discriminator = struct.pack("<Q", 12502976635542562355)
# Encode OptionBool for track_volume: [1, 1] = Some(true)
track_volume_bytes = bytes([1, 1])
data = ( data = (
discriminator discriminator
+ struct.pack("<Q", amount) + struct.pack("<Q", amount)
+ struct.pack("<Q", min_sol_output) + struct.pack("<Q", min_sol_output)
+ track_volume_bytes
) )
sell_ix = Instruction(PUMP_PROGRAM, data, accounts) sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
@@ -248,8 +358,13 @@ async def sell_token(
async def main(): async def main():
# Replace these with the actual values for the token you want to sell # 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) 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: async with AsyncClient(RPC_ENDPOINT) as client:
curve_state = await get_pump_curve_state(client, bonding_curve) 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"Bonding curve address: {bonding_curve}")
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...") print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
await sell_token( 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_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( AccountMeta(
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
), ),
AccountMeta( AccountMeta(
pubkey=_find_user_volume_accumulator(user), 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: Key concepts demonstrated:
- Finding market addresses by token mint - Finding AMM pool addresses by token mint
- Fetching and parsing market data from PUMP AMM pools - Parsing binary account data structures
- Calculating token prices in AMM pools - Dynamic fee recipient calculation (mayhem mode vs standard)
- Creating associated token accounts (ATAs) idempotently - Program Derived Address (PDA) derivation
- Buying tokens on the PUMP AMM with slippage protection - WSOL wrapping (converting SOL to wrapped SOL for SPL token operations)
- Volume tracking incentives integration
- Transaction simulation before sending
- Slippage protection mechanisms
""" """
import asyncio import asyncio
@@ -34,31 +38,33 @@ from spl.token.instructions import (
load_dotenv() load_dotenv()
# Configuration constants # ============================================================================
# Configuration
# ============================================================================
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") 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")) PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(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 # Token configuration
BUY_DISCRIMINATOR = bytes.fromhex( TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
"66063d1201daebea"
) # Program instruction identifier for the buy function # 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") SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string( PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw" "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") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
@@ -67,61 +73,91 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR" "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
) )
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") 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 LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use 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( async def get_market_address_by_base_mint(
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
) -> 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 Uses getProgramAccounts RPC method with a memcmp filter to find the pool
by querying program accounts with a filter for the base_mint field. that matches the given token mint address.
Args: Args:
client: Solana RPC client instance client: Solana RPC client
base_mint_address: Address of the token mint you want to find the market for base_mint_address: Token mint to find the pool for
amm_program_id: Address of the AMM program amm_program_id: PUMP AMM program address
Returns: 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) filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, 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)]
response = await client.get_program_accounts( response = await client.get_program_accounts(
amm_program_id, encoding="base64", filters=filters amm_program_id, encoding="base64", filters=filters
) )
return response.value[0].pubkey
market_address = [account.pubkey for account in response.value][0]
return market_address
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: 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 The pool account stores data in a specific binary format. This function
into a structured dictionary containing key market information. deserializes that data based on the known structure.
Args: Args:
client: Solana RPC client instance client: Solana RPC client
market_address: Address of the market (AMM pool) to fetch data for market_address: Address of the pool account
Returns: Returns:
Dictionary containing the parsed market data Dictionary with parsed pool data fields
""" """
response = await client.get_account_info(market_address, encoding="base64") response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data data = response.value.data
parsed_data: dict = {} parsed_data: dict = {}
# Start after the 8-byte discriminator offset = POOL_DISCRIMINATOR_SIZE
offset = 8
# Define the structure of the market account data # Field definitions: (name, type)
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
fields = [ fields = [
("pool_bump", "u8"), ("pool_bump", "u8"),
("index", "u16"), ("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") parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32 offset += 32
elif field_type in {"u64", "i64"}: elif field_type in {"u64", "i64"}:
value = ( format_char = "<Q" if field_type == "u64" else "<q"
struct.unpack("<Q", data[offset : offset + 8])[0] parsed_data[field_name] = struct.unpack(
if field_type == "u64" format_char, data[offset : offset + 8]
else struct.unpack("<q", data[offset : offset + 8])[0] )[0]
)
parsed_data[field_name] = value
offset += 8 offset += 8
elif field_type == "u16": elif field_type == "u16":
value = struct.unpack("<H", data[offset : offset + 2])[0] parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
parsed_data[field_name] = value
offset += 2 offset += 2
elif field_type == "u8": elif field_type == "u8":
value = data[offset] parsed_data[field_name] = data[offset]
parsed_data[field_name] = value
offset += 1 offset += 1
return parsed_data 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: Args:
coin_creator: Pubkey of the coin creator account coin_creator: Public key of the token creator
Returns: Returns:
Pubkey of the derived coin creator vault authority PDA of the creator's vault authority
Note:
This vault is used to collect creator fees from token transactions
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"creator_vault", bytes(coin_creator)], [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: 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 This account tracks total trading volume across all pools.
across all pools in the PUMP AMM protocol. Volume tracking is used for incentive programs and analytics.
Returns: Returns:
Pubkey of the derived global volume accumulator account PDA of the global volume accumulator
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"global_volume_accumulator"], [b"global_volume_accumulator"],
@@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey:
def find_user_volume_accumulator(user: Pubkey) -> 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 Tracks individual user's trading volume, which may qualify them
for a specific user in the PUMP AMM protocol. for incentives or rewards based on trading activity.
Args: Args:
user: Pubkey of the user account user: Public key of the user
Returns: Returns:
Pubkey of the derived user volume accumulator account PDA of the user's volume accumulator
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"user_volume_accumulator", bytes(user)], [b"user_volume_accumulator", bytes(user)],
@@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
def find_fee_config() -> 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: This account stores fee-related configuration for the AMM.
Pubkey of the derived fee config account
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)], [b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
@@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey:
return derived_address 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( async def calculate_token_pool_price(
client: AsyncClient, client: AsyncClient,
pool_base_token_account: Pubkey, pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey, pool_quote_token_account: Pubkey,
) -> float: ) -> 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 AMM price is determined by the ratio of tokens in the pool:
between the base token and quote token (typically SOL). price = quote_balance / base_balance
Args: Args:
client: Solana RPC client instance client: Solana RPC client
pool_base_token_account: Address of the pool's base token account (your token) pool_base_token_account: Pool's token account (the token being priced)
pool_quote_token_account: Address of the pool's quote token account (SOL) pool_quote_token_account: Pool's SOL account (the quote currency)
Returns: 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) base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
quote_balance_resp = await client.get_token_account_balance( quote_balance_resp = await client.get_token_account_balance(
pool_quote_token_account pool_quote_token_account
) )
# Extract the UI amounts (human-readable with decimals)
base_amount = float(base_balance_resp.value.ui_amount) base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_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( async def buy_pump_swap(
client: AsyncClient, client: AsyncClient,
pump_fun_amm_market: Pubkey, market: Pubkey,
payer: Keypair, payer: Keypair,
base_mint: Pubkey, base_mint: Pubkey,
user_base_token_account: Pubkey, user_base_token_account: Pubkey,
@@ -273,52 +406,64 @@ async def buy_pump_swap(
pool_quote_token_account: Pubkey, pool_quote_token_account: Pubkey,
coin_creator_vault_authority: Pubkey, coin_creator_vault_authority: Pubkey,
coin_creator_vault_ata: Pubkey, coin_creator_vault_ata: Pubkey,
sol_amount_to_spend: int, sol_amount_to_spend: float,
slippage: float = 0.25, slippage: float = 0.25,
) -> str | None: ) -> 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 This function:
token amount based on the current pool price and applying slippage protection. 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: Args:
client: Solana RPC client instance client: Solana RPC client
pump_fun_amm_market: Address of the AMM market market: AMM pool address
payer: Keypair of the transaction signer and token buyer payer: Wallet keypair for signing
base_mint: Address of the token mint being purchased base_mint: Token mint address
user_base_token_account: Address of the user's token account for receiving purchased tokens user_base_token_account: User's token account (for receiving tokens)
user_quote_token_account: Address of the user's SOL token account user_quote_token_account: User's WSOL account
pool_base_token_account: Address of the pool's token account for the token being purchased pool_base_token_account: Pool's token account
pool_quote_token_account: Address of the pool's SOL token account pool_quote_token_account: Pool's WSOL account
coin_creator_vault_authority: Address of the coin creator's vault authority coin_creator_vault_authority: Creator vault PDA
coin_creator_vault_ata: Address of the coin creator's associated token account for fees coin_creator_vault_ata: Creator's WSOL account
sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports) sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports)
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) slippage: Maximum acceptable slippage (0.25 = 25%)
Returns: Returns:
Transaction signature if successful, None otherwise 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( token_price_sol = await calculate_token_pool_price(
client, pool_base_token_account, pool_quote_token_account client, pool_base_token_account, pool_quote_token_account
) )
print(f"Token price in SOL: {token_price_sol:.10f} SOL") 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) 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 * (1 + slippage)) * LAMPORTS_PER_SOL)
max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens") print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL") 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() global_volume_accumulator = find_global_volume_accumulator()
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey()) 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 = [ 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=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
AccountMeta(pubkey=base_mint, 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=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_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_quote_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( AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
), ),
AccountMeta( AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, pubkey=token_program_id, is_signer=False, is_writable=False
is_signer=False, ), # Use dynamic token_program_id
is_writable=True,
),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( AccountMeta(
@@ -352,34 +495,43 @@ async def buy_pump_swap(
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
), ),
AccountMeta( 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), 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), 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), 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 = ( data = (
BUY_DISCRIMINATOR BUY_DISCRIMINATOR
+ struct.pack("<Q", base_amount_out) + struct.pack("<Q", base_amount_out) # Expected token amount
+ struct.pack("<Q", max_sol_input) + struct.pack("<Q", max_sol_input) # Maximum SOL to spend
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking) + 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_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE) 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( 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 # Calculate amount to wrap (includes buffer for fees)
pump_protocol_fees = sol_amount_to_spend * 0.1 # adding some buffer fees wrap_amount = int(
wrap_amount = int((sol_amount_to_spend + pump_protocol_fees) * LAMPORTS_PER_SOL) (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( transfer_sol_ix = transfer(
TransferParams( TransferParams(
from_pubkey=payer.pubkey(), from_pubkey=payer.pubkey(),
@@ -387,20 +539,24 @@ async def buy_pump_swap(
lamports=wrap_amount, lamports=wrap_amount,
) )
) )
sync_native_ix = sync_native( 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( # Create token account for receiving purchased tokens
payer.pubkey(), payer.pubkey(), base_mint, SYSTEM_TOKEN_PROGRAM 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) buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
# Build and sign transaction
blockhash_resp = await client.get_latest_blockhash() blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash( msg = Message.new_with_blockhash(
[ [
compute_limit_ix, compute_limit_ix,
@@ -408,64 +564,77 @@ async def buy_pump_swap(
create_wsol_ata_ix, create_wsol_ata_ix,
transfer_sol_ix, transfer_sol_ix,
sync_native_ix, sync_native_ix,
idempotent_ata_ix, create_token_ata_ix,
buy_ix, buy_ix,
], ],
payer.pubkey(), payer.pubkey(),
recent_blockhash, blockhash_resp.value.blockhash,
) )
tx = VersionedTransaction(message=msg, keypairs=[payer])
tx_buy = VersionedTransaction(message=msg, keypairs=[payer]) # Simulate first to catch errors before sending
simulation = await client.simulate_transaction(tx)
# Optionally, you can simulate the transaction first and check for errors and get the compute units used
simulation = await client.simulate_transaction(tx_buy)
if simulation.value.err: if simulation.value.err:
print(f"Simulation error: {simulation.value.err}") print(f"Simulation error: {simulation.value.err}")
return None return None
compute_units_used = simulation.value.units_consumed print(
print(f"Simulation successful, compute units used: {compute_units_used}") f"Simulation successful, compute units used: {simulation.value.units_consumed}"
)
try: try:
# Skip preflight since we already simulated (faster execution)
tx_sig = await client.send_transaction( tx_sig = await client.send_transaction(
tx_buy, tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
) )
tx_hash = tx_sig.value tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}") 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") print("Transaction confirmed")
return tx_hash return tx_hash
except Exception as e: except Exception as e:
print(f"Error sending transaction: {e!s}") print(f"Error: {e!s}")
return None return None
async def main(): # ============================================================================
"""Main function to execute the token buying process.""" # Main Execution
sol_amount_to_spend = 0.000001 # ============================================================================
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: 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( market_address = await get_market_address_by_base_mint(
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID 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) 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( coin_creator_vault_authority = find_coin_creator_vault(
Pubkey.from_string(market_data["coin_creator"]) Pubkey.from_string(market_data["coin_creator"])
) )
coin_creator_vault_ata = get_associated_token_address( 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( await buy_pump_swap(
client, client,
market_address, market_address,
PAYER, PAYER,
TOKEN_MINT, TOKEN_MINT,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
get_associated_token_address(PAYER.pubkey(), SOL), 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_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]), Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority, coin_creator_vault_authority,
+294 -137
View File
@@ -1,10 +1,14 @@
""" """
This module provides functionality to: This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol.
- Find market addresses by token mint. It covers the complete flow from finding markets to executing sells with mayhem mode support.
- Fetch and parse market data from PUMP AMM pools.
- Calculate token prices in AMM pools. Key concepts demonstrated:
- Create associated token accounts (ATAs) idempotently. - Finding AMM pool addresses by token mint
- Sell tokens on the PUMP AMM with slippage protection. - 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 import asyncio
@@ -26,31 +30,33 @@ from spl.token.instructions import get_associated_token_address
load_dotenv() load_dotenv()
# Configuration constants # ============================================================================
# Configuration
# ============================================================================
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") 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")) PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(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 # Token configuration
SELL_DISCRIMINATOR = bytes.fromhex( TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
"33e685a4017f83ad"
) # Program instruction identifier for the sell function # 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") SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string( PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw" "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") SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
@@ -59,59 +65,87 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR" "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
) )
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ") 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 LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
# ============================================================================
# Market Discovery
# ============================================================================
async def get_market_address_by_base_mint( async def get_market_address_by_base_mint(
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
) -> 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: Args:
client: Solana RPC client instance client: Solana RPC client
base_mint_address: Address of the token mint you want to find the market for base_mint_address: Token mint to find the pool for
amm_program_id: Address of the AMM program amm_program_id: PUMP AMM program address
Returns: 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) filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, 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)]
response = await client.get_program_accounts( response = await client.get_program_accounts(
amm_program_id, encoding="base64", filters=filters amm_program_id, encoding="base64", filters=filters
) )
return response.value[0].pubkey
market_address = [account.pubkey for account in response.value][0]
return market_address
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: 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: Args:
client: Solana RPC client instance client: Solana RPC client
market_address: Address of the market (AMM pool) to fetch data for market_address: Address of the pool account
Returns: Returns:
Dictionary containing the parsed market data Dictionary with parsed pool data fields
""" """
response = await client.get_account_info(market_address, encoding="base64") response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data data = response.value.data
parsed_data: dict = {} parsed_data: dict = {}
# Start after the 8-byte discriminator offset = POOL_DISCRIMINATOR_SIZE
offset = 8
# Define the structure of the market account data # Field definitions: (name, type)
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
fields = [ fields = [
("pool_bump", "u8"), ("pool_bump", "u8"),
("index", "u16"), ("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") parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32 offset += 32
elif field_type in {"u64", "i64"}: elif field_type in {"u64", "i64"}:
value = ( format_char = "<Q" if field_type == "u64" else "<q"
struct.unpack("<Q", data[offset : offset + 8])[0] parsed_data[field_name] = struct.unpack(
if field_type == "u64" format_char, data[offset : offset + 8]
else struct.unpack("<q", data[offset : offset + 8])[0] )[0]
)
parsed_data[field_name] = value
offset += 8 offset += 8
elif field_type == "u16": elif field_type == "u16":
value = struct.unpack("<H", data[offset : offset + 2])[0] parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
parsed_data[field_name] = value
offset += 2 offset += 2
elif field_type == "u8": elif field_type == "u8":
value = data[offset] parsed_data[field_name] = data[offset]
parsed_data[field_name] = value
offset += 1 offset += 1
return parsed_data 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: Args:
coin_creator: Pubkey of the coin creator account coin_creator: Public key of the token creator
Returns: Returns:
Pubkey of the derived coin creator vault authority PDA of the creator's vault authority
Note:
This vault is used to collect creator fees from token transactions
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"creator_vault", bytes(coin_creator)], [b"creator_vault", bytes(coin_creator)],
@@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
def find_fee_config() -> 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: This account stores fee-related configuration for the AMM.
Pubkey of the derived fee config account
""" """
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)], [b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
@@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey:
return derived_address 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( async def calculate_token_pool_price(
client: AsyncClient, client: AsyncClient,
pool_base_token_account: Pubkey, pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey, pool_quote_token_account: Pubkey,
) -> float: ) -> 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: Args:
client: Solana RPC client instance client: Solana RPC client
pool_base_token_account: Address of the pool's base token account (your token) pool_base_token_account: Pool's token account (the token being priced)
pool_quote_token_account: Address of the pool's quote token account (SOL) pool_quote_token_account: Pool's SOL account (the quote currency)
Returns: 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) base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
quote_balance_resp = await client.get_token_account_balance( quote_balance_resp = await client.get_token_account_balance(
pool_quote_token_account pool_quote_token_account
) )
# Extract the UI amounts (human-readable with decimals)
base_amount = float(base_balance_resp.value.ui_amount) base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_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: 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 Idempotent means this instruction won't fail if the ATA already exists.
if it doesn't already exist. See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
Args: 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: Returns:
An instruction to create the ATA Instruction to create the ATA
""" """
associated_token_address = get_associated_token_address(payer_pubkey, SOL) 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), 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 # Instruction data: single byte with value 1 = CreateIdempotent
# Check the details here:
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
data = bytes([1])
return Instruction( 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( async def sell_pump_swap(
client: AsyncClient, client: AsyncClient,
pump_fun_amm_market: Pubkey, market: Pubkey,
payer: Keypair, payer: Keypair,
base_mint: Pubkey, base_mint: Pubkey,
token_program_id: Pubkey,
user_base_token_account: Pubkey, user_base_token_account: Pubkey,
user_quote_token_account: Pubkey, user_quote_token_account: Pubkey,
pool_base_token_account: Pubkey, pool_base_token_account: Pubkey,
@@ -261,54 +401,61 @@ async def sell_pump_swap(
coin_creator_vault_ata: Pubkey, coin_creator_vault_ata: Pubkey,
slippage: float = 0.25, slippage: float = 0.25,
) -> str | None: ) -> 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: Args:
client: Solana RPC client instance client: Solana RPC client
pump_fun_amm_market: Address of the AMM market market: AMM pool address
payer: Keypair of the transaction signer and token seller payer: Wallet keypair for signing
base_mint: Address of the token mint being sold base_mint: Token mint address
user_base_token_account: Address of the user's token account for the token being sold user_base_token_account: User's token account
user_quote_token_account: Address of the user's SOL token account user_quote_token_account: User's WSOL account
pool_base_token_account: Address of the pool's token account for the token being sold pool_base_token_account: Pool's token account
pool_quote_token_account: Address of the pool's SOL token account pool_quote_token_account: Pool's WSOL account
coin_creator_vault_authority: Address of the coin creator's vault authority coin_creator_vault_authority: Creator vault PDA
coin_creator_vault_ata: Address of the coin creator's associated token account for fees coin_creator_vault_ata: Creator's WSOL account
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) slippage: Maximum acceptable slippage (0.25 = 25%)
Returns: Returns:
Transaction signature if successful, None otherwise Transaction signature if successful, None otherwise
""" """
# Get token balance
token_balance = int( token_balance = int(
(await client.get_token_account_balance(user_base_token_account)).value.amount (await client.get_token_account_balance(user_base_token_account)).value.amount
) )
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
print(f"Token balance: {token_balance_decimal}") print(f"Token balance: {token_balance_decimal}")
if token_balance == 0: if token_balance == 0:
print("No tokens to sell.") print("No tokens to sell.")
return None return None
# Calculate token price
token_price_sol = await calculate_token_pool_price( token_price_sol = await calculate_token_pool_price(
client, pool_base_token_account, pool_quote_token_account client, pool_base_token_account, pool_quote_token_account
) )
print(f"Price per Token: {token_price_sol:.20f} SOL") print(f"Price per Token: {token_price_sol:.20f} SOL")
# Calculate minimum SOL output with slippage protection # Calculate minimum SOL we're willing to receive (slippage protection)
amount = token_balance expected_sol_output = token_balance_decimal * token_price_sol
min_sol_output = float(token_balance_decimal) * float(token_price_sol) min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL)
slippage_factor = 1 - slippage
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens") print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") 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 = [ 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=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
AccountMeta(pubkey=base_mint, 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=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_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_quote_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( AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
), ),
AccountMeta( AccountMeta(
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, pubkey=token_program_id, is_signer=False, is_writable=False
is_signer=False, ), # Use dynamic token_program_id
is_writable=True,
),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta( AccountMeta(
@@ -341,76 +486,88 @@ async def sell_pump_swap(
AccountMeta( AccountMeta(
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False 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), 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), 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 = ( data = (
SELL_DISCRIMINATOR SELL_DISCRIMINATOR
+ struct.pack("<Q", amount) + struct.pack("<Q", token_balance) # Amount to sell
+ struct.pack("<Q", min_sol_output) + 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_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE) compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
create_ata_ix = create_ata_idempotent_ix( # Ensure WSOL ATA exists (needed to receive SOL from sell)
payer_pubkey=payer.pubkey(), create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
)
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts) sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
# Build and sign transaction
blockhash_resp = await client.get_latest_blockhash() blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash( msg = Message.new_with_blockhash(
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix], [compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
payer.pubkey(), payer.pubkey(),
recent_blockhash, blockhash_resp.value.blockhash,
) )
tx = VersionedTransaction(message=msg, keypairs=[payer])
tx_sell = VersionedTransaction(message=msg, keypairs=[payer])
try: try:
# Skip preflight to send transaction faster (useful in competitive scenarios)
tx_sig = await client.send_transaction( tx_sig = await client.send_transaction(
tx_sell, tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
) )
tx_hash = tx_sig.value tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}") 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") print("Transaction confirmed")
return tx_hash return tx_hash
except Exception as e: except Exception as e:
print(f"Error sending transaction: {e!s}") print(f"Error: {e!s}")
return None 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: 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( market_address = await get_market_address_by_base_mint(
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID 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) 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( coin_creator_vault_authority = find_coin_creator_vault(
Pubkey.from_string(market_data["coin_creator"]) Pubkey.from_string(market_data["coin_creator"])
) )
coin_creator_vault_ata = get_associated_token_address( 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( await sell_pump_swap(
client, client,
market_address, market_address,
PAYER, PAYER,
TOKEN_MINT, TOKEN_MINT,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), token_program_id,
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_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]), Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority, coin_creator_vault_authority,
+11 -4
View File
@@ -34,12 +34,19 @@ class AccountCleanupManager:
self.use_priority_fee = use_priority_fee self.use_priority_fee = use_priority_fee
self.close_with_force_burn = force_burn 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. Attempt to burn any remaining tokens and close the ATA.
Skips if account doesn't exist or is already empty/closed. 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() solana_client = await self.client.get_client()
priority_fee = ( priority_fee = (
@@ -70,7 +77,7 @@ class AccountCleanupManager:
mint=mint, mint=mint,
owner=self.wallet.pubkey, owner=self.wallet.pubkey,
amount=balance, amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM, program_id=token_program_id,
) )
) )
instructions.append(burn_ix) instructions.append(burn_ix)
@@ -89,7 +96,7 @@ class AccountCleanupManager:
account=ata, account=ata,
dest=self.wallet.pubkey, dest=self.wallet.pubkey,
owner=self.wallet.pubkey, owner=self.wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM, program_id=token_program_id,
) )
) )
instructions.append(close_ix) instructions.append(close_ix)
+7 -4
View File
@@ -20,6 +20,7 @@ async def handle_cleanup_after_failure(
client, client,
wallet, wallet,
mint, mint,
token_program_id,
priority_fee_manager, priority_fee_manager,
cleanup_mode, cleanup_mode,
cleanup_with_prior_fee, cleanup_with_prior_fee,
@@ -30,13 +31,14 @@ async def handle_cleanup_after_failure(
manager = AccountCleanupManager( manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn 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( async def handle_cleanup_after_sell(
client, client,
wallet, wallet,
mint, mint,
token_program_id,
priority_fee_manager, priority_fee_manager,
cleanup_mode, cleanup_mode,
cleanup_with_prior_fee, cleanup_with_prior_fee,
@@ -47,13 +49,14 @@ async def handle_cleanup_after_sell(
manager = AccountCleanupManager( manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn 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( async def handle_cleanup_post_session(
client, client,
wallet, wallet,
mints, mints,
token_program_ids,
priority_fee_manager, priority_fee_manager,
cleanup_mode, cleanup_mode,
cleanup_with_prior_fee, cleanup_with_prior_fee,
@@ -64,5 +67,5 @@ async def handle_cleanup_post_session(
manager = AccountCleanupManager( manager = AccountCleanupManager(
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
) )
for mint in mints: for mint, token_program_id in zip(mints, token_program_ids):
await manager.cleanup_ata(mint) 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( TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
) )
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
)
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
) )
@@ -42,6 +45,7 @@ class SystemAddresses:
# Reference the module-level constants # Reference the module-level constants
SYSTEM_PROGRAM = SYSTEM_PROGRAM SYSTEM_PROGRAM = SYSTEM_PROGRAM
TOKEN_PROGRAM = TOKEN_PROGRAM TOKEN_PROGRAM = TOKEN_PROGRAM
TOKEN_2022_PROGRAM = TOKEN_2022_PROGRAM
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
RENT = RENT RENT = RENT
SOL_MINT = SOL_MINT SOL_MINT = SOL_MINT
@@ -56,6 +60,7 @@ class SystemAddresses:
return { return {
"system_program": cls.SYSTEM_PROGRAM, "system_program": cls.SYSTEM_PROGRAM,
"token_program": cls.TOKEN_PROGRAM, "token_program": cls.TOKEN_PROGRAM,
"token_2022_program": cls.TOKEN_2022_PROGRAM,
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM, "associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
"rent": cls.RENT, "rent": cls.RENT,
"sol_mint": cls.SOL_MINT, "sol_mint": cls.SOL_MINT,
+9 -2
View File
@@ -7,6 +7,8 @@ from solders.keypair import Keypair
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from spl.token.instructions import get_associated_token_address from spl.token.instructions import get_associated_token_address
from core.pubkeys import SystemAddresses
class Wallet: class Wallet:
"""Manages a Solana wallet for trading operations.""" """Manages a Solana wallet for trading operations."""
@@ -30,16 +32,21 @@ class Wallet:
"""Get the keypair for signing transactions.""" """Get the keypair for signing transactions."""
return self._keypair 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. """Get the associated token account address for a mint.
Args: Args:
mint: Token mint address mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns: Returns:
Associated token account address 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 @staticmethod
def _load_keypair(private_key: str) -> Keypair: def _load_keypair(private_key: str) -> Keypair:
+2
View File
@@ -45,6 +45,8 @@ class TokenInfo:
user: Pubkey | None = None user: Pubkey | None = None
creator: Pubkey | None = None creator: Pubkey | None = None
creator_vault: 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 # Metadata
creation_timestamp: float | None = None creation_timestamp: float | None = None
+25 -6
View File
@@ -147,17 +147,22 @@ class LetsBonkAddressProvider(AddressProvider):
) )
return quote_vault 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. """Derive user's associated token account address.
Args: Args:
user: User's wallet address user: User's wallet address
mint: Token mint address mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns: Returns:
User's associated token account address 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]: def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
"""Get LetsBonk-specific additional accounts needed for trading. """Get LetsBonk-specific additional accounts needed for trading.
@@ -296,6 +301,13 @@ class LetsBonkAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) 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 # Use global_config from TokenInfo if available, otherwise use default
global_config = ( global_config = (
token_info.global_config token_info.global_config
@@ -316,12 +328,12 @@ class LetsBonkAddressProvider(AddressProvider):
"global_config": global_config, "global_config": global_config,
"platform_config": platform_config, "platform_config": platform_config,
"pool_state": additional_accounts["pool_state"], "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"], "base_vault": additional_accounts["base_vault"],
"quote_vault": additional_accounts["quote_vault"], "quote_vault": additional_accounts["quote_vault"],
"base_token_mint": token_info.mint, "base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_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, "quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"], "event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM, "program": LetsBonkAddresses.PROGRAM,
@@ -351,6 +363,13 @@ class LetsBonkAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) 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 # Use global_config from TokenInfo if available, otherwise use default
global_config = ( global_config = (
token_info.global_config token_info.global_config
@@ -371,12 +390,12 @@ class LetsBonkAddressProvider(AddressProvider):
"global_config": global_config, "global_config": global_config,
"platform_config": platform_config, "platform_config": platform_config,
"pool_state": additional_accounts["pool_state"], "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"], "base_vault": additional_accounts["base_vault"],
"quote_vault": additional_accounts["quote_vault"], "quote_vault": additional_accounts["quote_vault"],
"base_token_mint": token_info.mint, "base_token_mint": token_info.mint,
"quote_token_mint": SystemAddresses.SOL_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, "quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"], "event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM, "program": LetsBonkAddresses.PROGRAM,
+14 -7
View File
@@ -194,14 +194,21 @@ class LetsBonkCurveManager(CurveManager):
} }
# Calculate additional metrics # Calculate additional metrics
if pool_data["virtual_base"] > 0: # Validate reserves are positive before calculating price
pool_data["price_per_token"] = ( if pool_data["virtual_base"] <= 0:
(pool_data["virtual_quote"] / pool_data["virtual_base"]) raise ValueError(
* (10**TOKEN_DECIMALS) f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price"
/ LAMPORTS_PER_SOL
) )
else: if pool_data["virtual_quote"] <= 0:
pool_data["price_per_token"] = 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( logger.debug(
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, " 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.pubkey import Pubkey
from solders.transaction import VersionedTransaction from solders.transaction import VersionedTransaction
from core.pubkeys import SystemAddresses
from interfaces.core import EventParser, Platform, TokenInfo from interfaces.core import EventParser, Platform, TokenInfo
from platforms.letsbonk.address_provider import LetsBonkAddressProvider from platforms.letsbonk.address_provider import LetsBonkAddressProvider
from utils.idl_parser import IDLParser from utils.idl_parser import IDLParser
@@ -114,6 +115,15 @@ class LetsBonkEventParser(EventParser):
}: }:
return None 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", {}) args = decoded.get("args", {})
# Extract MintParams from the decoded arguments # Extract MintParams from the decoded arguments
@@ -174,6 +184,7 @@ class LetsBonkEventParser(EventParser):
platform_config=platform_config, platform_config=platform_config,
user=creator, user=creator,
creator=creator, creator=creator,
token_program_id=token_program_id,
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
+12 -5
View File
@@ -75,12 +75,19 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
# Get all required accounts # Get all required accounts
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) 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 # 1. Create idempotent ATA for base token
ata_instruction = create_idempotent_associated_token_account( ata_instruction = create_idempotent_associated_token_account(
user, # payer user, # payer
user, # owner user, # owner
token_info.mint, # mint token_info.mint, # mint
SystemAddresses.TOKEN_PROGRAM, # token program token_program_id, # token program (dynamic for token2022 support)
) )
instructions.append(ata_instruction) instructions.append(ata_instruction)
@@ -151,10 +158,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint ), # quote_token_mint
AccountMeta( 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 ), # base_token_program
AccountMeta( 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 ), # quote_token_program
AccountMeta( AccountMeta(
pubkey=accounts_info["event_authority"], pubkey=accounts_info["event_authority"],
@@ -306,10 +313,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
), # quote_token_mint ), # quote_token_mint
AccountMeta( 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 ), # base_token_program
AccountMeta( 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 ), # quote_token_program
AccountMeta( AccountMeta(
pubkey=accounts_info["event_authority"], pubkey=accounts_info["event_authority"],
+63 -11
View File
@@ -31,6 +31,12 @@ class PumpFunAddresses:
FEE: Final[Pubkey] = Pubkey.from_string( FEE: Final[Pubkey] = Pubkey.from_string(
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" "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( LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
) )
@@ -139,17 +145,22 @@ class PumpFunAddressProvider(AddressProvider):
) )
return bonding_curve 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. """Derive user's associated token account address.
Args: Args:
user: User's wallet address user: User's wallet address
mint: Token mint address mint: Token mint address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns: Returns:
User's associated token account address 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]: def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
"""Get pump.fun-specific additional accounts needed for trading. """Get pump.fun-specific additional accounts needed for trading.
@@ -177,7 +188,7 @@ class PumpFunAddressProvider(AddressProvider):
# Derive associated bonding curve if not provided # Derive associated bonding curve if not provided
if not token_info.associated_bonding_curve and token_info.bonding_curve: if not token_info.associated_bonding_curve and token_info.bonding_curve:
accounts["associated_bonding_curve"] = self.derive_associated_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 # Derive creator vault if not provided but creator is available
@@ -187,21 +198,25 @@ class PumpFunAddressProvider(AddressProvider):
return accounts return accounts
def derive_associated_bonding_curve( def derive_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey: ) -> Pubkey:
"""Derive the associated bonding curve (ATA of bonding curve for the token). """Derive the associated bonding curve (ATA of bonding curve for the token).
Args: Args:
mint: Token mint address mint: Token mint address
bonding_curve: Bonding curve address bonding_curve: Bonding curve address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
Returns: Returns:
Associated bonding curve address Associated bonding curve address
""" """
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[ [
bytes(bonding_curve), bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM), bytes(token_program_id),
bytes(mint), bytes(mint),
], ],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
@@ -249,6 +264,19 @@ class PumpFunAddressProvider(AddressProvider):
""" """
return PumpFunAddresses.find_fee_config() 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( def get_buy_instruction_accounts(
self, token_info: TokenInfo, user: Pubkey self, token_info: TokenInfo, user: Pubkey
) -> dict[str, Pubkey]: ) -> dict[str, Pubkey]:
@@ -263,9 +291,19 @@ class PumpFunAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) 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 { return {
"global": PumpFunAddresses.GLOBAL, "global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE, "fee": fee_recipient,
"mint": token_info.mint, "mint": token_info.mint,
"bonding_curve": additional_accounts.get( "bonding_curve": additional_accounts.get(
"bonding_curve", token_info.bonding_curve "bonding_curve", token_info.bonding_curve
@@ -273,10 +311,12 @@ class PumpFunAddressProvider(AddressProvider):
"associated_bonding_curve": additional_accounts.get( "associated_bonding_curve": additional_accounts.get(
"associated_bonding_curve", token_info.associated_bonding_curve "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, "user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM, "system_program": SystemAddresses.SYSTEM_PROGRAM,
"token_program": SystemAddresses.TOKEN_PROGRAM, "token_program": token_program_id,
"creator_vault": additional_accounts.get( "creator_vault": additional_accounts.get(
"creator_vault", token_info.creator_vault "creator_vault", token_info.creator_vault
), ),
@@ -302,9 +342,19 @@ class PumpFunAddressProvider(AddressProvider):
""" """
additional_accounts = self.get_additional_accounts(token_info) 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 { return {
"global": PumpFunAddresses.GLOBAL, "global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE, "fee": fee_recipient,
"mint": token_info.mint, "mint": token_info.mint,
"bonding_curve": additional_accounts.get( "bonding_curve": additional_accounts.get(
"bonding_curve", token_info.bonding_curve "bonding_curve", token_info.bonding_curve
@@ -312,13 +362,15 @@ class PumpFunAddressProvider(AddressProvider):
"associated_bonding_curve": additional_accounts.get( "associated_bonding_curve": additional_accounts.get(
"associated_bonding_curve", token_info.associated_bonding_curve "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, "user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM, "system_program": SystemAddresses.SYSTEM_PROGRAM,
"creator_vault": additional_accounts.get( "creator_vault": additional_accounts.get(
"creator_vault", token_info.creator_vault "creator_vault", token_info.creator_vault
), ),
"token_program": SystemAddresses.TOKEN_PROGRAM, "token_program": token_program_id,
"event_authority": PumpFunAddresses.EVENT_AUTHORITY, "event_authority": PumpFunAddresses.EVENT_AUTHORITY,
"program": PumpFunAddresses.PROGRAM, "program": PumpFunAddresses.PROGRAM,
"fee_config": self.derive_fee_config(), "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), "token_total_supply": decoded_curve_state.get("token_total_supply", 0),
"complete": decoded_curve_state.get("complete", False), "complete": decoded_curve_state.get("complete", False),
"creator": decoded_curve_state.get("creator", ""), "creator": decoded_curve_state.get("creator", ""),
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
} }
# Calculate additional metrics # Calculate additional metrics
if curve_data["virtual_token_reserves"] > 0: # Validate reserves are positive before calculating price
curve_data["price_per_token"] = ( if curve_data["virtual_token_reserves"] <= 0:
( raise ValueError(
curve_data["virtual_sol_reserves"] f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price"
/ curve_data["virtual_token_reserves"]
)
* (10**TOKEN_DECIMALS)
/ LAMPORTS_PER_SOL
) )
else: if curve_data["virtual_sol_reserves"] <= 0:
curve_data["price_per_token"] = 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 # Add convenience decimal fields
curve_data["token_reserves_decimal"] = ( curve_data["token_reserves_decimal"] = (
+114 -21
View File
@@ -47,6 +47,16 @@ class PumpFunEventParser(EventParser):
"<Q", self._create_instruction_discriminator_bytes "<Q", self._create_instruction_discriminator_bytes
)[0] )[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( logger.info(
"Pump.Fun event parser initialized with IDL-based event and instruction parsing" "Pump.Fun event parser initialized with IDL-based event and instruction parsing"
) )
@@ -56,6 +66,10 @@ class PumpFunEventParser(EventParser):
logger.info( logger.info(
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}" 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 @property
def platform(self) -> Platform: def platform(self) -> Platform:
@@ -74,8 +88,12 @@ class PumpFunEventParser(EventParser):
Returns: Returns:
TokenInfo if token creation found, None otherwise TokenInfo if token creation found, None otherwise
""" """
# Check if this is a token creation transaction # Check if this is a token creation transaction (create or create_v2 for token2022)
if not any("Program log: Instruction: Create" in log for log in logs): if not any(
"Program log: Instruction: Create" in log
or "Program log: Instruction: Create_v2" in log
for log in logs
):
return None return None
# Skip swaps as the first condition may pass them # 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 # First, collect all Program data entries and note when Create instruction happens
for i, log in enumerate(logs): 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 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: elif "Program data:" in log:
# Extract base64 encoded event data # Extract base64 encoded event data
encoded_data = log.split("Program data: ")[1].strip() encoded_data = log.split("Program data: ")[1].strip()
@@ -104,7 +123,7 @@ class PumpFunEventParser(EventParser):
) )
if not create_instruction_found: 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 return None
if not program_data_entries: if not program_data_entries:
@@ -224,9 +243,13 @@ class PumpFunEventParser(EventParser):
logger.info(f"❌ Failed to convert pubkey fields: {e}") logger.info(f"❌ Failed to convert pubkey fields: {e}")
continue 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( associated_bonding_curve = self._derive_associated_bonding_curve(
mint, bonding_curve mint, bonding_curve, token_program_id
) )
creator_vault = self._derive_creator_vault(creator) creator_vault = self._derive_creator_vault(creator)
@@ -245,6 +268,7 @@ class PumpFunEventParser(EventParser):
user=user, user=user,
creator=creator, creator=creator,
creator_vault=creator_vault, creator_vault=creator_vault,
token_program_id=token_program_id,
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
@@ -274,9 +298,16 @@ class PumpFunEventParser(EventParser):
Returns: Returns:
TokenInfo if token creation found, None otherwise TokenInfo if token creation found, None otherwise
""" """
if not instruction_data.startswith( # Determine which create instruction (standard or v2 for token2022)
self._create_instruction_discriminator_bytes 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 return None
try: try:
@@ -293,7 +324,8 @@ class PumpFunEventParser(EventParser):
decoded = self._idl_parser.decode_instruction( decoded = self._idl_parser.decode_instruction(
instruction_data, account_keys, accounts 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 return None
args = decoded.get("args", {}) args = decoded.get("args", {})
@@ -315,6 +347,13 @@ class PumpFunEventParser(EventParser):
) )
creator_vault = self._derive_creator_vault(creator) 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( return TokenInfo(
name=args.get("name", ""), name=args.get("name", ""),
symbol=args.get("symbol", ""), symbol=args.get("symbol", ""),
@@ -326,6 +365,7 @@ class PumpFunEventParser(EventParser):
user=user, user=user,
creator=creator, creator=creator,
creator_vault=creator_vault, creator_vault=creator_vault,
token_program_id=token_program_id,
creation_timestamp=monotonic(), creation_timestamp=monotonic(),
) )
@@ -435,14 +475,19 @@ class PumpFunEventParser(EventParser):
ix_data = bytes(ix.data) ix_data = bytes(ix.data)
# Check for create discriminator # Check for create or create_v2 discriminator
if len(ix_data) >= 8: if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0] discriminator = struct.unpack("<Q", ix_data[:8])[0]
if ( is_create = (
discriminator discriminator == self._create_instruction_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 # Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10: if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue continue
@@ -497,10 +542,15 @@ class PumpFunEventParser(EventParser):
if len(ix_data) >= 8: if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0] discriminator = struct.unpack("<Q", ix_data[:8])[0]
if ( is_create = (
discriminator discriminator == self._create_instruction_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: if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue continue
@@ -544,27 +594,70 @@ class PumpFunEventParser(EventParser):
return derived_address return derived_address
def _derive_associated_bonding_curve( def _derive_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
) -> Pubkey: ) -> Pubkey:
"""Derive the associated bonding curve (ATA of bonding curve for the token). """Derive the associated bonding curve (ATA of bonding curve for the token).
Args: Args:
mint: Token mint address mint: Token mint address
bonding_curve: Bonding curve address bonding_curve: Bonding curve address
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_PROGRAM
Returns: Returns:
Associated bonding curve address Associated bonding curve address
""" """
if token_program_id is None:
token_program_id = SystemAddresses.TOKEN_PROGRAM
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[ [
bytes(bonding_curve), bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM), bytes(token_program_id),
bytes(mint), bytes(mint),
], ],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
) )
return derived_address 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 @property
def verbose(self) -> bool: def verbose(self) -> bool:
"""Check if verbose logging is enabled.""" """Check if verbose logging is enabled."""
+13 -6
View File
@@ -64,15 +64,16 @@ class PumpFunInstructionBuilder(InstructionBuilder):
""" """
instructions = [] 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) accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
# 1. Create idempotent ATA instruction (won't fail if ATA already exists) # 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( ata_instruction = create_idempotent_associated_token_account(
user, # payer user, # payer
user, # owner user, # owner
token_info.mint, # mint token_info.mint, # mint
SystemAddresses.TOKEN_PROGRAM, # token program accounts_info["token_program"], # token program from AddressProvider
) )
instructions.append(ata_instruction) instructions.append(ata_instruction)
@@ -123,7 +124,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
AccountMeta( AccountMeta(
pubkey=accounts_info["global_volume_accumulator"], pubkey=accounts_info["global_volume_accumulator"],
is_signer=False, is_signer=False,
is_writable=True, is_writable=False,
), ),
AccountMeta( AccountMeta(
pubkey=accounts_info["user_volume_accumulator"], 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 = ( instruction_data = (
self._buy_discriminator self._buy_discriminator
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units + struct.pack("<Q", minimum_amount_out) # token amount in raw units
+ struct.pack("<Q", amount_in) # max SOL cost in lamports + struct.pack("<Q", amount_in) # max SOL cost in lamports
+ track_volume_bytes # enable volume tracking
) )
buy_instruction = Instruction( buy_instruction = Instruction(
@@ -182,7 +186,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
""" """
instructions = [] 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) accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
# Build sell instruction accounts # 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 = ( instruction_data = (
self._sell_discriminator self._sell_discriminator
+ struct.pack("<Q", amount_in) # token amount in raw units + struct.pack("<Q", amount_in) # token amount in raw units
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports + struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
+ track_volume_bytes # enable volume tracking
) )
sell_instruction = Instruction( sell_instruction = Instruction(
@@ -5,6 +5,7 @@ File: src/platforms/pumpfun/pumpportal_processor.py
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from core.pubkeys import SystemAddresses
from interfaces.core import Platform, TokenInfo from interfaces.core import Platform, TokenInfo
from platforms.pumpfun.address_provider import PumpFunAddressProvider from platforms.pumpfun.address_provider import PumpFunAddressProvider
from utils.logger import get_logger from utils.logger import get_logger
@@ -81,9 +82,15 @@ class PumpFunPumpPortalProcessor:
creator = user creator = user
# Derive additional addresses using platform provider # 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 = ( associated_bonding_curve = (
self.address_provider.derive_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) creator_vault = self.address_provider.derive_creator_vault(creator)
@@ -99,6 +106,7 @@ class PumpFunPumpPortalProcessor:
user=user, user=user,
creator=creator, creator=creator,
creator_vault=creator_vault, creator_vault=creator_vault,
token_program_id=token_program_id,
) )
except Exception: except Exception:
+27 -5
View File
@@ -66,10 +66,20 @@ class PlatformAwareBuyer(Trader):
pool_address = self._get_pool_address(token_info, address_provider) pool_address = self._get_pool_address(token_info, address_provider)
# Regular behavior with RPC call # Regular behavior with RPC call
token_price_sol = await curve_manager.calculate_price(pool_address) # Fetch pool state to get price and mayhem mode status
token_amount = ( pool_state = await curve_manager.get_pool_state(pool_address)
self.amount / token_price_sol if token_price_sol > 0 else 0 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 # Calculate minimum token amount with slippage
minimum_token_amount = token_amount * (1 - self.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 # Get pool address and current price using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider) 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") logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
+16 -1
View File
@@ -193,6 +193,7 @@ class UniversalTrader:
# State tracking # State tracking
self.traded_mints: set[Pubkey] = set() 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.token_queue: asyncio.Queue = asyncio.Queue()
self.processing: bool = False self.processing: bool = False
self.processed_tokens: set[str] = set() self.processed_tokens: set[str] = set()
@@ -325,10 +326,17 @@ class UniversalTrader:
if self.traded_mints: if self.traded_mints:
try: try:
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...") 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( await handle_cleanup_post_session(
self.solana_client, self.solana_client,
self.wallet, self.wallet,
list(self.traded_mints), mints_list,
token_program_ids,
self.priority_fee_manager, self.priority_fee_manager,
self.cleanup_mode, self.cleanup_mode,
self.cleanup_with_priority_fee, self.cleanup_with_priority_fee,
@@ -447,6 +455,10 @@ class UniversalTrader:
buy_result.tx_signature, buy_result.tx_signature,
) )
self.traded_mints.add(token_info.mint) 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 # Choose exit strategy
if not self.marry_mode: if not self.marry_mode:
@@ -469,6 +481,7 @@ class UniversalTrader:
self.solana_client, self.solana_client,
self.wallet, self.wallet,
token_info.mint, token_info.mint,
token_info.token_program_id,
self.priority_fee_manager, self.priority_fee_manager,
self.cleanup_mode, self.cleanup_mode,
self.cleanup_with_priority_fee, self.cleanup_with_priority_fee,
@@ -521,6 +534,7 @@ class UniversalTrader:
self.solana_client, self.solana_client,
self.wallet, self.wallet,
token_info.mint, token_info.mint,
token_info.token_program_id,
self.priority_fee_manager, self.priority_fee_manager,
self.cleanup_mode, self.cleanup_mode,
self.cleanup_with_priority_fee, self.cleanup_with_priority_fee,
@@ -590,6 +604,7 @@ class UniversalTrader:
self.solana_client, self.solana_client,
self.wallet, self.wallet,
token_info.mint, token_info.mint,
token_info.token_program_id,
self.priority_fee_manager, self.priority_fee_manager,
self.cleanup_mode, self.cleanup_mode,
self.cleanup_with_priority_fee, self.cleanup_with_priority_fee,