mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
Add mayhem mode support and Token2022 integration (#149)
* feat: mayhem update in idl * feat(examples): update bonding curve scripts * feat(example): update listen_blocksubscribe * feat(examples): update geyser listener * feat(examples): update all new token listeners * feat(examples): add comments, fix printing, formatting * feat(examples): pumpswap buy and sell update with mayhem mode * fix(examples): sell pump amm fee recipient * feat(examples): update decode scripts * feat(examples): update fetch price * feat(examples): buy and sell bonding curve scripts * feat(examples): add mint with mayhem mode enabled * feat(examples): improve listening to wallet txs * feat(examples): migration listener improvements * feat(examples): global vol accumulator is not writable * feat(examples): support token/token2022 programs in buy instructions * feat(examples): token/token2022 for pumpswap buy * feat(examples): token/token2022 supprot for sell instructions * feat(bot): support create_v2 with token2022, mayhem mode, other fixes * fix(bot): support only token2022 in logs and pumportal listeners * feat(bot): token2022 support in cleanup flow * fix(bot): update token program handling and improve price validation in trading logic * feat(bot): enhance token program handling for LetsBonk integration
This commit is contained in:
+825
-150
File diff suppressed because it is too large
Load Diff
+241
-2
@@ -543,7 +543,6 @@
|
||||
},
|
||||
{
|
||||
"name": "global_volume_accumulator",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
@@ -972,7 +971,6 @@
|
||||
},
|
||||
{
|
||||
"name": "global_volume_accumulator",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
@@ -2085,6 +2083,10 @@
|
||||
{
|
||||
"name": "coin_creator",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "is_mayhem_mode",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3006,6 +3008,92 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "set_reserved_fee_recipients",
|
||||
"discriminator": [
|
||||
111,
|
||||
172,
|
||||
162,
|
||||
232,
|
||||
114,
|
||||
89,
|
||||
213,
|
||||
142
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "global_config",
|
||||
"writable": true,
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
103,
|
||||
108,
|
||||
111,
|
||||
98,
|
||||
97,
|
||||
108,
|
||||
95,
|
||||
99,
|
||||
111,
|
||||
110,
|
||||
102,
|
||||
105,
|
||||
103
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"signer": true,
|
||||
"relations": [
|
||||
"global_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "event_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
95,
|
||||
95,
|
||||
101,
|
||||
118,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "program"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "whitelist_pda",
|
||||
"type": "pubkey"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sync_user_volume_accumulator",
|
||||
"discriminator": [
|
||||
@@ -3134,6 +3222,70 @@
|
||||
],
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "toggle_mayhem_mode",
|
||||
"discriminator": [
|
||||
1,
|
||||
9,
|
||||
111,
|
||||
208,
|
||||
100,
|
||||
31,
|
||||
255,
|
||||
163
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "admin",
|
||||
"signer": true,
|
||||
"relations": [
|
||||
"global_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "global_config",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"name": "event_authority",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [
|
||||
95,
|
||||
95,
|
||||
101,
|
||||
118,
|
||||
101,
|
||||
110,
|
||||
116,
|
||||
95,
|
||||
97,
|
||||
117,
|
||||
116,
|
||||
104,
|
||||
111,
|
||||
114,
|
||||
105,
|
||||
116,
|
||||
121
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "program"
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update_admin",
|
||||
"discriminator": [
|
||||
@@ -3644,6 +3796,19 @@
|
||||
216
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ReservedFeeRecipientsEvent",
|
||||
"discriminator": [
|
||||
43,
|
||||
188,
|
||||
250,
|
||||
18,
|
||||
221,
|
||||
75,
|
||||
187,
|
||||
95
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SellEvent",
|
||||
"discriminator": [
|
||||
@@ -3902,6 +4067,22 @@
|
||||
"code": 6040,
|
||||
"name": "BuySlippageBelowMinBaseAmountOut",
|
||||
"msg": "buy: slippage - would buy less tokens than expected min_base_amount_out"
|
||||
},
|
||||
{
|
||||
"code": 6041,
|
||||
"name": "MayhemModeDisabled"
|
||||
},
|
||||
{
|
||||
"code": 6042,
|
||||
"name": "OnlyPumpPoolsMayhemMode"
|
||||
},
|
||||
{
|
||||
"code": 6043,
|
||||
"name": "MayhemModeInDesiredState"
|
||||
},
|
||||
{
|
||||
"code": 6044,
|
||||
"name": "NotEnoughRemainingAccounts"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
@@ -4005,6 +4186,10 @@
|
||||
{
|
||||
"name": "creator",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "is_mayhem_mode",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4357,6 +4542,10 @@
|
||||
{
|
||||
"name": "coin_creator",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "is_mayhem_mode",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4625,6 +4814,27 @@
|
||||
"The admin authority for setting coin creators"
|
||||
],
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "whitelist_pda",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "reserved_fee_recipient",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "mayhem_mode_enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "reserved_fee_recipients",
|
||||
"type": {
|
||||
"array": [
|
||||
"pubkey",
|
||||
7
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4747,6 +4957,35 @@
|
||||
{
|
||||
"name": "coin_creator",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "is_mayhem_mode",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReservedFeeRecipientsEvent",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "i64"
|
||||
},
|
||||
{
|
||||
"name": "reserved_fee_recipient",
|
||||
"type": "pubkey"
|
||||
},
|
||||
{
|
||||
"name": "reserved_fee_recipients",
|
||||
"type": {
|
||||
"array": [
|
||||
"pubkey",
|
||||
7
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""
|
||||
Module for checking the status of a token's bonding curve on the Solana network using
|
||||
the Pump.fun program. It allows querying the bonding curve state and completion status.
|
||||
|
||||
Note: creator fee upgrade introduced updates in bonding curve structure.
|
||||
https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -42,20 +39,11 @@ class BondingCurveState:
|
||||
real_sol_reserves: Real SOL reserves in the curve
|
||||
token_total_supply: Total token supply in the curve
|
||||
complete: Whether the curve has completed and liquidity migrated
|
||||
is_mayhem_mode: Whether the curve is in mayhem mode
|
||||
"""
|
||||
|
||||
_STRUCT_1 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
)
|
||||
|
||||
# Struct after creator fee update has been introduced
|
||||
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
||||
_STRUCT_2 = Struct(
|
||||
# V2: Struct with creator field (81 bytes total: 8 discriminator + 73 data)
|
||||
_STRUCT_V2 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
@@ -65,26 +53,43 @@ class BondingCurveState:
|
||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||
)
|
||||
|
||||
# V3: Struct with creator + mayhem mode (82 bytes total: 8 discriminator + 74 data)
|
||||
_STRUCT_V3 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32),
|
||||
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
if len(data) < 150:
|
||||
parsed = self._STRUCT_1.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
total_length = len(data)
|
||||
|
||||
else:
|
||||
parsed = self._STRUCT_2.parse(data[8:])
|
||||
if total_length == 81: # V2: Creator only
|
||||
parsed = self._STRUCT_V2.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
self.is_mayhem_mode = False
|
||||
|
||||
elif total_length >= 82: # V3: Creator + mayhem mode
|
||||
parsed = self._STRUCT_V3.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unexpected bonding curve size: {total_length} bytes")
|
||||
|
||||
|
||||
def get_associated_bonding_curve_address(
|
||||
mint: Pubkey, program_id: Pubkey
|
||||
) -> tuple[Pubkey, int]:
|
||||
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
|
||||
"""
|
||||
Derives the associated bonding curve address for a given mint.
|
||||
|
||||
@@ -134,17 +139,14 @@ async def check_token_status(mint_address: str) -> None:
|
||||
"""
|
||||
try:
|
||||
mint = Pubkey.from_string(mint_address)
|
||||
|
||||
# Get the associated bonding curve address
|
||||
bonding_curve_address, bump = get_associated_bonding_curve_address(
|
||||
mint, PUMP_PROGRAM_ID
|
||||
)
|
||||
bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM_ID)
|
||||
|
||||
print("\nToken status:")
|
||||
print("-" * 50)
|
||||
print(f"Token mint: {mint}")
|
||||
print(f"Associated bonding curve: {bonding_curve_address}")
|
||||
print(f"Bump seed: {bump}")
|
||||
print(f"Bonding curve: {bonding_curve_address}")
|
||||
if bump is not None:
|
||||
print(f"Bump seed: {bump}")
|
||||
print("-" * 50)
|
||||
|
||||
# Check completion status
|
||||
@@ -156,9 +158,25 @@ async def check_token_status(mint_address: str) -> None:
|
||||
|
||||
print("\nBonding curve status:")
|
||||
print("-" * 50)
|
||||
print(f"Creator: {curve_state.creator}")
|
||||
print(
|
||||
f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}"
|
||||
f"Mayhem Mode: {'✅ Enabled' if curve_state.is_mayhem_mode else '❌ Disabled'}"
|
||||
)
|
||||
print(
|
||||
f"Completed: {'✅ Migrated' if curve_state.complete else '❌ Bonding curve'}"
|
||||
)
|
||||
|
||||
print("\nBonding curve reserves:")
|
||||
print(f"Virtual Token: {curve_state.virtual_token_reserves:,}")
|
||||
print(
|
||||
f"Virtual SOL: {curve_state.virtual_sol_reserves:,} lamports"
|
||||
)
|
||||
print(f"Real Token: {curve_state.real_token_reserves:,}")
|
||||
print(
|
||||
f"Real SOL: {curve_state.real_sol_reserves:,} lamports"
|
||||
)
|
||||
print(f"Total Supply: {curve_state.token_total_supply:,}")
|
||||
|
||||
if curve_state.complete:
|
||||
print(
|
||||
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
|
||||
|
||||
@@ -17,7 +17,7 @@ load_dotenv()
|
||||
# Constants
|
||||
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
|
||||
TOKEN_MINT: Final[str] = (
|
||||
"YOUR_TOKEN_MINT_ADDRESS_HERE" # Replace with actual token mint address
|
||||
"5ZHx2GGGj87xpidVJpBqadMUutqBirhL2TqUR9T9taKc" # Replace with actual token mint address
|
||||
)
|
||||
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
|
||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
||||
@@ -30,7 +30,7 @@ EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(
|
||||
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check
|
||||
|
||||
|
||||
def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
|
||||
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Derive the bonding curve PDA address from a mint address.
|
||||
|
||||
@@ -81,8 +81,9 @@ def parse_curve_state(data: bytes) -> dict:
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid discriminator for bonding curve")
|
||||
|
||||
# Parse common fields (present in all versions)
|
||||
fields = struct.unpack_from("<QQQQQ?", data, 8)
|
||||
return {
|
||||
result = {
|
||||
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
|
||||
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
|
||||
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
|
||||
@@ -91,6 +92,20 @@ def parse_curve_state(data: bytes) -> dict:
|
||||
"complete": fields[5],
|
||||
}
|
||||
|
||||
# Parse creator field if present
|
||||
data_length = len(data) - 8
|
||||
if data_length >= 73: # Has creator field
|
||||
creator_bytes = data[49:81] # 8 (discriminator) + 41 (base fields) = 49
|
||||
result["creator"] = Pubkey.from_bytes(creator_bytes)
|
||||
|
||||
# Parse is_mayhem_mode if present
|
||||
if data_length >= 74: # Has mayhem mode field
|
||||
result["is_mayhem_mode"] = bool(data[81])
|
||||
else:
|
||||
result["is_mayhem_mode"] = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def print_curve_status(state: dict) -> None:
|
||||
"""
|
||||
@@ -130,9 +145,7 @@ async def track_curve() -> None:
|
||||
return
|
||||
|
||||
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
|
||||
curve_pubkey: Pubkey = get_associated_bonding_curve_address(
|
||||
mint_pubkey, PUMP_PROGRAM_ID
|
||||
)
|
||||
curve_pubkey: Pubkey = get_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID)
|
||||
|
||||
print("Tracking bonding curve for:", mint_pubkey)
|
||||
print("Curve address:", curve_pubkey, "\n")
|
||||
|
||||
@@ -19,6 +19,10 @@ PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY")
|
||||
# Update this address to MINT address of a token you want to close
|
||||
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump")
|
||||
|
||||
# Token program for the mint - use TOKEN_PROGRAM for legacy SPL tokens, TOKEN_2022_PROGRAM for Token-2022
|
||||
# This must match the actual token's program to derive the correct ATA address
|
||||
TOKEN_PROGRAM = SystemAddresses.TOKEN_PROGRAM
|
||||
|
||||
|
||||
async def close_account_if_exists(
|
||||
client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey
|
||||
@@ -41,7 +45,7 @@ async def close_account_if_exists(
|
||||
mint=mint,
|
||||
owner=wallet.pubkey,
|
||||
amount=balance,
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
program_id=TOKEN_PROGRAM,
|
||||
)
|
||||
)
|
||||
await client.build_and_send_transaction([burn_ix], wallet.keypair)
|
||||
@@ -54,7 +58,7 @@ async def close_account_if_exists(
|
||||
account=account,
|
||||
dest=wallet.pubkey,
|
||||
owner=wallet.pubkey,
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
program_id=TOKEN_PROGRAM,
|
||||
)
|
||||
ix = close_account(close_params)
|
||||
|
||||
@@ -78,7 +82,7 @@ async def main():
|
||||
wallet = Wallet(PRIVATE_KEY)
|
||||
|
||||
# Get user's ATA for the token
|
||||
ata = wallet.get_associated_token_address(MINT_ADDRESS)
|
||||
ata = wallet.get_associated_token_address(MINT_ADDRESS, TOKEN_PROGRAM)
|
||||
await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -121,13 +121,27 @@ def parse_trade_event(logs):
|
||||
|
||||
|
||||
def decode_trade_event(data):
|
||||
"""Decode TradeEvent structure from raw bytes."""
|
||||
if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check
|
||||
"""Decode TradeEvent structure from raw bytes with progressive parsing.
|
||||
|
||||
Supports both pre-mayhem and post-mayhem IDL versions by parsing fields
|
||||
progressively based on available bytes. This ensures backward compatibility
|
||||
with older transaction logs.
|
||||
|
||||
Core fields (always present): mint, sol_amount, token_amount, is_buy, user,
|
||||
timestamp, virtual_sol_reserves, virtual_token_reserves
|
||||
|
||||
Extended fields (added later): real_sol_reserves, real_token_reserves,
|
||||
fee_recipient, fee_basis_points, fee, creator, creator_fee_basis_points,
|
||||
creator_fee, track_volume, total_unclaimed_tokens, total_claimed_tokens,
|
||||
current_sol_volume, last_update_timestamp, ix_name
|
||||
"""
|
||||
# Minimum size for core fields: 32+8+8+1+32+8+8+8 = 105 bytes
|
||||
if len(data) < 105:
|
||||
return None
|
||||
|
||||
offset = 0
|
||||
|
||||
# Parse fields according to TradeEvent structure
|
||||
# Parse core fields (always present in all versions)
|
||||
mint = data[offset : offset + 32]
|
||||
offset += 32
|
||||
|
||||
@@ -152,6 +166,73 @@ def decode_trade_event(data):
|
||||
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
|
||||
# Parse extended fields if bytes remaining (added in later versions)
|
||||
# Real reserves (8+8 = 16 bytes)
|
||||
if len(data) >= offset + 16:
|
||||
real_sol_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
real_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
else:
|
||||
real_sol_reserves = 0
|
||||
real_token_reserves = 0
|
||||
|
||||
# Fee recipient and fee details (32+8+8 = 48 bytes)
|
||||
if len(data) >= offset + 48:
|
||||
fee_recipient = data[offset : offset + 32]
|
||||
offset += 32
|
||||
fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
fee = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
else:
|
||||
fee_recipient = b'\x00' * 32
|
||||
fee_basis_points = 0
|
||||
fee = 0
|
||||
|
||||
# Creator and creator fee details (32+8+8 = 48 bytes)
|
||||
if len(data) >= offset + 48:
|
||||
creator = data[offset : offset + 32]
|
||||
offset += 32
|
||||
creator_fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
creator_fee = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
else:
|
||||
creator = b'\x00' * 32
|
||||
creator_fee_basis_points = 0
|
||||
creator_fee = 0
|
||||
|
||||
# Volume tracking fields (1+8+8+8+8 = 33 bytes)
|
||||
if len(data) >= offset + 33:
|
||||
track_volume = bool(data[offset])
|
||||
offset += 1
|
||||
total_unclaimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
total_claimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
current_sol_volume = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
last_update_timestamp = struct.unpack("<q", data[offset : offset + 8])[0]
|
||||
offset += 8
|
||||
else:
|
||||
track_volume = False
|
||||
total_unclaimed_tokens = 0
|
||||
total_claimed_tokens = 0
|
||||
current_sol_volume = 0
|
||||
last_update_timestamp = 0
|
||||
|
||||
# Parse string field (ix_name): 4 bytes for length + string data
|
||||
if len(data) >= offset + 4:
|
||||
string_length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if len(data) >= offset + string_length:
|
||||
ix_name = data[offset : offset + string_length].decode("utf-8")
|
||||
else:
|
||||
ix_name = ""
|
||||
else:
|
||||
ix_name = ""
|
||||
|
||||
return {
|
||||
"mint": base58.b58encode(mint).decode(),
|
||||
"sol_amount": sol_amount,
|
||||
@@ -161,6 +242,20 @@ def decode_trade_event(data):
|
||||
"timestamp": timestamp,
|
||||
"virtual_sol_reserves": virtual_sol_reserves,
|
||||
"virtual_token_reserves": virtual_token_reserves,
|
||||
"real_sol_reserves": real_sol_reserves,
|
||||
"real_token_reserves": real_token_reserves,
|
||||
"fee_recipient": base58.b58encode(fee_recipient).decode() if fee_recipient != b'\x00' * 32 else None,
|
||||
"fee_basis_points": fee_basis_points,
|
||||
"fee": fee,
|
||||
"creator": base58.b58encode(creator).decode() if creator != b'\x00' * 32 else None,
|
||||
"creator_fee_basis_points": creator_fee_basis_points,
|
||||
"creator_fee": creator_fee,
|
||||
"track_volume": track_volume,
|
||||
"total_unclaimed_tokens": total_unclaimed_tokens,
|
||||
"total_claimed_tokens": total_claimed_tokens,
|
||||
"current_sol_volume": current_sol_volume,
|
||||
"last_update_timestamp": last_update_timestamp,
|
||||
"ix_name": ix_name,
|
||||
"price_per_token": (sol_amount * 1_000_000) / token_amount
|
||||
if token_amount > 0
|
||||
else 0,
|
||||
@@ -178,7 +273,10 @@ def display_transaction_info(signature, logs):
|
||||
# Parse trade event data
|
||||
trade_data = parse_trade_event(logs)
|
||||
if trade_data:
|
||||
print(f" Type: {'BUY' if trade_data['is_buy'] else 'SELL'}")
|
||||
# Core transaction info (always present)
|
||||
ix_name = trade_data.get('ix_name', '')
|
||||
trade_type = 'BUY' if trade_data['is_buy'] else 'SELL'
|
||||
print(f" Type: {trade_type}{f' ({ix_name})' if ix_name else ''}")
|
||||
print(f" Token: {trade_data['mint']}")
|
||||
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
|
||||
print(f" Token Amount: {trade_data['token_amount']:,}")
|
||||
@@ -187,6 +285,25 @@ def display_transaction_info(signature, logs):
|
||||
)
|
||||
print(f" Trader: {trade_data['user']}")
|
||||
|
||||
# Fee info (may not be present in older transactions)
|
||||
if trade_data['fee'] > 0 or trade_data['fee_basis_points'] > 0:
|
||||
print(f" Fee: {trade_data['fee'] / 1_000_000_000:.6f} SOL ({trade_data['fee_basis_points']} bps)")
|
||||
|
||||
if trade_data['creator_fee'] > 0 or trade_data['creator_fee_basis_points'] > 0:
|
||||
print(f" Creator Fee: {trade_data['creator_fee'] / 1_000_000_000:.6f} SOL ({trade_data['creator_fee_basis_points']} bps)")
|
||||
|
||||
if trade_data['creator']:
|
||||
print(f" Creator: {trade_data['creator']}")
|
||||
|
||||
if trade_data['fee_recipient']:
|
||||
print(f" Fee Recipient: {trade_data['fee_recipient']}")
|
||||
|
||||
# Reserve info
|
||||
print(f" Virtual Reserves: {trade_data['virtual_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['virtual_token_reserves']:,} tokens")
|
||||
|
||||
if trade_data['real_sol_reserves'] > 0 or trade_data['real_token_reserves'] > 0:
|
||||
print(f" Real Reserves: {trade_data['real_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['real_token_reserves']:,} tokens")
|
||||
|
||||
# Extract and display program info
|
||||
display_program_info(logs)
|
||||
|
||||
|
||||
@@ -11,18 +11,7 @@ EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT_1 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
)
|
||||
|
||||
# Struct after creator fee update has been introduced
|
||||
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
||||
_STRUCT_2 = Struct(
|
||||
_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
@@ -30,23 +19,43 @@ class BondingCurveState:
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
"""Parse bonding curve data - supports all versions."""
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
if len(data) < 150:
|
||||
parsed = self._STRUCT_1.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
# Required fields (always present)
|
||||
offset = 8
|
||||
self.virtual_token_reserves = int.from_bytes(
|
||||
data[offset : offset + 8], "little"
|
||||
)
|
||||
offset += 8
|
||||
self.virtual_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||
offset += 8
|
||||
self.real_token_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||
offset += 8
|
||||
self.real_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||
offset += 8
|
||||
self.token_total_supply = int.from_bytes(data[offset : offset + 8], "little")
|
||||
offset += 8
|
||||
self.complete = bool(data[offset])
|
||||
offset += 1
|
||||
|
||||
# Optional fields (may not be present in older versions)
|
||||
if len(data) >= offset + 32:
|
||||
self.creator = Pubkey.from_bytes(data[offset : offset + 32])
|
||||
offset += 32
|
||||
|
||||
if len(data) > offset:
|
||||
self.is_mayhem_mode = bool(data[offset])
|
||||
else:
|
||||
self.is_mayhem_mode = None
|
||||
|
||||
else:
|
||||
parsed = self._STRUCT_2.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
self.creator = None
|
||||
|
||||
|
||||
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
|
||||
|
||||
@@ -27,6 +27,7 @@ print(json.dumps(tx_data, indent=2))
|
||||
|
||||
|
||||
def decode_create_instruction(data):
|
||||
"""Decode legacy Create instruction (Metaplex tokens)."""
|
||||
# The Create instruction has 3 string arguments: name, symbol, uri
|
||||
offset = 8 # Skip the 8-byte discriminator
|
||||
results = []
|
||||
@@ -36,7 +37,42 @@ def decode_create_instruction(data):
|
||||
string_data = data[offset : offset + length].decode("utf-8")
|
||||
results.append(string_data)
|
||||
offset += length
|
||||
return {"name": results[0], "symbol": results[1], "uri": results[2]}
|
||||
return {
|
||||
"name": results[0],
|
||||
"symbol": results[1],
|
||||
"uri": results[2],
|
||||
"token_standard": "legacy",
|
||||
"is_mayhem_mode": False,
|
||||
}
|
||||
|
||||
|
||||
def decode_create_v2_instruction(data):
|
||||
"""Decode CreateV2 instruction (Token2022 tokens)."""
|
||||
# The CreateV2 instruction has 3 string arguments: name, symbol, uri + is_mayhem_mode
|
||||
offset = 8 # Skip the 8-byte discriminator
|
||||
results = []
|
||||
for _ in range(3):
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
string_data = data[offset : offset + length].decode("utf-8")
|
||||
results.append(string_data)
|
||||
offset += length
|
||||
|
||||
# Skip creator pubkey (32 bytes)
|
||||
offset += 32
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
is_mayhem_mode = False
|
||||
if offset < len(data):
|
||||
is_mayhem_mode = bool(data[offset])
|
||||
|
||||
return {
|
||||
"name": results[0],
|
||||
"symbol": results[1],
|
||||
"uri": results[2],
|
||||
"token_standard": "token2022",
|
||||
"is_mayhem_mode": is_mayhem_mode,
|
||||
}
|
||||
|
||||
|
||||
def decode_buy_instruction(data):
|
||||
@@ -48,6 +84,8 @@ def decode_buy_instruction(data):
|
||||
def decode_instruction_data(instruction, accounts, data):
|
||||
if instruction["name"] == "create":
|
||||
return decode_create_instruction(data)
|
||||
elif instruction["name"] == "createV2":
|
||||
return decode_create_v2_instruction(data)
|
||||
elif instruction["name"] == "buy":
|
||||
return decode_buy_instruction(data)
|
||||
else:
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import asyncio
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from construct import Bytes, Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
|
||||
TOKEN_DECIMALS: Final[int] = 6
|
||||
CURVE_ADDRESS: Final[str] = (
|
||||
"YOUR_BONDING_CURVE_ADDRESS_HERE" # Replace with actual bonding curve address
|
||||
)
|
||||
CURVE_ADDRESS: Final[str] = "..." # Replace with actual bonding curve address
|
||||
|
||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
||||
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
|
||||
@@ -23,7 +18,9 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT = Struct(
|
||||
"""Parse bonding curve account data - supports all versions."""
|
||||
|
||||
_STRUCT_V1 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
@@ -32,9 +29,47 @@ class BondingCurveState:
|
||||
"complete" / Flag,
|
||||
)
|
||||
|
||||
_STRUCT_V2 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32),
|
||||
)
|
||||
|
||||
_STRUCT_V3 = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32),
|
||||
"is_mayhem_mode" / Flag,
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
"""Parse bonding curve data - auto-detects version."""
|
||||
data_length = len(data) - 8
|
||||
|
||||
if data_length < 73: # V1: without creator and mayhem mode
|
||||
parsed = self._STRUCT_V1.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
self.creator = None
|
||||
self.is_mayhem_mode = False
|
||||
elif data_length == 73: # V2: with creator, without mayhem mode
|
||||
parsed = self._STRUCT_V2.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
if isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
self.is_mayhem_mode = False
|
||||
else: # V3: with creator and mayhem mode
|
||||
parsed = self._STRUCT_V3.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
if isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
|
||||
|
||||
async def get_bonding_curve_state(
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
"""
|
||||
This script compares two methods of detecting migrations:
|
||||
1. Migration program listener (listens Migration program) - detects markets via successful migration transactions
|
||||
2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription
|
||||
|
||||
The script tracks which method detects new markets first and provides detailed performance statistics.
|
||||
1. Migration Program Listener - Listens to migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
|
||||
which emits detailed migration events via logsSubscribe
|
||||
|
||||
Note: multiple endpoints available. Scroll down to change providers which you want to test.
|
||||
2. Direct Pool Account Listener - Listens to pump_amm program (pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA)
|
||||
for new Pool account creations via programSubscribe
|
||||
|
||||
Note: The migration wrapper program emits a different event structure than
|
||||
CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||
|
||||
The script tracks which method detects new migrations first and provides detailed performance
|
||||
statistics including message counts, detection timing, provider latency comparison.
|
||||
|
||||
Configure multiple RPC endpoints in .env file to test provider performance.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -33,7 +41,7 @@ QUOTE_MINT_SOL = base58.b58encode(
|
||||
).decode()
|
||||
|
||||
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
|
||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # Pool account with is_mayhem_mode = 244 bytes
|
||||
|
||||
|
||||
class DetectionTracker:
|
||||
@@ -306,9 +314,9 @@ async def fetch_existing_market_pubkeys():
|
||||
|
||||
def parse_market_account_data(data):
|
||||
"""
|
||||
Parse binary market account data into a structured format
|
||||
Parse binary Pool account data according to pump_swap_idl.json structure
|
||||
|
||||
This function matches the parser from the market listener script
|
||||
Total 11 fields including is_mayhem_mode field added with mayhem update
|
||||
"""
|
||||
parsed_data = {}
|
||||
offset = 8 # Skip discriminator
|
||||
@@ -323,6 +331,8 @@ def parse_market_account_data(data):
|
||||
("pool_base_token_account", "pubkey"),
|
||||
("pool_quote_token_account", "pubkey"),
|
||||
("lp_supply", "u64"),
|
||||
("coin_creator", "pubkey"),
|
||||
("is_mayhem_mode", "bool"),
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -347,6 +357,10 @@ def parse_market_account_data(data):
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
offset += 1
|
||||
elif field_type == "bool":
|
||||
value = bool(data[offset])
|
||||
parsed_data[field_name] = value
|
||||
offset += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse market data: {e}")
|
||||
|
||||
@@ -355,9 +369,11 @@ def parse_market_account_data(data):
|
||||
|
||||
def parse_migrate_instruction(data):
|
||||
"""
|
||||
Parse binary migration instruction data into a structured format
|
||||
Parse migration event from the migration wrapper program
|
||||
|
||||
This function matches the parser from the migration listener script
|
||||
Note: This parses the event emitted by the migration wrapper program
|
||||
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
|
||||
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||
"""
|
||||
if len(data) < 8:
|
||||
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""
|
||||
Listens for 'Migrate' instructions from a Solana migration program via WebSocket.
|
||||
Listens for 'Migrate' instructions from Solana migration program via WebSocket.
|
||||
Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations.
|
||||
|
||||
Note: skips transactions with truncated logs (no Program data in the logs -> no parsed data).
|
||||
Note: This uses a migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
|
||||
that emits a different event structure than the CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||
|
||||
Skips transactions with truncated logs (no Program data in the logs -> no parsed data).
|
||||
To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs.
|
||||
"""
|
||||
|
||||
@@ -26,6 +29,12 @@ MIGRATION_PROGRAM_ID = Pubkey.from_string(
|
||||
|
||||
|
||||
def parse_migrate_instruction(data):
|
||||
"""Parse migration event from the migration wrapper program.
|
||||
|
||||
Note: This parses the event emitted by the migration wrapper program
|
||||
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
|
||||
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||
"""
|
||||
if len(data) < 8:
|
||||
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
||||
return None
|
||||
|
||||
@@ -24,7 +24,7 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
|
||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
|
||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # discriminator + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode = 244 bytes
|
||||
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
||||
QUOTE_MINT_SOL = base58.b58encode(
|
||||
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
|
||||
@@ -58,6 +58,10 @@ async def fetch_existing_market_pubkeys():
|
||||
|
||||
|
||||
def parse_market_account_data(data):
|
||||
"""Parse Pool account data according to pump_swap_idl.json structure.
|
||||
|
||||
Total 11 fields including the new is_mayhem_mode field added with mayhem update.
|
||||
"""
|
||||
parsed_data = {}
|
||||
offset = 8 # Discriminator
|
||||
|
||||
@@ -72,6 +76,7 @@ def parse_market_account_data(data):
|
||||
("pool_quote_token_account", "pubkey"),
|
||||
("lp_supply", "u64"),
|
||||
("coin_creator", "pubkey"),
|
||||
("is_mayhem_mode", "bool"),
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -96,6 +101,10 @@ def parse_market_account_data(data):
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
offset += 1
|
||||
elif field_type == "bool":
|
||||
value = bool(data[offset])
|
||||
parsed_data[field_name] = value
|
||||
offset += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse market data: {e}")
|
||||
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
"""
|
||||
This script compares four methods of detecting new Pump.fun tokens:
|
||||
1. Block subscription listener - listens for blocks containing Pump.fun program
|
||||
2. Geyser gRPC listener - uses Geyser gRPC API to get transactions containing Pump.fun program
|
||||
3. Logs subscription listener - listens for logs containing Pump.fun program
|
||||
4. PumpPortal WebSocket listener - connects to PumpPortal WebSocket and listens for token events
|
||||
Performance Comparison Tool for Pump.fun Token Detection Methods
|
||||
|
||||
The script tracks which method detects new tokens first and provides detailed performance statistics.
|
||||
This script compares four methods of detecting new Pump.fun tokens in real-time:
|
||||
|
||||
Note: multiple endpoints available. Scroll down to change providers which you want to test.
|
||||
1. Block Subscription (blockSubscribe)
|
||||
- Method: WebSocket subscription to blocks mentioning Pump.fun program
|
||||
- Speed: Slowest (processes entire blocks)
|
||||
- Reference: https://solana.com/docs/rpc/websocket/blocksubscribe
|
||||
|
||||
2. Logs Subscription (logsSubscribe)
|
||||
- Method: WebSocket subscription to program logs
|
||||
- Speed: Fast (event data includes all fields)
|
||||
- Reference: https://solana.com/docs/rpc/websocket/logssubscribe
|
||||
|
||||
3. Geyser gRPC
|
||||
- Method: Yellowstone Dragon's Mouth gRPC streaming
|
||||
- Speed: Fastest (optimized streaming protocol)
|
||||
- Reference: https://docs.triton.one/rpc-pool/grpc-subscriptions
|
||||
|
||||
4. PumpPortal WebSocket
|
||||
- Method: Third-party aggregated WebSocket feed
|
||||
- Speed: Fast (pre-processed data)
|
||||
- Note: Requires trust in third-party provider
|
||||
|
||||
The script tracks which method detects each token first and provides detailed
|
||||
performance statistics including:
|
||||
- First detection counts per method
|
||||
- Average latency between methods
|
||||
- Message counts per provider
|
||||
- Token detection coverage
|
||||
|
||||
Configuration: Set provider endpoints in .env or modify the providers dict at the bottom.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -26,13 +49,31 @@ from solders.transaction import VersionedTransaction
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
# Constants
|
||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
|
||||
TEST_DURATION = 30 # seconds
|
||||
# ============ CONSTANTS ============
|
||||
|
||||
GEYSER_AUTH_TYPE = "x-token" # or "basic"
|
||||
# Pump.fun program ID
|
||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
|
||||
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||
# and sha256("global:createV2") for Token2022 CreateV2
|
||||
# See: learning-examples/calculate_discriminator.py
|
||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||
|
||||
# Event discriminator for CreateEvent (8-byte identifier)
|
||||
# This is emitted by both Create and CreateV2 instructions
|
||||
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||
|
||||
# PumpPortal WebSocket endpoint (third-party service)
|
||||
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
|
||||
|
||||
# Test duration in seconds
|
||||
TEST_DURATION = 30
|
||||
|
||||
# Geyser authentication type: "x-token" or "basic"
|
||||
GEYSER_AUTH_TYPE = "x-token"
|
||||
|
||||
|
||||
class DetectionTracker:
|
||||
@@ -217,44 +258,198 @@ async def fetch_existing_token_mints():
|
||||
return set()
|
||||
|
||||
|
||||
def parse_create_instruction(data):
|
||||
"""
|
||||
Parse binary create instruction data into a structured format
|
||||
"""
|
||||
if len(data) < 8:
|
||||
def decode_create_instruction(ix_data, account_keys):
|
||||
"""Decode legacy Create instruction (Metaplex tokens) from instruction data."""
|
||||
if len(ix_data) < 8:
|
||||
return None
|
||||
|
||||
offset = 8 # Skip discriminator
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Parse name (string)
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
parsed_data["name"] = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
# Read string fields from instruction data
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
value = ix_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
return value
|
||||
|
||||
# Parse symbol (string)
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
parsed_data["symbol"] = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
def read_pubkey():
|
||||
nonlocal offset
|
||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
return value
|
||||
|
||||
# Parse uri (string)
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
parsed_data["uri"] = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
# Parse instruction arguments
|
||||
parsed_data["name"] = read_string()
|
||||
parsed_data["symbol"] = read_string()
|
||||
parsed_data["uri"] = read_string()
|
||||
parsed_data["creator"] = read_pubkey()
|
||||
|
||||
# Parse mint (pubkey)
|
||||
parsed_data["mint"] = base58.b58encode(data[offset : offset + 32]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
offset += 32
|
||||
# Extract accounts from account_keys array
|
||||
if len(account_keys) >= 8:
|
||||
parsed_data["mint"] = account_keys[0]
|
||||
parsed_data["bondingCurve"] = account_keys[2]
|
||||
parsed_data["user"] = account_keys[7]
|
||||
elif len(account_keys) > 0:
|
||||
parsed_data["mint"] = account_keys[0]
|
||||
|
||||
parsed_data["token_standard"] = "legacy"
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse create instruction: {e}")
|
||||
print(f"[ERROR] Failed to decode create instruction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def decode_create_v2_instruction(ix_data, account_keys):
|
||||
"""Decode CreateV2 instruction (Token2022 tokens) from instruction data."""
|
||||
if len(ix_data) < 8:
|
||||
return None
|
||||
|
||||
offset = 8 # Skip discriminator
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Read string fields from instruction data
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
value = ix_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
return value
|
||||
|
||||
def read_pubkey():
|
||||
nonlocal offset
|
||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
return value
|
||||
|
||||
# Parse instruction arguments
|
||||
parsed_data["name"] = read_string()
|
||||
parsed_data["symbol"] = read_string()
|
||||
parsed_data["uri"] = read_string()
|
||||
parsed_data["creator"] = read_pubkey()
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
if offset < len(ix_data):
|
||||
parsed_data["is_mayhem_mode"] = bool(ix_data[offset])
|
||||
else:
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
|
||||
# Extract accounts from account_keys array
|
||||
if len(account_keys) >= 6:
|
||||
parsed_data["mint"] = account_keys[0]
|
||||
parsed_data["bondingCurve"] = account_keys[2]
|
||||
parsed_data["user"] = account_keys[5]
|
||||
elif len(account_keys) > 0:
|
||||
parsed_data["mint"] = account_keys[0]
|
||||
|
||||
parsed_data["token_standard"] = "token2022"
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to decode create v2 instruction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_create_event(data):
|
||||
"""Parse legacy Create event from logs (event data includes all fields)."""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
offset = 8 # Skip discriminator
|
||||
parsed_data = {}
|
||||
|
||||
# Parse fields based on CreateEvent structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
("mint", "publicKey"),
|
||||
("bondingCurve", "publicKey"),
|
||||
("user", "publicKey"),
|
||||
("creator", "publicKey"),
|
||||
]
|
||||
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
parsed_data["token_standard"] = "legacy"
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse create event: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_create_v2_event(data):
|
||||
"""Parse CreateV2 event from logs (event data includes all fields)."""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
offset = 8 # Skip discriminator
|
||||
parsed_data = {}
|
||||
|
||||
# Parse fields based on CreateV2Event structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
("mint", "publicKey"),
|
||||
("bondingCurve", "publicKey"),
|
||||
("user", "publicKey"),
|
||||
("creator", "publicKey"),
|
||||
]
|
||||
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
if offset < len(data):
|
||||
is_mayhem_mode = bool(data[offset])
|
||||
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||
else:
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
|
||||
parsed_data["token_standard"] = "token2022"
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to parse create v2 event: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -269,6 +464,51 @@ def is_transaction_successful(logs):
|
||||
# ============ WEBSOCKET LISTENERS ============
|
||||
|
||||
|
||||
def get_account_keys(transaction, instruction, loaded_addresses=None):
|
||||
"""
|
||||
Safely extract account keys for an instruction from a versioned transaction.
|
||||
Handles both static account keys and loaded addresses from lookup tables.
|
||||
|
||||
Args:
|
||||
transaction: VersionedTransaction object
|
||||
instruction: Instruction object
|
||||
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
|
||||
|
||||
Returns:
|
||||
List of account keys as strings, or None if unable to resolve
|
||||
"""
|
||||
account_keys = []
|
||||
static_keys = transaction.message.account_keys
|
||||
|
||||
# Combine all available account keys: static + loaded
|
||||
all_keys = list(static_keys)
|
||||
|
||||
if loaded_addresses:
|
||||
# Add loaded writable addresses
|
||||
if "writable" in loaded_addresses:
|
||||
for addr in loaded_addresses["writable"]:
|
||||
all_keys.append(Pubkey.from_string(addr))
|
||||
|
||||
# Add loaded readonly addresses
|
||||
if "readonly" in loaded_addresses:
|
||||
for addr in loaded_addresses["readonly"]:
|
||||
all_keys.append(Pubkey.from_string(addr))
|
||||
|
||||
# Now resolve account indices
|
||||
for index in instruction.accounts:
|
||||
try:
|
||||
if index < len(all_keys):
|
||||
account_keys.append(str(all_keys[index]))
|
||||
else:
|
||||
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
|
||||
return None
|
||||
except (IndexError, Exception) as e:
|
||||
print(f"Error resolving account at index {index}: {e}")
|
||||
return None
|
||||
|
||||
return account_keys
|
||||
|
||||
|
||||
async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
|
||||
"""
|
||||
Listen for new tokens via block subscription
|
||||
@@ -330,6 +570,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
||||
|
||||
try:
|
||||
transaction = VersionedTransaction.from_bytes(tx_data)
|
||||
|
||||
# Extract loaded addresses from transaction metadata
|
||||
loaded_addresses = None
|
||||
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
|
||||
loaded_addresses = tx["meta"]["loadedAddresses"]
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
if (
|
||||
transaction.message.account_keys[
|
||||
@@ -339,39 +585,53 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
||||
):
|
||||
data_bytes = bytes(ix.data)
|
||||
|
||||
if not data_bytes.startswith(
|
||||
PUMP_CREATE_PREFIX
|
||||
):
|
||||
# Check for both Create and CreateV2 instructions
|
||||
is_create = data_bytes.startswith(PUMP_CREATE_PREFIX)
|
||||
is_create_v2 = data_bytes.startswith(PUMP_CREATE_V2_PREFIX)
|
||||
|
||||
if not (is_create or is_create_v2):
|
||||
continue
|
||||
|
||||
parsed = parse_create_instruction(data_bytes)
|
||||
if not parsed:
|
||||
# Get account keys with ALT support
|
||||
account_keys = get_account_keys(
|
||||
transaction, ix, loaded_addresses
|
||||
)
|
||||
if account_keys is None:
|
||||
print("Skipping transaction due to unresolved accounts")
|
||||
continue
|
||||
|
||||
if len(ix.accounts) > 0:
|
||||
try:
|
||||
mint = str(
|
||||
transaction.message.account_keys[
|
||||
ix.accounts[0]
|
||||
]
|
||||
) # First account is usually the mint
|
||||
# Decode based on instruction type
|
||||
if is_create_v2:
|
||||
print(f"[{provider_name}_block] Detected: CreateV2 instruction (Token2022)")
|
||||
decoded = decode_create_v2_instruction(data_bytes, account_keys)
|
||||
else:
|
||||
print(f"[{provider_name}_block] Detected: Create instruction (Legacy/Metaplex)")
|
||||
decoded = decode_create_instruction(data_bytes, account_keys)
|
||||
|
||||
if mint in known_tokens:
|
||||
continue
|
||||
if not decoded:
|
||||
continue
|
||||
|
||||
ts = time.time()
|
||||
tracker.add_token(
|
||||
mint,
|
||||
parsed["name"],
|
||||
parsed["symbol"],
|
||||
f"{provider_name}_block",
|
||||
ts,
|
||||
)
|
||||
known_tokens.add(mint)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[ERROR] Failed to process block instruction: {e}"
|
||||
)
|
||||
mint = decoded.get("mint")
|
||||
if not mint:
|
||||
continue
|
||||
|
||||
if mint in known_tokens:
|
||||
continue
|
||||
|
||||
try:
|
||||
ts = time.time()
|
||||
tracker.add_token(
|
||||
mint,
|
||||
decoded["name"],
|
||||
decoded["symbol"],
|
||||
f"{provider_name}_block",
|
||||
ts,
|
||||
)
|
||||
known_tokens.add(mint)
|
||||
except Exception as e:
|
||||
print(
|
||||
f"[ERROR] Failed to process block instruction: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to process transaction: {e}")
|
||||
|
||||
@@ -424,9 +684,17 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
|
||||
log_data = data["params"]["result"]["value"]
|
||||
logs = log_data.get("logs", [])
|
||||
|
||||
if not any(
|
||||
"Program log: Instruction: Create" in log for log in logs
|
||||
):
|
||||
# Detect both Create and CreateV2 instructions
|
||||
is_create = any(
|
||||
"Program log: Instruction: Create" in log
|
||||
for log in logs
|
||||
)
|
||||
is_create_v2 = any(
|
||||
"Program log: Instruction: CreateV2" in log
|
||||
for log in logs
|
||||
)
|
||||
|
||||
if not (is_create or is_create_v2):
|
||||
continue
|
||||
|
||||
for log in logs:
|
||||
@@ -435,7 +703,23 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
|
||||
encoded_data = log.split(": ")[1]
|
||||
data_bytes = base64.b64decode(encoded_data)
|
||||
|
||||
parsed = parse_create_instruction(data_bytes)
|
||||
# Check if this is a CreateEvent by validating discriminator
|
||||
if len(data_bytes) < 8:
|
||||
continue
|
||||
|
||||
event_discriminator = data_bytes[:8]
|
||||
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||
continue
|
||||
|
||||
# Parse based on instruction type
|
||||
if is_create_v2:
|
||||
print(f"[{provider_name}_logs] Detected: CreateV2 instruction (Token2022)")
|
||||
parsed = parse_create_v2_event(data_bytes)
|
||||
else:
|
||||
print(f"[{provider_name}_logs] Detected: Create instruction (Legacy/Metaplex)")
|
||||
parsed = parse_create_event(data_bytes)
|
||||
|
||||
if not parsed:
|
||||
continue
|
||||
|
||||
@@ -492,11 +776,11 @@ async def listen_geyser_grpc(
|
||||
|
||||
if GEYSER_AUTH_TYPE == "x-token":
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda context, callback: callback((("x-token", api_token),), None)
|
||||
lambda _context, callback: callback((("x-token", api_token),), None)
|
||||
)
|
||||
else:
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda context, callback: callback(
|
||||
lambda _context, callback: callback(
|
||||
(("authorization", f"Basic {api_token}"),), None
|
||||
)
|
||||
)
|
||||
@@ -529,28 +813,44 @@ async def listen_geyser_grpc(
|
||||
continue
|
||||
|
||||
for ix in msg.instructions:
|
||||
if not ix.data.startswith(PUMP_CREATE_PREFIX):
|
||||
# Check for both Create and CreateV2 instructions
|
||||
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
|
||||
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
|
||||
|
||||
if not (is_create or is_create_v2):
|
||||
continue
|
||||
|
||||
parsed = parse_create_instruction(ix.data)
|
||||
if not parsed:
|
||||
# Convert account keys to string format
|
||||
account_keys = []
|
||||
for account_idx in ix.accounts:
|
||||
if account_idx < len(msg.account_keys):
|
||||
account_keys.append(
|
||||
base58.b58encode(bytes(msg.account_keys[account_idx])).decode()
|
||||
)
|
||||
|
||||
if len(account_keys) == 0:
|
||||
continue
|
||||
|
||||
if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
mint = base58.b58encode(
|
||||
bytes(msg.account_keys[ix.accounts[0]])
|
||||
).decode()
|
||||
|
||||
mint = account_keys[0]
|
||||
if mint in known_tokens:
|
||||
continue
|
||||
|
||||
# Decode based on instruction type
|
||||
if is_create_v2:
|
||||
print(f"[{provider_name}_geyser] Detected: CreateV2 instruction (Token2022)")
|
||||
decoded = decode_create_v2_instruction(ix.data, account_keys)
|
||||
else:
|
||||
print(f"[{provider_name}_geyser] Detected: Create instruction (Legacy/Metaplex)")
|
||||
decoded = decode_create_instruction(ix.data, account_keys)
|
||||
|
||||
if not decoded:
|
||||
continue
|
||||
|
||||
ts = time.time()
|
||||
tracker.add_token(
|
||||
mint,
|
||||
parsed["name"],
|
||||
parsed["symbol"],
|
||||
decoded["name"],
|
||||
decoded["symbol"],
|
||||
f"{provider_name}_geyser",
|
||||
ts,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
|
||||
Decodes transaction data to extract mint, bonding curve, and user details.
|
||||
|
||||
It is usually slower than other listeners.
|
||||
Performance: Usually slower than other listeners due to block-level processing.
|
||||
|
||||
This script uses blockSubscribe which receives entire blocks containing transactions
|
||||
that mention the Pump.fun program. It then decodes the instruction data from each
|
||||
transaction to extract token creation details.
|
||||
|
||||
WebSocket API Reference:
|
||||
https://solana.com/docs/rpc/websocket/blocksubscribe
|
||||
|
||||
Address Lookup Tables (ALT) Support:
|
||||
https://solana.com/docs/advanced/lookup-tables
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -22,6 +32,93 @@ load_dotenv()
|
||||
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
|
||||
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||
# and sha256("global:createV2") for Token2022 CreateV2
|
||||
# See: learning-examples/calculate_discriminator.py
|
||||
CREATE_DISCRIMINATOR = 8576854823835016728
|
||||
CREATE_V2_DISCRIMINATOR = struct.unpack("<Q", bytes([214, 144, 76, 236, 95, 139, 49, 180]))[0]
|
||||
|
||||
|
||||
def print_token_info(token_data, signature=None):
|
||||
"""
|
||||
Print token information in a consistent, user-friendly format.
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing token fields
|
||||
signature: Optional transaction signature
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("🎯 NEW TOKEN DETECTED")
|
||||
print("=" * 80)
|
||||
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||
|
||||
if "bondingCurve" in token_data:
|
||||
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||
if "associatedBondingCurve" in token_data:
|
||||
print(f"Associated BC: {token_data['associatedBondingCurve']}")
|
||||
if "user" in token_data:
|
||||
print(f"User: {token_data['user']}")
|
||||
if "creator" in token_data:
|
||||
print(f"Creator: {token_data['creator']}")
|
||||
|
||||
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||
|
||||
if "uri" in token_data:
|
||||
print(f"URI: {token_data['uri']}")
|
||||
if signature:
|
||||
print(f"Signature: {signature}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
def get_account_keys(transaction, instruction, loaded_addresses=None):
|
||||
"""
|
||||
Safely extract account keys for an instruction from a versioned transaction.
|
||||
Handles both static account keys and loaded addresses from lookup tables.
|
||||
|
||||
Args:
|
||||
transaction: VersionedTransaction object
|
||||
instruction: Instruction object
|
||||
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
|
||||
|
||||
Returns:
|
||||
List of account keys as strings, or None if unable to resolve
|
||||
"""
|
||||
account_keys = []
|
||||
static_keys = transaction.message.account_keys
|
||||
|
||||
# Combine all available account keys: static + loaded
|
||||
all_keys = list(static_keys)
|
||||
|
||||
if loaded_addresses:
|
||||
# Add loaded writable addresses
|
||||
if "writable" in loaded_addresses:
|
||||
for addr in loaded_addresses["writable"]:
|
||||
all_keys.append(Pubkey.from_string(addr))
|
||||
|
||||
# Add loaded readonly addresses
|
||||
if "readonly" in loaded_addresses:
|
||||
for addr in loaded_addresses["readonly"]:
|
||||
all_keys.append(Pubkey.from_string(addr))
|
||||
|
||||
# Now resolve account indices
|
||||
for index in instruction.accounts:
|
||||
try:
|
||||
if index < len(all_keys):
|
||||
account_keys.append(str(all_keys[index]))
|
||||
else:
|
||||
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
|
||||
return None
|
||||
except (IndexError, Exception) as e:
|
||||
print(f"Error resolving account at index {index}: {e}")
|
||||
return None
|
||||
|
||||
return account_keys
|
||||
|
||||
|
||||
def load_idl(file_path):
|
||||
with open(file_path) as f:
|
||||
@@ -29,16 +126,34 @@ def load_idl(file_path):
|
||||
|
||||
|
||||
def decode_create_instruction(ix_data, ix_def, accounts):
|
||||
"""
|
||||
Decode legacy Create instruction (Metaplex tokens).
|
||||
|
||||
The Create instruction creates tokens using the Metaplex Token Metadata standard.
|
||||
Instruction data contains: name, symbol, uri, and additional creator pubkey.
|
||||
Account references are extracted from the accounts array.
|
||||
|
||||
Args:
|
||||
ix_data: Raw instruction data bytes
|
||||
ix_def: Instruction definition from IDL
|
||||
accounts: List of account pubkeys involved in the instruction
|
||||
|
||||
Returns:
|
||||
Dictionary containing decoded token information
|
||||
"""
|
||||
args = {}
|
||||
offset = 8 # Skip 8-byte discriminator
|
||||
|
||||
# Parse instruction arguments according to IDL definition
|
||||
for arg in ix_def["args"]:
|
||||
if arg["type"] == "string":
|
||||
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||
offset += 4
|
||||
value = ix_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif arg["type"] == "pubkey":
|
||||
# Pubkey is 32 bytes, encoded as base58
|
||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
else:
|
||||
@@ -46,19 +161,90 @@ def decode_create_instruction(ix_data, ix_def, accounts):
|
||||
|
||||
args[arg["name"]] = value
|
||||
|
||||
# Add accounts
|
||||
# Extract account addresses from the accounts array
|
||||
# Account layout for Create instruction:
|
||||
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
|
||||
# 4: tokenProgram, 5: systemProgram, 6: rent, 7: user
|
||||
args["mint"] = str(accounts[0])
|
||||
args["bondingCurve"] = str(accounts[2])
|
||||
args["associatedBondingCurve"] = str(accounts[3])
|
||||
args["user"] = str(accounts[7])
|
||||
args["token_standard"] = "legacy"
|
||||
args["is_mayhem_mode"] = False
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def decode_create_v2_instruction(ix_data, ix_def, accounts):
|
||||
"""
|
||||
Decode CreateV2 instruction (Token2022 tokens).
|
||||
|
||||
The CreateV2 instruction creates tokens using the Token-2022 standard, which supports
|
||||
additional features like transfer fees, interest-bearing tokens, and more.
|
||||
This instruction includes an optional is_mayhem_mode flag.
|
||||
|
||||
Token-2022 Reference:
|
||||
https://spl.solana.com/token-2022
|
||||
|
||||
Args:
|
||||
ix_data: Raw instruction data bytes
|
||||
ix_def: Instruction definition from IDL
|
||||
accounts: List of account pubkeys involved in the instruction
|
||||
|
||||
Returns:
|
||||
Dictionary containing decoded token information
|
||||
"""
|
||||
args = {}
|
||||
offset = 8 # Skip 8-byte discriminator
|
||||
|
||||
# Parse instruction arguments according to IDL definition
|
||||
for arg in ix_def["args"]:
|
||||
if arg["type"] == "string":
|
||||
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||
offset += 4
|
||||
value = ix_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif arg["type"] == "pubkey":
|
||||
# Pubkey is 32 bytes, encoded as base58
|
||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {arg['type']}")
|
||||
|
||||
args[arg["name"]] = value
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
# Format: 1 byte (0 = false/None, 1 = true)
|
||||
is_mayhem_mode = False
|
||||
if offset < len(ix_data):
|
||||
is_mayhem_mode = bool(ix_data[offset])
|
||||
|
||||
# Extract account addresses from the accounts array
|
||||
# Account layout for CreateV2 instruction:
|
||||
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
|
||||
# 4: tokenProgram (Token2022), 5: user, 6: systemProgram, 7: rent
|
||||
args["mint"] = str(accounts[0])
|
||||
args["bondingCurve"] = str(accounts[2])
|
||||
args["associatedBondingCurve"] = str(accounts[3])
|
||||
args["user"] = str(accounts[5])
|
||||
args["token_standard"] = "token2022"
|
||||
args["is_mayhem_mode"] = is_mayhem_mode
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
||||
async def listen_and_decode_create():
|
||||
"""
|
||||
Main listener function that subscribes to Solana blocks and decodes Pump.fun token creations.
|
||||
|
||||
This function:
|
||||
1. Loads the Pump.fun IDL for instruction parsing
|
||||
2. Subscribes to blocks mentioning the Pump.fun program
|
||||
3. Decodes transactions to extract Create/CreateV2 instructions
|
||||
4. Handles Address Lookup Tables (ALTs) for account resolution
|
||||
"""
|
||||
idl = load_idl("idl/pump_fun_idl.json")
|
||||
create_discriminator = 8576854823835016728
|
||||
|
||||
async with websockets.connect(WSS_ENDPOINT) as websocket:
|
||||
subscription_message = json.dumps(
|
||||
@@ -101,6 +287,11 @@ async def listen_and_decode_create():
|
||||
tx_data_decoded
|
||||
)
|
||||
|
||||
# Extract loaded addresses from transaction metadata
|
||||
loaded_addresses = None
|
||||
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
|
||||
loaded_addresses = tx["meta"]["loadedAddresses"]
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
if str(
|
||||
transaction.message.account_keys[
|
||||
@@ -112,36 +303,68 @@ async def listen_and_decode_create():
|
||||
"<Q", ix_data[:8]
|
||||
)[0]
|
||||
|
||||
if (
|
||||
discriminator
|
||||
== create_discriminator
|
||||
):
|
||||
if discriminator == CREATE_DISCRIMINATOR:
|
||||
# Legacy Create instruction (Metaplex tokens)
|
||||
create_ix = next(
|
||||
instr
|
||||
for instr in idl["instructions"]
|
||||
if instr["name"] == "create"
|
||||
)
|
||||
account_keys = [
|
||||
str(
|
||||
transaction.message.account_keys[
|
||||
index
|
||||
]
|
||||
)
|
||||
for index in ix.accounts
|
||||
]
|
||||
decoded_args = (
|
||||
decode_create_instruction(
|
||||
ix_data,
|
||||
create_ix,
|
||||
account_keys,
|
||||
)
|
||||
account_keys = get_account_keys(
|
||||
transaction, ix, loaded_addresses
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
decoded_args, indent=2
|
||||
)
|
||||
if account_keys is None:
|
||||
print("⚠️ Skipping transaction due to unresolved accounts")
|
||||
continue
|
||||
|
||||
# Decode instruction data
|
||||
decoded_args = decode_create_instruction(
|
||||
ix_data,
|
||||
create_ix,
|
||||
account_keys,
|
||||
)
|
||||
print("--------------------")
|
||||
|
||||
# Print token information
|
||||
print_token_info(decoded_args)
|
||||
|
||||
# Note if using Address Lookup Tables
|
||||
if loaded_addresses:
|
||||
writable_count = len(loaded_addresses.get("writable", []))
|
||||
readonly_count = len(loaded_addresses.get("readonly", []))
|
||||
if writable_count > 0 or readonly_count > 0:
|
||||
print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
|
||||
|
||||
elif discriminator == CREATE_V2_DISCRIMINATOR:
|
||||
# CreateV2 instruction (Token2022 tokens)
|
||||
create_v2_ix = next(
|
||||
(instr for instr in idl["instructions"]
|
||||
if instr["name"] == "createV2"),
|
||||
next(instr for instr in idl["instructions"]
|
||||
if instr["name"] == "create")
|
||||
)
|
||||
account_keys = get_account_keys(
|
||||
transaction, ix, loaded_addresses
|
||||
)
|
||||
if account_keys is None:
|
||||
print("⚠️ Skipping transaction due to unresolved accounts")
|
||||
continue
|
||||
|
||||
# Decode instruction data
|
||||
decoded_args = decode_create_v2_instruction(
|
||||
ix_data,
|
||||
create_v2_ix,
|
||||
account_keys,
|
||||
)
|
||||
|
||||
# Print token information
|
||||
print_token_info(decoded_args)
|
||||
|
||||
# Note if using Address Lookup Tables
|
||||
if loaded_addresses:
|
||||
writable_count = len(loaded_addresses.get("writable", []))
|
||||
readonly_count = len(loaded_addresses.get("readonly", []))
|
||||
if writable_count > 0 or readonly_count > 0:
|
||||
print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
|
||||
elif "result" in data:
|
||||
print("Subscription confirmed")
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"""
|
||||
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
|
||||
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
|
||||
Requires a Geyser API token for access.
|
||||
Supports both Basic and X-Token authentication methods.
|
||||
|
||||
It is proven to be the fastest listener.
|
||||
Performance: Proven to be the fastest listener method available.
|
||||
|
||||
This script uses Yellowstone Dragon's Mouth Geyser gRPC interface, which provides
|
||||
real-time streaming of Solana blockchain data with lower latency than WebSocket methods.
|
||||
Requires a Geyser API token for access.
|
||||
|
||||
Geyser gRPC Reference:
|
||||
https://docs.triton.one/rpc-pool/grpc-subscriptions
|
||||
|
||||
Authentication: Supports both Basic and X-Token authentication methods.
|
||||
Configure via GEYSER_ENDPOINT, GEYSER_API_TOKEN, and AUTH_TYPE variables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -22,11 +30,53 @@ load_dotenv()
|
||||
|
||||
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
|
||||
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
|
||||
# Default to x-token auth, can be set to "basic"
|
||||
# Authentication type: "x-token" or "basic"
|
||||
AUTH_TYPE = "x-token"
|
||||
|
||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
|
||||
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||
# and sha256("global:createV2") for Token2022 CreateV2
|
||||
# See: learning-examples/calculate_discriminator.py
|
||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||
|
||||
|
||||
def print_token_info(token_data, signature=None):
|
||||
"""
|
||||
Print token information in a consistent, user-friendly format.
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing token fields
|
||||
signature: Optional transaction signature
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("🎯 NEW TOKEN DETECTED")
|
||||
print("=" * 80)
|
||||
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||
|
||||
if "bonding_curve" in token_data:
|
||||
print(f"Bonding Curve: {token_data['bonding_curve']}")
|
||||
if "associated_bonding_curve" in token_data:
|
||||
print(f"Associated BC: {token_data['associated_bonding_curve']}")
|
||||
if "user" in token_data:
|
||||
print(f"User: {token_data['user']}")
|
||||
if "creator" in token_data:
|
||||
print(f"Creator: {token_data['creator']}")
|
||||
|
||||
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||
|
||||
if "uri" in token_data:
|
||||
print(f"URI: {token_data['uri']}")
|
||||
if signature:
|
||||
print(f"Signature: {signature}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
|
||||
async def create_geyser_connection():
|
||||
@@ -57,7 +107,7 @@ def create_subscription_request():
|
||||
|
||||
|
||||
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
|
||||
"""Decode a create instruction from transaction data."""
|
||||
"""Decode a legacy create instruction (Metaplex) from transaction data."""
|
||||
# Skip past the 8-byte discriminator prefix
|
||||
offset = 8
|
||||
|
||||
@@ -103,20 +153,68 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
|
||||
"system_program": get_account_key(5),
|
||||
"rent": get_account_key(6),
|
||||
"user": get_account_key(7),
|
||||
"token_standard": "legacy",
|
||||
"is_mayhem_mode": False,
|
||||
}
|
||||
|
||||
return token_info
|
||||
|
||||
|
||||
def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict:
|
||||
"""Decode a CreateV2 instruction (Token2022) from transaction data."""
|
||||
# Skip past the 8-byte discriminator prefix
|
||||
offset = 8
|
||||
|
||||
# Extract account keys in base58 format
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return "N/A"
|
||||
account_index = accounts[index]
|
||||
return base58.b58encode(keys[account_index]).decode()
|
||||
|
||||
# Read string fields (prefixed with length)
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
# Get string length (4-byte uint)
|
||||
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||
offset += 4
|
||||
# Extract and decode the string
|
||||
value = ix_data[offset : offset + length].decode()
|
||||
offset += length
|
||||
return value
|
||||
|
||||
def read_pubkey():
|
||||
nonlocal offset
|
||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
return value
|
||||
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
creator = read_pubkey()
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
is_mayhem_mode = False
|
||||
if offset < len(ix_data):
|
||||
is_mayhem_mode = bool(ix_data[offset])
|
||||
|
||||
token_info = {
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"uri": uri,
|
||||
"creator": creator,
|
||||
"mint": get_account_key(0),
|
||||
"bonding_curve": get_account_key(2),
|
||||
"associated_bonding_curve": get_account_key(3),
|
||||
"user": get_account_key(5),
|
||||
"token_standard": "token2022",
|
||||
"is_mayhem_mode": is_mayhem_mode,
|
||||
}
|
||||
|
||||
return token_info
|
||||
|
||||
|
||||
def print_token_info(info, signature):
|
||||
"""Print formatted token information."""
|
||||
print("\n🎯 New Pump.fun token detected!")
|
||||
print(f"Name: {info['name']} | Symbol: {info['symbol']}")
|
||||
print(f"Mint: {info['mint']}")
|
||||
print(f"Bonding curve: {info['bonding_curve']}")
|
||||
print(f"Associated bonding curve: {info['associated_bonding_curve']}")
|
||||
print(f"Creator: {info['creator']}")
|
||||
print(f"Signature: {signature}")
|
||||
|
||||
|
||||
async def monitor_pump():
|
||||
@@ -137,14 +235,28 @@ async def monitor_pump():
|
||||
|
||||
# Check each instruction in the transaction
|
||||
for ix in msg.instructions:
|
||||
if not ix.data.startswith(PUMP_CREATE_PREFIX):
|
||||
# Check for both Create and CreateV2 instructions
|
||||
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
|
||||
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
|
||||
|
||||
if not (is_create or is_create_v2):
|
||||
continue
|
||||
|
||||
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
|
||||
# Decode based on instruction type
|
||||
if is_create_v2:
|
||||
info = decode_create_v2_instruction(
|
||||
ix.data, msg.account_keys, ix.accounts
|
||||
)
|
||||
else:
|
||||
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
|
||||
|
||||
# Extract transaction signature
|
||||
signature = base58.b58encode(
|
||||
bytes(update.transaction.transaction.signature)
|
||||
).decode()
|
||||
print_token_info(info, signature)
|
||||
|
||||
# Print token information in consistent format
|
||||
print_token_info(info, signature=signature)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
Listens for new Pump.fun token creations via Solana WebSocket.
|
||||
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
||||
|
||||
It is usually faster than blockSubscribe, but slower than Geyser.
|
||||
Performance: Usually faster than blockSubscribe, but slower than Geyser.
|
||||
|
||||
This script uses logsSubscribe which receives program logs containing event data.
|
||||
Event logs include all token fields directly, making parsing simpler and faster than
|
||||
decoding full transactions.
|
||||
|
||||
WebSocket API Reference:
|
||||
https://solana.com/docs/rpc/websocket/logssubscribe
|
||||
|
||||
Program Logs and Events:
|
||||
https://solana.com/docs/programs/debugging#logging
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -21,11 +31,71 @@ load_dotenv()
|
||||
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
|
||||
# Event discriminator for CreateEvent (8-byte identifier)
|
||||
# This is emitted by both Create and CreateV2 instructions
|
||||
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||
|
||||
|
||||
def print_token_info(token_data, signature=None):
|
||||
"""
|
||||
Print token information in a consistent, user-friendly format.
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing token fields
|
||||
signature: Optional transaction signature
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("🎯 NEW TOKEN DETECTED")
|
||||
print("=" * 80)
|
||||
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||
|
||||
if "bondingCurve" in token_data:
|
||||
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||
if "user" in token_data:
|
||||
print(f"User: {token_data['user']}")
|
||||
if "creator" in token_data:
|
||||
print(f"Creator: {token_data['creator']}")
|
||||
|
||||
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||
|
||||
if "uri" in token_data:
|
||||
print(f"URI: {token_data['uri']}")
|
||||
if signature:
|
||||
print(f"Signature: {signature}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
|
||||
def parse_create_instruction(data):
|
||||
"""
|
||||
Parse CreateEvent data from legacy Create instruction (Metaplex tokens).
|
||||
|
||||
Event logs contain all fields directly embedded in the event data, unlike
|
||||
instruction data which requires account lookup. Event format:
|
||||
- 8 bytes: event discriminator
|
||||
- Variable: name (4-byte length + UTF-8 string)
|
||||
- Variable: symbol (4-byte length + UTF-8 string)
|
||||
- Variable: uri (4-byte length + UTF-8 string)
|
||||
- 32 bytes: mint pubkey
|
||||
- 32 bytes: bondingCurve pubkey
|
||||
- 32 bytes: user pubkey
|
||||
- 32 bytes: creator pubkey
|
||||
|
||||
Args:
|
||||
data: Raw event data bytes from program logs
|
||||
|
||||
Returns:
|
||||
Dictionary containing decoded token information, or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
|
||||
return None
|
||||
offset = 8
|
||||
offset = 8 # Skip event discriminator
|
||||
parsed_data = {}
|
||||
|
||||
# Parse fields based on CreateEvent structure
|
||||
@@ -42,18 +112,102 @@ def parse_create_instruction(data):
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
# Pubkey is 32 bytes, encoded as base58
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
parsed_data["token_standard"] = "legacy"
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
return parsed_data
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"❌ Parse Create error: {e}")
|
||||
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||
print(f" Data hex: {data.hex()[:200]}...")
|
||||
return None
|
||||
|
||||
|
||||
def parse_create_v2_instruction(data):
|
||||
"""
|
||||
Parse CreateEvent data from CreateV2 instruction (Token2022 tokens).
|
||||
|
||||
CreateV2 uses Token-2022 standard with additional features. The event format
|
||||
is identical to Create, with an additional optional is_mayhem_mode flag at the end.
|
||||
|
||||
Token-2022 Reference:
|
||||
https://spl.solana.com/token-2022
|
||||
|
||||
Args:
|
||||
data: Raw event data bytes from program logs
|
||||
|
||||
Returns:
|
||||
Dictionary containing decoded token information, or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
print(f"⚠️ Data too short for CreateV2 event: {len(data)} bytes")
|
||||
return None
|
||||
offset = 8 # Skip event discriminator
|
||||
parsed_data = {}
|
||||
|
||||
# Parse fields based on CreateV2Event structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
("mint", "publicKey"),
|
||||
("bondingCurve", "publicKey"),
|
||||
("user", "publicKey"),
|
||||
("creator", "publicKey"),
|
||||
]
|
||||
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
# Pubkey is 32 bytes, encoded as base58
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
# Format: 1 byte (0 = false/None, 1 = true)
|
||||
if offset < len(data):
|
||||
is_mayhem_mode = bool(data[offset])
|
||||
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||
else:
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
|
||||
parsed_data["token_standard"] = "token2022"
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"❌ Parse CreateV2 error: {e}")
|
||||
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||
print(f" Data hex: {data.hex()[:200]}...")
|
||||
return None
|
||||
|
||||
|
||||
@@ -90,10 +244,17 @@ async def listen_for_new_tokens():
|
||||
log_data = data["params"]["result"]["value"]
|
||||
logs = log_data.get("logs", [])
|
||||
|
||||
if any(
|
||||
# Detect both Create and CreateV2 instructions
|
||||
is_create = any(
|
||||
"Program log: Instruction: Create" in log
|
||||
for log in logs
|
||||
):
|
||||
)
|
||||
is_create_v2 = any(
|
||||
"Program log: Instruction: CreateV2" in log
|
||||
for log in logs
|
||||
)
|
||||
|
||||
if is_create or is_create_v2:
|
||||
for log in logs:
|
||||
if "Program data:" in log:
|
||||
try:
|
||||
@@ -101,22 +262,40 @@ async def listen_for_new_tokens():
|
||||
decoded_data = base64.b64decode(
|
||||
encoded_data
|
||||
)
|
||||
parsed_data = parse_create_instruction(
|
||||
decoded_data
|
||||
)
|
||||
|
||||
# Check if this is a CreateEvent by validating discriminator
|
||||
if len(decoded_data) < 8:
|
||||
continue
|
||||
|
||||
event_discriminator = decoded_data[:8]
|
||||
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||
continue
|
||||
|
||||
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
|
||||
print(f" Signature: {log_data.get('signature')}")
|
||||
|
||||
# Both create and create_v2 emit the same CreateEvent
|
||||
# The difference is in the optional is_mayhem_mode field
|
||||
if is_create_v2:
|
||||
parsed_data = parse_create_v2_instruction(
|
||||
decoded_data
|
||||
)
|
||||
else:
|
||||
parsed_data = parse_create_instruction(
|
||||
decoded_data
|
||||
)
|
||||
|
||||
if parsed_data and "name" in parsed_data:
|
||||
print(
|
||||
"Signature:",
|
||||
log_data.get("signature"),
|
||||
)
|
||||
for key, value in parsed_data.items():
|
||||
print(f"{key}: {value}")
|
||||
print(
|
||||
"##########################################################################################"
|
||||
# Print token information in consistent format
|
||||
print_token_info(
|
||||
parsed_data,
|
||||
signature=log_data.get("signature")
|
||||
)
|
||||
else:
|
||||
print(f"⚠️ Parsing failed for CreateEvent")
|
||||
except Exception as e:
|
||||
print(f"Failed to decode: {log}")
|
||||
print(f"Error: {e!s}")
|
||||
print(f"❌ Error processing log: {e!s}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while processing message: {e}")
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
"""
|
||||
Listens for new Pump.fun token creations via Solana WebSocket.
|
||||
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
||||
Additionally, calculates an associated bonding curve address for each token.
|
||||
Additionally, calculates the associated bonding curve address for each token using PDA derivation.
|
||||
|
||||
It is usually faster than blockSubscribe, but slower than Geyser.
|
||||
Performance: Usually faster than blockSubscribe, but slower than Geyser.
|
||||
|
||||
This script demonstrates Program Derived Address (PDA) calculation for the associated
|
||||
bonding curve, which is the token account owned by the bonding curve that holds
|
||||
the minted tokens.
|
||||
|
||||
WebSocket API Reference:
|
||||
https://solana.com/docs/rpc/websocket/logssubscribe
|
||||
|
||||
Program Derived Addresses:
|
||||
https://solana.com/docs/core/pda
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -26,11 +36,67 @@ ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
|
||||
# Event discriminator for CreateEvent (8-byte identifier)
|
||||
# This is emitted by both Create and CreateV2 instructions
|
||||
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||
|
||||
|
||||
def print_token_info(token_data, signature=None, associated_bonding_curve=None):
|
||||
"""
|
||||
Print token information in a consistent, user-friendly format.
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing token fields
|
||||
signature: Optional transaction signature
|
||||
associated_bonding_curve: Optional associated bonding curve address
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("🎯 NEW TOKEN DETECTED")
|
||||
print("=" * 80)
|
||||
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||
|
||||
if "bondingCurve" in token_data:
|
||||
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||
if associated_bonding_curve:
|
||||
print(f"Associated BC: {associated_bonding_curve}")
|
||||
if "user" in token_data:
|
||||
print(f"User: {token_data['user']}")
|
||||
if "creator" in token_data:
|
||||
print(f"Creator: {token_data['creator']}")
|
||||
|
||||
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||
|
||||
if "uri" in token_data:
|
||||
print(f"URI: {token_data['uri']}")
|
||||
if signature:
|
||||
print(f"Signature: {signature}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the associated bonding curve for a given mint and bonding curve.
|
||||
This uses the standard ATA derivation.
|
||||
Calculate the associated token account (ATA) address for the bonding curve.
|
||||
|
||||
The associated bonding curve is a Program Derived Address (PDA) that holds
|
||||
the token supply controlled by the bonding curve. It's derived using the
|
||||
standard Associated Token Account (ATA) derivation.
|
||||
|
||||
ATA Derivation: find_program_address(
|
||||
[bonding_curve_pubkey, token_program_id, mint_pubkey],
|
||||
associated_token_program_id
|
||||
)
|
||||
|
||||
Args:
|
||||
mint: The token mint pubkey
|
||||
bonding_curve: The bonding curve pubkey
|
||||
|
||||
Returns:
|
||||
The derived associated bonding curve address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
@@ -44,7 +110,9 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey
|
||||
|
||||
|
||||
def parse_create_instruction(data):
|
||||
"""Parse legacy Create instruction (Metaplex tokens)."""
|
||||
if len(data) < 8:
|
||||
print(f"⚠️ Data too short for Create instruction: {len(data)} bytes")
|
||||
return None
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
@@ -63,18 +131,81 @@ def parse_create_instruction(data):
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
parsed_data["token_standard"] = "legacy"
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
return parsed_data
|
||||
except:
|
||||
except Exception as e:
|
||||
print(f"❌ Parse Create error: {e}")
|
||||
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_create_v2_instruction(data):
|
||||
"""Parse CreateV2 instruction (Token2022 tokens)."""
|
||||
if len(data) < 8:
|
||||
print(f"⚠️ Data too short for CreateV2 instruction: {len(data)} bytes")
|
||||
return None
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
# Parse fields based on CreateV2Event structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
("mint", "publicKey"),
|
||||
("bondingCurve", "publicKey"),
|
||||
("user", "publicKey"),
|
||||
("creator", "publicKey"),
|
||||
]
|
||||
|
||||
try:
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
# Parse is_mayhem_mode (OptionBool at the end)
|
||||
if offset < len(data):
|
||||
is_mayhem_mode = bool(data[offset])
|
||||
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||
else:
|
||||
parsed_data["is_mayhem_mode"] = False
|
||||
|
||||
parsed_data["token_standard"] = "token2022"
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
print(f"❌ Parse CreateV2 error: {e}")
|
||||
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -111,10 +242,17 @@ async def listen_for_new_tokens():
|
||||
log_data = data["params"]["result"]["value"]
|
||||
logs = log_data.get("logs", [])
|
||||
|
||||
if any(
|
||||
# Detect both Create and CreateV2 instructions
|
||||
is_create = any(
|
||||
"Program log: Instruction: Create" in log
|
||||
for log in logs
|
||||
):
|
||||
)
|
||||
is_create_v2 = any(
|
||||
"Program log: Instruction: CreateV2" in log
|
||||
for log in logs
|
||||
)
|
||||
|
||||
if is_create or is_create_v2:
|
||||
for log in logs:
|
||||
if "Program data:" in log:
|
||||
try:
|
||||
@@ -122,38 +260,56 @@ async def listen_for_new_tokens():
|
||||
decoded_data = base64.b64decode(
|
||||
encoded_data
|
||||
)
|
||||
parsed_data = parse_create_instruction(
|
||||
decoded_data
|
||||
)
|
||||
if parsed_data and "name" in parsed_data:
|
||||
print(
|
||||
"Signature:",
|
||||
log_data.get("signature"),
|
||||
)
|
||||
for key, value in parsed_data.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
# Calculate associated bonding curve
|
||||
# Check if this is a CreateEvent by validating discriminator
|
||||
if len(decoded_data) < 8:
|
||||
continue
|
||||
|
||||
event_discriminator = decoded_data[:8]
|
||||
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||
continue
|
||||
|
||||
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
|
||||
print(f" Signature: {log_data.get('signature')}")
|
||||
|
||||
# Both create and create_v2 emit the same CreateEvent
|
||||
# The difference is in the optional is_mayhem_mode field
|
||||
if is_create_v2:
|
||||
print("📝 Instruction: CreateV2 (Token2022)")
|
||||
parsed_data = (
|
||||
parse_create_v2_instruction(
|
||||
decoded_data
|
||||
)
|
||||
)
|
||||
else:
|
||||
print("📝 Instruction: Create (Legacy/Metaplex)")
|
||||
parsed_data = parse_create_instruction(
|
||||
decoded_data
|
||||
)
|
||||
|
||||
if parsed_data and "name" in parsed_data:
|
||||
# Calculate associated bonding curve using PDA derivation
|
||||
mint = Pubkey.from_string(
|
||||
parsed_data["mint"]
|
||||
)
|
||||
bonding_curve = Pubkey.from_string(
|
||||
parsed_data["bondingCurve"]
|
||||
)
|
||||
associated_curve = (
|
||||
find_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
)
|
||||
associated_curve = find_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
)
|
||||
print(
|
||||
f"Associated Bonding Curve: {associated_curve}"
|
||||
)
|
||||
print(
|
||||
"##########################################################################################"
|
||||
|
||||
# Print token information in consistent format
|
||||
print_token_info(
|
||||
parsed_data,
|
||||
signature=log_data.get("signature"),
|
||||
associated_bonding_curve=str(associated_curve)
|
||||
)
|
||||
else:
|
||||
print(f"⚠️ Parsing failed for CreateEvent")
|
||||
except Exception as e:
|
||||
print(f"Failed to decode: {log}")
|
||||
print(f"Error: {e!s}")
|
||||
print(f"❌ Error processing log: {e!s}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while processing message: {e}")
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
"""
|
||||
Listens for new Pump.fun token creations via PumpPortal WebSocket.
|
||||
|
||||
Performance: Fast, real-time data via third-party API.
|
||||
|
||||
This script uses PumpPortal's WebSocket API, a third-party service that aggregates
|
||||
and provides real-time Pump.fun token creation events. This provides additional
|
||||
market data like initial buy amounts and market cap that aren't available in
|
||||
raw blockchain data.
|
||||
|
||||
PumpPortal API: https://pumpportal.fun/
|
||||
|
||||
Note: This is a third-party service and requires trust in the data provider.
|
||||
For trustless monitoring, use the direct blockchain listeners (logs, block, geyser).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -12,12 +24,52 @@ import websockets
|
||||
WS_URL = "wss://pumpportal.fun/api/data"
|
||||
|
||||
|
||||
def format_sol(value):
|
||||
return f"{value:.6f} SOL"
|
||||
def print_token_info(token_data):
|
||||
"""
|
||||
Print token information in a consistent, user-friendly format.
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing token fields from PumpPortal
|
||||
"""
|
||||
print("\n" + "=" * 80)
|
||||
print("🎯 NEW TOKEN DETECTED (via PumpPortal)")
|
||||
print("=" * 80)
|
||||
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||
|
||||
# PumpPortal-specific fields
|
||||
if "initialBuy" in token_data:
|
||||
initial_buy_sol = token_data['initialBuy']
|
||||
print(f"Initial Buy: {initial_buy_sol:.6f} SOL")
|
||||
|
||||
if "marketCapSol" in token_data:
|
||||
market_cap_sol = token_data['marketCapSol']
|
||||
print(f"Market Cap: {market_cap_sol:.6f} SOL")
|
||||
|
||||
if "bondingCurveKey" in token_data:
|
||||
print(f"Bonding Curve: {token_data['bondingCurveKey']}")
|
||||
|
||||
if "traderPublicKey" in token_data:
|
||||
print(f"Creator: {token_data['traderPublicKey']}")
|
||||
|
||||
# Virtual reserves
|
||||
if "vSolInBondingCurve" in token_data:
|
||||
v_sol = token_data['vSolInBondingCurve']
|
||||
print(f"Virtual SOL: {v_sol:.6f} SOL")
|
||||
|
||||
if "vTokensInBondingCurve" in token_data:
|
||||
v_tokens = token_data['vTokensInBondingCurve']
|
||||
print(f"Virtual Tokens: {v_tokens:,.0f}")
|
||||
|
||||
if "uri" in token_data:
|
||||
print(f"URI: {token_data['uri']}")
|
||||
|
||||
if "signature" in token_data:
|
||||
print(f"Signature: {token_data['signature']}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
def format_timestamp(timestamp):
|
||||
return datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
async def listen_for_new_tokens():
|
||||
@@ -39,27 +91,8 @@ async def listen_for_new_tokens():
|
||||
else:
|
||||
continue
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(
|
||||
f"New token created: {token_info.get('name')} ({token_info.get('symbol')})"
|
||||
)
|
||||
print("=" * 50)
|
||||
print(f"Address: {token_info.get('mint')}")
|
||||
print(f"Creator: {token_info.get('traderPublicKey')}")
|
||||
print(f"Initial Buy: {format_sol(token_info.get('initialBuy', 0))}")
|
||||
print(
|
||||
f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}"
|
||||
)
|
||||
print(f"Bonding Curve: {token_info.get('bondingCurveKey')}")
|
||||
print(
|
||||
f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}"
|
||||
)
|
||||
print(
|
||||
f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}"
|
||||
)
|
||||
print(f"Metadata URI: {token_info.get('uri')}")
|
||||
print(f"Signature: {token_info.get('signature')}")
|
||||
print("=" * 50)
|
||||
# Print token information in consistent format
|
||||
print_token_info(token_info)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
print("\nWebSocket connection closed. Reconnecting...")
|
||||
break
|
||||
|
||||
+122
-16
@@ -7,7 +7,7 @@ import struct
|
||||
|
||||
import base58
|
||||
import websockets
|
||||
from construct import Bytes, Flag, Int64ul, Struct
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
from solana.rpc.types import TxOpts
|
||||
@@ -36,6 +36,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
@@ -48,27 +49,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT = Struct(
|
||||
"""Bonding curve state parser with progressive field parsing.
|
||||
|
||||
Parses bonding curve account data progressively based on available bytes,
|
||||
making it forward-compatible with future schema versions.
|
||||
"""
|
||||
|
||||
# Base struct present in all versions
|
||||
_BASE_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
"""Parse bonding curve data progressively based on available bytes.
|
||||
|
||||
Args:
|
||||
data: Raw account data including discriminator
|
||||
|
||||
Raises:
|
||||
ValueError: If discriminator is invalid or data is too short
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Data too short to contain discriminator")
|
||||
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
# Parse base fields (always present)
|
||||
offset = 8
|
||||
base_data = data[offset:]
|
||||
parsed = self._BASE_STRUCT.parse(base_data)
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
# Calculate offset after base struct
|
||||
offset += self._BASE_STRUCT.sizeof()
|
||||
|
||||
# Parse creator if bytes remaining (added in V2)
|
||||
if len(data) >= offset + 32:
|
||||
creator_bytes = data[offset : offset + 32]
|
||||
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||
offset += 32
|
||||
else:
|
||||
self.creator = None
|
||||
|
||||
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||
if len(data) >= offset + 1:
|
||||
self.is_mayhem_mode = bool(data[offset])
|
||||
else:
|
||||
self.is_mayhem_mode = False
|
||||
|
||||
|
||||
async def get_pump_curve_state(
|
||||
@@ -126,11 +159,59 @@ def _find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
async def get_fee_recipient(
|
||||
client: AsyncClient, curve_state: BondingCurveState
|
||||
) -> Pubkey:
|
||||
"""Determine the correct fee recipient based on mayhem mode.
|
||||
|
||||
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||
instead of the standard fee recipient. This function checks the bonding curve state
|
||||
and returns the appropriate fee recipient.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client to fetch Global account data
|
||||
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||
|
||||
Returns:
|
||||
Appropriate fee recipient pubkey (mayhem or standard)
|
||||
"""
|
||||
if not curve_state.is_mayhem_mode:
|
||||
return PUMP_FEE
|
||||
|
||||
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
# Fallback to standard fee if Global account cannot be fetched
|
||||
return PUMP_FEE
|
||||
|
||||
data = response.value.data
|
||||
|
||||
# Parse reserved_fee_recipient from Global account
|
||||
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||
|
||||
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||
# Fallback if account data is too short
|
||||
return PUMP_FEE
|
||||
|
||||
reserved_fee_recipient_bytes = data[
|
||||
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||
|
||||
|
||||
async def buy_token(
|
||||
mint: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
amount: float,
|
||||
slippage: float = 0.25,
|
||||
max_retries=5,
|
||||
@@ -139,10 +220,12 @@ async def buy_token(
|
||||
payer = Keypair.from_bytes(private_key)
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
||||
associated_token_account = get_associated_token_address(
|
||||
payer.pubkey(), mint, token_program_id=token_program
|
||||
)
|
||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||
|
||||
# Fetch the token price
|
||||
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||
token_amount = amount / token_price_sol
|
||||
@@ -150,9 +233,12 @@ async def buy_token(
|
||||
# Calculate maximum SOL to spend with slippage
|
||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||
|
||||
# Determine fee recipient based on whether token uses mayhem mode
|
||||
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||
|
||||
accounts = [
|
||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -168,7 +254,7 @@ async def buy_token(
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=token_program, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -178,7 +264,7 @@ async def buy_token(
|
||||
AccountMeta(
|
||||
pubkey=_find_global_volume_accumulator(),
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||
@@ -200,14 +286,17 @@ async def buy_token(
|
||||
]
|
||||
|
||||
discriminator = struct.pack("<Q", 16927863322537952870)
|
||||
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||
track_volume_bytes = bytes([1, 1])
|
||||
data = (
|
||||
discriminator
|
||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||
+ struct.pack("<Q", max_amount_lamports)
|
||||
+ track_volume_bytes
|
||||
)
|
||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), mint
|
||||
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
|
||||
)
|
||||
msg = Message(
|
||||
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
||||
@@ -284,6 +373,7 @@ async def listen_for_create_transaction():
|
||||
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
||||
idl = load_idl(idl_path)
|
||||
create_discriminator = calculate_discriminator("global:create")
|
||||
create_v2_discriminator = calculate_discriminator("global:create_v2")
|
||||
|
||||
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
||||
subscription_message = json.dumps(
|
||||
@@ -336,11 +426,22 @@ async def listen_for_create_transaction():
|
||||
"<Q", ix_data[:8]
|
||||
)[0]
|
||||
|
||||
# Check which create instruction was used
|
||||
instruction_name = None
|
||||
token_program = None
|
||||
|
||||
if discriminator == create_discriminator:
|
||||
instruction_name = "create"
|
||||
token_program = SYSTEM_TOKEN_PROGRAM
|
||||
elif discriminator == create_v2_discriminator:
|
||||
instruction_name = "create_v2"
|
||||
token_program = TOKEN_2022_PROGRAM
|
||||
|
||||
if instruction_name:
|
||||
create_ix = next(
|
||||
instr
|
||||
for instr in idl["instructions"]
|
||||
if instr["name"] == "create"
|
||||
if instr["name"] == instruction_name
|
||||
)
|
||||
account_keys = [
|
||||
str(
|
||||
@@ -355,6 +456,9 @@ async def listen_for_create_transaction():
|
||||
ix_data, create_ix, account_keys
|
||||
)
|
||||
)
|
||||
# Add token program info to decoded args
|
||||
decoded_args["token_program"] = str(token_program)
|
||||
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
|
||||
return decoded_args
|
||||
|
||||
|
||||
@@ -372,6 +476,7 @@ async def main():
|
||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||
token_program = Pubkey.from_string(token_data["token_program"])
|
||||
|
||||
# Fetch the token price
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
@@ -383,12 +488,13 @@ async def main():
|
||||
slippage = 0.3 # 30% slippage tolerance
|
||||
|
||||
print(f"Bonding curve address: {bonding_curve}")
|
||||
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||
print(f"Token price: {token_price_sol:.10f} SOL")
|
||||
print(
|
||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||
)
|
||||
await buy_token(
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import struct
|
||||
|
||||
import base58
|
||||
import websockets
|
||||
from construct import Bytes, Flag, Int64ul, Struct
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
from solana.rpc.types import TxOpts
|
||||
@@ -55,6 +55,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
@@ -69,27 +70,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT = Struct(
|
||||
"""Bonding curve state parser with progressive field parsing.
|
||||
|
||||
Parses bonding curve account data progressively based on available bytes,
|
||||
making it forward-compatible with future schema versions.
|
||||
"""
|
||||
|
||||
# Base struct present in all versions
|
||||
_BASE_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32),
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
"""Parse bonding curve data progressively based on available bytes.
|
||||
|
||||
Args:
|
||||
data: Raw account data including discriminator
|
||||
|
||||
Raises:
|
||||
ValueError: If discriminator is invalid or data is too short
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Data too short to contain discriminator")
|
||||
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
# Parse base fields (always present)
|
||||
offset = 8
|
||||
base_data = data[offset:]
|
||||
parsed = self._BASE_STRUCT.parse(base_data)
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
# Calculate offset after base struct
|
||||
offset += self._BASE_STRUCT.sizeof()
|
||||
|
||||
# Parse creator if bytes remaining (added in V2)
|
||||
if len(data) >= offset + 32:
|
||||
creator_bytes = data[offset : offset + 32]
|
||||
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||
offset += 32
|
||||
else:
|
||||
self.creator = None
|
||||
|
||||
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||
if len(data) >= offset + 1:
|
||||
self.is_mayhem_mode = bool(data[offset])
|
||||
else:
|
||||
self.is_mayhem_mode = False
|
||||
|
||||
|
||||
async def get_pump_curve_state(
|
||||
@@ -147,6 +180,53 @@ def _find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
async def get_fee_recipient(
|
||||
client: AsyncClient, curve_state: BondingCurveState
|
||||
) -> Pubkey:
|
||||
"""Determine the correct fee recipient based on mayhem mode.
|
||||
|
||||
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||
instead of the standard fee recipient. This function checks the bonding curve state
|
||||
and returns the appropriate fee recipient.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client to fetch Global account data
|
||||
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||
|
||||
Returns:
|
||||
Appropriate fee recipient pubkey (mayhem or standard)
|
||||
"""
|
||||
if not curve_state.is_mayhem_mode:
|
||||
return PUMP_FEE
|
||||
|
||||
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
# Fallback to standard fee if Global account cannot be fetched
|
||||
return PUMP_FEE
|
||||
|
||||
data = response.value.data
|
||||
|
||||
# Parse reserved_fee_recipient from Global account
|
||||
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||
|
||||
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||
# Fallback if account data is too short
|
||||
return PUMP_FEE
|
||||
|
||||
reserved_fee_recipient_bytes = data[
|
||||
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||
|
||||
|
||||
def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
|
||||
"""
|
||||
Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption.
|
||||
@@ -169,6 +249,7 @@ async def buy_token(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
amount: float,
|
||||
slippage: float = 0.25,
|
||||
max_retries=5,
|
||||
@@ -177,10 +258,12 @@ async def buy_token(
|
||||
payer = Keypair.from_bytes(private_key)
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
||||
associated_token_account = get_associated_token_address(
|
||||
payer.pubkey(), mint, token_program_id=token_program
|
||||
)
|
||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||
|
||||
# Fetch the token price
|
||||
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||
token_amount = amount / token_price_sol
|
||||
@@ -188,9 +271,12 @@ async def buy_token(
|
||||
# Calculate maximum SOL to spend with slippage
|
||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||
|
||||
# Determine fee recipient based on whether token uses mayhem mode
|
||||
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||
|
||||
accounts = [
|
||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -206,7 +292,7 @@ async def buy_token(
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=token_program, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -216,7 +302,7 @@ async def buy_token(
|
||||
AccountMeta(
|
||||
pubkey=_find_global_volume_accumulator(),
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||
@@ -242,10 +328,11 @@ async def buy_token(
|
||||
discriminator
|
||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||
+ struct.pack("<Q", max_amount_lamports)
|
||||
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||
)
|
||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), mint
|
||||
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
|
||||
)
|
||||
|
||||
# CU OPTIMIZATION: Limit account data to 512KB (down from 64MB default)
|
||||
@@ -328,6 +415,7 @@ async def listen_for_create_transaction():
|
||||
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
||||
idl = load_idl(idl_path)
|
||||
create_discriminator = calculate_discriminator("global:create")
|
||||
create_v2_discriminator = calculate_discriminator("global:create_v2")
|
||||
|
||||
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
||||
subscription_message = json.dumps(
|
||||
@@ -380,11 +468,22 @@ async def listen_for_create_transaction():
|
||||
"<Q", ix_data[:8]
|
||||
)[0]
|
||||
|
||||
# Check which create instruction was used
|
||||
instruction_name = None
|
||||
token_program = None
|
||||
|
||||
if discriminator == create_discriminator:
|
||||
instruction_name = "create"
|
||||
token_program = SYSTEM_TOKEN_PROGRAM
|
||||
elif discriminator == create_v2_discriminator:
|
||||
instruction_name = "create_v2"
|
||||
token_program = TOKEN_2022_PROGRAM
|
||||
|
||||
if instruction_name:
|
||||
create_ix = next(
|
||||
instr
|
||||
for instr in idl["instructions"]
|
||||
if instr["name"] == "create"
|
||||
if instr["name"] == instruction_name
|
||||
)
|
||||
account_keys = [
|
||||
str(
|
||||
@@ -399,6 +498,9 @@ async def listen_for_create_transaction():
|
||||
ix_data, create_ix, account_keys
|
||||
)
|
||||
)
|
||||
# Add token program info to decoded args
|
||||
decoded_args["token_program"] = str(token_program)
|
||||
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
|
||||
return decoded_args
|
||||
|
||||
|
||||
@@ -415,6 +517,7 @@ async def main():
|
||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||
token_program = Pubkey.from_string(token_data["token_program"])
|
||||
|
||||
# Fetch the token price
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
@@ -426,13 +529,14 @@ async def main():
|
||||
slippage = 0.3 # 30% slippage tolerance
|
||||
|
||||
print(f"Bonding curve address: {bonding_curve}")
|
||||
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||
print(f"Token price: {token_price_sol:.10f} SOL")
|
||||
print(
|
||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||
)
|
||||
print("CU Optimization: Enabled (512KB account data limit)")
|
||||
await buy_token(
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
|
||||
import base58
|
||||
import grpc
|
||||
from construct import Bytes, Flag, Int64ul, Struct
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
from solana.rpc.types import TxOpts
|
||||
@@ -58,30 +58,63 @@ GEYSER_API_TOKEN = os.environ.get("GEYSER_API_TOKEN")
|
||||
AUTH_TYPE = os.environ.get("GEYSER_AUTH_TYPE", "x-token") # Default to x-token
|
||||
|
||||
PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728)
|
||||
PUMP_CREATE_V2_DISCRIMINATOR = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT = Struct(
|
||||
"""Bonding curve state parser with progressive field parsing.
|
||||
|
||||
Parses bonding curve account data progressively based on available bytes,
|
||||
making it forward-compatible with future schema versions.
|
||||
"""
|
||||
|
||||
# Base struct present in all versions
|
||||
_BASE_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
"""Parse bonding curve data progressively based on available bytes.
|
||||
|
||||
Args:
|
||||
data: Raw account data including discriminator
|
||||
|
||||
Raises:
|
||||
ValueError: If discriminator is invalid or data is too short
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Data too short to contain discriminator")
|
||||
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
# Parse base fields (always present)
|
||||
offset = 8
|
||||
base_data = data[offset:]
|
||||
parsed = self._BASE_STRUCT.parse(base_data)
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
# Calculate offset after base struct
|
||||
offset += self._BASE_STRUCT.sizeof()
|
||||
|
||||
# Parse creator if bytes remaining (added in V2)
|
||||
if len(data) >= offset + 32:
|
||||
creator_bytes = data[offset : offset + 32]
|
||||
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||
offset += 32
|
||||
else:
|
||||
self.creator = None
|
||||
|
||||
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||
if len(data) >= offset + 1:
|
||||
self.is_mayhem_mode = bool(data[offset])
|
||||
else:
|
||||
self.is_mayhem_mode = False
|
||||
|
||||
|
||||
async def get_pump_curve_state(
|
||||
@@ -139,6 +172,53 @@ def _find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
async def get_fee_recipient(
|
||||
client: AsyncClient, curve_state: BondingCurveState
|
||||
) -> Pubkey:
|
||||
"""Determine the correct fee recipient based on mayhem mode.
|
||||
|
||||
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||
instead of the standard fee recipient. This function checks the bonding curve state
|
||||
and returns the appropriate fee recipient.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client to fetch Global account data
|
||||
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||
|
||||
Returns:
|
||||
Appropriate fee recipient pubkey (mayhem or standard)
|
||||
"""
|
||||
if not curve_state.is_mayhem_mode:
|
||||
return PUMP_FEE
|
||||
|
||||
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
# Fallback to standard fee if Global account cannot be fetched
|
||||
return PUMP_FEE
|
||||
|
||||
data = response.value.data
|
||||
|
||||
# Parse reserved_fee_recipient from Global account
|
||||
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||
|
||||
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||
# Fallback if account data is too short
|
||||
return PUMP_FEE
|
||||
|
||||
reserved_fee_recipient_bytes = data[
|
||||
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||
|
||||
|
||||
async def create_geyser_connection():
|
||||
"""Establish a secure connection to the Geyser endpoint using the configured auth type."""
|
||||
if AUTH_TYPE == "x-token":
|
||||
@@ -232,7 +312,13 @@ async def listen_for_create_transaction_geyser():
|
||||
|
||||
# Check each instruction in the transaction
|
||||
for ix in msg.instructions:
|
||||
if not ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
|
||||
# Check which create instruction was used
|
||||
token_program = None
|
||||
if ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
|
||||
token_program = SYSTEM_TOKEN_PROGRAM
|
||||
elif ix.data.startswith(PUMP_CREATE_V2_DISCRIMINATOR):
|
||||
token_program = SYSTEM_TOKEN_2022_PROGRAM
|
||||
else:
|
||||
continue
|
||||
|
||||
# Found a create instruction
|
||||
@@ -240,6 +326,10 @@ async def listen_for_create_transaction_geyser():
|
||||
ix.data, msg.account_keys, ix.accounts
|
||||
)
|
||||
|
||||
# Add token program info to decoded args
|
||||
token_data["token_program"] = str(token_program)
|
||||
token_data["is_token_2022"] = (token_program == SYSTEM_TOKEN_2022_PROGRAM)
|
||||
|
||||
signature = base58.b58encode(
|
||||
bytes(update.transaction.transaction.signature)
|
||||
).decode()
|
||||
@@ -253,6 +343,7 @@ async def buy_token(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
amount: float,
|
||||
slippage: float = 0.25,
|
||||
max_retries=5,
|
||||
@@ -262,22 +353,30 @@ async def buy_token(
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
associated_token_account = get_associated_token_address(
|
||||
payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
|
||||
payer.pubkey(), mint, token_program
|
||||
)
|
||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||
|
||||
# Fetch the token price
|
||||
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||
# NOTE: Price calculation is commented out to speed up testing - using fixed values
|
||||
# For production, uncomment the lines below to:
|
||||
# 1. Calculate proper token amounts based on current price
|
||||
# 2. Detect mayhem mode and use correct fee recipient
|
||||
# curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||
# token_price_sol = calculate_pump_curve_price(curve_state)
|
||||
# token_amount = amount / token_price_sol
|
||||
token_amount = 100
|
||||
# fee_recipient = await get_fee_recipient(client, curve_state)
|
||||
|
||||
# Testing values - replace with code above for production
|
||||
token_amount = 100 # Fixed token amount
|
||||
fee_recipient = PUMP_FEE # Standard fee recipient (doesn't detect mayhem mode)
|
||||
|
||||
# Calculate maximum SOL to spend with slippage
|
||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||
|
||||
accounts = [
|
||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -293,7 +392,7 @@ async def buy_token(
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=token_program, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -303,7 +402,7 @@ async def buy_token(
|
||||
AccountMeta(
|
||||
pubkey=_find_global_volume_accumulator(),
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||
@@ -329,10 +428,11 @@ async def buy_token(
|
||||
discriminator
|
||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||
+ struct.pack("<Q", max_amount_lamports)
|
||||
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||
)
|
||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
|
||||
payer.pubkey(), payer.pubkey(), mint, token_program
|
||||
)
|
||||
msg = Message(
|
||||
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
||||
@@ -396,6 +496,7 @@ async def main():
|
||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||
token_program = Pubkey.from_string(token_data["token_program"])
|
||||
|
||||
# Fetch the token price
|
||||
# async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
@@ -407,12 +508,13 @@ async def main():
|
||||
slippage = 0.3 # 30% slippage tolerance
|
||||
|
||||
print(f"Bonding curve address: {bonding_curve}")
|
||||
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||
# print(f"Token price: {token_price_sol:.10f} SOL")
|
||||
print(
|
||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||
)
|
||||
await buy_token(
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
||||
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import struct
|
||||
|
||||
import base58
|
||||
from construct import Bytes, Flag, Int64ul, Struct
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
from solana.rpc.types import TxOpts
|
||||
@@ -18,7 +18,7 @@ from spl.token.instructions import get_associated_token_address
|
||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
||||
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
||||
TOKEN_DECIMALS = 6
|
||||
TOKEN_MINT = Pubkey.from_string("...")
|
||||
TOKEN_MINT = Pubkey.from_string("...") # Replace with actual token mint address
|
||||
|
||||
# Global constants
|
||||
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
@@ -30,6 +30,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
@@ -43,27 +44,59 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
_STRUCT = Struct(
|
||||
"""Bonding curve state parser with progressive field parsing.
|
||||
|
||||
Parses bonding curve account data progressively based on available bytes,
|
||||
making it forward-compatible with future schema versions.
|
||||
"""
|
||||
|
||||
# Base struct present in all versions
|
||||
_BASE_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data."""
|
||||
"""Parse bonding curve data progressively based on available bytes.
|
||||
|
||||
Args:
|
||||
data: Raw account data including discriminator
|
||||
|
||||
Raises:
|
||||
ValueError: If discriminator is invalid or data is too short
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Data too short to contain discriminator")
|
||||
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
# Parse base fields (always present)
|
||||
offset = 8
|
||||
base_data = data[offset:]
|
||||
parsed = self._BASE_STRUCT.parse(base_data)
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
# Calculate offset after base struct
|
||||
offset += self._BASE_STRUCT.sizeof()
|
||||
|
||||
# Parse creator if bytes remaining (added in V2)
|
||||
if len(data) >= offset + 32:
|
||||
creator_bytes = data[offset : offset + 32]
|
||||
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||
offset += 32
|
||||
else:
|
||||
self.creator = None
|
||||
|
||||
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||
if len(data) >= offset + 1:
|
||||
self.is_mayhem_mode = bool(data[offset])
|
||||
else:
|
||||
self.is_mayhem_mode = False
|
||||
|
||||
|
||||
async def get_pump_curve_state(
|
||||
@@ -84,11 +117,13 @@ def get_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]:
|
||||
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)
|
||||
|
||||
|
||||
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
def find_associated_bonding_curve(
|
||||
mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey
|
||||
) -> Pubkey:
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
bytes(bonding_curve),
|
||||
bytes(SYSTEM_TOKEN_PROGRAM),
|
||||
bytes(token_program_id),
|
||||
bytes(mint),
|
||||
],
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
|
||||
@@ -112,6 +147,53 @@ def _find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
async def get_fee_recipient(
|
||||
client: AsyncClient, curve_state: BondingCurveState
|
||||
) -> Pubkey:
|
||||
"""Determine the correct fee recipient based on mayhem mode.
|
||||
|
||||
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||
instead of the standard fee recipient. This function checks the bonding curve state
|
||||
and returns the appropriate fee recipient.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client to fetch Global account data
|
||||
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||
|
||||
Returns:
|
||||
Appropriate fee recipient pubkey (mayhem or standard)
|
||||
"""
|
||||
if not curve_state.is_mayhem_mode:
|
||||
return PUMP_FEE
|
||||
|
||||
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
# Fallback to standard fee if Global account cannot be fetched
|
||||
return PUMP_FEE
|
||||
|
||||
data = response.value.data
|
||||
|
||||
# Parse reserved_fee_recipient from Global account
|
||||
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||
|
||||
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||
# Fallback if account data is too short
|
||||
return PUMP_FEE
|
||||
|
||||
reserved_fee_recipient_bytes = data[
|
||||
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||
|
||||
|
||||
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
|
||||
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
@@ -128,11 +210,31 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
|
||||
return 0
|
||||
|
||||
|
||||
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||
mint_info = await client.get_account_info(mint_address)
|
||||
|
||||
if not mint_info.value:
|
||||
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||
|
||||
owner = mint_info.value.owner
|
||||
|
||||
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||
return SYSTEM_TOKEN_PROGRAM
|
||||
elif owner == TOKEN_2022_PROGRAM:
|
||||
return TOKEN_2022_PROGRAM
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||
)
|
||||
|
||||
|
||||
async def sell_token(
|
||||
mint: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
token_program_id: Pubkey,
|
||||
slippage: float = 0.25,
|
||||
max_retries=5,
|
||||
):
|
||||
@@ -140,7 +242,9 @@ async def sell_token(
|
||||
payer = Keypair.from_bytes(private_key)
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
||||
associated_token_account = get_associated_token_address(
|
||||
payer.pubkey(), mint, token_program_id
|
||||
)
|
||||
|
||||
# Get token balance
|
||||
token_balance = await get_token_balance(client, associated_token_account)
|
||||
@@ -150,7 +254,7 @@ async def sell_token(
|
||||
print("No tokens to sell.")
|
||||
return
|
||||
|
||||
# Fetch the token price
|
||||
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||
@@ -164,9 +268,12 @@ async def sell_token(
|
||||
print(f"Selling {token_balance_decimal} tokens")
|
||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||
|
||||
# Determine fee recipient based on whether token uses mayhem mode
|
||||
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||
|
||||
accounts = [
|
||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
@@ -187,8 +294,8 @@ async def sell_token(
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
),
|
||||
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||
), # Use dynamic token_program_id
|
||||
AccountMeta(
|
||||
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
|
||||
),
|
||||
@@ -208,10 +315,13 @@ async def sell_token(
|
||||
]
|
||||
|
||||
discriminator = struct.pack("<Q", 12502976635542562355)
|
||||
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||
track_volume_bytes = bytes([1, 1])
|
||||
data = (
|
||||
discriminator
|
||||
+ struct.pack("<Q", amount)
|
||||
+ struct.pack("<Q", min_sol_output)
|
||||
+ track_volume_bytes
|
||||
)
|
||||
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||
|
||||
@@ -248,8 +358,13 @@ async def sell_token(
|
||||
|
||||
async def main():
|
||||
# Replace these with the actual values for the token you want to sell
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||
|
||||
bonding_curve, _ = get_bonding_curve_address(TOKEN_MINT)
|
||||
associated_bonding_curve = find_associated_bonding_curve(TOKEN_MINT, bonding_curve)
|
||||
associated_bonding_curve = find_associated_bonding_curve(
|
||||
TOKEN_MINT, bonding_curve, token_program_id
|
||||
)
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||
@@ -261,7 +376,12 @@ async def main():
|
||||
print(f"Bonding curve address: {bonding_curve}")
|
||||
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
|
||||
await sell_token(
|
||||
TOKEN_MINT, bonding_curve, associated_bonding_curve, creator_vault, slippage
|
||||
TOKEN_MINT,
|
||||
bonding_curve,
|
||||
associated_bonding_curve,
|
||||
creator_vault,
|
||||
token_program_id,
|
||||
slippage,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ def create_buy_instruction(
|
||||
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True
|
||||
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=_find_user_volume_accumulator(user),
|
||||
|
||||
@@ -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())
|
||||
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
Solana PUMP AMM Interface
|
||||
This standalone script demonstrates how to buy tokens on the PUMP AMM (pAMM) protocol.
|
||||
It covers the complete flow from finding markets to executing buys with mayhem mode support.
|
||||
|
||||
This module provides functionality to interact with the PUMP AMM program on Solana, enabling:
|
||||
- Finding market addresses by token mint
|
||||
- Fetching and parsing market data from PUMP AMM pools
|
||||
- Calculating token prices in AMM pools
|
||||
- Creating associated token accounts (ATAs) idempotently
|
||||
- Buying tokens on the PUMP AMM with slippage protection
|
||||
Key concepts demonstrated:
|
||||
- Finding AMM pool addresses by token mint
|
||||
- Parsing binary account data structures
|
||||
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||
- Program Derived Address (PDA) derivation
|
||||
- WSOL wrapping (converting SOL to wrapped SOL for SPL token operations)
|
||||
- Volume tracking incentives integration
|
||||
- Transaction simulation before sending
|
||||
- Slippage protection mechanisms
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -34,31 +38,33 @@ from spl.token.instructions import (
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration constants
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
TOKEN_MINT = Pubkey.from_string("...")
|
||||
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||
SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept
|
||||
SLIPPAGE = 0.3 # 30% - maximum acceptable price movement during trade
|
||||
|
||||
TOKEN_DECIMALS = 6
|
||||
BUY_DISCRIMINATOR = bytes.fromhex(
|
||||
"66063d1201daebea"
|
||||
) # Program instruction identifier for the buy function
|
||||
# Token configuration
|
||||
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||
|
||||
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
|
||||
|
||||
# ============================================================================
|
||||
# Solana Program IDs and System Accounts
|
||||
# ============================================================================
|
||||
|
||||
# Solana system addresses and program IDs
|
||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
||||
)
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
@@ -67,61 +73,91 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||
)
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
|
||||
# ============================================================================
|
||||
# Constants for Account Structure Parsing
|
||||
# ============================================================================
|
||||
|
||||
# Pool account structure offsets
|
||||
POOL_DISCRIMINATOR_SIZE = 8
|
||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||
|
||||
# GlobalConfig structure offsets
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||
+ GLOBALCONFIG_ADMIN_SIZE
|
||||
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||
)
|
||||
|
||||
# Fee recipients
|
||||
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
|
||||
# Solana constants
|
||||
LAMPORTS_PER_SOL = 1_000_000_000
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 200_000 # Max compute units for transaction
|
||||
|
||||
# Buy-specific constants
|
||||
PROTOCOL_FEE_BUFFER = 0.1 # 10% buffer for protocol fees when wrapping SOL
|
||||
VOLUME_TRACKING_ENABLED = 1 # 1 = true, 0 = false
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Market Discovery
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_market_address_by_base_mint(
|
||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Find the market address for a given token mint.
|
||||
"""Find the AMM pool address for a specific token.
|
||||
|
||||
Searches for the AMM pool that contains the specified token mint as its base token
|
||||
by querying program accounts with a filter for the base_mint field.
|
||||
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||
that matches the given token mint address.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
base_mint_address: Address of the token mint you want to find the market for
|
||||
amm_program_id: Address of the AMM program
|
||||
client: Solana RPC client
|
||||
base_mint_address: Token mint to find the pool for
|
||||
amm_program_id: PUMP AMM program address
|
||||
|
||||
Returns:
|
||||
The Pubkey of the market (AMM pool) for the token
|
||||
Address of the AMM pool (market) for the token
|
||||
"""
|
||||
base_mint_bytes = bytes(base_mint_address)
|
||||
offset = (
|
||||
43 # Offset where the base_mint field is stored in the account data structure
|
||||
)
|
||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
||||
|
||||
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||
response = await client.get_program_accounts(
|
||||
amm_program_id, encoding="base64", filters=filters
|
||||
)
|
||||
|
||||
market_address = [account.pubkey for account in response.value][0]
|
||||
return market_address
|
||||
return response.value[0].pubkey
|
||||
|
||||
|
||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
"""Fetch and parse market data from the blockchain.
|
||||
"""Parse binary pool account data into a structured dictionary.
|
||||
|
||||
Retrieves and deserializes the binary data stored in the market account
|
||||
into a structured dictionary containing key market information.
|
||||
The pool account stores data in a specific binary format. This function
|
||||
deserializes that data based on the known structure.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
market_address: Address of the market (AMM pool) to fetch data for
|
||||
client: Solana RPC client
|
||||
market_address: Address of the pool account
|
||||
|
||||
Returns:
|
||||
Dictionary containing the parsed market data
|
||||
Dictionary with parsed pool data fields
|
||||
"""
|
||||
response = await client.get_account_info(market_address, encoding="base64")
|
||||
data = response.value.data
|
||||
parsed_data: dict = {}
|
||||
|
||||
# Start after the 8-byte discriminator
|
||||
offset = 8
|
||||
# Define the structure of the market account data
|
||||
offset = POOL_DISCRIMINATOR_SIZE
|
||||
|
||||
# Field definitions: (name, type)
|
||||
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||
fields = [
|
||||
("pool_bump", "u8"),
|
||||
("index", "u16"),
|
||||
@@ -141,39 +177,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||
offset += 32
|
||||
elif field_type in {"u64", "i64"}:
|
||||
value = (
|
||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
if field_type == "u64"
|
||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
||||
)
|
||||
parsed_data[field_name] = value
|
||||
format_char = "<Q" if field_type == "u64" else "<q"
|
||||
parsed_data[field_name] = struct.unpack(
|
||||
format_char, data[offset : offset + 8]
|
||||
)[0]
|
||||
offset += 8
|
||||
elif field_type == "u16":
|
||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
offset += 2
|
||||
elif field_type == "u8":
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = data[offset]
|
||||
offset += 1
|
||||
|
||||
return parsed_data
|
||||
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
||||
# ============================================================================
|
||||
# Program Derived Address (PDA) Derivation
|
||||
# ============================================================================
|
||||
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||
# They allow programs to own accounts without needing a private key.
|
||||
|
||||
Calculates the deterministic PDA that serves as the vault authority
|
||||
for a specific coin creator in the PUMP AMM protocol.
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the PDA for the coin creator's fee vault.
|
||||
|
||||
The creator vault collects fees on behalf of the token creator.
|
||||
This is a deterministic address that can be recalculated by anyone.
|
||||
|
||||
Args:
|
||||
coin_creator: Pubkey of the coin creator account
|
||||
coin_creator: Public key of the token creator
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived coin creator vault authority
|
||||
|
||||
Note:
|
||||
This vault is used to collect creator fees from token transactions
|
||||
PDA of the creator's vault authority
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator_vault", bytes(coin_creator)],
|
||||
@@ -183,13 +219,13 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_global_volume_accumulator() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the global volume accumulator.
|
||||
"""Derive the PDA for the global volume accumulator.
|
||||
|
||||
Calculates the deterministic PDA that tracks global trading volume
|
||||
across all pools in the PUMP AMM protocol.
|
||||
This account tracks total trading volume across all pools.
|
||||
Volume tracking is used for incentive programs and analytics.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived global volume accumulator account
|
||||
PDA of the global volume accumulator
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"global_volume_accumulator"],
|
||||
@@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey:
|
||||
|
||||
|
||||
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a user's volume accumulator.
|
||||
"""Derive the PDA for a user's volume accumulator.
|
||||
|
||||
Calculates the deterministic PDA that tracks trading volume
|
||||
for a specific user in the PUMP AMM protocol.
|
||||
Tracks individual user's trading volume, which may qualify them
|
||||
for incentives or rewards based on trading activity.
|
||||
|
||||
Args:
|
||||
user: Pubkey of the user account
|
||||
user: Public key of the user
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived user volume accumulator account
|
||||
PDA of the user's volume accumulator
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"user_volume_accumulator", bytes(user)],
|
||||
@@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_fee_config() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
||||
"""Derive the PDA for the fee configuration account.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived fee config account
|
||||
This account stores fee-related configuration for the AMM.
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||
@@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mayhem Mode Fee Handling
|
||||
# ============================================================================
|
||||
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||
|
||||
|
||||
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||
|
||||
When mayhem mode is active, fees are redirected to a special recipient
|
||||
stored in the GlobalConfig account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
Public key of the mayhem mode fee recipient
|
||||
"""
|
||||
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch GlobalConfig account"
|
||||
raise ValueError(msg)
|
||||
|
||||
data = response.value.data
|
||||
recipient_bytes = data[
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(recipient_bytes)
|
||||
|
||||
|
||||
async def get_pumpswap_fee_recipients(
|
||||
client: AsyncClient, pool: Pubkey
|
||||
) -> tuple[Pubkey, Pubkey]:
|
||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||
|
||||
This function checks if mayhem mode is enabled for the pool and returns
|
||||
the appropriate fee recipient and their WSOL token account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool: Address of the AMM pool
|
||||
|
||||
Returns:
|
||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||
"""
|
||||
response = await client.get_account_info(pool, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch pool account"
|
||||
raise ValueError(msg)
|
||||
|
||||
pool_data = response.value.data
|
||||
|
||||
# Check if mayhem mode flag exists and is enabled
|
||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||
)
|
||||
|
||||
# Select appropriate fee recipient
|
||||
if is_mayhem_mode:
|
||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||
else:
|
||||
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||
|
||||
# Get the fee recipient's WSOL token account
|
||||
fee_recipient_token_account = get_associated_token_address(
|
||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
return (fee_recipient, fee_recipient_token_account)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Price Calculation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def calculate_token_pool_price(
|
||||
client: AsyncClient,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
) -> float:
|
||||
"""Calculate the current price of tokens in an AMM pool.
|
||||
"""Calculate current token price from AMM pool balances.
|
||||
|
||||
Fetches the balance of tokens in the pool and calculates the price ratio
|
||||
between the base token and quote token (typically SOL).
|
||||
AMM price is determined by the ratio of tokens in the pool:
|
||||
price = quote_balance / base_balance
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pool_base_token_account: Address of the pool's base token account (your token)
|
||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
||||
client: Solana RPC client
|
||||
pool_base_token_account: Pool's token account (the token being priced)
|
||||
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||
|
||||
Returns:
|
||||
The price of the base token in terms of the quote token (SOL per token)
|
||||
Price in SOL per token
|
||||
"""
|
||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||
quote_balance_resp = await client.get_token_account_balance(
|
||||
pool_quote_token_account
|
||||
)
|
||||
|
||||
# Extract the UI amounts (human-readable with decimals)
|
||||
base_amount = float(base_balance_resp.value.ui_amount)
|
||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||
|
||||
token_price = quote_amount / base_amount
|
||||
return quote_amount / base_amount
|
||||
|
||||
return token_price
|
||||
|
||||
# ============================================================================
|
||||
# Token Buying
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||
mint_info = await client.get_account_info(mint_address)
|
||||
|
||||
if not mint_info.value:
|
||||
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||
|
||||
owner = mint_info.value.owner
|
||||
|
||||
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||
return SYSTEM_TOKEN_PROGRAM
|
||||
elif owner == TOKEN_2022_PROGRAM:
|
||||
return TOKEN_2022_PROGRAM
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||
)
|
||||
|
||||
|
||||
async def buy_pump_swap(
|
||||
client: AsyncClient,
|
||||
pump_fun_amm_market: Pubkey,
|
||||
market: Pubkey,
|
||||
payer: Keypair,
|
||||
base_mint: Pubkey,
|
||||
user_base_token_account: Pubkey,
|
||||
@@ -273,52 +406,64 @@ async def buy_pump_swap(
|
||||
pool_quote_token_account: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
sol_amount_to_spend: int,
|
||||
sol_amount_to_spend: float,
|
||||
slippage: float = 0.25,
|
||||
) -> str | None:
|
||||
"""Buy tokens on the PUMP AMM with slippage protection.
|
||||
"""Execute a token buy on the PUMP AMM with slippage protection.
|
||||
|
||||
Executes a token purchase on the PUMP AMM protocol, calculating the expected
|
||||
token amount based on the current pool price and applying slippage protection.
|
||||
This function:
|
||||
1. Calculates expected token output based on current price
|
||||
2. Wraps SOL into WSOL (required for SPL token operations)
|
||||
3. Constructs and simulates the transaction
|
||||
4. Sends the buy transaction if simulation succeeds
|
||||
|
||||
Why WSOL wrapping is needed:
|
||||
SPL tokens can only interact with other SPL tokens. Native SOL must be
|
||||
wrapped into WSOL (an SPL token representation of SOL) before trading.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pump_fun_amm_market: Address of the AMM market
|
||||
payer: Keypair of the transaction signer and token buyer
|
||||
base_mint: Address of the token mint being purchased
|
||||
user_base_token_account: Address of the user's token account for receiving purchased tokens
|
||||
user_quote_token_account: Address of the user's SOL token account
|
||||
pool_base_token_account: Address of the pool's token account for the token being purchased
|
||||
pool_quote_token_account: Address of the pool's SOL token account
|
||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
||||
sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports)
|
||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
||||
client: Solana RPC client
|
||||
market: AMM pool address
|
||||
payer: Wallet keypair for signing
|
||||
base_mint: Token mint address
|
||||
user_base_token_account: User's token account (for receiving tokens)
|
||||
user_quote_token_account: User's WSOL account
|
||||
pool_base_token_account: Pool's token account
|
||||
pool_quote_token_account: Pool's WSOL account
|
||||
coin_creator_vault_authority: Creator vault PDA
|
||||
coin_creator_vault_ata: Creator's WSOL account
|
||||
sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports)
|
||||
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||
|
||||
Returns:
|
||||
Transaction signature if successful, None otherwise
|
||||
"""
|
||||
# Calculate token price
|
||||
token_program_id = await get_token_program_id(client, base_mint)
|
||||
token_price_sol = await calculate_token_pool_price(
|
||||
client, pool_base_token_account, pool_quote_token_account
|
||||
)
|
||||
print(f"Token price in SOL: {token_price_sol:.10f} SOL")
|
||||
|
||||
# Calculate maximum SOL input with slippage protection
|
||||
# Calculate expected token amount and maximum SOL we're willing to spend
|
||||
base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
|
||||
slippage_factor = 1 + slippage
|
||||
max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
|
||||
max_sol_input = int((sol_amount_to_spend * (1 + slippage)) * LAMPORTS_PER_SOL)
|
||||
|
||||
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
|
||||
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
|
||||
|
||||
# Calculate required PDAs for volume tracking
|
||||
# Derive volume accumulator PDAs for incentive tracking
|
||||
global_volume_accumulator = find_global_volume_accumulator()
|
||||
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
||||
|
||||
# Define all accounts needed for the buy instruction
|
||||
# Get fee recipient based on mayhem mode
|
||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||
client, market
|
||||
)
|
||||
|
||||
# Build account list for buy instruction
|
||||
# Order matters! Must match the program's expected account layout
|
||||
accounts = [
|
||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||
@@ -327,15 +472,13 @@ async def buy_pump_swap(
|
||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
||||
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||
), # Use dynamic token_program_id
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
@@ -352,34 +495,43 @@ async def buy_pump_swap(
|
||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=global_volume_accumulator, is_signer=False, is_writable=True
|
||||
pubkey=global_volume_accumulator, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
||||
# Index 21: fee_config (readonly)
|
||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||
# Index 22: fee_program (readonly)
|
||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Instruction data format:
|
||||
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
|
||||
# All integers are little-endian (<)
|
||||
data = (
|
||||
BUY_DISCRIMINATOR
|
||||
+ struct.pack("<Q", base_amount_out)
|
||||
+ struct.pack("<Q", max_sol_input)
|
||||
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||
+ struct.pack("<Q", base_amount_out) # Expected token amount
|
||||
+ struct.pack("<Q", max_sol_input) # Maximum SOL to spend
|
||||
+ struct.pack("<B", VOLUME_TRACKING_ENABLED) # Enable volume tracking
|
||||
)
|
||||
|
||||
# Set compute budget to avoid transaction failures
|
||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||
|
||||
# Wrapping SOL
|
||||
# Create WSOL account if it doesn't exist
|
||||
# Note: WSOL always uses the standard Token program, never Token2022
|
||||
create_wsol_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM
|
||||
payer.pubkey(),
|
||||
payer.pubkey(),
|
||||
SOL,
|
||||
SYSTEM_TOKEN_PROGRAM, # WSOL always uses standard Token program
|
||||
)
|
||||
|
||||
# Calculate the amount of SOL to wrap(can also use `max_sol_input`), can get the SOL back by closing the WSOL account while selling
|
||||
pump_protocol_fees = sol_amount_to_spend * 0.1 # adding some buffer fees
|
||||
wrap_amount = int((sol_amount_to_spend + pump_protocol_fees) * LAMPORTS_PER_SOL)
|
||||
# Calculate amount to wrap (includes buffer for fees)
|
||||
wrap_amount = int(
|
||||
(sol_amount_to_spend * (1 + PROTOCOL_FEE_BUFFER)) * LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
# Transfer SOL to WSOL account and sync
|
||||
# This converts native SOL to the SPL token version (WSOL)
|
||||
transfer_sol_ix = transfer(
|
||||
TransferParams(
|
||||
from_pubkey=payer.pubkey(),
|
||||
@@ -387,20 +539,24 @@ async def buy_pump_swap(
|
||||
lamports=wrap_amount,
|
||||
)
|
||||
)
|
||||
|
||||
sync_native_ix = sync_native(
|
||||
SyncNativeParams(SYSTEM_TOKEN_PROGRAM, user_quote_token_account)
|
||||
SyncNativeParams(
|
||||
SYSTEM_TOKEN_PROGRAM, user_quote_token_account
|
||||
) # WSOL always uses standard Token program
|
||||
)
|
||||
|
||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), base_mint, SYSTEM_TOKEN_PROGRAM
|
||||
# Create token account for receiving purchased tokens
|
||||
create_token_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(),
|
||||
payer.pubkey(),
|
||||
base_mint,
|
||||
token_program_id, # Use dynamic token_program_id
|
||||
)
|
||||
|
||||
buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||
|
||||
# Build and sign transaction
|
||||
blockhash_resp = await client.get_latest_blockhash()
|
||||
recent_blockhash = blockhash_resp.value.blockhash
|
||||
|
||||
msg = Message.new_with_blockhash(
|
||||
[
|
||||
compute_limit_ix,
|
||||
@@ -408,64 +564,77 @@ async def buy_pump_swap(
|
||||
create_wsol_ata_ix,
|
||||
transfer_sol_ix,
|
||||
sync_native_ix,
|
||||
idempotent_ata_ix,
|
||||
create_token_ata_ix,
|
||||
buy_ix,
|
||||
],
|
||||
payer.pubkey(),
|
||||
recent_blockhash,
|
||||
blockhash_resp.value.blockhash,
|
||||
)
|
||||
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
tx_buy = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
# Optionally, you can simulate the transaction first and check for errors and get the compute units used
|
||||
simulation = await client.simulate_transaction(tx_buy)
|
||||
# Simulate first to catch errors before sending
|
||||
simulation = await client.simulate_transaction(tx)
|
||||
if simulation.value.err:
|
||||
print(f"Simulation error: {simulation.value.err}")
|
||||
return None
|
||||
|
||||
compute_units_used = simulation.value.units_consumed
|
||||
print(f"Simulation successful, compute units used: {compute_units_used}")
|
||||
print(
|
||||
f"Simulation successful, compute units used: {simulation.value.units_consumed}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Skip preflight since we already simulated (faster execution)
|
||||
tx_sig = await client.send_transaction(
|
||||
tx_buy,
|
||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
||||
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||
)
|
||||
|
||||
tx_hash = tx_sig.value
|
||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
print("Transaction confirmed")
|
||||
return tx_hash
|
||||
except Exception as e:
|
||||
print(f"Error sending transaction: {e!s}")
|
||||
print(f"Error: {e!s}")
|
||||
return None
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to execute the token buying process."""
|
||||
sol_amount_to_spend = 0.000001
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Execute the complete buy flow."""
|
||||
sol_amount_to_spend = 0.000001 # Amount of SOL to spend on the purchase
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
# Step 1: Find the pool address for our token
|
||||
market_address = await get_market_address_by_base_mint(
|
||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||
)
|
||||
|
||||
# Step 2: Parse pool data to get necessary accounts
|
||||
market_data = await get_market_data(client, market_address)
|
||||
|
||||
# Determine token program ID for the base mint
|
||||
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||
|
||||
# Step 3: Derive PDAs needed for the transaction
|
||||
coin_creator_vault_authority = find_coin_creator_vault(
|
||||
Pubkey.from_string(market_data["coin_creator"])
|
||||
)
|
||||
coin_creator_vault_ata = get_associated_token_address(
|
||||
coin_creator_vault_authority, SOL
|
||||
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Step 4: Execute the buy
|
||||
await buy_pump_swap(
|
||||
client,
|
||||
market_address,
|
||||
PAYER,
|
||||
TOKEN_MINT,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||
coin_creator_vault_authority,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""
|
||||
This module provides functionality to:
|
||||
- Find market addresses by token mint.
|
||||
- Fetch and parse market data from PUMP AMM pools.
|
||||
- Calculate token prices in AMM pools.
|
||||
- Create associated token accounts (ATAs) idempotently.
|
||||
- Sell tokens on the PUMP AMM with slippage protection.
|
||||
This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol.
|
||||
It covers the complete flow from finding markets to executing sells with mayhem mode support.
|
||||
|
||||
Key concepts demonstrated:
|
||||
- Finding AMM pool addresses by token mint
|
||||
- Parsing binary account data structures
|
||||
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||
- Program Derived Address (PDA) derivation
|
||||
- Transaction construction with compute budgets
|
||||
- Slippage protection mechanisms
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -26,31 +30,33 @@ from spl.token.instructions import get_associated_token_address
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration constants
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
TOKEN_MINT = Pubkey.from_string("...")
|
||||
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
|
||||
SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade
|
||||
|
||||
TOKEN_DECIMALS = 6
|
||||
SELL_DISCRIMINATOR = bytes.fromhex(
|
||||
"33e685a4017f83ad"
|
||||
) # Program instruction identifier for the sell function
|
||||
# Token configuration
|
||||
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||
|
||||
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
|
||||
|
||||
# ============================================================================
|
||||
# Solana Program IDs and System Accounts
|
||||
# ============================================================================
|
||||
|
||||
# Solana system addresses and program IDs
|
||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
||||
)
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
@@ -59,59 +65,87 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||
)
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
|
||||
# ============================================================================
|
||||
# Constants for Account Structure Parsing
|
||||
# ============================================================================
|
||||
|
||||
# Pool account structure offsets
|
||||
POOL_DISCRIMINATOR_SIZE = 8
|
||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||
|
||||
# GlobalConfig structure offsets
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||
+ GLOBALCONFIG_ADMIN_SIZE
|
||||
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||
)
|
||||
|
||||
# Fee recipients
|
||||
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
|
||||
# Solana constants
|
||||
LAMPORTS_PER_SOL = 1_000_000_000
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Market Discovery
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_market_address_by_base_mint(
|
||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Find the market address for a given token mint.
|
||||
"""Find the AMM pool address for a specific token.
|
||||
|
||||
Searches for the AMM pool that contains the specified token as its base token.
|
||||
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||
that matches the given token mint address.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
base_mint_address: Address of the token mint you want to find the market for
|
||||
amm_program_id: Address of the AMM program
|
||||
client: Solana RPC client
|
||||
base_mint_address: Token mint to find the pool for
|
||||
amm_program_id: PUMP AMM program address
|
||||
|
||||
Returns:
|
||||
The Pubkey of the market (AMM pool) for the token
|
||||
Address of the AMM pool (market) for the token
|
||||
"""
|
||||
base_mint_bytes = bytes(base_mint_address)
|
||||
offset = (
|
||||
43 # Offset where the base_mint field is stored in the account data structure
|
||||
)
|
||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
||||
|
||||
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||
response = await client.get_program_accounts(
|
||||
amm_program_id, encoding="base64", filters=filters
|
||||
)
|
||||
|
||||
market_address = [account.pubkey for account in response.value][0]
|
||||
return market_address
|
||||
return response.value[0].pubkey
|
||||
|
||||
|
||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
"""Fetch and parse market data from the blockchain.
|
||||
"""Parse binary pool account data into a structured dictionary.
|
||||
|
||||
Retrieves and deserializes the data stored in the market account.
|
||||
The pool account stores data in a specific binary format. This function
|
||||
deserializes that data based on the known structure.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
market_address: Address of the market (AMM pool) to fetch data for
|
||||
client: Solana RPC client
|
||||
market_address: Address of the pool account
|
||||
|
||||
Returns:
|
||||
Dictionary containing the parsed market data
|
||||
Dictionary with parsed pool data fields
|
||||
"""
|
||||
response = await client.get_account_info(market_address, encoding="base64")
|
||||
data = response.value.data
|
||||
parsed_data: dict = {}
|
||||
|
||||
# Start after the 8-byte discriminator
|
||||
offset = 8
|
||||
# Define the structure of the market account data
|
||||
offset = POOL_DISCRIMINATOR_SIZE
|
||||
|
||||
# Field definitions: (name, type)
|
||||
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||
fields = [
|
||||
("pool_bump", "u8"),
|
||||
("index", "u16"),
|
||||
@@ -131,39 +165,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||
offset += 32
|
||||
elif field_type in {"u64", "i64"}:
|
||||
value = (
|
||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
if field_type == "u64"
|
||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
||||
)
|
||||
parsed_data[field_name] = value
|
||||
format_char = "<Q" if field_type == "u64" else "<q"
|
||||
parsed_data[field_name] = struct.unpack(
|
||||
format_char, data[offset : offset + 8]
|
||||
)[0]
|
||||
offset += 8
|
||||
elif field_type == "u16":
|
||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
offset += 2
|
||||
elif field_type == "u8":
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = data[offset]
|
||||
offset += 1
|
||||
|
||||
return parsed_data
|
||||
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
||||
# ============================================================================
|
||||
# Program Derived Address (PDA) Derivation
|
||||
# ============================================================================
|
||||
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||
# They allow programs to own accounts without needing a private key.
|
||||
|
||||
Calculates the deterministic PDA that serves as the vault authority
|
||||
for a specific coin creator in the PUMP AMM protocol.
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the PDA for the coin creator's fee vault.
|
||||
|
||||
The creator vault collects fees on behalf of the token creator.
|
||||
This is a deterministic address that can be recalculated by anyone.
|
||||
|
||||
Args:
|
||||
coin_creator: Pubkey of the coin creator account
|
||||
coin_creator: Public key of the token creator
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived coin creator vault authority
|
||||
|
||||
Note:
|
||||
This vault is used to collect creator fees from token transactions
|
||||
PDA of the creator's vault authority
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator_vault", bytes(coin_creator)],
|
||||
@@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_fee_config() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
||||
"""Derive the PDA for the fee configuration account.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived fee config account
|
||||
This account stores fee-related configuration for the AMM.
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||
@@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mayhem Mode Fee Handling
|
||||
# ============================================================================
|
||||
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||
|
||||
|
||||
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||
|
||||
When mayhem mode is active, fees are redirected to a special recipient
|
||||
stored in the GlobalConfig account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
Public key of the mayhem mode fee recipient
|
||||
"""
|
||||
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch GlobalConfig account"
|
||||
raise ValueError(msg)
|
||||
|
||||
data = response.value.data
|
||||
recipient_bytes = data[
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(recipient_bytes)
|
||||
|
||||
|
||||
async def get_pumpswap_fee_recipients(
|
||||
client: AsyncClient, pool: Pubkey
|
||||
) -> tuple[Pubkey, Pubkey]:
|
||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||
|
||||
This function checks if mayhem mode is enabled for the pool and returns
|
||||
the appropriate fee recipient and their WSOL token account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool: Address of the AMM pool
|
||||
|
||||
Returns:
|
||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||
"""
|
||||
response = await client.get_account_info(pool, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch pool account"
|
||||
raise ValueError(msg)
|
||||
|
||||
pool_data = response.value.data
|
||||
|
||||
# Check if mayhem mode flag exists and is enabled
|
||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||
)
|
||||
|
||||
# Select appropriate fee recipient
|
||||
if is_mayhem_mode:
|
||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||
else:
|
||||
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||
|
||||
# Get the fee recipient's WSOL token account
|
||||
fee_recipient_token_account = get_associated_token_address(
|
||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
return (fee_recipient, fee_recipient_token_account)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Price Calculation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def calculate_token_pool_price(
|
||||
client: AsyncClient,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
) -> float:
|
||||
"""Calculate the price of tokens in the pool.
|
||||
"""Calculate current token price from AMM pool balances.
|
||||
|
||||
Fetches the balance of tokens in the pool and calculates the price ratio.
|
||||
AMM price is determined by the ratio of tokens in the pool:
|
||||
price = quote_balance / base_balance
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pool_base_token_account: Address of the pool's base token account (your token)
|
||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
||||
client: Solana RPC client
|
||||
pool_base_token_account: Pool's token account (the token being priced)
|
||||
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||
|
||||
Returns:
|
||||
The price of the base token in terms of the quote token (usually SOL)
|
||||
Price in SOL per token
|
||||
"""
|
||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||
quote_balance_resp = await client.get_token_account_balance(
|
||||
pool_quote_token_account
|
||||
)
|
||||
|
||||
# Extract the UI amounts (human-readable with decimals)
|
||||
base_amount = float(base_balance_resp.value.ui_amount)
|
||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||
|
||||
token_price = quote_amount / base_amount
|
||||
return quote_amount / base_amount
|
||||
|
||||
return token_price
|
||||
|
||||
# ============================================================================
|
||||
# Token Program Determination
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||
mint_info = await client.get_account_info(mint_address)
|
||||
|
||||
if not mint_info.value:
|
||||
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||
|
||||
owner = mint_info.value.owner
|
||||
|
||||
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||
return SYSTEM_TOKEN_PROGRAM
|
||||
elif owner == TOKEN_2022_PROGRAM:
|
||||
return TOKEN_2022_PROGRAM
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Associated Token Account (ATA) Creation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
||||
"""Create an instruction to create an Associated Token Account (ATA) if it doesn't exist.
|
||||
"""Create instruction to initialize a WSOL ATA if it doesn't exist.
|
||||
|
||||
This creates an instruction that will create an Associated Token Account for SOL
|
||||
if it doesn't already exist.
|
||||
Idempotent means this instruction won't fail if the ATA already exists.
|
||||
See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
||||
|
||||
Args:
|
||||
payer_pubkey: The public key of the account that will pay for the creation
|
||||
payer_pubkey: Account that will pay for ATA creation
|
||||
|
||||
Returns:
|
||||
An instruction to create the ATA
|
||||
Instruction to create the ATA
|
||||
"""
|
||||
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
|
||||
|
||||
@@ -239,20 +376,23 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# The data for creating an ATA idempotently is just a single byte with value 1
|
||||
# Check the details here:
|
||||
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
||||
data = bytes([1])
|
||||
# Instruction data: single byte with value 1 = CreateIdempotent
|
||||
return Instruction(
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Token Selling
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def sell_pump_swap(
|
||||
client: AsyncClient,
|
||||
pump_fun_amm_market: Pubkey,
|
||||
market: Pubkey,
|
||||
payer: Keypair,
|
||||
base_mint: Pubkey,
|
||||
token_program_id: Pubkey,
|
||||
user_base_token_account: Pubkey,
|
||||
user_quote_token_account: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
@@ -261,54 +401,61 @@ async def sell_pump_swap(
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
slippage: float = 0.25,
|
||||
) -> str | None:
|
||||
"""Sell tokens on the PUMP AMM.
|
||||
"""Execute a token sell on the PUMP AMM with slippage protection.
|
||||
|
||||
This function sells all tokens in the user's token account with the specified slippage tolerance.
|
||||
This function:
|
||||
1. Fetches current token balance and pool price
|
||||
2. Calculates minimum SOL output with slippage tolerance
|
||||
3. Constructs and sends the sell transaction
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pump_fun_amm_market: Address of the AMM market
|
||||
payer: Keypair of the transaction signer and token seller
|
||||
base_mint: Address of the token mint being sold
|
||||
user_base_token_account: Address of the user's token account for the token being sold
|
||||
user_quote_token_account: Address of the user's SOL token account
|
||||
pool_base_token_account: Address of the pool's token account for the token being sold
|
||||
pool_quote_token_account: Address of the pool's SOL token account
|
||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
||||
client: Solana RPC client
|
||||
market: AMM pool address
|
||||
payer: Wallet keypair for signing
|
||||
base_mint: Token mint address
|
||||
user_base_token_account: User's token account
|
||||
user_quote_token_account: User's WSOL account
|
||||
pool_base_token_account: Pool's token account
|
||||
pool_quote_token_account: Pool's WSOL account
|
||||
coin_creator_vault_authority: Creator vault PDA
|
||||
coin_creator_vault_ata: Creator's WSOL account
|
||||
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||
|
||||
Returns:
|
||||
Transaction signature if successful, None otherwise
|
||||
"""
|
||||
# Get token balance
|
||||
token_balance = int(
|
||||
(await client.get_token_account_balance(user_base_token_account)).value.amount
|
||||
)
|
||||
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
||||
|
||||
print(f"Token balance: {token_balance_decimal}")
|
||||
|
||||
if token_balance == 0:
|
||||
print("No tokens to sell.")
|
||||
return None
|
||||
|
||||
# Calculate token price
|
||||
token_price_sol = await calculate_token_pool_price(
|
||||
client, pool_base_token_account, pool_quote_token_account
|
||||
)
|
||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||
|
||||
# Calculate minimum SOL output with slippage protection
|
||||
amount = token_balance
|
||||
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
|
||||
slippage_factor = 1 - slippage
|
||||
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
|
||||
# Calculate minimum SOL we're willing to receive (slippage protection)
|
||||
expected_sol_output = token_balance_decimal * token_price_sol
|
||||
min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL)
|
||||
|
||||
print(f"Selling {token_balance_decimal} tokens")
|
||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||
|
||||
# Define all accounts needed for the sell instruction
|
||||
# Get fee recipient based on mayhem mode
|
||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||
client, market
|
||||
)
|
||||
|
||||
# Build account list for sell instruction
|
||||
# Order matters! Must match the program's expected account layout
|
||||
accounts = [
|
||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||
@@ -317,15 +464,13 @@ async def sell_pump_swap(
|
||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
||||
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||
), # Use dynamic token_program_id
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
@@ -341,76 +486,88 @@ async def sell_pump_swap(
|
||||
AccountMeta(
|
||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||
),
|
||||
# Index 19: fee_config (readonly)
|
||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||
# Index 20: fee_program (readonly)
|
||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
|
||||
# All integers are little-endian (<)
|
||||
data = (
|
||||
SELL_DISCRIMINATOR
|
||||
+ struct.pack("<Q", amount)
|
||||
+ struct.pack("<Q", min_sol_output)
|
||||
+ struct.pack("<Q", token_balance) # Amount to sell
|
||||
+ struct.pack("<Q", min_sol_output) # Minimum SOL to receive
|
||||
)
|
||||
|
||||
# Set compute budget to avoid transaction failures
|
||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||
|
||||
create_ata_ix = create_ata_idempotent_ix(
|
||||
payer_pubkey=payer.pubkey(),
|
||||
)
|
||||
# Ensure WSOL ATA exists (needed to receive SOL from sell)
|
||||
create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
|
||||
|
||||
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||
|
||||
# Build and sign transaction
|
||||
blockhash_resp = await client.get_latest_blockhash()
|
||||
recent_blockhash = blockhash_resp.value.blockhash
|
||||
|
||||
msg = Message.new_with_blockhash(
|
||||
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
|
||||
payer.pubkey(),
|
||||
recent_blockhash,
|
||||
blockhash_resp.value.blockhash,
|
||||
)
|
||||
|
||||
tx_sell = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
try:
|
||||
# Skip preflight to send transaction faster (useful in competitive scenarios)
|
||||
tx_sig = await client.send_transaction(
|
||||
tx_sell,
|
||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
||||
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||
)
|
||||
|
||||
tx_hash = tx_sig.value
|
||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
print("Transaction confirmed")
|
||||
return tx_hash
|
||||
except Exception as e:
|
||||
print(f"Error sending transaction: {e!s}")
|
||||
print(f"Error: {e!s}")
|
||||
return None
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to execute the token selling process."""
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Execute the complete sell flow."""
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
# Step 1: Find the pool address for our token
|
||||
market_address = await get_market_address_by_base_mint(
|
||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||
)
|
||||
|
||||
# Step 2: Parse pool data to get necessary accounts
|
||||
market_data = await get_market_data(client, market_address)
|
||||
|
||||
# Determine token program ID for the base mint
|
||||
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||
|
||||
# Step 3: Derive PDAs needed for the transaction
|
||||
coin_creator_vault_authority = find_coin_creator_vault(
|
||||
Pubkey.from_string(market_data["coin_creator"])
|
||||
)
|
||||
coin_creator_vault_ata = get_associated_token_address(
|
||||
coin_creator_vault_authority, SOL
|
||||
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Step 4: Execute the sell
|
||||
await sell_pump_swap(
|
||||
client,
|
||||
market_address,
|
||||
PAYER,
|
||||
TOKEN_MINT,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
||||
token_program_id,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||
coin_creator_vault_authority,
|
||||
|
||||
+11
-4
@@ -34,12 +34,19 @@ class AccountCleanupManager:
|
||||
self.use_priority_fee = use_priority_fee
|
||||
self.close_with_force_burn = force_burn
|
||||
|
||||
async def cleanup_ata(self, mint: Pubkey) -> None:
|
||||
async def cleanup_ata(self, mint: Pubkey, token_program_id: Pubkey | None = None) -> None:
|
||||
"""
|
||||
Attempt to burn any remaining tokens and close the ATA.
|
||||
Skips if account doesn't exist or is already empty/closed.
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||
"""
|
||||
ata = self.wallet.get_associated_token_address(mint)
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
|
||||
ata = self.wallet.get_associated_token_address(mint, token_program_id)
|
||||
solana_client = await self.client.get_client()
|
||||
|
||||
priority_fee = (
|
||||
@@ -70,7 +77,7 @@ class AccountCleanupManager:
|
||||
mint=mint,
|
||||
owner=self.wallet.pubkey,
|
||||
amount=balance,
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
program_id=token_program_id,
|
||||
)
|
||||
)
|
||||
instructions.append(burn_ix)
|
||||
@@ -89,7 +96,7 @@ class AccountCleanupManager:
|
||||
account=ata,
|
||||
dest=self.wallet.pubkey,
|
||||
owner=self.wallet.pubkey,
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
program_id=token_program_id,
|
||||
)
|
||||
)
|
||||
instructions.append(close_ix)
|
||||
|
||||
@@ -20,6 +20,7 @@ async def handle_cleanup_after_failure(
|
||||
client,
|
||||
wallet,
|
||||
mint,
|
||||
token_program_id,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
@@ -30,13 +31,14 @@ async def handle_cleanup_after_failure(
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
await manager.cleanup_ata(mint)
|
||||
await manager.cleanup_ata(mint, token_program_id)
|
||||
|
||||
|
||||
async def handle_cleanup_after_sell(
|
||||
client,
|
||||
wallet,
|
||||
mint,
|
||||
token_program_id,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
@@ -47,13 +49,14 @@ async def handle_cleanup_after_sell(
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
await manager.cleanup_ata(mint)
|
||||
await manager.cleanup_ata(mint, token_program_id)
|
||||
|
||||
|
||||
async def handle_cleanup_post_session(
|
||||
client,
|
||||
wallet,
|
||||
mints,
|
||||
token_program_ids,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
@@ -64,5 +67,5 @@ async def handle_cleanup_post_session(
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
for mint in mints:
|
||||
await manager.cleanup_ata(mint)
|
||||
for mint, token_program_id in zip(mints, token_program_ids):
|
||||
await manager.cleanup_ata(mint, token_program_id)
|
||||
|
||||
@@ -23,6 +23,9 @@ SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("1111111111111111111111111111
|
||||
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
)
|
||||
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||
)
|
||||
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
@@ -42,6 +45,7 @@ class SystemAddresses:
|
||||
# Reference the module-level constants
|
||||
SYSTEM_PROGRAM = SYSTEM_PROGRAM
|
||||
TOKEN_PROGRAM = TOKEN_PROGRAM
|
||||
TOKEN_2022_PROGRAM = TOKEN_2022_PROGRAM
|
||||
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
|
||||
RENT = RENT
|
||||
SOL_MINT = SOL_MINT
|
||||
@@ -56,6 +60,7 @@ class SystemAddresses:
|
||||
return {
|
||||
"system_program": cls.SYSTEM_PROGRAM,
|
||||
"token_program": cls.TOKEN_PROGRAM,
|
||||
"token_2022_program": cls.TOKEN_2022_PROGRAM,
|
||||
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
|
||||
"rent": cls.RENT,
|
||||
"sol_mint": cls.SOL_MINT,
|
||||
|
||||
+9
-2
@@ -7,6 +7,8 @@ from solders.keypair import Keypair
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from core.pubkeys import SystemAddresses
|
||||
|
||||
|
||||
class Wallet:
|
||||
"""Manages a Solana wallet for trading operations."""
|
||||
@@ -30,16 +32,21 @@ class Wallet:
|
||||
"""Get the keypair for signing transactions."""
|
||||
return self._keypair
|
||||
|
||||
def get_associated_token_address(self, mint: Pubkey) -> Pubkey:
|
||||
def get_associated_token_address(
|
||||
self, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Get the associated token account address for a mint.
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||
|
||||
Returns:
|
||||
Associated token account address
|
||||
"""
|
||||
return get_associated_token_address(self.pubkey, mint)
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
return get_associated_token_address(self.pubkey, mint, token_program_id)
|
||||
|
||||
@staticmethod
|
||||
def _load_keypair(private_key: str) -> Keypair:
|
||||
|
||||
@@ -45,6 +45,8 @@ class TokenInfo:
|
||||
user: Pubkey | None = None
|
||||
creator: Pubkey | None = None
|
||||
creator_vault: Pubkey | None = None
|
||||
token_program_id: Pubkey | None = None # Token or Token2022 program
|
||||
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
|
||||
|
||||
# Metadata
|
||||
creation_timestamp: float | None = None
|
||||
|
||||
@@ -147,17 +147,22 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
)
|
||||
return quote_vault
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
def derive_user_token_account(
|
||||
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
return get_associated_token_address(user, mint, token_program_id)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||
@@ -296,6 +301,13 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
# Determine token program to use
|
||||
token_program_id = (
|
||||
token_info.token_program_id
|
||||
if token_info.token_program_id
|
||||
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||
)
|
||||
|
||||
# Use global_config from TokenInfo if available, otherwise use default
|
||||
global_config = (
|
||||
token_info.global_config
|
||||
@@ -316,12 +328,12 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"global_config": global_config,
|
||||
"platform_config": platform_config,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
|
||||
"base_vault": additional_accounts["base_vault"],
|
||||
"quote_vault": additional_accounts["quote_vault"],
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": SystemAddresses.SOL_MINT,
|
||||
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"base_token_program": token_program_id,
|
||||
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": LetsBonkAddresses.PROGRAM,
|
||||
@@ -351,6 +363,13 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
# Determine token program to use
|
||||
token_program_id = (
|
||||
token_info.token_program_id
|
||||
if token_info.token_program_id
|
||||
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||
)
|
||||
|
||||
# Use global_config from TokenInfo if available, otherwise use default
|
||||
global_config = (
|
||||
token_info.global_config
|
||||
@@ -371,12 +390,12 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"global_config": global_config,
|
||||
"platform_config": platform_config,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
|
||||
"base_vault": additional_accounts["base_vault"],
|
||||
"quote_vault": additional_accounts["quote_vault"],
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": SystemAddresses.SOL_MINT,
|
||||
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"base_token_program": token_program_id,
|
||||
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": LetsBonkAddresses.PROGRAM,
|
||||
|
||||
@@ -194,14 +194,21 @@ class LetsBonkCurveManager(CurveManager):
|
||||
}
|
||||
|
||||
# Calculate additional metrics
|
||||
if pool_data["virtual_base"] > 0:
|
||||
pool_data["price_per_token"] = (
|
||||
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
# Validate reserves are positive before calculating price
|
||||
if pool_data["virtual_base"] <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price"
|
||||
)
|
||||
else:
|
||||
pool_data["price_per_token"] = 0
|
||||
if pool_data["virtual_quote"] <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid virtual_quote: {pool_data['virtual_quote']} - cannot calculate price"
|
||||
)
|
||||
|
||||
pool_data["price_per_token"] = (
|
||||
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.transaction import VersionedTransaction
|
||||
|
||||
from core.pubkeys import SystemAddresses
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.idl_parser import IDLParser
|
||||
@@ -114,6 +115,15 @@ class LetsBonkEventParser(EventParser):
|
||||
}:
|
||||
return None
|
||||
|
||||
# Determine token program based on instruction variant
|
||||
instruction_name = decoded["instruction_name"]
|
||||
is_token_2022 = instruction_name == "initialize_with_token_2022"
|
||||
token_program_id = (
|
||||
SystemAddresses.TOKEN_2022_PROGRAM
|
||||
if is_token_2022
|
||||
else SystemAddresses.TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
args = decoded.get("args", {})
|
||||
|
||||
# Extract MintParams from the decoded arguments
|
||||
@@ -174,6 +184,7 @@ class LetsBonkEventParser(EventParser):
|
||||
platform_config=platform_config,
|
||||
user=creator,
|
||||
creator=creator,
|
||||
token_program_id=token_program_id,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
|
||||
@@ -75,12 +75,19 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# Determine token program to use
|
||||
token_program_id = (
|
||||
token_info.token_program_id
|
||||
if token_info.token_program_id
|
||||
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||
)
|
||||
|
||||
# 1. Create idempotent ATA for base token
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
||||
token_program_id, # token program (dynamic for token2022 support)
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
@@ -151,10 +158,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
@@ -306,10 +313,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
|
||||
@@ -31,6 +31,12 @@ class PumpFunAddresses:
|
||||
FEE: Final[Pubkey] = Pubkey.from_string(
|
||||
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
|
||||
)
|
||||
# Mayhem mode fee recipient (hardcoded to avoid RPC calls)
|
||||
# To check if this address is up-to-date, fetch Global account data at offset 483
|
||||
# from the pump.fun Global account: 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf
|
||||
MAYHEM_FEE: Final[Pubkey] = Pubkey.from_string(
|
||||
"GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"
|
||||
)
|
||||
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
|
||||
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
|
||||
)
|
||||
@@ -139,17 +145,22 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
)
|
||||
return bonding_curve
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
def derive_user_token_account(
|
||||
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
return get_associated_token_address(user, mint, token_program_id)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get pump.fun-specific additional accounts needed for trading.
|
||||
@@ -177,7 +188,7 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
# Derive associated bonding curve if not provided
|
||||
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
||||
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
||||
token_info.mint, token_info.bonding_curve
|
||||
token_info.mint, token_info.bonding_curve, token_info.token_program_id
|
||||
)
|
||||
|
||||
# Derive creator vault if not provided but creator is available
|
||||
@@ -187,21 +198,25 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
return accounts
|
||||
|
||||
def derive_associated_bonding_curve(
|
||||
self, mint: Pubkey, bonding_curve: Pubkey
|
||||
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
bytes(bonding_curve),
|
||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
||||
bytes(token_program_id),
|
||||
bytes(mint),
|
||||
],
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
@@ -249,6 +264,19 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"""
|
||||
return PumpFunAddresses.find_fee_config()
|
||||
|
||||
def get_fee_recipient(self, token_info: TokenInfo) -> Pubkey:
|
||||
"""Get the correct fee recipient based on mayhem mode.
|
||||
|
||||
Args:
|
||||
token_info: Token information with is_mayhem_mode flag
|
||||
|
||||
Returns:
|
||||
Fee recipient address (mayhem or standard)
|
||||
"""
|
||||
if token_info.is_mayhem_mode:
|
||||
return PumpFunAddresses.MAYHEM_FEE
|
||||
return PumpFunAddresses.FEE
|
||||
|
||||
def get_buy_instruction_accounts(
|
||||
self, token_info: TokenInfo, user: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
@@ -263,9 +291,19 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
# Determine token program to use
|
||||
token_program_id = (
|
||||
token_info.token_program_id
|
||||
if token_info.token_program_id
|
||||
else SystemAddresses.TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Determine fee recipient based on mayhem mode
|
||||
fee_recipient = self.get_fee_recipient(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpFunAddresses.GLOBAL,
|
||||
"fee": PumpFunAddresses.FEE,
|
||||
"fee": fee_recipient,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get(
|
||||
"bonding_curve", token_info.bonding_curve
|
||||
@@ -273,10 +311,12 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"associated_bonding_curve": additional_accounts.get(
|
||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||
),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user_token_account": self.derive_user_token_account(
|
||||
user, token_info.mint, token_program_id
|
||||
),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"token_program": token_program_id,
|
||||
"creator_vault": additional_accounts.get(
|
||||
"creator_vault", token_info.creator_vault
|
||||
),
|
||||
@@ -302,9 +342,19 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
# Determine token program to use
|
||||
token_program_id = (
|
||||
token_info.token_program_id
|
||||
if token_info.token_program_id
|
||||
else SystemAddresses.TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Determine fee recipient based on mayhem mode
|
||||
fee_recipient = self.get_fee_recipient(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpFunAddresses.GLOBAL,
|
||||
"fee": PumpFunAddresses.FEE,
|
||||
"fee": fee_recipient,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get(
|
||||
"bonding_curve", token_info.bonding_curve
|
||||
@@ -312,13 +362,15 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"associated_bonding_curve": additional_accounts.get(
|
||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||
),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user_token_account": self.derive_user_token_account(
|
||||
user, token_info.mint, token_program_id
|
||||
),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||
"creator_vault": additional_accounts.get(
|
||||
"creator_vault", token_info.creator_vault
|
||||
),
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"token_program": token_program_id,
|
||||
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpFunAddresses.PROGRAM,
|
||||
"fee_config": self.derive_fee_config(),
|
||||
|
||||
@@ -190,20 +190,28 @@ class PumpFunCurveManager(CurveManager):
|
||||
"token_total_supply": decoded_curve_state.get("token_total_supply", 0),
|
||||
"complete": decoded_curve_state.get("complete", False),
|
||||
"creator": decoded_curve_state.get("creator", ""),
|
||||
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
|
||||
}
|
||||
|
||||
# Calculate additional metrics
|
||||
if curve_data["virtual_token_reserves"] > 0:
|
||||
curve_data["price_per_token"] = (
|
||||
(
|
||||
curve_data["virtual_sol_reserves"]
|
||||
/ curve_data["virtual_token_reserves"]
|
||||
)
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
# Validate reserves are positive before calculating price
|
||||
if curve_data["virtual_token_reserves"] <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price"
|
||||
)
|
||||
else:
|
||||
curve_data["price_per_token"] = 0
|
||||
if curve_data["virtual_sol_reserves"] <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid virtual_sol_reserves: {curve_data['virtual_sol_reserves']} - cannot calculate price"
|
||||
)
|
||||
|
||||
curve_data["price_per_token"] = (
|
||||
(
|
||||
curve_data["virtual_sol_reserves"]
|
||||
/ curve_data["virtual_token_reserves"]
|
||||
)
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
# Add convenience decimal fields
|
||||
curve_data["token_reserves_decimal"] = (
|
||||
|
||||
@@ -47,6 +47,16 @@ class PumpFunEventParser(EventParser):
|
||||
"<Q", self._create_instruction_discriminator_bytes
|
||||
)[0]
|
||||
|
||||
# Support for token2022 (create_v2 instruction)
|
||||
self._create_v2_instruction_discriminator_bytes = instruction_discriminators.get(
|
||||
"create_v2"
|
||||
)
|
||||
self._create_v2_instruction_discriminator = (
|
||||
struct.unpack("<Q", self._create_v2_instruction_discriminator_bytes)[0]
|
||||
if self._create_v2_instruction_discriminator_bytes
|
||||
else None
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Pump.Fun event parser initialized with IDL-based event and instruction parsing"
|
||||
)
|
||||
@@ -56,6 +66,10 @@ class PumpFunEventParser(EventParser):
|
||||
logger.info(
|
||||
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}"
|
||||
)
|
||||
if self._create_v2_instruction_discriminator_bytes:
|
||||
logger.info(
|
||||
f"create_v2 instruction discriminator: {self._create_v2_instruction_discriminator_bytes.hex()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
@@ -74,8 +88,12 @@ class PumpFunEventParser(EventParser):
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# Check if this is a token creation transaction
|
||||
if not any("Program log: Instruction: Create" in log for log in logs):
|
||||
# Check if this is a token creation transaction (create or create_v2 for token2022)
|
||||
if not any(
|
||||
"Program log: Instruction: Create" in log
|
||||
or "Program log: Instruction: Create_v2" in log
|
||||
for log in logs
|
||||
):
|
||||
return None
|
||||
|
||||
# Skip swaps as the first condition may pass them
|
||||
@@ -92,9 +110,10 @@ class PumpFunEventParser(EventParser):
|
||||
|
||||
# First, collect all Program data entries and note when Create instruction happens
|
||||
for i, log in enumerate(logs):
|
||||
if "Program log: Instruction: Create" in log:
|
||||
if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log:
|
||||
create_instruction_found = True
|
||||
logger.info(f"📝 Found Create instruction at log index {i}")
|
||||
instruction_type = "Create_v2" if "Create_v2" in log else "Create"
|
||||
logger.info(f"📝 Found {instruction_type} instruction at log index {i}")
|
||||
elif "Program data:" in log:
|
||||
# Extract base64 encoded event data
|
||||
encoded_data = log.split("Program data: ")[1].strip()
|
||||
@@ -104,7 +123,7 @@ class PumpFunEventParser(EventParser):
|
||||
)
|
||||
|
||||
if not create_instruction_found:
|
||||
logger.info("❌ No Create instruction found in logs")
|
||||
logger.info("❌ No Create or Create_v2 instruction found in logs")
|
||||
return None
|
||||
|
||||
if not program_data_entries:
|
||||
@@ -224,9 +243,13 @@ class PumpFunEventParser(EventParser):
|
||||
logger.info(f"❌ Failed to convert pubkey fields: {e}")
|
||||
continue
|
||||
|
||||
# Derive additional addresses
|
||||
# Derive additional addresses (default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2)
|
||||
# Note: As of recent pump.fun updates, all tokens are created via create_v2 instruction
|
||||
# This is a technical limitation of logs listener - cannot distinguish create vs create_v2
|
||||
# Risk is low since pump.fun now defaults to Token2022 for all new tokens
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
associated_bonding_curve = self._derive_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
mint, bonding_curve, token_program_id
|
||||
)
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
@@ -245,6 +268,7 @@ class PumpFunEventParser(EventParser):
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
token_program_id=token_program_id,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
@@ -274,9 +298,16 @@ class PumpFunEventParser(EventParser):
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(
|
||||
self._create_instruction_discriminator_bytes
|
||||
# Determine which create instruction (standard or v2 for token2022)
|
||||
is_create_v2 = False
|
||||
if instruction_data.startswith(self._create_instruction_discriminator_bytes):
|
||||
is_create_v2 = False
|
||||
elif (
|
||||
self._create_v2_instruction_discriminator_bytes
|
||||
and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes)
|
||||
):
|
||||
is_create_v2 = True
|
||||
else:
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -293,7 +324,8 @@ class PumpFunEventParser(EventParser):
|
||||
decoded = self._idl_parser.decode_instruction(
|
||||
instruction_data, account_keys, accounts
|
||||
)
|
||||
if not decoded or decoded["instruction_name"] != "create":
|
||||
expected_instruction_name = "create_v2" if is_create_v2 else "create"
|
||||
if not decoded or decoded["instruction_name"] != expected_instruction_name:
|
||||
return None
|
||||
|
||||
args = decoded.get("args", {})
|
||||
@@ -315,6 +347,13 @@ class PumpFunEventParser(EventParser):
|
||||
)
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
# Determine token program based on instruction type
|
||||
token_program_id = (
|
||||
SystemAddresses.TOKEN_2022_PROGRAM
|
||||
if is_create_v2
|
||||
else SystemAddresses.TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
return TokenInfo(
|
||||
name=args.get("name", ""),
|
||||
symbol=args.get("symbol", ""),
|
||||
@@ -326,6 +365,7 @@ class PumpFunEventParser(EventParser):
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
token_program_id=token_program_id,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
@@ -435,14 +475,19 @@ class PumpFunEventParser(EventParser):
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
# Check for create discriminator
|
||||
# Check for create or create_v2 discriminator
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
if (
|
||||
discriminator
|
||||
== self._create_instruction_discriminator
|
||||
):
|
||||
is_create = (
|
||||
discriminator == self._create_instruction_discriminator
|
||||
)
|
||||
is_create_v2 = (
|
||||
self._create_v2_instruction_discriminator
|
||||
and discriminator == self._create_v2_instruction_discriminator
|
||||
)
|
||||
|
||||
if is_create or is_create_v2:
|
||||
# Token creation should have substantial data and many accounts
|
||||
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
||||
continue
|
||||
@@ -497,10 +542,15 @@ class PumpFunEventParser(EventParser):
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
if (
|
||||
discriminator
|
||||
== self._create_instruction_discriminator
|
||||
):
|
||||
is_create = (
|
||||
discriminator == self._create_instruction_discriminator
|
||||
)
|
||||
is_create_v2 = (
|
||||
self._create_v2_instruction_discriminator
|
||||
and discriminator == self._create_v2_instruction_discriminator
|
||||
)
|
||||
|
||||
if is_create or is_create_v2:
|
||||
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
|
||||
continue
|
||||
|
||||
@@ -544,27 +594,70 @@ class PumpFunEventParser(EventParser):
|
||||
return derived_address
|
||||
|
||||
def _derive_associated_bonding_curve(
|
||||
self, mint: Pubkey, bonding_curve: Pubkey
|
||||
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_PROGRAM
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
if token_program_id is None:
|
||||
token_program_id = SystemAddresses.TOKEN_PROGRAM
|
||||
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
bytes(bonding_curve),
|
||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
||||
bytes(token_program_id),
|
||||
bytes(mint),
|
||||
],
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
def _parse_bonding_curve_state(self, data: bytes) -> dict[str, Any] | None:
|
||||
"""Parse bonding curve state from raw account data using IDL parser.
|
||||
|
||||
Args:
|
||||
data: Raw bonding curve account data
|
||||
|
||||
Returns:
|
||||
Dictionary with parsed bonding curve state or None if parsing fails
|
||||
"""
|
||||
try:
|
||||
decoded = self._idl_parser.decode_account_data(
|
||||
data, "BondingCurve", skip_discriminator=True
|
||||
)
|
||||
if not decoded:
|
||||
return None
|
||||
return decoded
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse bonding curve state: {e}")
|
||||
return None
|
||||
|
||||
def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool:
|
||||
"""Determine if a token is in mayhem mode based on bonding curve state.
|
||||
|
||||
Note: This would require an RPC call to fetch the bonding curve account.
|
||||
For now, we return False as a default since the event parser doesn't have
|
||||
access to an RPC client. The mayhem mode flag will be set by traders
|
||||
when they fetch bonding curve state for other operations.
|
||||
|
||||
Args:
|
||||
bonding_curve_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
True if mayhem mode, False otherwise (or if parsing fails)
|
||||
"""
|
||||
# Since event parser doesn't have RPC client access, we cannot fetch
|
||||
# and parse bonding curve state here. Mayhem mode will be set later
|
||||
# when traders fetch the bonding curve state.
|
||||
return False
|
||||
|
||||
@property
|
||||
def verbose(self) -> bool:
|
||||
"""Check if verbose logging is enabled."""
|
||||
|
||||
@@ -64,15 +64,16 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
# Get all required accounts (includes mayhem-mode-aware fee recipient)
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
|
||||
# Use token_program from accounts_info to ensure AddressProvider controls program selection
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
||||
accounts_info["token_program"], # token program from AddressProvider
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
@@ -123,7 +124,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global_volume_accumulator"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_volume_accumulator"],
|
||||
@@ -144,11 +145,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + token_amount + max_sol_cost
|
||||
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
|
||||
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||
track_volume_bytes = bytes([1, 1])
|
||||
instruction_data = (
|
||||
self._buy_discriminator
|
||||
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units
|
||||
+ struct.pack("<Q", amount_in) # max SOL cost in lamports
|
||||
+ track_volume_bytes # enable volume tracking
|
||||
)
|
||||
|
||||
buy_instruction = Instruction(
|
||||
@@ -182,7 +186,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
# Get all required accounts (includes mayhem-mode-aware fee recipient)
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
# Build sell instruction accounts
|
||||
@@ -243,11 +247,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + token_amount + min_sol_output
|
||||
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume
|
||||
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||
track_volume_bytes = bytes([1, 1])
|
||||
instruction_data = (
|
||||
self._sell_discriminator
|
||||
+ struct.pack("<Q", amount_in) # token amount in raw units
|
||||
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
||||
+ track_volume_bytes # enable volume tracking
|
||||
)
|
||||
|
||||
sell_instruction = Instruction(
|
||||
|
||||
@@ -5,6 +5,7 @@ File: src/platforms/pumpfun/pumpportal_processor.py
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.pubkeys import SystemAddresses
|
||||
from interfaces.core import Platform, TokenInfo
|
||||
from platforms.pumpfun.address_provider import PumpFunAddressProvider
|
||||
from utils.logger import get_logger
|
||||
@@ -81,9 +82,15 @@ class PumpFunPumpPortalProcessor:
|
||||
creator = user
|
||||
|
||||
# Derive additional addresses using platform provider
|
||||
# PumpPortal doesn't distinguish between Token and Token2022.
|
||||
# Default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2.
|
||||
# Technical limitation: Cannot distinguish from pre-parsed data, but risk is low
|
||||
# since pump.fun now defaults to Token2022 for all new tokens.
|
||||
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||
|
||||
associated_bonding_curve = (
|
||||
self.address_provider.derive_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
mint, bonding_curve, token_program_id
|
||||
)
|
||||
)
|
||||
creator_vault = self.address_provider.derive_creator_vault(creator)
|
||||
@@ -99,6 +106,7 @@ class PumpFunPumpPortalProcessor:
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
token_program_id=token_program_id,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -66,10 +66,20 @@ class PlatformAwareBuyer(Trader):
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
|
||||
# Regular behavior with RPC call
|
||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
||||
token_amount = (
|
||||
self.amount / token_price_sol if token_price_sol > 0 else 0
|
||||
)
|
||||
# Fetch pool state to get price and mayhem mode status
|
||||
pool_state = await curve_manager.get_pool_state(pool_address)
|
||||
token_price_sol = pool_state.get("price_per_token")
|
||||
|
||||
# Validate price_per_token is present and positive
|
||||
if token_price_sol is None or token_price_sol <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
|
||||
f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price"
|
||||
)
|
||||
|
||||
# Set is_mayhem_mode from bonding curve state
|
||||
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
|
||||
token_amount = self.amount / token_price_sol
|
||||
|
||||
# Calculate minimum token amount with slippage
|
||||
minimum_token_amount = token_amount * (1 - self.slippage)
|
||||
@@ -225,7 +235,19 @@ class PlatformAwareSeller(Trader):
|
||||
|
||||
# Get pool address and current price using platform-agnostic method
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
||||
# Fetch pool state to get price and mayhem mode status
|
||||
pool_state = await curve_manager.get_pool_state(pool_address)
|
||||
token_price_sol = pool_state.get("price_per_token")
|
||||
|
||||
# Validate price_per_token is present and positive
|
||||
if token_price_sol is None or token_price_sol <= 0:
|
||||
raise ValueError(
|
||||
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
|
||||
f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price"
|
||||
)
|
||||
|
||||
# Set is_mayhem_mode from bonding curve state
|
||||
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
|
||||
|
||||
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class UniversalTrader:
|
||||
|
||||
# State tracking
|
||||
self.traded_mints: set[Pubkey] = set()
|
||||
self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id
|
||||
self.token_queue: asyncio.Queue = asyncio.Queue()
|
||||
self.processing: bool = False
|
||||
self.processed_tokens: set[str] = set()
|
||||
@@ -325,10 +326,17 @@ class UniversalTrader:
|
||||
if self.traded_mints:
|
||||
try:
|
||||
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
|
||||
# Build parallel lists of mints and token_program_ids
|
||||
mints_list = list(self.traded_mints)
|
||||
token_program_ids = [
|
||||
self.traded_token_programs.get(str(mint))
|
||||
for mint in mints_list
|
||||
]
|
||||
await handle_cleanup_post_session(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
list(self.traded_mints),
|
||||
mints_list,
|
||||
token_program_ids,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
@@ -447,6 +455,10 @@ class UniversalTrader:
|
||||
buy_result.tx_signature,
|
||||
)
|
||||
self.traded_mints.add(token_info.mint)
|
||||
# Track token program for cleanup
|
||||
mint_str = str(token_info.mint)
|
||||
if token_info.token_program_id:
|
||||
self.traded_token_programs[mint_str] = token_info.token_program_id
|
||||
|
||||
# Choose exit strategy
|
||||
if not self.marry_mode:
|
||||
@@ -469,6 +481,7 @@ class UniversalTrader:
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
token_info.token_program_id,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
@@ -521,6 +534,7 @@ class UniversalTrader:
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
token_info.token_program_id,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
@@ -590,6 +604,7 @@ class UniversalTrader:
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
token_info.token_program_id,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
|
||||
Reference in New Issue
Block a user