mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-13 15:28:05 +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",
|
"name": "global_volume_accumulator",
|
||||||
"writable": true,
|
|
||||||
"pda": {
|
"pda": {
|
||||||
"seeds": [
|
"seeds": [
|
||||||
{
|
{
|
||||||
@@ -972,7 +971,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "global_volume_accumulator",
|
"name": "global_volume_accumulator",
|
||||||
"writable": true,
|
|
||||||
"pda": {
|
"pda": {
|
||||||
"seeds": [
|
"seeds": [
|
||||||
{
|
{
|
||||||
@@ -2085,6 +2083,10 @@
|
|||||||
{
|
{
|
||||||
"name": "coin_creator",
|
"name": "coin_creator",
|
||||||
"type": "pubkey"
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_mayhem_mode",
|
||||||
|
"type": "bool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3006,6 +3008,92 @@
|
|||||||
],
|
],
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "set_reserved_fee_recipients",
|
||||||
|
"discriminator": [
|
||||||
|
111,
|
||||||
|
172,
|
||||||
|
162,
|
||||||
|
232,
|
||||||
|
114,
|
||||||
|
89,
|
||||||
|
213,
|
||||||
|
142
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "global_config",
|
||||||
|
"writable": true,
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
103,
|
||||||
|
108,
|
||||||
|
111,
|
||||||
|
98,
|
||||||
|
97,
|
||||||
|
108,
|
||||||
|
95,
|
||||||
|
99,
|
||||||
|
111,
|
||||||
|
110,
|
||||||
|
102,
|
||||||
|
105,
|
||||||
|
103
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true,
|
||||||
|
"relations": [
|
||||||
|
"global_config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "event_authority",
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
95,
|
||||||
|
95,
|
||||||
|
101,
|
||||||
|
118,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
95,
|
||||||
|
97,
|
||||||
|
117,
|
||||||
|
116,
|
||||||
|
104,
|
||||||
|
111,
|
||||||
|
114,
|
||||||
|
105,
|
||||||
|
116,
|
||||||
|
121
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "program"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "whitelist_pda",
|
||||||
|
"type": "pubkey"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sync_user_volume_accumulator",
|
"name": "sync_user_volume_accumulator",
|
||||||
"discriminator": [
|
"discriminator": [
|
||||||
@@ -3134,6 +3222,70 @@
|
|||||||
],
|
],
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "toggle_mayhem_mode",
|
||||||
|
"discriminator": [
|
||||||
|
1,
|
||||||
|
9,
|
||||||
|
111,
|
||||||
|
208,
|
||||||
|
100,
|
||||||
|
31,
|
||||||
|
255,
|
||||||
|
163
|
||||||
|
],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"signer": true,
|
||||||
|
"relations": [
|
||||||
|
"global_config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "global_config",
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "event_authority",
|
||||||
|
"pda": {
|
||||||
|
"seeds": [
|
||||||
|
{
|
||||||
|
"kind": "const",
|
||||||
|
"value": [
|
||||||
|
95,
|
||||||
|
95,
|
||||||
|
101,
|
||||||
|
118,
|
||||||
|
101,
|
||||||
|
110,
|
||||||
|
116,
|
||||||
|
95,
|
||||||
|
97,
|
||||||
|
117,
|
||||||
|
116,
|
||||||
|
104,
|
||||||
|
111,
|
||||||
|
114,
|
||||||
|
105,
|
||||||
|
116,
|
||||||
|
121
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "program"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "update_admin",
|
"name": "update_admin",
|
||||||
"discriminator": [
|
"discriminator": [
|
||||||
@@ -3644,6 +3796,19 @@
|
|||||||
216
|
216
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ReservedFeeRecipientsEvent",
|
||||||
|
"discriminator": [
|
||||||
|
43,
|
||||||
|
188,
|
||||||
|
250,
|
||||||
|
18,
|
||||||
|
221,
|
||||||
|
75,
|
||||||
|
187,
|
||||||
|
95
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "SellEvent",
|
"name": "SellEvent",
|
||||||
"discriminator": [
|
"discriminator": [
|
||||||
@@ -3902,6 +4067,22 @@
|
|||||||
"code": 6040,
|
"code": 6040,
|
||||||
"name": "BuySlippageBelowMinBaseAmountOut",
|
"name": "BuySlippageBelowMinBaseAmountOut",
|
||||||
"msg": "buy: slippage - would buy less tokens than expected min_base_amount_out"
|
"msg": "buy: slippage - would buy less tokens than expected min_base_amount_out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6041,
|
||||||
|
"name": "MayhemModeDisabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6042,
|
||||||
|
"name": "OnlyPumpPoolsMayhemMode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6043,
|
||||||
|
"name": "MayhemModeInDesiredState"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 6044,
|
||||||
|
"name": "NotEnoughRemainingAccounts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
@@ -4005,6 +4186,10 @@
|
|||||||
{
|
{
|
||||||
"name": "creator",
|
"name": "creator",
|
||||||
"type": "pubkey"
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_mayhem_mode",
|
||||||
|
"type": "bool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4357,6 +4542,10 @@
|
|||||||
{
|
{
|
||||||
"name": "coin_creator",
|
"name": "coin_creator",
|
||||||
"type": "pubkey"
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_mayhem_mode",
|
||||||
|
"type": "bool"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4625,6 +4814,27 @@
|
|||||||
"The admin authority for setting coin creators"
|
"The admin authority for setting coin creators"
|
||||||
],
|
],
|
||||||
"type": "pubkey"
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whitelist_pda",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved_fee_recipient",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mayhem_mode_enabled",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved_fee_recipients",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"pubkey",
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4747,6 +4957,35 @@
|
|||||||
{
|
{
|
||||||
"name": "coin_creator",
|
"name": "coin_creator",
|
||||||
"type": "pubkey"
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_mayhem_mode",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ReservedFeeRecipientsEvent",
|
||||||
|
"type": {
|
||||||
|
"kind": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved_fee_recipient",
|
||||||
|
"type": "pubkey"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved_fee_recipients",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"pubkey",
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Module for checking the status of a token's bonding curve on the Solana network using
|
Module for checking the status of a token's bonding curve on the Solana network using
|
||||||
the Pump.fun program. It allows querying the bonding curve state and completion status.
|
the Pump.fun program. It allows querying the bonding curve state and completion status.
|
||||||
|
|
||||||
Note: creator fee upgrade introduced updates in bonding curve structure.
|
|
||||||
https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -42,20 +39,11 @@ class BondingCurveState:
|
|||||||
real_sol_reserves: Real SOL reserves in the curve
|
real_sol_reserves: Real SOL reserves in the curve
|
||||||
token_total_supply: Total token supply in the curve
|
token_total_supply: Total token supply in the curve
|
||||||
complete: Whether the curve has completed and liquidity migrated
|
complete: Whether the curve has completed and liquidity migrated
|
||||||
|
is_mayhem_mode: Whether the curve is in mayhem mode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_STRUCT_1 = Struct(
|
# V2: Struct with creator field (81 bytes total: 8 discriminator + 73 data)
|
||||||
"virtual_token_reserves" / Int64ul,
|
_STRUCT_V2 = Struct(
|
||||||
"virtual_sol_reserves" / Int64ul,
|
|
||||||
"real_token_reserves" / Int64ul,
|
|
||||||
"real_sol_reserves" / Int64ul,
|
|
||||||
"token_total_supply" / Int64ul,
|
|
||||||
"complete" / Flag,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Struct after creator fee update has been introduced
|
|
||||||
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
|
||||||
_STRUCT_2 = Struct(
|
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
@@ -65,26 +53,43 @@ class BondingCurveState:
|
|||||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# V3: Struct with creator + mayhem mode (82 bytes total: 8 discriminator + 74 data)
|
||||||
|
_STRUCT_V3 = Struct(
|
||||||
|
"virtual_token_reserves" / Int64ul,
|
||||||
|
"virtual_sol_reserves" / Int64ul,
|
||||||
|
"real_token_reserves" / Int64ul,
|
||||||
|
"real_sol_reserves" / Int64ul,
|
||||||
|
"token_total_supply" / Int64ul,
|
||||||
|
"complete" / Flag,
|
||||||
|
"creator" / Bytes(32),
|
||||||
|
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data."""
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
if len(data) < 150:
|
total_length = len(data)
|
||||||
parsed = self._STRUCT_1.parse(data[8:])
|
|
||||||
self.__dict__.update(parsed)
|
|
||||||
|
|
||||||
else:
|
if total_length == 81: # V2: Creator only
|
||||||
parsed = self._STRUCT_2.parse(data[8:])
|
parsed = self._STRUCT_V2.parse(data[8:])
|
||||||
self.__dict__.update(parsed)
|
self.__dict__.update(parsed)
|
||||||
# Convert raw bytes to Pubkey for creator field
|
# Convert raw bytes to Pubkey for creator field
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
self.creator = Pubkey.from_bytes(self.creator)
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
self.is_mayhem_mode = False
|
||||||
|
|
||||||
|
elif total_length >= 82: # V3: Creator + mayhem mode
|
||||||
|
parsed = self._STRUCT_V3.parse(data[8:])
|
||||||
|
self.__dict__.update(parsed)
|
||||||
|
# Convert raw bytes to Pubkey for creator field
|
||||||
|
self.creator = Pubkey.from_bytes(self.creator)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected bonding curve size: {total_length} bytes")
|
||||||
|
|
||||||
|
|
||||||
def get_associated_bonding_curve_address(
|
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
|
||||||
mint: Pubkey, program_id: Pubkey
|
|
||||||
) -> tuple[Pubkey, int]:
|
|
||||||
"""
|
"""
|
||||||
Derives the associated bonding curve address for a given mint.
|
Derives the associated bonding curve address for a given mint.
|
||||||
|
|
||||||
@@ -134,17 +139,14 @@ async def check_token_status(mint_address: str) -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
mint = Pubkey.from_string(mint_address)
|
mint = Pubkey.from_string(mint_address)
|
||||||
|
bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM_ID)
|
||||||
# Get the associated bonding curve address
|
|
||||||
bonding_curve_address, bump = get_associated_bonding_curve_address(
|
|
||||||
mint, PUMP_PROGRAM_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nToken status:")
|
print("\nToken status:")
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
print(f"Token mint: {mint}")
|
print(f"Token mint: {mint}")
|
||||||
print(f"Associated bonding curve: {bonding_curve_address}")
|
print(f"Bonding curve: {bonding_curve_address}")
|
||||||
print(f"Bump seed: {bump}")
|
if bump is not None:
|
||||||
|
print(f"Bump seed: {bump}")
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
|
|
||||||
# Check completion status
|
# Check completion status
|
||||||
@@ -156,9 +158,25 @@ async def check_token_status(mint_address: str) -> None:
|
|||||||
|
|
||||||
print("\nBonding curve status:")
|
print("\nBonding curve status:")
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
|
print(f"Creator: {curve_state.creator}")
|
||||||
print(
|
print(
|
||||||
f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}"
|
f"Mayhem Mode: {'✅ Enabled' if curve_state.is_mayhem_mode else '❌ Disabled'}"
|
||||||
)
|
)
|
||||||
|
print(
|
||||||
|
f"Completed: {'✅ Migrated' if curve_state.complete else '❌ Bonding curve'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nBonding curve reserves:")
|
||||||
|
print(f"Virtual Token: {curve_state.virtual_token_reserves:,}")
|
||||||
|
print(
|
||||||
|
f"Virtual SOL: {curve_state.virtual_sol_reserves:,} lamports"
|
||||||
|
)
|
||||||
|
print(f"Real Token: {curve_state.real_token_reserves:,}")
|
||||||
|
print(
|
||||||
|
f"Real SOL: {curve_state.real_sol_reserves:,} lamports"
|
||||||
|
)
|
||||||
|
print(f"Total Supply: {curve_state.token_total_supply:,}")
|
||||||
|
|
||||||
if curve_state.complete:
|
if curve_state.complete:
|
||||||
print(
|
print(
|
||||||
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
|
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ load_dotenv()
|
|||||||
# Constants
|
# Constants
|
||||||
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
|
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
|
||||||
TOKEN_MINT: Final[str] = (
|
TOKEN_MINT: Final[str] = (
|
||||||
"YOUR_TOKEN_MINT_ADDRESS_HERE" # Replace with actual token mint address
|
"5ZHx2GGGj87xpidVJpBqadMUutqBirhL2TqUR9T9taKc" # Replace with actual token mint address
|
||||||
)
|
)
|
||||||
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
|
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string(
|
||||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
||||||
@@ -30,7 +30,7 @@ EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(
|
|||||||
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check
|
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check
|
||||||
|
|
||||||
|
|
||||||
def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
|
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
|
||||||
"""
|
"""
|
||||||
Derive the bonding curve PDA address from a mint address.
|
Derive the bonding curve PDA address from a mint address.
|
||||||
|
|
||||||
@@ -81,8 +81,9 @@ def parse_curve_state(data: bytes) -> dict:
|
|||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid discriminator for bonding curve")
|
raise ValueError("Invalid discriminator for bonding curve")
|
||||||
|
|
||||||
|
# Parse common fields (present in all versions)
|
||||||
fields = struct.unpack_from("<QQQQQ?", data, 8)
|
fields = struct.unpack_from("<QQQQQ?", data, 8)
|
||||||
return {
|
result = {
|
||||||
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
|
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
|
||||||
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
|
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
|
||||||
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
|
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
|
||||||
@@ -91,6 +92,20 @@ def parse_curve_state(data: bytes) -> dict:
|
|||||||
"complete": fields[5],
|
"complete": fields[5],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Parse creator field if present
|
||||||
|
data_length = len(data) - 8
|
||||||
|
if data_length >= 73: # Has creator field
|
||||||
|
creator_bytes = data[49:81] # 8 (discriminator) + 41 (base fields) = 49
|
||||||
|
result["creator"] = Pubkey.from_bytes(creator_bytes)
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode if present
|
||||||
|
if data_length >= 74: # Has mayhem mode field
|
||||||
|
result["is_mayhem_mode"] = bool(data[81])
|
||||||
|
else:
|
||||||
|
result["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def print_curve_status(state: dict) -> None:
|
def print_curve_status(state: dict) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -130,9 +145,7 @@ async def track_curve() -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
|
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
|
||||||
curve_pubkey: Pubkey = get_associated_bonding_curve_address(
|
curve_pubkey: Pubkey = get_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID)
|
||||||
mint_pubkey, PUMP_PROGRAM_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Tracking bonding curve for:", mint_pubkey)
|
print("Tracking bonding curve for:", mint_pubkey)
|
||||||
print("Curve address:", curve_pubkey, "\n")
|
print("Curve address:", curve_pubkey, "\n")
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY")
|
|||||||
# Update this address to MINT address of a token you want to close
|
# Update this address to MINT address of a token you want to close
|
||||||
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump")
|
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump")
|
||||||
|
|
||||||
|
# Token program for the mint - use TOKEN_PROGRAM for legacy SPL tokens, TOKEN_2022_PROGRAM for Token-2022
|
||||||
|
# This must match the actual token's program to derive the correct ATA address
|
||||||
|
TOKEN_PROGRAM = SystemAddresses.TOKEN_PROGRAM
|
||||||
|
|
||||||
|
|
||||||
async def close_account_if_exists(
|
async def close_account_if_exists(
|
||||||
client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey
|
client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey
|
||||||
@@ -41,7 +45,7 @@ async def close_account_if_exists(
|
|||||||
mint=mint,
|
mint=mint,
|
||||||
owner=wallet.pubkey,
|
owner=wallet.pubkey,
|
||||||
amount=balance,
|
amount=balance,
|
||||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
program_id=TOKEN_PROGRAM,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await client.build_and_send_transaction([burn_ix], wallet.keypair)
|
await client.build_and_send_transaction([burn_ix], wallet.keypair)
|
||||||
@@ -54,7 +58,7 @@ async def close_account_if_exists(
|
|||||||
account=account,
|
account=account,
|
||||||
dest=wallet.pubkey,
|
dest=wallet.pubkey,
|
||||||
owner=wallet.pubkey,
|
owner=wallet.pubkey,
|
||||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
program_id=TOKEN_PROGRAM,
|
||||||
)
|
)
|
||||||
ix = close_account(close_params)
|
ix = close_account(close_params)
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ async def main():
|
|||||||
wallet = Wallet(PRIVATE_KEY)
|
wallet = Wallet(PRIVATE_KEY)
|
||||||
|
|
||||||
# Get user's ATA for the token
|
# Get user's ATA for the token
|
||||||
ata = wallet.get_associated_token_address(MINT_ADDRESS)
|
ata = wallet.get_associated_token_address(MINT_ADDRESS, TOKEN_PROGRAM)
|
||||||
await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
|
await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -121,13 +121,27 @@ def parse_trade_event(logs):
|
|||||||
|
|
||||||
|
|
||||||
def decode_trade_event(data):
|
def decode_trade_event(data):
|
||||||
"""Decode TradeEvent structure from raw bytes."""
|
"""Decode TradeEvent structure from raw bytes with progressive parsing.
|
||||||
if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check
|
|
||||||
|
Supports both pre-mayhem and post-mayhem IDL versions by parsing fields
|
||||||
|
progressively based on available bytes. This ensures backward compatibility
|
||||||
|
with older transaction logs.
|
||||||
|
|
||||||
|
Core fields (always present): mint, sol_amount, token_amount, is_buy, user,
|
||||||
|
timestamp, virtual_sol_reserves, virtual_token_reserves
|
||||||
|
|
||||||
|
Extended fields (added later): real_sol_reserves, real_token_reserves,
|
||||||
|
fee_recipient, fee_basis_points, fee, creator, creator_fee_basis_points,
|
||||||
|
creator_fee, track_volume, total_unclaimed_tokens, total_claimed_tokens,
|
||||||
|
current_sol_volume, last_update_timestamp, ix_name
|
||||||
|
"""
|
||||||
|
# Minimum size for core fields: 32+8+8+1+32+8+8+8 = 105 bytes
|
||||||
|
if len(data) < 105:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
# Parse fields according to TradeEvent structure
|
# Parse core fields (always present in all versions)
|
||||||
mint = data[offset : offset + 32]
|
mint = data[offset : offset + 32]
|
||||||
offset += 32
|
offset += 32
|
||||||
|
|
||||||
@@ -152,6 +166,73 @@ def decode_trade_event(data):
|
|||||||
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
|
# Parse extended fields if bytes remaining (added in later versions)
|
||||||
|
# Real reserves (8+8 = 16 bytes)
|
||||||
|
if len(data) >= offset + 16:
|
||||||
|
real_sol_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
real_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
else:
|
||||||
|
real_sol_reserves = 0
|
||||||
|
real_token_reserves = 0
|
||||||
|
|
||||||
|
# Fee recipient and fee details (32+8+8 = 48 bytes)
|
||||||
|
if len(data) >= offset + 48:
|
||||||
|
fee_recipient = data[offset : offset + 32]
|
||||||
|
offset += 32
|
||||||
|
fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
fee = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
else:
|
||||||
|
fee_recipient = b'\x00' * 32
|
||||||
|
fee_basis_points = 0
|
||||||
|
fee = 0
|
||||||
|
|
||||||
|
# Creator and creator fee details (32+8+8 = 48 bytes)
|
||||||
|
if len(data) >= offset + 48:
|
||||||
|
creator = data[offset : offset + 32]
|
||||||
|
offset += 32
|
||||||
|
creator_fee_basis_points = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
creator_fee = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
else:
|
||||||
|
creator = b'\x00' * 32
|
||||||
|
creator_fee_basis_points = 0
|
||||||
|
creator_fee = 0
|
||||||
|
|
||||||
|
# Volume tracking fields (1+8+8+8+8 = 33 bytes)
|
||||||
|
if len(data) >= offset + 33:
|
||||||
|
track_volume = bool(data[offset])
|
||||||
|
offset += 1
|
||||||
|
total_unclaimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
total_claimed_tokens = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
current_sol_volume = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
last_update_timestamp = struct.unpack("<q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
else:
|
||||||
|
track_volume = False
|
||||||
|
total_unclaimed_tokens = 0
|
||||||
|
total_claimed_tokens = 0
|
||||||
|
current_sol_volume = 0
|
||||||
|
last_update_timestamp = 0
|
||||||
|
|
||||||
|
# Parse string field (ix_name): 4 bytes for length + string data
|
||||||
|
if len(data) >= offset + 4:
|
||||||
|
string_length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
if len(data) >= offset + string_length:
|
||||||
|
ix_name = data[offset : offset + string_length].decode("utf-8")
|
||||||
|
else:
|
||||||
|
ix_name = ""
|
||||||
|
else:
|
||||||
|
ix_name = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"mint": base58.b58encode(mint).decode(),
|
"mint": base58.b58encode(mint).decode(),
|
||||||
"sol_amount": sol_amount,
|
"sol_amount": sol_amount,
|
||||||
@@ -161,6 +242,20 @@ def decode_trade_event(data):
|
|||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"virtual_sol_reserves": virtual_sol_reserves,
|
"virtual_sol_reserves": virtual_sol_reserves,
|
||||||
"virtual_token_reserves": virtual_token_reserves,
|
"virtual_token_reserves": virtual_token_reserves,
|
||||||
|
"real_sol_reserves": real_sol_reserves,
|
||||||
|
"real_token_reserves": real_token_reserves,
|
||||||
|
"fee_recipient": base58.b58encode(fee_recipient).decode() if fee_recipient != b'\x00' * 32 else None,
|
||||||
|
"fee_basis_points": fee_basis_points,
|
||||||
|
"fee": fee,
|
||||||
|
"creator": base58.b58encode(creator).decode() if creator != b'\x00' * 32 else None,
|
||||||
|
"creator_fee_basis_points": creator_fee_basis_points,
|
||||||
|
"creator_fee": creator_fee,
|
||||||
|
"track_volume": track_volume,
|
||||||
|
"total_unclaimed_tokens": total_unclaimed_tokens,
|
||||||
|
"total_claimed_tokens": total_claimed_tokens,
|
||||||
|
"current_sol_volume": current_sol_volume,
|
||||||
|
"last_update_timestamp": last_update_timestamp,
|
||||||
|
"ix_name": ix_name,
|
||||||
"price_per_token": (sol_amount * 1_000_000) / token_amount
|
"price_per_token": (sol_amount * 1_000_000) / token_amount
|
||||||
if token_amount > 0
|
if token_amount > 0
|
||||||
else 0,
|
else 0,
|
||||||
@@ -178,7 +273,10 @@ def display_transaction_info(signature, logs):
|
|||||||
# Parse trade event data
|
# Parse trade event data
|
||||||
trade_data = parse_trade_event(logs)
|
trade_data = parse_trade_event(logs)
|
||||||
if trade_data:
|
if trade_data:
|
||||||
print(f" Type: {'BUY' if trade_data['is_buy'] else 'SELL'}")
|
# Core transaction info (always present)
|
||||||
|
ix_name = trade_data.get('ix_name', '')
|
||||||
|
trade_type = 'BUY' if trade_data['is_buy'] else 'SELL'
|
||||||
|
print(f" Type: {trade_type}{f' ({ix_name})' if ix_name else ''}")
|
||||||
print(f" Token: {trade_data['mint']}")
|
print(f" Token: {trade_data['mint']}")
|
||||||
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
|
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
|
||||||
print(f" Token Amount: {trade_data['token_amount']:,}")
|
print(f" Token Amount: {trade_data['token_amount']:,}")
|
||||||
@@ -187,6 +285,25 @@ def display_transaction_info(signature, logs):
|
|||||||
)
|
)
|
||||||
print(f" Trader: {trade_data['user']}")
|
print(f" Trader: {trade_data['user']}")
|
||||||
|
|
||||||
|
# Fee info (may not be present in older transactions)
|
||||||
|
if trade_data['fee'] > 0 or trade_data['fee_basis_points'] > 0:
|
||||||
|
print(f" Fee: {trade_data['fee'] / 1_000_000_000:.6f} SOL ({trade_data['fee_basis_points']} bps)")
|
||||||
|
|
||||||
|
if trade_data['creator_fee'] > 0 or trade_data['creator_fee_basis_points'] > 0:
|
||||||
|
print(f" Creator Fee: {trade_data['creator_fee'] / 1_000_000_000:.6f} SOL ({trade_data['creator_fee_basis_points']} bps)")
|
||||||
|
|
||||||
|
if trade_data['creator']:
|
||||||
|
print(f" Creator: {trade_data['creator']}")
|
||||||
|
|
||||||
|
if trade_data['fee_recipient']:
|
||||||
|
print(f" Fee Recipient: {trade_data['fee_recipient']}")
|
||||||
|
|
||||||
|
# Reserve info
|
||||||
|
print(f" Virtual Reserves: {trade_data['virtual_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['virtual_token_reserves']:,} tokens")
|
||||||
|
|
||||||
|
if trade_data['real_sol_reserves'] > 0 or trade_data['real_token_reserves'] > 0:
|
||||||
|
print(f" Real Reserves: {trade_data['real_sol_reserves'] / 1_000_000_000:.6f} SOL / {trade_data['real_token_reserves']:,} tokens")
|
||||||
|
|
||||||
# Extract and display program info
|
# Extract and display program info
|
||||||
display_program_info(logs)
|
display_program_info(logs)
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,7 @@ EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
|||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT_1 = Struct(
|
_STRUCT = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
|
||||||
"virtual_sol_reserves" / Int64ul,
|
|
||||||
"real_token_reserves" / Int64ul,
|
|
||||||
"real_sol_reserves" / Int64ul,
|
|
||||||
"token_total_supply" / Int64ul,
|
|
||||||
"complete" / Flag,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Struct after creator fee update has been introduced
|
|
||||||
# https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md
|
|
||||||
_STRUCT_2 = Struct(
|
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
@@ -30,23 +19,43 @@ class BondingCurveState:
|
|||||||
"token_total_supply" / Int64ul,
|
"token_total_supply" / Int64ul,
|
||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
||||||
|
"is_mayhem_mode" / Flag, # Added mayhem mode flag - 1 byte
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data - supports all versions."""
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
if len(data) < 150:
|
# Required fields (always present)
|
||||||
parsed = self._STRUCT_1.parse(data[8:])
|
offset = 8
|
||||||
self.__dict__.update(parsed)
|
self.virtual_token_reserves = int.from_bytes(
|
||||||
|
data[offset : offset + 8], "little"
|
||||||
|
)
|
||||||
|
offset += 8
|
||||||
|
self.virtual_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||||
|
offset += 8
|
||||||
|
self.real_token_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||||
|
offset += 8
|
||||||
|
self.real_sol_reserves = int.from_bytes(data[offset : offset + 8], "little")
|
||||||
|
offset += 8
|
||||||
|
self.token_total_supply = int.from_bytes(data[offset : offset + 8], "little")
|
||||||
|
offset += 8
|
||||||
|
self.complete = bool(data[offset])
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
# Optional fields (may not be present in older versions)
|
||||||
|
if len(data) >= offset + 32:
|
||||||
|
self.creator = Pubkey.from_bytes(data[offset : offset + 32])
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
if len(data) > offset:
|
||||||
|
self.is_mayhem_mode = bool(data[offset])
|
||||||
|
else:
|
||||||
|
self.is_mayhem_mode = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
parsed = self._STRUCT_2.parse(data[8:])
|
self.creator = None
|
||||||
self.__dict__.update(parsed)
|
|
||||||
# Convert raw bytes to Pubkey for creator field
|
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
|
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ print(json.dumps(tx_data, indent=2))
|
|||||||
|
|
||||||
|
|
||||||
def decode_create_instruction(data):
|
def decode_create_instruction(data):
|
||||||
|
"""Decode legacy Create instruction (Metaplex tokens)."""
|
||||||
# The Create instruction has 3 string arguments: name, symbol, uri
|
# The Create instruction has 3 string arguments: name, symbol, uri
|
||||||
offset = 8 # Skip the 8-byte discriminator
|
offset = 8 # Skip the 8-byte discriminator
|
||||||
results = []
|
results = []
|
||||||
@@ -36,7 +37,42 @@ def decode_create_instruction(data):
|
|||||||
string_data = data[offset : offset + length].decode("utf-8")
|
string_data = data[offset : offset + length].decode("utf-8")
|
||||||
results.append(string_data)
|
results.append(string_data)
|
||||||
offset += length
|
offset += length
|
||||||
return {"name": results[0], "symbol": results[1], "uri": results[2]}
|
return {
|
||||||
|
"name": results[0],
|
||||||
|
"symbol": results[1],
|
||||||
|
"uri": results[2],
|
||||||
|
"token_standard": "legacy",
|
||||||
|
"is_mayhem_mode": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def decode_create_v2_instruction(data):
|
||||||
|
"""Decode CreateV2 instruction (Token2022 tokens)."""
|
||||||
|
# The CreateV2 instruction has 3 string arguments: name, symbol, uri + is_mayhem_mode
|
||||||
|
offset = 8 # Skip the 8-byte discriminator
|
||||||
|
results = []
|
||||||
|
for _ in range(3):
|
||||||
|
length = struct.unpack_from("<I", data, offset)[0]
|
||||||
|
offset += 4
|
||||||
|
string_data = data[offset : offset + length].decode("utf-8")
|
||||||
|
results.append(string_data)
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
# Skip creator pubkey (32 bytes)
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
is_mayhem_mode = False
|
||||||
|
if offset < len(data):
|
||||||
|
is_mayhem_mode = bool(data[offset])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": results[0],
|
||||||
|
"symbol": results[1],
|
||||||
|
"uri": results[2],
|
||||||
|
"token_standard": "token2022",
|
||||||
|
"is_mayhem_mode": is_mayhem_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def decode_buy_instruction(data):
|
def decode_buy_instruction(data):
|
||||||
@@ -48,6 +84,8 @@ def decode_buy_instruction(data):
|
|||||||
def decode_instruction_data(instruction, accounts, data):
|
def decode_instruction_data(instruction, accounts, data):
|
||||||
if instruction["name"] == "create":
|
if instruction["name"] == "create":
|
||||||
return decode_create_instruction(data)
|
return decode_create_instruction(data)
|
||||||
|
elif instruction["name"] == "createV2":
|
||||||
|
return decode_create_v2_instruction(data)
|
||||||
elif instruction["name"] == "buy":
|
elif instruction["name"] == "buy":
|
||||||
return decode_buy_instruction(data)
|
return decode_buy_instruction(data)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from construct import Flag, Int64ul, Struct
|
from construct import Bytes, Flag, Int64ul, Struct
|
||||||
from solana.rpc.async_api import AsyncClient
|
from solana.rpc.async_api import AsyncClient
|
||||||
from solders.pubkey import Pubkey
|
from solders.pubkey import Pubkey
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
||||||
|
|
||||||
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
|
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
|
||||||
TOKEN_DECIMALS: Final[int] = 6
|
TOKEN_DECIMALS: Final[int] = 6
|
||||||
CURVE_ADDRESS: Final[str] = (
|
CURVE_ADDRESS: Final[str] = "..." # Replace with actual bonding curve address
|
||||||
"YOUR_BONDING_CURVE_ADDRESS_HERE" # Replace with actual bonding curve address
|
|
||||||
)
|
|
||||||
|
|
||||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
||||||
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
|
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
|
||||||
@@ -23,7 +18,9 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
|||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT = Struct(
|
"""Parse bonding curve account data - supports all versions."""
|
||||||
|
|
||||||
|
_STRUCT_V1 = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
@@ -32,9 +29,47 @@ class BondingCurveState:
|
|||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_STRUCT_V2 = Struct(
|
||||||
|
"virtual_token_reserves" / Int64ul,
|
||||||
|
"virtual_sol_reserves" / Int64ul,
|
||||||
|
"real_token_reserves" / Int64ul,
|
||||||
|
"real_sol_reserves" / Int64ul,
|
||||||
|
"token_total_supply" / Int64ul,
|
||||||
|
"complete" / Flag,
|
||||||
|
"creator" / Bytes(32),
|
||||||
|
)
|
||||||
|
|
||||||
|
_STRUCT_V3 = Struct(
|
||||||
|
"virtual_token_reserves" / Int64ul,
|
||||||
|
"virtual_sol_reserves" / Int64ul,
|
||||||
|
"real_token_reserves" / Int64ul,
|
||||||
|
"real_sol_reserves" / Int64ul,
|
||||||
|
"token_total_supply" / Int64ul,
|
||||||
|
"complete" / Flag,
|
||||||
|
"creator" / Bytes(32),
|
||||||
|
"is_mayhem_mode" / Flag,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
parsed = self._STRUCT.parse(data[8:])
|
"""Parse bonding curve data - auto-detects version."""
|
||||||
self.__dict__.update(parsed)
|
data_length = len(data) - 8
|
||||||
|
|
||||||
|
if data_length < 73: # V1: without creator and mayhem mode
|
||||||
|
parsed = self._STRUCT_V1.parse(data[8:])
|
||||||
|
self.__dict__.update(parsed)
|
||||||
|
self.creator = None
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
elif data_length == 73: # V2: with creator, without mayhem mode
|
||||||
|
parsed = self._STRUCT_V2.parse(data[8:])
|
||||||
|
self.__dict__.update(parsed)
|
||||||
|
if isinstance(self.creator, bytes):
|
||||||
|
self.creator = Pubkey.from_bytes(self.creator)
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
else: # V3: with creator and mayhem mode
|
||||||
|
parsed = self._STRUCT_V3.parse(data[8:])
|
||||||
|
self.__dict__.update(parsed)
|
||||||
|
if isinstance(self.creator, bytes):
|
||||||
|
self.creator = Pubkey.from_bytes(self.creator)
|
||||||
|
|
||||||
|
|
||||||
async def get_bonding_curve_state(
|
async def get_bonding_curve_state(
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
This script compares two methods of detecting migrations:
|
This script compares two methods of detecting migrations:
|
||||||
1. Migration program listener (listens Migration program) - detects markets via successful migration transactions
|
|
||||||
2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription
|
|
||||||
|
|
||||||
The script tracks which method detects new markets first and provides detailed performance statistics.
|
1. Migration Program Listener - Listens to migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
|
||||||
|
which emits detailed migration events via logsSubscribe
|
||||||
|
|
||||||
Note: multiple endpoints available. Scroll down to change providers which you want to test.
|
2. Direct Pool Account Listener - Listens to pump_amm program (pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA)
|
||||||
|
for new Pool account creations via programSubscribe
|
||||||
|
|
||||||
|
Note: The migration wrapper program emits a different event structure than
|
||||||
|
CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||||
|
|
||||||
|
The script tracks which method detects new migrations first and provides detailed performance
|
||||||
|
statistics including message counts, detection timing, provider latency comparison.
|
||||||
|
|
||||||
|
Configure multiple RPC endpoints in .env file to test provider performance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -33,7 +41,7 @@ QUOTE_MINT_SOL = base58.b58encode(
|
|||||||
).decode()
|
).decode()
|
||||||
|
|
||||||
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
||||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
|
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # Pool account with is_mayhem_mode = 244 bytes
|
||||||
|
|
||||||
|
|
||||||
class DetectionTracker:
|
class DetectionTracker:
|
||||||
@@ -306,9 +314,9 @@ async def fetch_existing_market_pubkeys():
|
|||||||
|
|
||||||
def parse_market_account_data(data):
|
def parse_market_account_data(data):
|
||||||
"""
|
"""
|
||||||
Parse binary market account data into a structured format
|
Parse binary Pool account data according to pump_swap_idl.json structure
|
||||||
|
|
||||||
This function matches the parser from the market listener script
|
Total 11 fields including is_mayhem_mode field added with mayhem update
|
||||||
"""
|
"""
|
||||||
parsed_data = {}
|
parsed_data = {}
|
||||||
offset = 8 # Skip discriminator
|
offset = 8 # Skip discriminator
|
||||||
@@ -323,6 +331,8 @@ def parse_market_account_data(data):
|
|||||||
("pool_base_token_account", "pubkey"),
|
("pool_base_token_account", "pubkey"),
|
||||||
("pool_quote_token_account", "pubkey"),
|
("pool_quote_token_account", "pubkey"),
|
||||||
("lp_supply", "u64"),
|
("lp_supply", "u64"),
|
||||||
|
("coin_creator", "pubkey"),
|
||||||
|
("is_mayhem_mode", "bool"),
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -347,6 +357,10 @@ def parse_market_account_data(data):
|
|||||||
value = data[offset]
|
value = data[offset]
|
||||||
parsed_data[field_name] = value
|
parsed_data[field_name] = value
|
||||||
offset += 1
|
offset += 1
|
||||||
|
elif field_type == "bool":
|
||||||
|
value = bool(data[offset])
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
offset += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to parse market data: {e}")
|
print(f"[ERROR] Failed to parse market data: {e}")
|
||||||
|
|
||||||
@@ -355,9 +369,11 @@ def parse_market_account_data(data):
|
|||||||
|
|
||||||
def parse_migrate_instruction(data):
|
def parse_migrate_instruction(data):
|
||||||
"""
|
"""
|
||||||
Parse binary migration instruction data into a structured format
|
Parse migration event from the migration wrapper program
|
||||||
|
|
||||||
This function matches the parser from the migration listener script
|
Note: This parses the event emitted by the migration wrapper program
|
||||||
|
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
|
||||||
|
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||||
"""
|
"""
|
||||||
if len(data) < 8:
|
if len(data) < 8:
|
||||||
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Listens for 'Migrate' instructions from a Solana migration program via WebSocket.
|
Listens for 'Migrate' instructions from Solana migration program via WebSocket.
|
||||||
Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations.
|
Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations.
|
||||||
|
|
||||||
Note: skips transactions with truncated logs (no Program data in the logs -> no parsed data).
|
Note: This uses a migration wrapper program (39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg)
|
||||||
|
that emits a different event structure than the CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||||
|
|
||||||
|
Skips transactions with truncated logs (no Program data in the logs -> no parsed data).
|
||||||
To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs.
|
To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -26,6 +29,12 @@ MIGRATION_PROGRAM_ID = Pubkey.from_string(
|
|||||||
|
|
||||||
|
|
||||||
def parse_migrate_instruction(data):
|
def parse_migrate_instruction(data):
|
||||||
|
"""Parse migration event from the migration wrapper program.
|
||||||
|
|
||||||
|
Note: This parses the event emitted by the migration wrapper program
|
||||||
|
(39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg), which has a different
|
||||||
|
structure than CompletePumpAmmMigrationEvent in pump_fun_idl.json.
|
||||||
|
"""
|
||||||
if len(data) < 8:
|
if len(data) < 8:
|
||||||
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
print(f"[ERROR] Data length too short: {len(data)} bytes")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
|||||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||||
|
|
||||||
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
|
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 + 32 + 1 # discriminator + pool_bump + index + 6 pubkeys + lp_supply + coin_creator + is_mayhem_mode = 244 bytes
|
||||||
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
MARKET_DISCRIMINATOR = base58.b58encode(b"\xf1\x9am\x04\x11\xb1m\xbc").decode()
|
||||||
QUOTE_MINT_SOL = base58.b58encode(
|
QUOTE_MINT_SOL = base58.b58encode(
|
||||||
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
|
bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))
|
||||||
@@ -58,6 +58,10 @@ async def fetch_existing_market_pubkeys():
|
|||||||
|
|
||||||
|
|
||||||
def parse_market_account_data(data):
|
def parse_market_account_data(data):
|
||||||
|
"""Parse Pool account data according to pump_swap_idl.json structure.
|
||||||
|
|
||||||
|
Total 11 fields including the new is_mayhem_mode field added with mayhem update.
|
||||||
|
"""
|
||||||
parsed_data = {}
|
parsed_data = {}
|
||||||
offset = 8 # Discriminator
|
offset = 8 # Discriminator
|
||||||
|
|
||||||
@@ -72,6 +76,7 @@ def parse_market_account_data(data):
|
|||||||
("pool_quote_token_account", "pubkey"),
|
("pool_quote_token_account", "pubkey"),
|
||||||
("lp_supply", "u64"),
|
("lp_supply", "u64"),
|
||||||
("coin_creator", "pubkey"),
|
("coin_creator", "pubkey"),
|
||||||
|
("is_mayhem_mode", "bool"),
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -96,6 +101,10 @@ def parse_market_account_data(data):
|
|||||||
value = data[offset]
|
value = data[offset]
|
||||||
parsed_data[field_name] = value
|
parsed_data[field_name] = value
|
||||||
offset += 1
|
offset += 1
|
||||||
|
elif field_type == "bool":
|
||||||
|
value = bool(data[offset])
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
offset += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to parse market data: {e}")
|
print(f"[ERROR] Failed to parse market data: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
"""
|
"""
|
||||||
This script compares four methods of detecting new Pump.fun tokens:
|
Performance Comparison Tool for Pump.fun Token Detection Methods
|
||||||
1. Block subscription listener - listens for blocks containing Pump.fun program
|
|
||||||
2. Geyser gRPC listener - uses Geyser gRPC API to get transactions containing Pump.fun program
|
|
||||||
3. Logs subscription listener - listens for logs containing Pump.fun program
|
|
||||||
4. PumpPortal WebSocket listener - connects to PumpPortal WebSocket and listens for token events
|
|
||||||
|
|
||||||
The script tracks which method detects new tokens first and provides detailed performance statistics.
|
This script compares four methods of detecting new Pump.fun tokens in real-time:
|
||||||
|
|
||||||
Note: multiple endpoints available. Scroll down to change providers which you want to test.
|
1. Block Subscription (blockSubscribe)
|
||||||
|
- Method: WebSocket subscription to blocks mentioning Pump.fun program
|
||||||
|
- Speed: Slowest (processes entire blocks)
|
||||||
|
- Reference: https://solana.com/docs/rpc/websocket/blocksubscribe
|
||||||
|
|
||||||
|
2. Logs Subscription (logsSubscribe)
|
||||||
|
- Method: WebSocket subscription to program logs
|
||||||
|
- Speed: Fast (event data includes all fields)
|
||||||
|
- Reference: https://solana.com/docs/rpc/websocket/logssubscribe
|
||||||
|
|
||||||
|
3. Geyser gRPC
|
||||||
|
- Method: Yellowstone Dragon's Mouth gRPC streaming
|
||||||
|
- Speed: Fastest (optimized streaming protocol)
|
||||||
|
- Reference: https://docs.triton.one/rpc-pool/grpc-subscriptions
|
||||||
|
|
||||||
|
4. PumpPortal WebSocket
|
||||||
|
- Method: Third-party aggregated WebSocket feed
|
||||||
|
- Speed: Fast (pre-processed data)
|
||||||
|
- Note: Requires trust in third-party provider
|
||||||
|
|
||||||
|
The script tracks which method detects each token first and provides detailed
|
||||||
|
performance statistics including:
|
||||||
|
- First detection counts per method
|
||||||
|
- Average latency between methods
|
||||||
|
- Message counts per provider
|
||||||
|
- Token detection coverage
|
||||||
|
|
||||||
|
Configuration: Set provider endpoints in .env or modify the providers dict at the bottom.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -26,13 +49,31 @@ from solders.transaction import VersionedTransaction
|
|||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
# Constants
|
# ============ CONSTANTS ============
|
||||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
|
||||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
|
||||||
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
|
|
||||||
TEST_DURATION = 30 # seconds
|
|
||||||
|
|
||||||
GEYSER_AUTH_TYPE = "x-token" # or "basic"
|
# Pump.fun program ID
|
||||||
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
|
||||||
|
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||||
|
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||||
|
# and sha256("global:createV2") for Token2022 CreateV2
|
||||||
|
# See: learning-examples/calculate_discriminator.py
|
||||||
|
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||||
|
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||||
|
|
||||||
|
# Event discriminator for CreateEvent (8-byte identifier)
|
||||||
|
# This is emitted by both Create and CreateV2 instructions
|
||||||
|
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||||
|
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||||
|
|
||||||
|
# PumpPortal WebSocket endpoint (third-party service)
|
||||||
|
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
|
||||||
|
|
||||||
|
# Test duration in seconds
|
||||||
|
TEST_DURATION = 30
|
||||||
|
|
||||||
|
# Geyser authentication type: "x-token" or "basic"
|
||||||
|
GEYSER_AUTH_TYPE = "x-token"
|
||||||
|
|
||||||
|
|
||||||
class DetectionTracker:
|
class DetectionTracker:
|
||||||
@@ -217,44 +258,198 @@ async def fetch_existing_token_mints():
|
|||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def parse_create_instruction(data):
|
def decode_create_instruction(ix_data, account_keys):
|
||||||
"""
|
"""Decode legacy Create instruction (Metaplex tokens) from instruction data."""
|
||||||
Parse binary create instruction data into a structured format
|
if len(ix_data) < 8:
|
||||||
"""
|
|
||||||
if len(data) < 8:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
offset = 8 # Skip discriminator
|
offset = 8 # Skip discriminator
|
||||||
parsed_data = {}
|
parsed_data = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse name (string)
|
# Read string fields from instruction data
|
||||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
def read_string():
|
||||||
offset += 4
|
nonlocal offset
|
||||||
parsed_data["name"] = data[offset : offset + length].decode("utf-8")
|
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
|
||||||
offset += length
|
offset += 4
|
||||||
|
value = ix_data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
return value
|
||||||
|
|
||||||
# Parse symbol (string)
|
def read_pubkey():
|
||||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
nonlocal offset
|
||||||
offset += 4
|
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||||
parsed_data["symbol"] = data[offset : offset + length].decode("utf-8")
|
offset += 32
|
||||||
offset += length
|
return value
|
||||||
|
|
||||||
# Parse uri (string)
|
# Parse instruction arguments
|
||||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
parsed_data["name"] = read_string()
|
||||||
offset += 4
|
parsed_data["symbol"] = read_string()
|
||||||
parsed_data["uri"] = data[offset : offset + length].decode("utf-8")
|
parsed_data["uri"] = read_string()
|
||||||
offset += length
|
parsed_data["creator"] = read_pubkey()
|
||||||
|
|
||||||
# Parse mint (pubkey)
|
# Extract accounts from account_keys array
|
||||||
parsed_data["mint"] = base58.b58encode(data[offset : offset + 32]).decode(
|
if len(account_keys) >= 8:
|
||||||
"utf-8"
|
parsed_data["mint"] = account_keys[0]
|
||||||
)
|
parsed_data["bondingCurve"] = account_keys[2]
|
||||||
offset += 32
|
parsed_data["user"] = account_keys[7]
|
||||||
|
elif len(account_keys) > 0:
|
||||||
|
parsed_data["mint"] = account_keys[0]
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "legacy"
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
return parsed_data
|
return parsed_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to parse create instruction: {e}")
|
print(f"[ERROR] Failed to decode create instruction: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decode_create_v2_instruction(ix_data, account_keys):
|
||||||
|
"""Decode CreateV2 instruction (Token2022 tokens) from instruction data."""
|
||||||
|
if len(ix_data) < 8:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 8 # Skip discriminator
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read string fields from instruction data
|
||||||
|
def read_string():
|
||||||
|
nonlocal offset
|
||||||
|
length = struct.unpack("<I", ix_data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
value = ix_data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
return value
|
||||||
|
|
||||||
|
def read_pubkey():
|
||||||
|
nonlocal offset
|
||||||
|
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Parse instruction arguments
|
||||||
|
parsed_data["name"] = read_string()
|
||||||
|
parsed_data["symbol"] = read_string()
|
||||||
|
parsed_data["uri"] = read_string()
|
||||||
|
parsed_data["creator"] = read_pubkey()
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
if offset < len(ix_data):
|
||||||
|
parsed_data["is_mayhem_mode"] = bool(ix_data[offset])
|
||||||
|
else:
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
# Extract accounts from account_keys array
|
||||||
|
if len(account_keys) >= 6:
|
||||||
|
parsed_data["mint"] = account_keys[0]
|
||||||
|
parsed_data["bondingCurve"] = account_keys[2]
|
||||||
|
parsed_data["user"] = account_keys[5]
|
||||||
|
elif len(account_keys) > 0:
|
||||||
|
parsed_data["mint"] = account_keys[0]
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "token2022"
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to decode create v2 instruction: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_create_event(data):
|
||||||
|
"""Parse legacy Create event from logs (event data includes all fields)."""
|
||||||
|
if len(data) < 8:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 8 # Skip discriminator
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
# Parse fields based on CreateEvent structure
|
||||||
|
fields = [
|
||||||
|
("name", "string"),
|
||||||
|
("symbol", "string"),
|
||||||
|
("uri", "string"),
|
||||||
|
("mint", "publicKey"),
|
||||||
|
("bondingCurve", "publicKey"),
|
||||||
|
("user", "publicKey"),
|
||||||
|
("creator", "publicKey"),
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
for field_name, field_type in fields:
|
||||||
|
if field_type == "string":
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
elif field_type == "publicKey":
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "legacy"
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to parse create event: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_create_v2_event(data):
|
||||||
|
"""Parse CreateV2 event from logs (event data includes all fields)."""
|
||||||
|
if len(data) < 8:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 8 # Skip discriminator
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
# Parse fields based on CreateV2Event structure
|
||||||
|
fields = [
|
||||||
|
("name", "string"),
|
||||||
|
("symbol", "string"),
|
||||||
|
("uri", "string"),
|
||||||
|
("mint", "publicKey"),
|
||||||
|
("bondingCurve", "publicKey"),
|
||||||
|
("user", "publicKey"),
|
||||||
|
("creator", "publicKey"),
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
for field_name, field_type in fields:
|
||||||
|
if field_type == "string":
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
elif field_type == "publicKey":
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
if offset < len(data):
|
||||||
|
is_mayhem_mode = bool(data[offset])
|
||||||
|
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||||
|
else:
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "token2022"
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to parse create v2 event: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -269,6 +464,51 @@ def is_transaction_successful(logs):
|
|||||||
# ============ WEBSOCKET LISTENERS ============
|
# ============ WEBSOCKET LISTENERS ============
|
||||||
|
|
||||||
|
|
||||||
|
def get_account_keys(transaction, instruction, loaded_addresses=None):
|
||||||
|
"""
|
||||||
|
Safely extract account keys for an instruction from a versioned transaction.
|
||||||
|
Handles both static account keys and loaded addresses from lookup tables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transaction: VersionedTransaction object
|
||||||
|
instruction: Instruction object
|
||||||
|
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of account keys as strings, or None if unable to resolve
|
||||||
|
"""
|
||||||
|
account_keys = []
|
||||||
|
static_keys = transaction.message.account_keys
|
||||||
|
|
||||||
|
# Combine all available account keys: static + loaded
|
||||||
|
all_keys = list(static_keys)
|
||||||
|
|
||||||
|
if loaded_addresses:
|
||||||
|
# Add loaded writable addresses
|
||||||
|
if "writable" in loaded_addresses:
|
||||||
|
for addr in loaded_addresses["writable"]:
|
||||||
|
all_keys.append(Pubkey.from_string(addr))
|
||||||
|
|
||||||
|
# Add loaded readonly addresses
|
||||||
|
if "readonly" in loaded_addresses:
|
||||||
|
for addr in loaded_addresses["readonly"]:
|
||||||
|
all_keys.append(Pubkey.from_string(addr))
|
||||||
|
|
||||||
|
# Now resolve account indices
|
||||||
|
for index in instruction.accounts:
|
||||||
|
try:
|
||||||
|
if index < len(all_keys):
|
||||||
|
account_keys.append(str(all_keys[index]))
|
||||||
|
else:
|
||||||
|
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
|
||||||
|
return None
|
||||||
|
except (IndexError, Exception) as e:
|
||||||
|
print(f"Error resolving account at index {index}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return account_keys
|
||||||
|
|
||||||
|
|
||||||
async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
|
async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
|
||||||
"""
|
"""
|
||||||
Listen for new tokens via block subscription
|
Listen for new tokens via block subscription
|
||||||
@@ -330,6 +570,12 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
transaction = VersionedTransaction.from_bytes(tx_data)
|
transaction = VersionedTransaction.from_bytes(tx_data)
|
||||||
|
|
||||||
|
# Extract loaded addresses from transaction metadata
|
||||||
|
loaded_addresses = None
|
||||||
|
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
|
||||||
|
loaded_addresses = tx["meta"]["loadedAddresses"]
|
||||||
|
|
||||||
for ix in transaction.message.instructions:
|
for ix in transaction.message.instructions:
|
||||||
if (
|
if (
|
||||||
transaction.message.account_keys[
|
transaction.message.account_keys[
|
||||||
@@ -339,39 +585,53 @@ async def listen_block_subscription(wss_url, provider_name, tracker, known_token
|
|||||||
):
|
):
|
||||||
data_bytes = bytes(ix.data)
|
data_bytes = bytes(ix.data)
|
||||||
|
|
||||||
if not data_bytes.startswith(
|
# Check for both Create and CreateV2 instructions
|
||||||
PUMP_CREATE_PREFIX
|
is_create = data_bytes.startswith(PUMP_CREATE_PREFIX)
|
||||||
):
|
is_create_v2 = data_bytes.startswith(PUMP_CREATE_V2_PREFIX)
|
||||||
|
|
||||||
|
if not (is_create or is_create_v2):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parsed = parse_create_instruction(data_bytes)
|
# Get account keys with ALT support
|
||||||
if not parsed:
|
account_keys = get_account_keys(
|
||||||
|
transaction, ix, loaded_addresses
|
||||||
|
)
|
||||||
|
if account_keys is None:
|
||||||
|
print("Skipping transaction due to unresolved accounts")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ix.accounts) > 0:
|
# Decode based on instruction type
|
||||||
try:
|
if is_create_v2:
|
||||||
mint = str(
|
print(f"[{provider_name}_block] Detected: CreateV2 instruction (Token2022)")
|
||||||
transaction.message.account_keys[
|
decoded = decode_create_v2_instruction(data_bytes, account_keys)
|
||||||
ix.accounts[0]
|
else:
|
||||||
]
|
print(f"[{provider_name}_block] Detected: Create instruction (Legacy/Metaplex)")
|
||||||
) # First account is usually the mint
|
decoded = decode_create_instruction(data_bytes, account_keys)
|
||||||
|
|
||||||
if mint in known_tokens:
|
if not decoded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ts = time.time()
|
mint = decoded.get("mint")
|
||||||
tracker.add_token(
|
if not mint:
|
||||||
mint,
|
continue
|
||||||
parsed["name"],
|
|
||||||
parsed["symbol"],
|
if mint in known_tokens:
|
||||||
f"{provider_name}_block",
|
continue
|
||||||
ts,
|
|
||||||
)
|
try:
|
||||||
known_tokens.add(mint)
|
ts = time.time()
|
||||||
except Exception as e:
|
tracker.add_token(
|
||||||
print(
|
mint,
|
||||||
f"[ERROR] Failed to process block instruction: {e}"
|
decoded["name"],
|
||||||
)
|
decoded["symbol"],
|
||||||
|
f"{provider_name}_block",
|
||||||
|
ts,
|
||||||
|
)
|
||||||
|
known_tokens.add(mint)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"[ERROR] Failed to process block instruction: {e}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to process transaction: {e}")
|
print(f"[ERROR] Failed to process transaction: {e}")
|
||||||
|
|
||||||
@@ -424,9 +684,17 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
|
|||||||
log_data = data["params"]["result"]["value"]
|
log_data = data["params"]["result"]["value"]
|
||||||
logs = log_data.get("logs", [])
|
logs = log_data.get("logs", [])
|
||||||
|
|
||||||
if not any(
|
# Detect both Create and CreateV2 instructions
|
||||||
"Program log: Instruction: Create" in log for log in logs
|
is_create = any(
|
||||||
):
|
"Program log: Instruction: Create" in log
|
||||||
|
for log in logs
|
||||||
|
)
|
||||||
|
is_create_v2 = any(
|
||||||
|
"Program log: Instruction: CreateV2" in log
|
||||||
|
for log in logs
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (is_create or is_create_v2):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for log in logs:
|
for log in logs:
|
||||||
@@ -435,7 +703,23 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens
|
|||||||
encoded_data = log.split(": ")[1]
|
encoded_data = log.split(": ")[1]
|
||||||
data_bytes = base64.b64decode(encoded_data)
|
data_bytes = base64.b64decode(encoded_data)
|
||||||
|
|
||||||
parsed = parse_create_instruction(data_bytes)
|
# Check if this is a CreateEvent by validating discriminator
|
||||||
|
if len(data_bytes) < 8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_discriminator = data_bytes[:8]
|
||||||
|
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||||
|
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse based on instruction type
|
||||||
|
if is_create_v2:
|
||||||
|
print(f"[{provider_name}_logs] Detected: CreateV2 instruction (Token2022)")
|
||||||
|
parsed = parse_create_v2_event(data_bytes)
|
||||||
|
else:
|
||||||
|
print(f"[{provider_name}_logs] Detected: Create instruction (Legacy/Metaplex)")
|
||||||
|
parsed = parse_create_event(data_bytes)
|
||||||
|
|
||||||
if not parsed:
|
if not parsed:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -492,11 +776,11 @@ async def listen_geyser_grpc(
|
|||||||
|
|
||||||
if GEYSER_AUTH_TYPE == "x-token":
|
if GEYSER_AUTH_TYPE == "x-token":
|
||||||
auth = grpc.metadata_call_credentials(
|
auth = grpc.metadata_call_credentials(
|
||||||
lambda context, callback: callback((("x-token", api_token),), None)
|
lambda _context, callback: callback((("x-token", api_token),), None)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
auth = grpc.metadata_call_credentials(
|
auth = grpc.metadata_call_credentials(
|
||||||
lambda context, callback: callback(
|
lambda _context, callback: callback(
|
||||||
(("authorization", f"Basic {api_token}"),), None
|
(("authorization", f"Basic {api_token}"),), None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -529,28 +813,44 @@ async def listen_geyser_grpc(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for ix in msg.instructions:
|
for ix in msg.instructions:
|
||||||
if not ix.data.startswith(PUMP_CREATE_PREFIX):
|
# Check for both Create and CreateV2 instructions
|
||||||
|
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
|
||||||
|
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
|
||||||
|
|
||||||
|
if not (is_create or is_create_v2):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parsed = parse_create_instruction(ix.data)
|
# Convert account keys to string format
|
||||||
if not parsed:
|
account_keys = []
|
||||||
|
for account_idx in ix.accounts:
|
||||||
|
if account_idx < len(msg.account_keys):
|
||||||
|
account_keys.append(
|
||||||
|
base58.b58encode(bytes(msg.account_keys[account_idx])).decode()
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(account_keys) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys):
|
mint = account_keys[0]
|
||||||
continue
|
|
||||||
|
|
||||||
mint = base58.b58encode(
|
|
||||||
bytes(msg.account_keys[ix.accounts[0]])
|
|
||||||
).decode()
|
|
||||||
|
|
||||||
if mint in known_tokens:
|
if mint in known_tokens:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Decode based on instruction type
|
||||||
|
if is_create_v2:
|
||||||
|
print(f"[{provider_name}_geyser] Detected: CreateV2 instruction (Token2022)")
|
||||||
|
decoded = decode_create_v2_instruction(ix.data, account_keys)
|
||||||
|
else:
|
||||||
|
print(f"[{provider_name}_geyser] Detected: Create instruction (Legacy/Metaplex)")
|
||||||
|
decoded = decode_create_instruction(ix.data, account_keys)
|
||||||
|
|
||||||
|
if not decoded:
|
||||||
|
continue
|
||||||
|
|
||||||
ts = time.time()
|
ts = time.time()
|
||||||
tracker.add_token(
|
tracker.add_token(
|
||||||
mint,
|
mint,
|
||||||
parsed["name"],
|
decoded["name"],
|
||||||
parsed["symbol"],
|
decoded["symbol"],
|
||||||
f"{provider_name}_geyser",
|
f"{provider_name}_geyser",
|
||||||
ts,
|
ts,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,17 @@
|
|||||||
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
|
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
|
||||||
Decodes transaction data to extract mint, bonding curve, and user details.
|
Decodes transaction data to extract mint, bonding curve, and user details.
|
||||||
|
|
||||||
It is usually slower than other listeners.
|
Performance: Usually slower than other listeners due to block-level processing.
|
||||||
|
|
||||||
|
This script uses blockSubscribe which receives entire blocks containing transactions
|
||||||
|
that mention the Pump.fun program. It then decodes the instruction data from each
|
||||||
|
transaction to extract token creation details.
|
||||||
|
|
||||||
|
WebSocket API Reference:
|
||||||
|
https://solana.com/docs/rpc/websocket/blocksubscribe
|
||||||
|
|
||||||
|
Address Lookup Tables (ALT) Support:
|
||||||
|
https://solana.com/docs/advanced/lookup-tables
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -22,6 +32,93 @@ load_dotenv()
|
|||||||
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
|
||||||
|
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||||
|
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||||
|
# and sha256("global:createV2") for Token2022 CreateV2
|
||||||
|
# See: learning-examples/calculate_discriminator.py
|
||||||
|
CREATE_DISCRIMINATOR = 8576854823835016728
|
||||||
|
CREATE_V2_DISCRIMINATOR = struct.unpack("<Q", bytes([214, 144, 76, 236, 95, 139, 49, 180]))[0]
|
||||||
|
|
||||||
|
|
||||||
|
def print_token_info(token_data, signature=None):
|
||||||
|
"""
|
||||||
|
Print token information in a consistent, user-friendly format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_data: Dictionary containing token fields
|
||||||
|
signature: Optional transaction signature
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🎯 NEW TOKEN DETECTED")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||||
|
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||||
|
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||||
|
|
||||||
|
if "bondingCurve" in token_data:
|
||||||
|
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||||
|
if "associatedBondingCurve" in token_data:
|
||||||
|
print(f"Associated BC: {token_data['associatedBondingCurve']}")
|
||||||
|
if "user" in token_data:
|
||||||
|
print(f"User: {token_data['user']}")
|
||||||
|
if "creator" in token_data:
|
||||||
|
print(f"Creator: {token_data['creator']}")
|
||||||
|
|
||||||
|
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||||
|
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||||
|
|
||||||
|
if "uri" in token_data:
|
||||||
|
print(f"URI: {token_data['uri']}")
|
||||||
|
if signature:
|
||||||
|
print(f"Signature: {signature}")
|
||||||
|
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def get_account_keys(transaction, instruction, loaded_addresses=None):
|
||||||
|
"""
|
||||||
|
Safely extract account keys for an instruction from a versioned transaction.
|
||||||
|
Handles both static account keys and loaded addresses from lookup tables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
transaction: VersionedTransaction object
|
||||||
|
instruction: Instruction object
|
||||||
|
loaded_addresses: Dict with 'writable' and 'readonly' loaded addresses from tx meta
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of account keys as strings, or None if unable to resolve
|
||||||
|
"""
|
||||||
|
account_keys = []
|
||||||
|
static_keys = transaction.message.account_keys
|
||||||
|
|
||||||
|
# Combine all available account keys: static + loaded
|
||||||
|
all_keys = list(static_keys)
|
||||||
|
|
||||||
|
if loaded_addresses:
|
||||||
|
# Add loaded writable addresses
|
||||||
|
if "writable" in loaded_addresses:
|
||||||
|
for addr in loaded_addresses["writable"]:
|
||||||
|
all_keys.append(Pubkey.from_string(addr))
|
||||||
|
|
||||||
|
# Add loaded readonly addresses
|
||||||
|
if "readonly" in loaded_addresses:
|
||||||
|
for addr in loaded_addresses["readonly"]:
|
||||||
|
all_keys.append(Pubkey.from_string(addr))
|
||||||
|
|
||||||
|
# Now resolve account indices
|
||||||
|
for index in instruction.accounts:
|
||||||
|
try:
|
||||||
|
if index < len(all_keys):
|
||||||
|
account_keys.append(str(all_keys[index]))
|
||||||
|
else:
|
||||||
|
print(f"Warning: Account index {index} out of range (max: {len(all_keys)-1})")
|
||||||
|
return None
|
||||||
|
except (IndexError, Exception) as e:
|
||||||
|
print(f"Error resolving account at index {index}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return account_keys
|
||||||
|
|
||||||
|
|
||||||
def load_idl(file_path):
|
def load_idl(file_path):
|
||||||
with open(file_path) as f:
|
with open(file_path) as f:
|
||||||
@@ -29,16 +126,34 @@ def load_idl(file_path):
|
|||||||
|
|
||||||
|
|
||||||
def decode_create_instruction(ix_data, ix_def, accounts):
|
def decode_create_instruction(ix_data, ix_def, accounts):
|
||||||
|
"""
|
||||||
|
Decode legacy Create instruction (Metaplex tokens).
|
||||||
|
|
||||||
|
The Create instruction creates tokens using the Metaplex Token Metadata standard.
|
||||||
|
Instruction data contains: name, symbol, uri, and additional creator pubkey.
|
||||||
|
Account references are extracted from the accounts array.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ix_data: Raw instruction data bytes
|
||||||
|
ix_def: Instruction definition from IDL
|
||||||
|
accounts: List of account pubkeys involved in the instruction
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing decoded token information
|
||||||
|
"""
|
||||||
args = {}
|
args = {}
|
||||||
offset = 8 # Skip 8-byte discriminator
|
offset = 8 # Skip 8-byte discriminator
|
||||||
|
|
||||||
|
# Parse instruction arguments according to IDL definition
|
||||||
for arg in ix_def["args"]:
|
for arg in ix_def["args"]:
|
||||||
if arg["type"] == "string":
|
if arg["type"] == "string":
|
||||||
|
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||||
length = struct.unpack_from("<I", ix_data, offset)[0]
|
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
value = ix_data[offset : offset + length].decode("utf-8")
|
value = ix_data[offset : offset + length].decode("utf-8")
|
||||||
offset += length
|
offset += length
|
||||||
elif arg["type"] == "pubkey":
|
elif arg["type"] == "pubkey":
|
||||||
|
# Pubkey is 32 bytes, encoded as base58
|
||||||
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||||
offset += 32
|
offset += 32
|
||||||
else:
|
else:
|
||||||
@@ -46,19 +161,90 @@ def decode_create_instruction(ix_data, ix_def, accounts):
|
|||||||
|
|
||||||
args[arg["name"]] = value
|
args[arg["name"]] = value
|
||||||
|
|
||||||
# Add accounts
|
# Extract account addresses from the accounts array
|
||||||
|
# Account layout for Create instruction:
|
||||||
|
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
|
||||||
|
# 4: tokenProgram, 5: systemProgram, 6: rent, 7: user
|
||||||
args["mint"] = str(accounts[0])
|
args["mint"] = str(accounts[0])
|
||||||
args["bondingCurve"] = str(accounts[2])
|
args["bondingCurve"] = str(accounts[2])
|
||||||
args["associatedBondingCurve"] = str(accounts[3])
|
args["associatedBondingCurve"] = str(accounts[3])
|
||||||
args["user"] = str(accounts[7])
|
args["user"] = str(accounts[7])
|
||||||
|
args["token_standard"] = "legacy"
|
||||||
|
args["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def decode_create_v2_instruction(ix_data, ix_def, accounts):
|
||||||
|
"""
|
||||||
|
Decode CreateV2 instruction (Token2022 tokens).
|
||||||
|
|
||||||
|
The CreateV2 instruction creates tokens using the Token-2022 standard, which supports
|
||||||
|
additional features like transfer fees, interest-bearing tokens, and more.
|
||||||
|
This instruction includes an optional is_mayhem_mode flag.
|
||||||
|
|
||||||
|
Token-2022 Reference:
|
||||||
|
https://spl.solana.com/token-2022
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ix_data: Raw instruction data bytes
|
||||||
|
ix_def: Instruction definition from IDL
|
||||||
|
accounts: List of account pubkeys involved in the instruction
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing decoded token information
|
||||||
|
"""
|
||||||
|
args = {}
|
||||||
|
offset = 8 # Skip 8-byte discriminator
|
||||||
|
|
||||||
|
# Parse instruction arguments according to IDL definition
|
||||||
|
for arg in ix_def["args"]:
|
||||||
|
if arg["type"] == "string":
|
||||||
|
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||||
|
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||||
|
offset += 4
|
||||||
|
value = ix_data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
elif arg["type"] == "pubkey":
|
||||||
|
# Pubkey is 32 bytes, encoded as base58
|
||||||
|
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported type: {arg['type']}")
|
||||||
|
|
||||||
|
args[arg["name"]] = value
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
# Format: 1 byte (0 = false/None, 1 = true)
|
||||||
|
is_mayhem_mode = False
|
||||||
|
if offset < len(ix_data):
|
||||||
|
is_mayhem_mode = bool(ix_data[offset])
|
||||||
|
|
||||||
|
# Extract account addresses from the accounts array
|
||||||
|
# Account layout for CreateV2 instruction:
|
||||||
|
# 0: mint, 1: metadata, 2: bondingCurve, 3: associatedBondingCurve,
|
||||||
|
# 4: tokenProgram (Token2022), 5: user, 6: systemProgram, 7: rent
|
||||||
|
args["mint"] = str(accounts[0])
|
||||||
|
args["bondingCurve"] = str(accounts[2])
|
||||||
|
args["associatedBondingCurve"] = str(accounts[3])
|
||||||
|
args["user"] = str(accounts[5])
|
||||||
|
args["token_standard"] = "token2022"
|
||||||
|
args["is_mayhem_mode"] = is_mayhem_mode
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
|
||||||
async def listen_and_decode_create():
|
async def listen_and_decode_create():
|
||||||
|
"""
|
||||||
|
Main listener function that subscribes to Solana blocks and decodes Pump.fun token creations.
|
||||||
|
|
||||||
|
This function:
|
||||||
|
1. Loads the Pump.fun IDL for instruction parsing
|
||||||
|
2. Subscribes to blocks mentioning the Pump.fun program
|
||||||
|
3. Decodes transactions to extract Create/CreateV2 instructions
|
||||||
|
4. Handles Address Lookup Tables (ALTs) for account resolution
|
||||||
|
"""
|
||||||
idl = load_idl("idl/pump_fun_idl.json")
|
idl = load_idl("idl/pump_fun_idl.json")
|
||||||
create_discriminator = 8576854823835016728
|
|
||||||
|
|
||||||
async with websockets.connect(WSS_ENDPOINT) as websocket:
|
async with websockets.connect(WSS_ENDPOINT) as websocket:
|
||||||
subscription_message = json.dumps(
|
subscription_message = json.dumps(
|
||||||
@@ -101,6 +287,11 @@ async def listen_and_decode_create():
|
|||||||
tx_data_decoded
|
tx_data_decoded
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Extract loaded addresses from transaction metadata
|
||||||
|
loaded_addresses = None
|
||||||
|
if "meta" in tx and tx["meta"] and "loadedAddresses" in tx["meta"]:
|
||||||
|
loaded_addresses = tx["meta"]["loadedAddresses"]
|
||||||
|
|
||||||
for ix in transaction.message.instructions:
|
for ix in transaction.message.instructions:
|
||||||
if str(
|
if str(
|
||||||
transaction.message.account_keys[
|
transaction.message.account_keys[
|
||||||
@@ -112,36 +303,68 @@ async def listen_and_decode_create():
|
|||||||
"<Q", ix_data[:8]
|
"<Q", ix_data[:8]
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
if (
|
if discriminator == CREATE_DISCRIMINATOR:
|
||||||
discriminator
|
# Legacy Create instruction (Metaplex tokens)
|
||||||
== create_discriminator
|
|
||||||
):
|
|
||||||
create_ix = next(
|
create_ix = next(
|
||||||
instr
|
instr
|
||||||
for instr in idl["instructions"]
|
for instr in idl["instructions"]
|
||||||
if instr["name"] == "create"
|
if instr["name"] == "create"
|
||||||
)
|
)
|
||||||
account_keys = [
|
account_keys = get_account_keys(
|
||||||
str(
|
transaction, ix, loaded_addresses
|
||||||
transaction.message.account_keys[
|
|
||||||
index
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for index in ix.accounts
|
|
||||||
]
|
|
||||||
decoded_args = (
|
|
||||||
decode_create_instruction(
|
|
||||||
ix_data,
|
|
||||||
create_ix,
|
|
||||||
account_keys,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
print(
|
if account_keys is None:
|
||||||
json.dumps(
|
print("⚠️ Skipping transaction due to unresolved accounts")
|
||||||
decoded_args, indent=2
|
continue
|
||||||
)
|
|
||||||
|
# Decode instruction data
|
||||||
|
decoded_args = decode_create_instruction(
|
||||||
|
ix_data,
|
||||||
|
create_ix,
|
||||||
|
account_keys,
|
||||||
)
|
)
|
||||||
print("--------------------")
|
|
||||||
|
# Print token information
|
||||||
|
print_token_info(decoded_args)
|
||||||
|
|
||||||
|
# Note if using Address Lookup Tables
|
||||||
|
if loaded_addresses:
|
||||||
|
writable_count = len(loaded_addresses.get("writable", []))
|
||||||
|
readonly_count = len(loaded_addresses.get("readonly", []))
|
||||||
|
if writable_count > 0 or readonly_count > 0:
|
||||||
|
print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
|
||||||
|
|
||||||
|
elif discriminator == CREATE_V2_DISCRIMINATOR:
|
||||||
|
# CreateV2 instruction (Token2022 tokens)
|
||||||
|
create_v2_ix = next(
|
||||||
|
(instr for instr in idl["instructions"]
|
||||||
|
if instr["name"] == "createV2"),
|
||||||
|
next(instr for instr in idl["instructions"]
|
||||||
|
if instr["name"] == "create")
|
||||||
|
)
|
||||||
|
account_keys = get_account_keys(
|
||||||
|
transaction, ix, loaded_addresses
|
||||||
|
)
|
||||||
|
if account_keys is None:
|
||||||
|
print("⚠️ Skipping transaction due to unresolved accounts")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Decode instruction data
|
||||||
|
decoded_args = decode_create_v2_instruction(
|
||||||
|
ix_data,
|
||||||
|
create_v2_ix,
|
||||||
|
account_keys,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print token information
|
||||||
|
print_token_info(decoded_args)
|
||||||
|
|
||||||
|
# Note if using Address Lookup Tables
|
||||||
|
if loaded_addresses:
|
||||||
|
writable_count = len(loaded_addresses.get("writable", []))
|
||||||
|
readonly_count = len(loaded_addresses.get("readonly", []))
|
||||||
|
if writable_count > 0 or readonly_count > 0:
|
||||||
|
print(f"ℹ️ [ALT] Used Address Lookup Table: {writable_count} writable, {readonly_count} readonly\n")
|
||||||
elif "result" in data:
|
elif "result" in data:
|
||||||
print("Subscription confirmed")
|
print("Subscription confirmed")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
|
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
|
||||||
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
|
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
|
||||||
Requires a Geyser API token for access.
|
|
||||||
Supports both Basic and X-Token authentication methods.
|
|
||||||
|
|
||||||
It is proven to be the fastest listener.
|
Performance: Proven to be the fastest listener method available.
|
||||||
|
|
||||||
|
This script uses Yellowstone Dragon's Mouth Geyser gRPC interface, which provides
|
||||||
|
real-time streaming of Solana blockchain data with lower latency than WebSocket methods.
|
||||||
|
Requires a Geyser API token for access.
|
||||||
|
|
||||||
|
Geyser gRPC Reference:
|
||||||
|
https://docs.triton.one/rpc-pool/grpc-subscriptions
|
||||||
|
|
||||||
|
Authentication: Supports both Basic and X-Token authentication methods.
|
||||||
|
Configure via GEYSER_ENDPOINT, GEYSER_API_TOKEN, and AUTH_TYPE variables.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -22,11 +30,53 @@ load_dotenv()
|
|||||||
|
|
||||||
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
|
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
|
||||||
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
|
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
|
||||||
# Default to x-token auth, can be set to "basic"
|
# Authentication type: "x-token" or "basic"
|
||||||
AUTH_TYPE = "x-token"
|
AUTH_TYPE = "x-token"
|
||||||
|
|
||||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
|
||||||
|
# Instruction discriminators (8-byte identifiers for instruction types)
|
||||||
|
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
|
||||||
|
# and sha256("global:createV2") for Token2022 CreateV2
|
||||||
|
# See: learning-examples/calculate_discriminator.py
|
||||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||||
|
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||||
|
|
||||||
|
|
||||||
|
def print_token_info(token_data, signature=None):
|
||||||
|
"""
|
||||||
|
Print token information in a consistent, user-friendly format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_data: Dictionary containing token fields
|
||||||
|
signature: Optional transaction signature
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🎯 NEW TOKEN DETECTED")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||||
|
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||||
|
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||||
|
|
||||||
|
if "bonding_curve" in token_data:
|
||||||
|
print(f"Bonding Curve: {token_data['bonding_curve']}")
|
||||||
|
if "associated_bonding_curve" in token_data:
|
||||||
|
print(f"Associated BC: {token_data['associated_bonding_curve']}")
|
||||||
|
if "user" in token_data:
|
||||||
|
print(f"User: {token_data['user']}")
|
||||||
|
if "creator" in token_data:
|
||||||
|
print(f"Creator: {token_data['creator']}")
|
||||||
|
|
||||||
|
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||||
|
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||||
|
|
||||||
|
if "uri" in token_data:
|
||||||
|
print(f"URI: {token_data['uri']}")
|
||||||
|
if signature:
|
||||||
|
print(f"Signature: {signature}")
|
||||||
|
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def create_geyser_connection():
|
async def create_geyser_connection():
|
||||||
@@ -57,7 +107,7 @@ def create_subscription_request():
|
|||||||
|
|
||||||
|
|
||||||
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
|
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
|
||||||
"""Decode a create instruction from transaction data."""
|
"""Decode a legacy create instruction (Metaplex) from transaction data."""
|
||||||
# Skip past the 8-byte discriminator prefix
|
# Skip past the 8-byte discriminator prefix
|
||||||
offset = 8
|
offset = 8
|
||||||
|
|
||||||
@@ -103,20 +153,68 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
|
|||||||
"system_program": get_account_key(5),
|
"system_program": get_account_key(5),
|
||||||
"rent": get_account_key(6),
|
"rent": get_account_key(6),
|
||||||
"user": get_account_key(7),
|
"user": get_account_key(7),
|
||||||
|
"token_standard": "legacy",
|
||||||
|
"is_mayhem_mode": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return token_info
|
||||||
|
|
||||||
|
|
||||||
|
def decode_create_v2_instruction(ix_data: bytes, keys, accounts) -> dict:
|
||||||
|
"""Decode a CreateV2 instruction (Token2022) from transaction data."""
|
||||||
|
# Skip past the 8-byte discriminator prefix
|
||||||
|
offset = 8
|
||||||
|
|
||||||
|
# Extract account keys in base58 format
|
||||||
|
def get_account_key(index):
|
||||||
|
if index >= len(accounts):
|
||||||
|
return "N/A"
|
||||||
|
account_index = accounts[index]
|
||||||
|
return base58.b58encode(keys[account_index]).decode()
|
||||||
|
|
||||||
|
# Read string fields (prefixed with length)
|
||||||
|
def read_string():
|
||||||
|
nonlocal offset
|
||||||
|
# Get string length (4-byte uint)
|
||||||
|
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||||
|
offset += 4
|
||||||
|
# Extract and decode the string
|
||||||
|
value = ix_data[offset : offset + length].decode()
|
||||||
|
offset += length
|
||||||
|
return value
|
||||||
|
|
||||||
|
def read_pubkey():
|
||||||
|
nonlocal offset
|
||||||
|
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
return value
|
||||||
|
|
||||||
|
name = read_string()
|
||||||
|
symbol = read_string()
|
||||||
|
uri = read_string()
|
||||||
|
creator = read_pubkey()
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
is_mayhem_mode = False
|
||||||
|
if offset < len(ix_data):
|
||||||
|
is_mayhem_mode = bool(ix_data[offset])
|
||||||
|
|
||||||
|
token_info = {
|
||||||
|
"name": name,
|
||||||
|
"symbol": symbol,
|
||||||
|
"uri": uri,
|
||||||
|
"creator": creator,
|
||||||
|
"mint": get_account_key(0),
|
||||||
|
"bonding_curve": get_account_key(2),
|
||||||
|
"associated_bonding_curve": get_account_key(3),
|
||||||
|
"user": get_account_key(5),
|
||||||
|
"token_standard": "token2022",
|
||||||
|
"is_mayhem_mode": is_mayhem_mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
return token_info
|
return token_info
|
||||||
|
|
||||||
|
|
||||||
def print_token_info(info, signature):
|
|
||||||
"""Print formatted token information."""
|
|
||||||
print("\n🎯 New Pump.fun token detected!")
|
|
||||||
print(f"Name: {info['name']} | Symbol: {info['symbol']}")
|
|
||||||
print(f"Mint: {info['mint']}")
|
|
||||||
print(f"Bonding curve: {info['bonding_curve']}")
|
|
||||||
print(f"Associated bonding curve: {info['associated_bonding_curve']}")
|
|
||||||
print(f"Creator: {info['creator']}")
|
|
||||||
print(f"Signature: {signature}")
|
|
||||||
|
|
||||||
|
|
||||||
async def monitor_pump():
|
async def monitor_pump():
|
||||||
@@ -137,14 +235,28 @@ async def monitor_pump():
|
|||||||
|
|
||||||
# Check each instruction in the transaction
|
# Check each instruction in the transaction
|
||||||
for ix in msg.instructions:
|
for ix in msg.instructions:
|
||||||
if not ix.data.startswith(PUMP_CREATE_PREFIX):
|
# Check for both Create and CreateV2 instructions
|
||||||
|
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
|
||||||
|
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
|
||||||
|
|
||||||
|
if not (is_create or is_create_v2):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
|
# Decode based on instruction type
|
||||||
|
if is_create_v2:
|
||||||
|
info = decode_create_v2_instruction(
|
||||||
|
ix.data, msg.account_keys, ix.accounts
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
|
||||||
|
|
||||||
|
# Extract transaction signature
|
||||||
signature = base58.b58encode(
|
signature = base58.b58encode(
|
||||||
bytes(update.transaction.transaction.signature)
|
bytes(update.transaction.transaction.signature)
|
||||||
).decode()
|
).decode()
|
||||||
print_token_info(info, signature)
|
|
||||||
|
# Print token information in consistent format
|
||||||
|
print_token_info(info, signature=signature)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -2,7 +2,17 @@
|
|||||||
Listens for new Pump.fun token creations via Solana WebSocket.
|
Listens for new Pump.fun token creations via Solana WebSocket.
|
||||||
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
||||||
|
|
||||||
It is usually faster than blockSubscribe, but slower than Geyser.
|
Performance: Usually faster than blockSubscribe, but slower than Geyser.
|
||||||
|
|
||||||
|
This script uses logsSubscribe which receives program logs containing event data.
|
||||||
|
Event logs include all token fields directly, making parsing simpler and faster than
|
||||||
|
decoding full transactions.
|
||||||
|
|
||||||
|
WebSocket API Reference:
|
||||||
|
https://solana.com/docs/rpc/websocket/logssubscribe
|
||||||
|
|
||||||
|
Program Logs and Events:
|
||||||
|
https://solana.com/docs/programs/debugging#logging
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -21,11 +31,71 @@ load_dotenv()
|
|||||||
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
|
||||||
|
# Event discriminator for CreateEvent (8-byte identifier)
|
||||||
|
# This is emitted by both Create and CreateV2 instructions
|
||||||
|
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||||
|
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||||
|
|
||||||
|
|
||||||
|
def print_token_info(token_data, signature=None):
|
||||||
|
"""
|
||||||
|
Print token information in a consistent, user-friendly format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_data: Dictionary containing token fields
|
||||||
|
signature: Optional transaction signature
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🎯 NEW TOKEN DETECTED")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||||
|
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||||
|
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||||
|
|
||||||
|
if "bondingCurve" in token_data:
|
||||||
|
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||||
|
if "user" in token_data:
|
||||||
|
print(f"User: {token_data['user']}")
|
||||||
|
if "creator" in token_data:
|
||||||
|
print(f"Creator: {token_data['creator']}")
|
||||||
|
|
||||||
|
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||||
|
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||||
|
|
||||||
|
if "uri" in token_data:
|
||||||
|
print(f"URI: {token_data['uri']}")
|
||||||
|
if signature:
|
||||||
|
print(f"Signature: {signature}")
|
||||||
|
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_create_instruction(data):
|
def parse_create_instruction(data):
|
||||||
|
"""
|
||||||
|
Parse CreateEvent data from legacy Create instruction (Metaplex tokens).
|
||||||
|
|
||||||
|
Event logs contain all fields directly embedded in the event data, unlike
|
||||||
|
instruction data which requires account lookup. Event format:
|
||||||
|
- 8 bytes: event discriminator
|
||||||
|
- Variable: name (4-byte length + UTF-8 string)
|
||||||
|
- Variable: symbol (4-byte length + UTF-8 string)
|
||||||
|
- Variable: uri (4-byte length + UTF-8 string)
|
||||||
|
- 32 bytes: mint pubkey
|
||||||
|
- 32 bytes: bondingCurve pubkey
|
||||||
|
- 32 bytes: user pubkey
|
||||||
|
- 32 bytes: creator pubkey
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw event data bytes from program logs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing decoded token information, or None if parsing fails
|
||||||
|
"""
|
||||||
if len(data) < 8:
|
if len(data) < 8:
|
||||||
|
print(f"⚠️ Data too short for Create event: {len(data)} bytes")
|
||||||
return None
|
return None
|
||||||
offset = 8
|
offset = 8 # Skip event discriminator
|
||||||
parsed_data = {}
|
parsed_data = {}
|
||||||
|
|
||||||
# Parse fields based on CreateEvent structure
|
# Parse fields based on CreateEvent structure
|
||||||
@@ -42,18 +112,102 @@ def parse_create_instruction(data):
|
|||||||
try:
|
try:
|
||||||
for field_name, field_type in fields:
|
for field_name, field_type in fields:
|
||||||
if field_type == "string":
|
if field_type == "string":
|
||||||
|
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
value = data[offset : offset + length].decode("utf-8")
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
offset += length
|
offset += length
|
||||||
elif field_type == "publicKey":
|
elif field_type == "publicKey":
|
||||||
|
# Pubkey is 32 bytes, encoded as base58
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
offset += 32
|
offset += 32
|
||||||
|
|
||||||
parsed_data[field_name] = value
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "legacy"
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
return parsed_data
|
return parsed_data
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(f"❌ Parse Create error: {e}")
|
||||||
|
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||||
|
print(f" Data hex: {data.hex()[:200]}...")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_create_v2_instruction(data):
|
||||||
|
"""
|
||||||
|
Parse CreateEvent data from CreateV2 instruction (Token2022 tokens).
|
||||||
|
|
||||||
|
CreateV2 uses Token-2022 standard with additional features. The event format
|
||||||
|
is identical to Create, with an additional optional is_mayhem_mode flag at the end.
|
||||||
|
|
||||||
|
Token-2022 Reference:
|
||||||
|
https://spl.solana.com/token-2022
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw event data bytes from program logs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing decoded token information, or None if parsing fails
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
print(f"⚠️ Data too short for CreateV2 event: {len(data)} bytes")
|
||||||
|
return None
|
||||||
|
offset = 8 # Skip event discriminator
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
# Parse fields based on CreateV2Event structure
|
||||||
|
fields = [
|
||||||
|
("name", "string"),
|
||||||
|
("symbol", "string"),
|
||||||
|
("uri", "string"),
|
||||||
|
("mint", "publicKey"),
|
||||||
|
("bondingCurve", "publicKey"),
|
||||||
|
("user", "publicKey"),
|
||||||
|
("creator", "publicKey"),
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
for field_name, field_type in fields:
|
||||||
|
if field_type == "string":
|
||||||
|
# String format: 4-byte length prefix + UTF-8 encoded string
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
elif field_type == "publicKey":
|
||||||
|
# Pubkey is 32 bytes, encoded as base58
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
# Format: 1 byte (0 = false/None, 1 = true)
|
||||||
|
if offset < len(data):
|
||||||
|
is_mayhem_mode = bool(data[offset])
|
||||||
|
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||||
|
else:
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "token2022"
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Parse CreateV2 error: {e}")
|
||||||
|
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||||
|
print(f" Data hex: {data.hex()[:200]}...")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -90,10 +244,17 @@ async def listen_for_new_tokens():
|
|||||||
log_data = data["params"]["result"]["value"]
|
log_data = data["params"]["result"]["value"]
|
||||||
logs = log_data.get("logs", [])
|
logs = log_data.get("logs", [])
|
||||||
|
|
||||||
if any(
|
# Detect both Create and CreateV2 instructions
|
||||||
|
is_create = any(
|
||||||
"Program log: Instruction: Create" in log
|
"Program log: Instruction: Create" in log
|
||||||
for log in logs
|
for log in logs
|
||||||
):
|
)
|
||||||
|
is_create_v2 = any(
|
||||||
|
"Program log: Instruction: CreateV2" in log
|
||||||
|
for log in logs
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_create or is_create_v2:
|
||||||
for log in logs:
|
for log in logs:
|
||||||
if "Program data:" in log:
|
if "Program data:" in log:
|
||||||
try:
|
try:
|
||||||
@@ -101,22 +262,40 @@ async def listen_for_new_tokens():
|
|||||||
decoded_data = base64.b64decode(
|
decoded_data = base64.b64decode(
|
||||||
encoded_data
|
encoded_data
|
||||||
)
|
)
|
||||||
parsed_data = parse_create_instruction(
|
|
||||||
decoded_data
|
# Check if this is a CreateEvent by validating discriminator
|
||||||
)
|
if len(decoded_data) < 8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_discriminator = decoded_data[:8]
|
||||||
|
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||||
|
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
|
||||||
|
print(f" Signature: {log_data.get('signature')}")
|
||||||
|
|
||||||
|
# Both create and create_v2 emit the same CreateEvent
|
||||||
|
# The difference is in the optional is_mayhem_mode field
|
||||||
|
if is_create_v2:
|
||||||
|
parsed_data = parse_create_v2_instruction(
|
||||||
|
decoded_data
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parsed_data = parse_create_instruction(
|
||||||
|
decoded_data
|
||||||
|
)
|
||||||
|
|
||||||
if parsed_data and "name" in parsed_data:
|
if parsed_data and "name" in parsed_data:
|
||||||
print(
|
# Print token information in consistent format
|
||||||
"Signature:",
|
print_token_info(
|
||||||
log_data.get("signature"),
|
parsed_data,
|
||||||
)
|
signature=log_data.get("signature")
|
||||||
for key, value in parsed_data.items():
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
print(
|
|
||||||
"##########################################################################################"
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Parsing failed for CreateEvent")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to decode: {log}")
|
print(f"❌ Error processing log: {e!s}")
|
||||||
print(f"Error: {e!s}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred while processing message: {e}")
|
print(f"An error occurred while processing message: {e}")
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
Listens for new Pump.fun token creations via Solana WebSocket.
|
Listens for new Pump.fun token creations via Solana WebSocket.
|
||||||
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
|
||||||
Additionally, calculates an associated bonding curve address for each token.
|
Additionally, calculates the associated bonding curve address for each token using PDA derivation.
|
||||||
|
|
||||||
It is usually faster than blockSubscribe, but slower than Geyser.
|
Performance: Usually faster than blockSubscribe, but slower than Geyser.
|
||||||
|
|
||||||
|
This script demonstrates Program Derived Address (PDA) calculation for the associated
|
||||||
|
bonding curve, which is the token account owned by the bonding curve that holds
|
||||||
|
the minted tokens.
|
||||||
|
|
||||||
|
WebSocket API Reference:
|
||||||
|
https://solana.com/docs/rpc/websocket/logssubscribe
|
||||||
|
|
||||||
|
Program Derived Addresses:
|
||||||
|
https://solana.com/docs/core/pda
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -26,11 +36,67 @@ ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string(
|
|||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Event discriminator for CreateEvent (8-byte identifier)
|
||||||
|
# This is emitted by both Create and CreateV2 instructions
|
||||||
|
# Calculated using the first 8 bytes of sha256("event:CreateEvent")
|
||||||
|
CREATE_EVENT_DISCRIMINATOR = bytes([27, 114, 169, 77, 222, 235, 99, 118])
|
||||||
|
|
||||||
|
|
||||||
|
def print_token_info(token_data, signature=None, associated_bonding_curve=None):
|
||||||
|
"""
|
||||||
|
Print token information in a consistent, user-friendly format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_data: Dictionary containing token fields
|
||||||
|
signature: Optional transaction signature
|
||||||
|
associated_bonding_curve: Optional associated bonding curve address
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🎯 NEW TOKEN DETECTED")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||||
|
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||||
|
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||||
|
|
||||||
|
if "bondingCurve" in token_data:
|
||||||
|
print(f"Bonding Curve: {token_data['bondingCurve']}")
|
||||||
|
if associated_bonding_curve:
|
||||||
|
print(f"Associated BC: {associated_bonding_curve}")
|
||||||
|
if "user" in token_data:
|
||||||
|
print(f"User: {token_data['user']}")
|
||||||
|
if "creator" in token_data:
|
||||||
|
print(f"Creator: {token_data['creator']}")
|
||||||
|
|
||||||
|
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
|
||||||
|
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
|
||||||
|
|
||||||
|
if "uri" in token_data:
|
||||||
|
print(f"URI: {token_data['uri']}")
|
||||||
|
if signature:
|
||||||
|
print(f"Signature: {signature}")
|
||||||
|
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||||
"""
|
"""
|
||||||
Find the associated bonding curve for a given mint and bonding curve.
|
Calculate the associated token account (ATA) address for the bonding curve.
|
||||||
This uses the standard ATA derivation.
|
|
||||||
|
The associated bonding curve is a Program Derived Address (PDA) that holds
|
||||||
|
the token supply controlled by the bonding curve. It's derived using the
|
||||||
|
standard Associated Token Account (ATA) derivation.
|
||||||
|
|
||||||
|
ATA Derivation: find_program_address(
|
||||||
|
[bonding_curve_pubkey, token_program_id, mint_pubkey],
|
||||||
|
associated_token_program_id
|
||||||
|
)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mint: The token mint pubkey
|
||||||
|
bonding_curve: The bonding curve pubkey
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The derived associated bonding curve address
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[
|
[
|
||||||
@@ -44,7 +110,9 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey
|
|||||||
|
|
||||||
|
|
||||||
def parse_create_instruction(data):
|
def parse_create_instruction(data):
|
||||||
|
"""Parse legacy Create instruction (Metaplex tokens)."""
|
||||||
if len(data) < 8:
|
if len(data) < 8:
|
||||||
|
print(f"⚠️ Data too short for Create instruction: {len(data)} bytes")
|
||||||
return None
|
return None
|
||||||
offset = 8
|
offset = 8
|
||||||
parsed_data = {}
|
parsed_data = {}
|
||||||
@@ -63,18 +131,81 @@ def parse_create_instruction(data):
|
|||||||
try:
|
try:
|
||||||
for field_name, field_type in fields:
|
for field_name, field_type in fields:
|
||||||
if field_type == "string":
|
if field_type == "string":
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
value = data[offset : offset + length].decode("utf-8")
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
offset += length
|
offset += length
|
||||||
elif field_type == "publicKey":
|
elif field_type == "publicKey":
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
offset += 32
|
offset += 32
|
||||||
|
|
||||||
parsed_data[field_name] = value
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "legacy"
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
return parsed_data
|
return parsed_data
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(f"❌ Parse Create error: {e}")
|
||||||
|
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_create_v2_instruction(data):
|
||||||
|
"""Parse CreateV2 instruction (Token2022 tokens)."""
|
||||||
|
if len(data) < 8:
|
||||||
|
print(f"⚠️ Data too short for CreateV2 instruction: {len(data)} bytes")
|
||||||
|
return None
|
||||||
|
offset = 8
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
# Parse fields based on CreateV2Event structure
|
||||||
|
fields = [
|
||||||
|
("name", "string"),
|
||||||
|
("symbol", "string"),
|
||||||
|
("uri", "string"),
|
||||||
|
("mint", "publicKey"),
|
||||||
|
("bondingCurve", "publicKey"),
|
||||||
|
("user", "publicKey"),
|
||||||
|
("creator", "publicKey"),
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
for field_name, field_type in fields:
|
||||||
|
if field_type == "string":
|
||||||
|
if offset + 4 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} length at offset {offset}")
|
||||||
|
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
if offset + length > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} value (length={length}) at offset {offset}")
|
||||||
|
value = data[offset : offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
elif field_type == "publicKey":
|
||||||
|
if offset + 32 > len(data):
|
||||||
|
raise ValueError(f"Not enough data for {field_name} at offset {offset}")
|
||||||
|
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
parsed_data[field_name] = value
|
||||||
|
|
||||||
|
# Parse is_mayhem_mode (OptionBool at the end)
|
||||||
|
if offset < len(data):
|
||||||
|
is_mayhem_mode = bool(data[offset])
|
||||||
|
parsed_data["is_mayhem_mode"] = is_mayhem_mode
|
||||||
|
else:
|
||||||
|
parsed_data["is_mayhem_mode"] = False
|
||||||
|
|
||||||
|
parsed_data["token_standard"] = "token2022"
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Parse CreateV2 error: {e}")
|
||||||
|
print(f" Data length: {len(data)} bytes, offset: {offset}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -111,10 +242,17 @@ async def listen_for_new_tokens():
|
|||||||
log_data = data["params"]["result"]["value"]
|
log_data = data["params"]["result"]["value"]
|
||||||
logs = log_data.get("logs", [])
|
logs = log_data.get("logs", [])
|
||||||
|
|
||||||
if any(
|
# Detect both Create and CreateV2 instructions
|
||||||
|
is_create = any(
|
||||||
"Program log: Instruction: Create" in log
|
"Program log: Instruction: Create" in log
|
||||||
for log in logs
|
for log in logs
|
||||||
):
|
)
|
||||||
|
is_create_v2 = any(
|
||||||
|
"Program log: Instruction: CreateV2" in log
|
||||||
|
for log in logs
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_create or is_create_v2:
|
||||||
for log in logs:
|
for log in logs:
|
||||||
if "Program data:" in log:
|
if "Program data:" in log:
|
||||||
try:
|
try:
|
||||||
@@ -122,38 +260,56 @@ async def listen_for_new_tokens():
|
|||||||
decoded_data = base64.b64decode(
|
decoded_data = base64.b64decode(
|
||||||
encoded_data
|
encoded_data
|
||||||
)
|
)
|
||||||
parsed_data = parse_create_instruction(
|
|
||||||
decoded_data
|
|
||||||
)
|
|
||||||
if parsed_data and "name" in parsed_data:
|
|
||||||
print(
|
|
||||||
"Signature:",
|
|
||||||
log_data.get("signature"),
|
|
||||||
)
|
|
||||||
for key, value in parsed_data.items():
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
|
|
||||||
# Calculate associated bonding curve
|
# Check if this is a CreateEvent by validating discriminator
|
||||||
|
if len(decoded_data) < 8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_discriminator = decoded_data[:8]
|
||||||
|
if event_discriminator != CREATE_EVENT_DISCRIMINATOR:
|
||||||
|
# Skip non-CreateEvent logs (e.g., TradeEvent, ExtendAccountEvent)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\n🔍 Found CreateEvent, length: {len(decoded_data)} bytes")
|
||||||
|
print(f" Signature: {log_data.get('signature')}")
|
||||||
|
|
||||||
|
# Both create and create_v2 emit the same CreateEvent
|
||||||
|
# The difference is in the optional is_mayhem_mode field
|
||||||
|
if is_create_v2:
|
||||||
|
print("📝 Instruction: CreateV2 (Token2022)")
|
||||||
|
parsed_data = (
|
||||||
|
parse_create_v2_instruction(
|
||||||
|
decoded_data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("📝 Instruction: Create (Legacy/Metaplex)")
|
||||||
|
parsed_data = parse_create_instruction(
|
||||||
|
decoded_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if parsed_data and "name" in parsed_data:
|
||||||
|
# Calculate associated bonding curve using PDA derivation
|
||||||
mint = Pubkey.from_string(
|
mint = Pubkey.from_string(
|
||||||
parsed_data["mint"]
|
parsed_data["mint"]
|
||||||
)
|
)
|
||||||
bonding_curve = Pubkey.from_string(
|
bonding_curve = Pubkey.from_string(
|
||||||
parsed_data["bondingCurve"]
|
parsed_data["bondingCurve"]
|
||||||
)
|
)
|
||||||
associated_curve = (
|
associated_curve = find_associated_bonding_curve(
|
||||||
find_associated_bonding_curve(
|
mint, bonding_curve
|
||||||
mint, bonding_curve
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
print(
|
|
||||||
f"Associated Bonding Curve: {associated_curve}"
|
# Print token information in consistent format
|
||||||
)
|
print_token_info(
|
||||||
print(
|
parsed_data,
|
||||||
"##########################################################################################"
|
signature=log_data.get("signature"),
|
||||||
|
associated_bonding_curve=str(associated_curve)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Parsing failed for CreateEvent")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to decode: {log}")
|
print(f"❌ Error processing log: {e!s}")
|
||||||
print(f"Error: {e!s}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred while processing message: {e}")
|
print(f"An error occurred while processing message: {e}")
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Listens for new Pump.fun token creations via PumpPortal WebSocket.
|
Listens for new Pump.fun token creations via PumpPortal WebSocket.
|
||||||
|
|
||||||
|
Performance: Fast, real-time data via third-party API.
|
||||||
|
|
||||||
|
This script uses PumpPortal's WebSocket API, a third-party service that aggregates
|
||||||
|
and provides real-time Pump.fun token creation events. This provides additional
|
||||||
|
market data like initial buy amounts and market cap that aren't available in
|
||||||
|
raw blockchain data.
|
||||||
|
|
||||||
|
PumpPortal API: https://pumpportal.fun/
|
||||||
|
|
||||||
|
Note: This is a third-party service and requires trust in the data provider.
|
||||||
|
For trustless monitoring, use the direct blockchain listeners (logs, block, geyser).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -12,12 +24,52 @@ import websockets
|
|||||||
WS_URL = "wss://pumpportal.fun/api/data"
|
WS_URL = "wss://pumpportal.fun/api/data"
|
||||||
|
|
||||||
|
|
||||||
def format_sol(value):
|
def print_token_info(token_data):
|
||||||
return f"{value:.6f} SOL"
|
"""
|
||||||
|
Print token information in a consistent, user-friendly format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_data: Dictionary containing token fields from PumpPortal
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🎯 NEW TOKEN DETECTED (via PumpPortal)")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Name: {token_data.get('name', 'N/A')}")
|
||||||
|
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
|
||||||
|
print(f"Mint: {token_data.get('mint', 'N/A')}")
|
||||||
|
|
||||||
|
# PumpPortal-specific fields
|
||||||
|
if "initialBuy" in token_data:
|
||||||
|
initial_buy_sol = token_data['initialBuy']
|
||||||
|
print(f"Initial Buy: {initial_buy_sol:.6f} SOL")
|
||||||
|
|
||||||
|
if "marketCapSol" in token_data:
|
||||||
|
market_cap_sol = token_data['marketCapSol']
|
||||||
|
print(f"Market Cap: {market_cap_sol:.6f} SOL")
|
||||||
|
|
||||||
|
if "bondingCurveKey" in token_data:
|
||||||
|
print(f"Bonding Curve: {token_data['bondingCurveKey']}")
|
||||||
|
|
||||||
|
if "traderPublicKey" in token_data:
|
||||||
|
print(f"Creator: {token_data['traderPublicKey']}")
|
||||||
|
|
||||||
|
# Virtual reserves
|
||||||
|
if "vSolInBondingCurve" in token_data:
|
||||||
|
v_sol = token_data['vSolInBondingCurve']
|
||||||
|
print(f"Virtual SOL: {v_sol:.6f} SOL")
|
||||||
|
|
||||||
|
if "vTokensInBondingCurve" in token_data:
|
||||||
|
v_tokens = token_data['vTokensInBondingCurve']
|
||||||
|
print(f"Virtual Tokens: {v_tokens:,.0f}")
|
||||||
|
|
||||||
|
if "uri" in token_data:
|
||||||
|
print(f"URI: {token_data['uri']}")
|
||||||
|
|
||||||
|
if "signature" in token_data:
|
||||||
|
print(f"Signature: {token_data['signature']}")
|
||||||
|
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
def format_timestamp(timestamp):
|
|
||||||
return datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
async def listen_for_new_tokens():
|
async def listen_for_new_tokens():
|
||||||
@@ -39,27 +91,8 @@ async def listen_for_new_tokens():
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
# Print token information in consistent format
|
||||||
print(
|
print_token_info(token_info)
|
||||||
f"New token created: {token_info.get('name')} ({token_info.get('symbol')})"
|
|
||||||
)
|
|
||||||
print("=" * 50)
|
|
||||||
print(f"Address: {token_info.get('mint')}")
|
|
||||||
print(f"Creator: {token_info.get('traderPublicKey')}")
|
|
||||||
print(f"Initial Buy: {format_sol(token_info.get('initialBuy', 0))}")
|
|
||||||
print(
|
|
||||||
f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}"
|
|
||||||
)
|
|
||||||
print(f"Bonding Curve: {token_info.get('bondingCurveKey')}")
|
|
||||||
print(
|
|
||||||
f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}"
|
|
||||||
)
|
|
||||||
print(f"Metadata URI: {token_info.get('uri')}")
|
|
||||||
print(f"Signature: {token_info.get('signature')}")
|
|
||||||
print("=" * 50)
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
print("\nWebSocket connection closed. Reconnecting...")
|
print("\nWebSocket connection closed. Reconnecting...")
|
||||||
break
|
break
|
||||||
|
|||||||
+122
-16
@@ -7,7 +7,7 @@ import struct
|
|||||||
|
|
||||||
import base58
|
import base58
|
||||||
import websockets
|
import websockets
|
||||||
from construct import Bytes, Flag, Int64ul, Struct
|
from construct import Flag, Int64ul, Struct
|
||||||
from solana.rpc.async_api import AsyncClient
|
from solana.rpc.async_api import AsyncClient
|
||||||
from solana.rpc.commitment import Confirmed
|
from solana.rpc.commitment import Confirmed
|
||||||
from solana.rpc.types import TxOpts
|
from solana.rpc.types import TxOpts
|
||||||
@@ -36,6 +36,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
|||||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
)
|
)
|
||||||
@@ -48,27 +49,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
|||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT = Struct(
|
"""Bonding curve state parser with progressive field parsing.
|
||||||
|
|
||||||
|
Parses bonding curve account data progressively based on available bytes,
|
||||||
|
making it forward-compatible with future schema versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base struct present in all versions
|
||||||
|
_BASE_STRUCT = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
"real_sol_reserves" / Int64ul,
|
"real_sol_reserves" / Int64ul,
|
||||||
"token_total_supply" / Int64ul,
|
"token_total_supply" / Int64ul,
|
||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data progressively based on available bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw account data including discriminator
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If discriminator is invalid or data is too short
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
raise ValueError("Data too short to contain discriminator")
|
||||||
|
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
parsed = self._STRUCT.parse(data[8:])
|
# Parse base fields (always present)
|
||||||
|
offset = 8
|
||||||
|
base_data = data[offset:]
|
||||||
|
parsed = self._BASE_STRUCT.parse(base_data)
|
||||||
self.__dict__.update(parsed)
|
self.__dict__.update(parsed)
|
||||||
|
|
||||||
# Convert raw bytes to Pubkey for creator field
|
# Calculate offset after base struct
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
offset += self._BASE_STRUCT.sizeof()
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
|
||||||
|
# Parse creator if bytes remaining (added in V2)
|
||||||
|
if len(data) >= offset + 32:
|
||||||
|
creator_bytes = data[offset : offset + 32]
|
||||||
|
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||||
|
offset += 32
|
||||||
|
else:
|
||||||
|
self.creator = None
|
||||||
|
|
||||||
|
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||||
|
if len(data) >= offset + 1:
|
||||||
|
self.is_mayhem_mode = bool(data[offset])
|
||||||
|
else:
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
|
||||||
|
|
||||||
async def get_pump_curve_state(
|
async def get_pump_curve_state(
|
||||||
@@ -126,11 +159,59 @@ def _find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fee_recipient(
|
||||||
|
client: AsyncClient, curve_state: BondingCurveState
|
||||||
|
) -> Pubkey:
|
||||||
|
"""Determine the correct fee recipient based on mayhem mode.
|
||||||
|
|
||||||
|
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||||
|
instead of the standard fee recipient. This function checks the bonding curve state
|
||||||
|
and returns the appropriate fee recipient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client to fetch Global account data
|
||||||
|
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Appropriate fee recipient pubkey (mayhem or standard)
|
||||||
|
"""
|
||||||
|
if not curve_state.is_mayhem_mode:
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||||
|
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
# Fallback to standard fee if Global account cannot be fetched
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
|
||||||
|
# Parse reserved_fee_recipient from Global account
|
||||||
|
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||||
|
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||||
|
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||||
|
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||||
|
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||||
|
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||||
|
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||||
|
|
||||||
|
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||||
|
# Fallback if account data is too short
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
reserved_fee_recipient_bytes = data[
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
async def buy_token(
|
async def buy_token(
|
||||||
mint: Pubkey,
|
mint: Pubkey,
|
||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
associated_bonding_curve: Pubkey,
|
associated_bonding_curve: Pubkey,
|
||||||
creator_vault: Pubkey,
|
creator_vault: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
amount: float,
|
amount: float,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
max_retries=5,
|
max_retries=5,
|
||||||
@@ -139,10 +220,12 @@ async def buy_token(
|
|||||||
payer = Keypair.from_bytes(private_key)
|
payer = Keypair.from_bytes(private_key)
|
||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
associated_token_account = get_associated_token_address(
|
||||||
|
payer.pubkey(), mint, token_program_id=token_program
|
||||||
|
)
|
||||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||||
token_amount = amount / token_price_sol
|
token_amount = amount / token_price_sol
|
||||||
@@ -150,9 +233,12 @@ async def buy_token(
|
|||||||
# Calculate maximum SOL to spend with slippage
|
# Calculate maximum SOL to spend with slippage
|
||||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||||
|
|
||||||
|
# Determine fee recipient based on whether token uses mayhem mode
|
||||||
|
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||||
|
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -168,7 +254,7 @@ async def buy_token(
|
|||||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=token_program, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -178,7 +264,7 @@ async def buy_token(
|
|||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_global_volume_accumulator(),
|
pubkey=_find_global_volume_accumulator(),
|
||||||
is_signer=False,
|
is_signer=False,
|
||||||
is_writable=True,
|
is_writable=False,
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||||
@@ -200,14 +286,17 @@ async def buy_token(
|
|||||||
]
|
]
|
||||||
|
|
||||||
discriminator = struct.pack("<Q", 16927863322537952870)
|
discriminator = struct.pack("<Q", 16927863322537952870)
|
||||||
|
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||||
|
track_volume_bytes = bytes([1, 1])
|
||||||
data = (
|
data = (
|
||||||
discriminator
|
discriminator
|
||||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||||
+ struct.pack("<Q", max_amount_lamports)
|
+ struct.pack("<Q", max_amount_lamports)
|
||||||
|
+ track_volume_bytes
|
||||||
)
|
)
|
||||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||||
payer.pubkey(), payer.pubkey(), mint
|
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
|
||||||
)
|
)
|
||||||
msg = Message(
|
msg = Message(
|
||||||
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
||||||
@@ -284,6 +373,7 @@ async def listen_for_create_transaction():
|
|||||||
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
||||||
idl = load_idl(idl_path)
|
idl = load_idl(idl_path)
|
||||||
create_discriminator = calculate_discriminator("global:create")
|
create_discriminator = calculate_discriminator("global:create")
|
||||||
|
create_v2_discriminator = calculate_discriminator("global:create_v2")
|
||||||
|
|
||||||
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
||||||
subscription_message = json.dumps(
|
subscription_message = json.dumps(
|
||||||
@@ -336,11 +426,22 @@ async def listen_for_create_transaction():
|
|||||||
"<Q", ix_data[:8]
|
"<Q", ix_data[:8]
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
# Check which create instruction was used
|
||||||
|
instruction_name = None
|
||||||
|
token_program = None
|
||||||
|
|
||||||
if discriminator == create_discriminator:
|
if discriminator == create_discriminator:
|
||||||
|
instruction_name = "create"
|
||||||
|
token_program = SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif discriminator == create_v2_discriminator:
|
||||||
|
instruction_name = "create_v2"
|
||||||
|
token_program = TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
|
if instruction_name:
|
||||||
create_ix = next(
|
create_ix = next(
|
||||||
instr
|
instr
|
||||||
for instr in idl["instructions"]
|
for instr in idl["instructions"]
|
||||||
if instr["name"] == "create"
|
if instr["name"] == instruction_name
|
||||||
)
|
)
|
||||||
account_keys = [
|
account_keys = [
|
||||||
str(
|
str(
|
||||||
@@ -355,6 +456,9 @@ async def listen_for_create_transaction():
|
|||||||
ix_data, create_ix, account_keys
|
ix_data, create_ix, account_keys
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# Add token program info to decoded args
|
||||||
|
decoded_args["token_program"] = str(token_program)
|
||||||
|
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
|
||||||
return decoded_args
|
return decoded_args
|
||||||
|
|
||||||
|
|
||||||
@@ -372,6 +476,7 @@ async def main():
|
|||||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||||
|
token_program = Pubkey.from_string(token_data["token_program"])
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch the token price
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
@@ -383,12 +488,13 @@ async def main():
|
|||||||
slippage = 0.3 # 30% slippage tolerance
|
slippage = 0.3 # 30% slippage tolerance
|
||||||
|
|
||||||
print(f"Bonding curve address: {bonding_curve}")
|
print(f"Bonding curve address: {bonding_curve}")
|
||||||
|
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||||
print(f"Token price: {token_price_sol:.10f} SOL")
|
print(f"Token price: {token_price_sol:.10f} SOL")
|
||||||
print(
|
print(
|
||||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||||
)
|
)
|
||||||
await buy_token(
|
await buy_token(
|
||||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import struct
|
|||||||
|
|
||||||
import base58
|
import base58
|
||||||
import websockets
|
import websockets
|
||||||
from construct import Bytes, Flag, Int64ul, Struct
|
from construct import Flag, Int64ul, Struct
|
||||||
from solana.rpc.async_api import AsyncClient
|
from solana.rpc.async_api import AsyncClient
|
||||||
from solana.rpc.commitment import Confirmed
|
from solana.rpc.commitment import Confirmed
|
||||||
from solana.rpc.types import TxOpts
|
from solana.rpc.types import TxOpts
|
||||||
@@ -55,6 +55,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
|||||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
)
|
)
|
||||||
@@ -69,27 +70,59 @@ RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
|||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT = Struct(
|
"""Bonding curve state parser with progressive field parsing.
|
||||||
|
|
||||||
|
Parses bonding curve account data progressively based on available bytes,
|
||||||
|
making it forward-compatible with future schema versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base struct present in all versions
|
||||||
|
_BASE_STRUCT = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
"real_sol_reserves" / Int64ul,
|
"real_sol_reserves" / Int64ul,
|
||||||
"token_total_supply" / Int64ul,
|
"token_total_supply" / Int64ul,
|
||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
"creator" / Bytes(32),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data progressively based on available bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw account data including discriminator
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If discriminator is invalid or data is too short
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
raise ValueError("Data too short to contain discriminator")
|
||||||
|
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
parsed = self._STRUCT.parse(data[8:])
|
# Parse base fields (always present)
|
||||||
|
offset = 8
|
||||||
|
base_data = data[offset:]
|
||||||
|
parsed = self._BASE_STRUCT.parse(base_data)
|
||||||
self.__dict__.update(parsed)
|
self.__dict__.update(parsed)
|
||||||
|
|
||||||
# Convert raw bytes to Pubkey for creator field
|
# Calculate offset after base struct
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
offset += self._BASE_STRUCT.sizeof()
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
|
||||||
|
# Parse creator if bytes remaining (added in V2)
|
||||||
|
if len(data) >= offset + 32:
|
||||||
|
creator_bytes = data[offset : offset + 32]
|
||||||
|
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||||
|
offset += 32
|
||||||
|
else:
|
||||||
|
self.creator = None
|
||||||
|
|
||||||
|
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||||
|
if len(data) >= offset + 1:
|
||||||
|
self.is_mayhem_mode = bool(data[offset])
|
||||||
|
else:
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
|
||||||
|
|
||||||
async def get_pump_curve_state(
|
async def get_pump_curve_state(
|
||||||
@@ -147,6 +180,53 @@ def _find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fee_recipient(
|
||||||
|
client: AsyncClient, curve_state: BondingCurveState
|
||||||
|
) -> Pubkey:
|
||||||
|
"""Determine the correct fee recipient based on mayhem mode.
|
||||||
|
|
||||||
|
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||||
|
instead of the standard fee recipient. This function checks the bonding curve state
|
||||||
|
and returns the appropriate fee recipient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client to fetch Global account data
|
||||||
|
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Appropriate fee recipient pubkey (mayhem or standard)
|
||||||
|
"""
|
||||||
|
if not curve_state.is_mayhem_mode:
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||||
|
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
# Fallback to standard fee if Global account cannot be fetched
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
|
||||||
|
# Parse reserved_fee_recipient from Global account
|
||||||
|
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||||
|
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||||
|
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||||
|
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||||
|
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||||
|
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||||
|
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||||
|
|
||||||
|
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||||
|
# Fallback if account data is too short
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
reserved_fee_recipient_bytes = data[
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
|
def set_loaded_accounts_data_size_limit(bytes_limit: int) -> Instruction:
|
||||||
"""
|
"""
|
||||||
Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption.
|
Create SetLoadedAccountsDataSizeLimit instruction to reduce CU consumption.
|
||||||
@@ -169,6 +249,7 @@ async def buy_token(
|
|||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
associated_bonding_curve: Pubkey,
|
associated_bonding_curve: Pubkey,
|
||||||
creator_vault: Pubkey,
|
creator_vault: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
amount: float,
|
amount: float,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
max_retries=5,
|
max_retries=5,
|
||||||
@@ -177,10 +258,12 @@ async def buy_token(
|
|||||||
payer = Keypair.from_bytes(private_key)
|
payer = Keypair.from_bytes(private_key)
|
||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
associated_token_account = get_associated_token_address(
|
||||||
|
payer.pubkey(), mint, token_program_id=token_program
|
||||||
|
)
|
||||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||||
token_amount = amount / token_price_sol
|
token_amount = amount / token_price_sol
|
||||||
@@ -188,9 +271,12 @@ async def buy_token(
|
|||||||
# Calculate maximum SOL to spend with slippage
|
# Calculate maximum SOL to spend with slippage
|
||||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||||
|
|
||||||
|
# Determine fee recipient based on whether token uses mayhem mode
|
||||||
|
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||||
|
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -206,7 +292,7 @@ async def buy_token(
|
|||||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=token_program, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -216,7 +302,7 @@ async def buy_token(
|
|||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_global_volume_accumulator(),
|
pubkey=_find_global_volume_accumulator(),
|
||||||
is_signer=False,
|
is_signer=False,
|
||||||
is_writable=True,
|
is_writable=False,
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||||
@@ -242,10 +328,11 @@ async def buy_token(
|
|||||||
discriminator
|
discriminator
|
||||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||||
+ struct.pack("<Q", max_amount_lamports)
|
+ struct.pack("<Q", max_amount_lamports)
|
||||||
|
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||||
)
|
)
|
||||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||||
payer.pubkey(), payer.pubkey(), mint
|
payer.pubkey(), payer.pubkey(), mint, token_program_id=token_program
|
||||||
)
|
)
|
||||||
|
|
||||||
# CU OPTIMIZATION: Limit account data to 512KB (down from 64MB default)
|
# CU OPTIMIZATION: Limit account data to 512KB (down from 64MB default)
|
||||||
@@ -328,6 +415,7 @@ async def listen_for_create_transaction():
|
|||||||
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
|
||||||
idl = load_idl(idl_path)
|
idl = load_idl(idl_path)
|
||||||
create_discriminator = calculate_discriminator("global:create")
|
create_discriminator = calculate_discriminator("global:create")
|
||||||
|
create_v2_discriminator = calculate_discriminator("global:create_v2")
|
||||||
|
|
||||||
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
async with websockets.connect(RPC_WEBSOCKET) as websocket:
|
||||||
subscription_message = json.dumps(
|
subscription_message = json.dumps(
|
||||||
@@ -380,11 +468,22 @@ async def listen_for_create_transaction():
|
|||||||
"<Q", ix_data[:8]
|
"<Q", ix_data[:8]
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
# Check which create instruction was used
|
||||||
|
instruction_name = None
|
||||||
|
token_program = None
|
||||||
|
|
||||||
if discriminator == create_discriminator:
|
if discriminator == create_discriminator:
|
||||||
|
instruction_name = "create"
|
||||||
|
token_program = SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif discriminator == create_v2_discriminator:
|
||||||
|
instruction_name = "create_v2"
|
||||||
|
token_program = TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
|
if instruction_name:
|
||||||
create_ix = next(
|
create_ix = next(
|
||||||
instr
|
instr
|
||||||
for instr in idl["instructions"]
|
for instr in idl["instructions"]
|
||||||
if instr["name"] == "create"
|
if instr["name"] == instruction_name
|
||||||
)
|
)
|
||||||
account_keys = [
|
account_keys = [
|
||||||
str(
|
str(
|
||||||
@@ -399,6 +498,9 @@ async def listen_for_create_transaction():
|
|||||||
ix_data, create_ix, account_keys
|
ix_data, create_ix, account_keys
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# Add token program info to decoded args
|
||||||
|
decoded_args["token_program"] = str(token_program)
|
||||||
|
decoded_args["is_token_2022"] = (token_program == TOKEN_2022_PROGRAM)
|
||||||
return decoded_args
|
return decoded_args
|
||||||
|
|
||||||
|
|
||||||
@@ -415,6 +517,7 @@ async def main():
|
|||||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||||
|
token_program = Pubkey.from_string(token_data["token_program"])
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch the token price
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
@@ -426,13 +529,14 @@ async def main():
|
|||||||
slippage = 0.3 # 30% slippage tolerance
|
slippage = 0.3 # 30% slippage tolerance
|
||||||
|
|
||||||
print(f"Bonding curve address: {bonding_curve}")
|
print(f"Bonding curve address: {bonding_curve}")
|
||||||
|
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||||
print(f"Token price: {token_price_sol:.10f} SOL")
|
print(f"Token price: {token_price_sol:.10f} SOL")
|
||||||
print(
|
print(
|
||||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||||
)
|
)
|
||||||
print("CU Optimization: Enabled (512KB account data limit)")
|
print("CU Optimization: Enabled (512KB account data limit)")
|
||||||
await buy_token(
|
await buy_token(
|
||||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import sys
|
|||||||
|
|
||||||
import base58
|
import base58
|
||||||
import grpc
|
import grpc
|
||||||
from construct import Bytes, Flag, Int64ul, Struct
|
from construct import Flag, Int64ul, Struct
|
||||||
from solana.rpc.async_api import AsyncClient
|
from solana.rpc.async_api import AsyncClient
|
||||||
from solana.rpc.commitment import Confirmed
|
from solana.rpc.commitment import Confirmed
|
||||||
from solana.rpc.types import TxOpts
|
from solana.rpc.types import TxOpts
|
||||||
@@ -58,30 +58,63 @@ GEYSER_API_TOKEN = os.environ.get("GEYSER_API_TOKEN")
|
|||||||
AUTH_TYPE = os.environ.get("GEYSER_AUTH_TYPE", "x-token") # Default to x-token
|
AUTH_TYPE = os.environ.get("GEYSER_AUTH_TYPE", "x-token") # Default to x-token
|
||||||
|
|
||||||
PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728)
|
PUMP_CREATE_DISCRIMINATOR = struct.pack("<Q", 8576854823835016728)
|
||||||
|
PUMP_CREATE_V2_DISCRIMINATOR = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT = Struct(
|
"""Bonding curve state parser with progressive field parsing.
|
||||||
|
|
||||||
|
Parses bonding curve account data progressively based on available bytes,
|
||||||
|
making it forward-compatible with future schema versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base struct present in all versions
|
||||||
|
_BASE_STRUCT = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
"real_sol_reserves" / Int64ul,
|
"real_sol_reserves" / Int64ul,
|
||||||
"token_total_supply" / Int64ul,
|
"token_total_supply" / Int64ul,
|
||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data progressively based on available bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw account data including discriminator
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If discriminator is invalid or data is too short
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
raise ValueError("Data too short to contain discriminator")
|
||||||
|
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
parsed = self._STRUCT.parse(data[8:])
|
# Parse base fields (always present)
|
||||||
|
offset = 8
|
||||||
|
base_data = data[offset:]
|
||||||
|
parsed = self._BASE_STRUCT.parse(base_data)
|
||||||
self.__dict__.update(parsed)
|
self.__dict__.update(parsed)
|
||||||
|
|
||||||
# Convert raw bytes to Pubkey for creator field
|
# Calculate offset after base struct
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
offset += self._BASE_STRUCT.sizeof()
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
|
||||||
|
# Parse creator if bytes remaining (added in V2)
|
||||||
|
if len(data) >= offset + 32:
|
||||||
|
creator_bytes = data[offset : offset + 32]
|
||||||
|
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||||
|
offset += 32
|
||||||
|
else:
|
||||||
|
self.creator = None
|
||||||
|
|
||||||
|
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||||
|
if len(data) >= offset + 1:
|
||||||
|
self.is_mayhem_mode = bool(data[offset])
|
||||||
|
else:
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
|
||||||
|
|
||||||
async def get_pump_curve_state(
|
async def get_pump_curve_state(
|
||||||
@@ -139,6 +172,53 @@ def _find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fee_recipient(
|
||||||
|
client: AsyncClient, curve_state: BondingCurveState
|
||||||
|
) -> Pubkey:
|
||||||
|
"""Determine the correct fee recipient based on mayhem mode.
|
||||||
|
|
||||||
|
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||||
|
instead of the standard fee recipient. This function checks the bonding curve state
|
||||||
|
and returns the appropriate fee recipient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client to fetch Global account data
|
||||||
|
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Appropriate fee recipient pubkey (mayhem or standard)
|
||||||
|
"""
|
||||||
|
if not curve_state.is_mayhem_mode:
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||||
|
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
# Fallback to standard fee if Global account cannot be fetched
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
|
||||||
|
# Parse reserved_fee_recipient from Global account
|
||||||
|
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||||
|
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||||
|
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||||
|
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||||
|
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||||
|
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||||
|
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||||
|
|
||||||
|
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||||
|
# Fallback if account data is too short
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
reserved_fee_recipient_bytes = data[
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
async def create_geyser_connection():
|
async def create_geyser_connection():
|
||||||
"""Establish a secure connection to the Geyser endpoint using the configured auth type."""
|
"""Establish a secure connection to the Geyser endpoint using the configured auth type."""
|
||||||
if AUTH_TYPE == "x-token":
|
if AUTH_TYPE == "x-token":
|
||||||
@@ -232,7 +312,13 @@ async def listen_for_create_transaction_geyser():
|
|||||||
|
|
||||||
# Check each instruction in the transaction
|
# Check each instruction in the transaction
|
||||||
for ix in msg.instructions:
|
for ix in msg.instructions:
|
||||||
if not ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
|
# Check which create instruction was used
|
||||||
|
token_program = None
|
||||||
|
if ix.data.startswith(PUMP_CREATE_DISCRIMINATOR):
|
||||||
|
token_program = SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif ix.data.startswith(PUMP_CREATE_V2_DISCRIMINATOR):
|
||||||
|
token_program = SYSTEM_TOKEN_2022_PROGRAM
|
||||||
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Found a create instruction
|
# Found a create instruction
|
||||||
@@ -240,6 +326,10 @@ async def listen_for_create_transaction_geyser():
|
|||||||
ix.data, msg.account_keys, ix.accounts
|
ix.data, msg.account_keys, ix.accounts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add token program info to decoded args
|
||||||
|
token_data["token_program"] = str(token_program)
|
||||||
|
token_data["is_token_2022"] = (token_program == SYSTEM_TOKEN_2022_PROGRAM)
|
||||||
|
|
||||||
signature = base58.b58encode(
|
signature = base58.b58encode(
|
||||||
bytes(update.transaction.transaction.signature)
|
bytes(update.transaction.transaction.signature)
|
||||||
).decode()
|
).decode()
|
||||||
@@ -253,6 +343,7 @@ async def buy_token(
|
|||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
associated_bonding_curve: Pubkey,
|
associated_bonding_curve: Pubkey,
|
||||||
creator_vault: Pubkey,
|
creator_vault: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
amount: float,
|
amount: float,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
max_retries=5,
|
max_retries=5,
|
||||||
@@ -262,22 +353,30 @@ async def buy_token(
|
|||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
associated_token_account = get_associated_token_address(
|
associated_token_account = get_associated_token_address(
|
||||||
payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
|
payer.pubkey(), mint, token_program
|
||||||
)
|
)
|
||||||
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
amount_lamports = int(amount * LAMPORTS_PER_SOL)
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||||
|
# NOTE: Price calculation is commented out to speed up testing - using fixed values
|
||||||
|
# For production, uncomment the lines below to:
|
||||||
|
# 1. Calculate proper token amounts based on current price
|
||||||
|
# 2. Detect mayhem mode and use correct fee recipient
|
||||||
# curve_state = await get_pump_curve_state(client, bonding_curve)
|
# curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||||
# token_price_sol = calculate_pump_curve_price(curve_state)
|
# token_price_sol = calculate_pump_curve_price(curve_state)
|
||||||
# token_amount = amount / token_price_sol
|
# token_amount = amount / token_price_sol
|
||||||
token_amount = 100
|
# fee_recipient = await get_fee_recipient(client, curve_state)
|
||||||
|
|
||||||
|
# Testing values - replace with code above for production
|
||||||
|
token_amount = 100 # Fixed token amount
|
||||||
|
fee_recipient = PUMP_FEE # Standard fee recipient (doesn't detect mayhem mode)
|
||||||
|
|
||||||
# Calculate maximum SOL to spend with slippage
|
# Calculate maximum SOL to spend with slippage
|
||||||
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
max_amount_lamports = int(amount_lamports * (1 + slippage))
|
||||||
|
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -293,7 +392,7 @@ async def buy_token(
|
|||||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=token_program, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -303,7 +402,7 @@ async def buy_token(
|
|||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_global_volume_accumulator(),
|
pubkey=_find_global_volume_accumulator(),
|
||||||
is_signer=False,
|
is_signer=False,
|
||||||
is_writable=True,
|
is_writable=False,
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
pubkey=_find_user_volume_accumulator(payer.pubkey()),
|
||||||
@@ -329,10 +428,11 @@ async def buy_token(
|
|||||||
discriminator
|
discriminator
|
||||||
+ struct.pack("<Q", int(token_amount * 10**6))
|
+ struct.pack("<Q", int(token_amount * 10**6))
|
||||||
+ struct.pack("<Q", max_amount_lamports)
|
+ struct.pack("<Q", max_amount_lamports)
|
||||||
|
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||||
)
|
)
|
||||||
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||||
payer.pubkey(), payer.pubkey(), mint, SYSTEM_TOKEN_PROGRAM
|
payer.pubkey(), payer.pubkey(), mint, token_program
|
||||||
)
|
)
|
||||||
msg = Message(
|
msg = Message(
|
||||||
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
[set_compute_unit_price(1_000), idempotent_ata_ix, buy_ix], payer.pubkey()
|
||||||
@@ -396,6 +496,7 @@ async def main():
|
|||||||
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
|
||||||
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
|
||||||
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
creator_vault = _find_creator_vault(Pubkey.from_string(token_data["creator"]))
|
||||||
|
token_program = Pubkey.from_string(token_data["token_program"])
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch the token price
|
||||||
# async with AsyncClient(RPC_ENDPOINT) as client:
|
# async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
@@ -407,12 +508,13 @@ async def main():
|
|||||||
slippage = 0.3 # 30% slippage tolerance
|
slippage = 0.3 # 30% slippage tolerance
|
||||||
|
|
||||||
print(f"Bonding curve address: {bonding_curve}")
|
print(f"Bonding curve address: {bonding_curve}")
|
||||||
|
print(f"Token Program: {token_program} ({'Token2022' if token_data['is_token_2022'] else 'Standard Token'})")
|
||||||
# print(f"Token price: {token_price_sol:.10f} SOL")
|
# print(f"Token price: {token_price_sol:.10f} SOL")
|
||||||
print(
|
print(
|
||||||
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
|
||||||
)
|
)
|
||||||
await buy_token(
|
await buy_token(
|
||||||
mint, bonding_curve, associated_bonding_curve, creator_vault, amount, slippage
|
mint, bonding_curve, associated_bonding_curve, creator_vault, token_program, amount, slippage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
import base58
|
import base58
|
||||||
from construct import Bytes, Flag, Int64ul, Struct
|
from construct import Flag, Int64ul, Struct
|
||||||
from solana.rpc.async_api import AsyncClient
|
from solana.rpc.async_api import AsyncClient
|
||||||
from solana.rpc.commitment import Confirmed
|
from solana.rpc.commitment import Confirmed
|
||||||
from solana.rpc.types import TxOpts
|
from solana.rpc.types import TxOpts
|
||||||
@@ -18,7 +18,7 @@ from spl.token.instructions import get_associated_token_address
|
|||||||
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py
|
||||||
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
||||||
TOKEN_DECIMALS = 6
|
TOKEN_DECIMALS = 6
|
||||||
TOKEN_MINT = Pubkey.from_string("...")
|
TOKEN_MINT = Pubkey.from_string("...") # Replace with actual token mint address
|
||||||
|
|
||||||
# Global constants
|
# Global constants
|
||||||
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
@@ -30,6 +30,7 @@ PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
|||||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
)
|
)
|
||||||
@@ -43,27 +44,59 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
|||||||
|
|
||||||
|
|
||||||
class BondingCurveState:
|
class BondingCurveState:
|
||||||
_STRUCT = Struct(
|
"""Bonding curve state parser with progressive field parsing.
|
||||||
|
|
||||||
|
Parses bonding curve account data progressively based on available bytes,
|
||||||
|
making it forward-compatible with future schema versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base struct present in all versions
|
||||||
|
_BASE_STRUCT = Struct(
|
||||||
"virtual_token_reserves" / Int64ul,
|
"virtual_token_reserves" / Int64ul,
|
||||||
"virtual_sol_reserves" / Int64ul,
|
"virtual_sol_reserves" / Int64ul,
|
||||||
"real_token_reserves" / Int64ul,
|
"real_token_reserves" / Int64ul,
|
||||||
"real_sol_reserves" / Int64ul,
|
"real_sol_reserves" / Int64ul,
|
||||||
"token_total_supply" / Int64ul,
|
"token_total_supply" / Int64ul,
|
||||||
"complete" / Flag,
|
"complete" / Flag,
|
||||||
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data: bytes) -> None:
|
def __init__(self, data: bytes) -> None:
|
||||||
"""Parse bonding curve data."""
|
"""Parse bonding curve data progressively based on available bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw account data including discriminator
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If discriminator is invalid or data is too short
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
raise ValueError("Data too short to contain discriminator")
|
||||||
|
|
||||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||||
raise ValueError("Invalid curve state discriminator")
|
raise ValueError("Invalid curve state discriminator")
|
||||||
|
|
||||||
parsed = self._STRUCT.parse(data[8:])
|
# Parse base fields (always present)
|
||||||
|
offset = 8
|
||||||
|
base_data = data[offset:]
|
||||||
|
parsed = self._BASE_STRUCT.parse(base_data)
|
||||||
self.__dict__.update(parsed)
|
self.__dict__.update(parsed)
|
||||||
|
|
||||||
# Convert raw bytes to Pubkey for creator field
|
# Calculate offset after base struct
|
||||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
offset += self._BASE_STRUCT.sizeof()
|
||||||
self.creator = Pubkey.from_bytes(self.creator)
|
|
||||||
|
# Parse creator if bytes remaining (added in V2)
|
||||||
|
if len(data) >= offset + 32:
|
||||||
|
creator_bytes = data[offset : offset + 32]
|
||||||
|
self.creator = Pubkey.from_bytes(creator_bytes)
|
||||||
|
offset += 32
|
||||||
|
else:
|
||||||
|
self.creator = None
|
||||||
|
|
||||||
|
# Parse mayhem mode flag if bytes remaining (added in V3)
|
||||||
|
if len(data) >= offset + 1:
|
||||||
|
self.is_mayhem_mode = bool(data[offset])
|
||||||
|
else:
|
||||||
|
self.is_mayhem_mode = False
|
||||||
|
|
||||||
|
|
||||||
async def get_pump_curve_state(
|
async def get_pump_curve_state(
|
||||||
@@ -84,11 +117,13 @@ def get_bonding_curve_address(mint: Pubkey) -> tuple[Pubkey, int]:
|
|||||||
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)
|
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM)
|
||||||
|
|
||||||
|
|
||||||
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
def find_associated_bonding_curve(
|
||||||
|
mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey
|
||||||
|
) -> Pubkey:
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[
|
[
|
||||||
bytes(bonding_curve),
|
bytes(bonding_curve),
|
||||||
bytes(SYSTEM_TOKEN_PROGRAM),
|
bytes(token_program_id),
|
||||||
bytes(mint),
|
bytes(mint),
|
||||||
],
|
],
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
|
||||||
@@ -112,6 +147,53 @@ def _find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
async def get_fee_recipient(
|
||||||
|
client: AsyncClient, curve_state: BondingCurveState
|
||||||
|
) -> Pubkey:
|
||||||
|
"""Determine the correct fee recipient based on mayhem mode.
|
||||||
|
|
||||||
|
Mayhem mode tokens use a different fee recipient (reserved_fee_recipient from Global account)
|
||||||
|
instead of the standard fee recipient. This function checks the bonding curve state
|
||||||
|
and returns the appropriate fee recipient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client to fetch Global account data
|
||||||
|
curve_state: Parsed bonding curve state containing is_mayhem_mode flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Appropriate fee recipient pubkey (mayhem or standard)
|
||||||
|
"""
|
||||||
|
if not curve_state.is_mayhem_mode:
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
# Fetch Global account to get reserved_fee_recipient for mayhem mode tokens
|
||||||
|
response = await client.get_account_info(PUMP_GLOBAL, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
# Fallback to standard fee if Global account cannot be fetched
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
|
||||||
|
# Parse reserved_fee_recipient from Global account
|
||||||
|
# Offset calculation based on pump_fun_idl.json Global struct:
|
||||||
|
# discriminator(8) + initialized(1) + authority(32) + fee_recipient(32) +
|
||||||
|
# initial_virtual_token_reserves(8) + initial_virtual_sol_reserves(8) +
|
||||||
|
# initial_real_token_reserves(8) + token_total_supply(8) + fee_basis_points(8) +
|
||||||
|
# withdraw_authority(32) + enable_migrate(1) + pool_migration_fee(8) +
|
||||||
|
# creator_fee_basis_points(8) + fee_recipients[7](224) + set_creator_authority(32) +
|
||||||
|
# admin_set_creator_authority(32) + create_v2_enabled(1) + whitelist_pda(32) = 483
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET = 483
|
||||||
|
|
||||||
|
if len(data) < RESERVED_FEE_RECIPIENT_OFFSET + 32:
|
||||||
|
# Fallback if account data is too short
|
||||||
|
return PUMP_FEE
|
||||||
|
|
||||||
|
reserved_fee_recipient_bytes = data[
|
||||||
|
RESERVED_FEE_RECIPIENT_OFFSET : RESERVED_FEE_RECIPIENT_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(reserved_fee_recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
|
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
|
||||||
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
|
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
|
||||||
raise ValueError("Invalid reserve state")
|
raise ValueError("Invalid reserve state")
|
||||||
@@ -128,11 +210,31 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||||
|
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||||
|
mint_info = await client.get_account_info(mint_address)
|
||||||
|
|
||||||
|
if not mint_info.value:
|
||||||
|
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||||
|
|
||||||
|
owner = mint_info.value.owner
|
||||||
|
|
||||||
|
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||||
|
return SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif owner == TOKEN_2022_PROGRAM:
|
||||||
|
return TOKEN_2022_PROGRAM
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def sell_token(
|
async def sell_token(
|
||||||
mint: Pubkey,
|
mint: Pubkey,
|
||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
associated_bonding_curve: Pubkey,
|
associated_bonding_curve: Pubkey,
|
||||||
creator_vault: Pubkey,
|
creator_vault: Pubkey,
|
||||||
|
token_program_id: Pubkey,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
max_retries=5,
|
max_retries=5,
|
||||||
):
|
):
|
||||||
@@ -140,7 +242,9 @@ async def sell_token(
|
|||||||
payer = Keypair.from_bytes(private_key)
|
payer = Keypair.from_bytes(private_key)
|
||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
|
associated_token_account = get_associated_token_address(
|
||||||
|
payer.pubkey(), mint, token_program_id
|
||||||
|
)
|
||||||
|
|
||||||
# Get token balance
|
# Get token balance
|
||||||
token_balance = await get_token_balance(client, associated_token_account)
|
token_balance = await get_token_balance(client, associated_token_account)
|
||||||
@@ -150,7 +254,7 @@ async def sell_token(
|
|||||||
print("No tokens to sell.")
|
print("No tokens to sell.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch the token price
|
# Fetch bonding curve state to calculate price and determine fee recipient
|
||||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||||
token_price_sol = calculate_pump_curve_price(curve_state)
|
token_price_sol = calculate_pump_curve_price(curve_state)
|
||||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||||
@@ -164,9 +268,12 @@ async def sell_token(
|
|||||||
print(f"Selling {token_balance_decimal} tokens")
|
print(f"Selling {token_balance_decimal} tokens")
|
||||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||||
|
|
||||||
|
# Determine fee recipient based on whether token uses mayhem mode
|
||||||
|
fee_recipient = await get_fee_recipient(client, curve_state)
|
||||||
|
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -187,8 +294,8 @@ async def sell_token(
|
|||||||
is_writable=True,
|
is_writable=True,
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||||
),
|
), # Use dynamic token_program_id
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
|
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
@@ -208,10 +315,13 @@ async def sell_token(
|
|||||||
]
|
]
|
||||||
|
|
||||||
discriminator = struct.pack("<Q", 12502976635542562355)
|
discriminator = struct.pack("<Q", 12502976635542562355)
|
||||||
|
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||||
|
track_volume_bytes = bytes([1, 1])
|
||||||
data = (
|
data = (
|
||||||
discriminator
|
discriminator
|
||||||
+ struct.pack("<Q", amount)
|
+ struct.pack("<Q", amount)
|
||||||
+ struct.pack("<Q", min_sol_output)
|
+ struct.pack("<Q", min_sol_output)
|
||||||
|
+ track_volume_bytes
|
||||||
)
|
)
|
||||||
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
|
||||||
|
|
||||||
@@ -248,8 +358,13 @@ async def sell_token(
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Replace these with the actual values for the token you want to sell
|
# Replace these with the actual values for the token you want to sell
|
||||||
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
|
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||||
|
|
||||||
bonding_curve, _ = get_bonding_curve_address(TOKEN_MINT)
|
bonding_curve, _ = get_bonding_curve_address(TOKEN_MINT)
|
||||||
associated_bonding_curve = find_associated_bonding_curve(TOKEN_MINT, bonding_curve)
|
associated_bonding_curve = find_associated_bonding_curve(
|
||||||
|
TOKEN_MINT, bonding_curve, token_program_id
|
||||||
|
)
|
||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
curve_state = await get_pump_curve_state(client, bonding_curve)
|
curve_state = await get_pump_curve_state(client, bonding_curve)
|
||||||
@@ -261,7 +376,12 @@ async def main():
|
|||||||
print(f"Bonding curve address: {bonding_curve}")
|
print(f"Bonding curve address: {bonding_curve}")
|
||||||
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
|
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
|
||||||
await sell_token(
|
await sell_token(
|
||||||
TOKEN_MINT, bonding_curve, associated_bonding_curve, creator_vault, slippage
|
TOKEN_MINT,
|
||||||
|
bonding_curve,
|
||||||
|
associated_bonding_curve,
|
||||||
|
creator_vault,
|
||||||
|
token_program_id,
|
||||||
|
slippage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ def create_buy_instruction(
|
|||||||
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True
|
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=_find_user_volume_accumulator(user),
|
pubkey=_find_user_volume_accumulator(user),
|
||||||
|
|||||||
@@ -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:
|
Key concepts demonstrated:
|
||||||
- Finding market addresses by token mint
|
- Finding AMM pool addresses by token mint
|
||||||
- Fetching and parsing market data from PUMP AMM pools
|
- Parsing binary account data structures
|
||||||
- Calculating token prices in AMM pools
|
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||||
- Creating associated token accounts (ATAs) idempotently
|
- Program Derived Address (PDA) derivation
|
||||||
- Buying tokens on the PUMP AMM with slippage protection
|
- WSOL wrapping (converting SOL to wrapped SOL for SPL token operations)
|
||||||
|
- Volume tracking incentives integration
|
||||||
|
- Transaction simulation before sending
|
||||||
|
- Slippage protection mechanisms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -34,31 +38,33 @@ from spl.token.instructions import (
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Configuration constants
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||||
TOKEN_MINT = Pubkey.from_string("...")
|
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||||
SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept
|
SLIPPAGE = 0.3 # 30% - maximum acceptable price movement during trade
|
||||||
|
|
||||||
TOKEN_DECIMALS = 6
|
# Token configuration
|
||||||
BUY_DISCRIMINATOR = bytes.fromhex(
|
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||||
"66063d1201daebea"
|
|
||||||
) # Program instruction identifier for the buy function
|
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||||
|
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Solana Program IDs and System Accounts
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# Solana system addresses and program IDs
|
|
||||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||||
)
|
)
|
||||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
|
||||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
|
||||||
)
|
|
||||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
|
||||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
|
||||||
)
|
|
||||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
@@ -67,61 +73,91 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
|||||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||||
)
|
)
|
||||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Constants for Account Structure Parsing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Pool account structure offsets
|
||||||
|
POOL_DISCRIMINATOR_SIZE = 8
|
||||||
|
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||||
|
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||||
|
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||||
|
|
||||||
|
# GlobalConfig structure offsets
|
||||||
|
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||||
|
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||||
|
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||||
|
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||||
|
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||||
|
+ GLOBALCONFIG_ADMIN_SIZE
|
||||||
|
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fee recipients
|
||||||
|
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||||
|
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Solana constants
|
||||||
LAMPORTS_PER_SOL = 1_000_000_000
|
LAMPORTS_PER_SOL = 1_000_000_000
|
||||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||||
COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use
|
COMPUTE_UNIT_BUDGET = 200_000 # Max compute units for transaction
|
||||||
|
|
||||||
|
# Buy-specific constants
|
||||||
|
PROTOCOL_FEE_BUFFER = 0.1 # 10% buffer for protocol fees when wrapping SOL
|
||||||
|
VOLUME_TRACKING_ENABLED = 1 # 1 = true, 0 = false
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Market Discovery
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
async def get_market_address_by_base_mint(
|
async def get_market_address_by_base_mint(
|
||||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||||
) -> Pubkey:
|
) -> Pubkey:
|
||||||
"""Find the market address for a given token mint.
|
"""Find the AMM pool address for a specific token.
|
||||||
|
|
||||||
Searches for the AMM pool that contains the specified token mint as its base token
|
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||||
by querying program accounts with a filter for the base_mint field.
|
that matches the given token mint address.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
base_mint_address: Address of the token mint you want to find the market for
|
base_mint_address: Token mint to find the pool for
|
||||||
amm_program_id: Address of the AMM program
|
amm_program_id: PUMP AMM program address
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The Pubkey of the market (AMM pool) for the token
|
Address of the AMM pool (market) for the token
|
||||||
"""
|
"""
|
||||||
base_mint_bytes = bytes(base_mint_address)
|
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||||
offset = (
|
|
||||||
43 # Offset where the base_mint field is stored in the account data structure
|
|
||||||
)
|
|
||||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
|
||||||
|
|
||||||
response = await client.get_program_accounts(
|
response = await client.get_program_accounts(
|
||||||
amm_program_id, encoding="base64", filters=filters
|
amm_program_id, encoding="base64", filters=filters
|
||||||
)
|
)
|
||||||
|
return response.value[0].pubkey
|
||||||
market_address = [account.pubkey for account in response.value][0]
|
|
||||||
return market_address
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||||
"""Fetch and parse market data from the blockchain.
|
"""Parse binary pool account data into a structured dictionary.
|
||||||
|
|
||||||
Retrieves and deserializes the binary data stored in the market account
|
The pool account stores data in a specific binary format. This function
|
||||||
into a structured dictionary containing key market information.
|
deserializes that data based on the known structure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
market_address: Address of the market (AMM pool) to fetch data for
|
market_address: Address of the pool account
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing the parsed market data
|
Dictionary with parsed pool data fields
|
||||||
"""
|
"""
|
||||||
response = await client.get_account_info(market_address, encoding="base64")
|
response = await client.get_account_info(market_address, encoding="base64")
|
||||||
data = response.value.data
|
data = response.value.data
|
||||||
parsed_data: dict = {}
|
parsed_data: dict = {}
|
||||||
|
|
||||||
# Start after the 8-byte discriminator
|
offset = POOL_DISCRIMINATOR_SIZE
|
||||||
offset = 8
|
|
||||||
# Define the structure of the market account data
|
# Field definitions: (name, type)
|
||||||
|
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||||
fields = [
|
fields = [
|
||||||
("pool_bump", "u8"),
|
("pool_bump", "u8"),
|
||||||
("index", "u16"),
|
("index", "u16"),
|
||||||
@@ -141,39 +177,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
|||||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||||
offset += 32
|
offset += 32
|
||||||
elif field_type in {"u64", "i64"}:
|
elif field_type in {"u64", "i64"}:
|
||||||
value = (
|
format_char = "<Q" if field_type == "u64" else "<q"
|
||||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
parsed_data[field_name] = struct.unpack(
|
||||||
if field_type == "u64"
|
format_char, data[offset : offset + 8]
|
||||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
)[0]
|
||||||
)
|
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 8
|
offset += 8
|
||||||
elif field_type == "u16":
|
elif field_type == "u16":
|
||||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 2
|
offset += 2
|
||||||
elif field_type == "u8":
|
elif field_type == "u8":
|
||||||
value = data[offset]
|
parsed_data[field_name] = data[offset]
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
return parsed_data
|
return parsed_data
|
||||||
|
|
||||||
|
|
||||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
# ============================================================================
|
||||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
# Program Derived Address (PDA) Derivation
|
||||||
|
# ============================================================================
|
||||||
|
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||||
|
# They allow programs to own accounts without needing a private key.
|
||||||
|
|
||||||
Calculates the deterministic PDA that serves as the vault authority
|
|
||||||
for a specific coin creator in the PUMP AMM protocol.
|
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||||
|
"""Derive the PDA for the coin creator's fee vault.
|
||||||
|
|
||||||
|
The creator vault collects fees on behalf of the token creator.
|
||||||
|
This is a deterministic address that can be recalculated by anyone.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coin_creator: Pubkey of the coin creator account
|
coin_creator: Public key of the token creator
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Pubkey of the derived coin creator vault authority
|
PDA of the creator's vault authority
|
||||||
|
|
||||||
Note:
|
|
||||||
This vault is used to collect creator fees from token transactions
|
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"creator_vault", bytes(coin_creator)],
|
[b"creator_vault", bytes(coin_creator)],
|
||||||
@@ -183,13 +219,13 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
|||||||
|
|
||||||
|
|
||||||
def find_global_volume_accumulator() -> Pubkey:
|
def find_global_volume_accumulator() -> Pubkey:
|
||||||
"""Derive the Program Derived Address (PDA) for the global volume accumulator.
|
"""Derive the PDA for the global volume accumulator.
|
||||||
|
|
||||||
Calculates the deterministic PDA that tracks global trading volume
|
This account tracks total trading volume across all pools.
|
||||||
across all pools in the PUMP AMM protocol.
|
Volume tracking is used for incentive programs and analytics.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Pubkey of the derived global volume accumulator account
|
PDA of the global volume accumulator
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"global_volume_accumulator"],
|
[b"global_volume_accumulator"],
|
||||||
@@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey:
|
|||||||
|
|
||||||
|
|
||||||
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||||
"""Derive the Program Derived Address (PDA) for a user's volume accumulator.
|
"""Derive the PDA for a user's volume accumulator.
|
||||||
|
|
||||||
Calculates the deterministic PDA that tracks trading volume
|
Tracks individual user's trading volume, which may qualify them
|
||||||
for a specific user in the PUMP AMM protocol.
|
for incentives or rewards based on trading activity.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: Pubkey of the user account
|
user: Public key of the user
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Pubkey of the derived user volume accumulator account
|
PDA of the user's volume accumulator
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"user_volume_accumulator", bytes(user)],
|
[b"user_volume_accumulator", bytes(user)],
|
||||||
@@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
|||||||
|
|
||||||
|
|
||||||
def find_fee_config() -> Pubkey:
|
def find_fee_config() -> Pubkey:
|
||||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
"""Derive the PDA for the fee configuration account.
|
||||||
|
|
||||||
Returns:
|
This account stores fee-related configuration for the AMM.
|
||||||
Pubkey of the derived fee config account
|
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||||
@@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Mayhem Mode Fee Handling
|
||||||
|
# ============================================================================
|
||||||
|
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||||
|
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||||
|
|
||||||
|
|
||||||
|
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||||
|
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||||
|
|
||||||
|
When mayhem mode is active, fees are redirected to a special recipient
|
||||||
|
stored in the GlobalConfig account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Public key of the mayhem mode fee recipient
|
||||||
|
"""
|
||||||
|
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
msg = "Cannot fetch GlobalConfig account"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
recipient_bytes = data[
|
||||||
|
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pumpswap_fee_recipients(
|
||||||
|
client: AsyncClient, pool: Pubkey
|
||||||
|
) -> tuple[Pubkey, Pubkey]:
|
||||||
|
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||||
|
|
||||||
|
This function checks if mayhem mode is enabled for the pool and returns
|
||||||
|
the appropriate fee recipient and their WSOL token account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client
|
||||||
|
pool: Address of the AMM pool
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||||
|
"""
|
||||||
|
response = await client.get_account_info(pool, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
msg = "Cannot fetch pool account"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
pool_data = response.value.data
|
||||||
|
|
||||||
|
# Check if mayhem mode flag exists and is enabled
|
||||||
|
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||||
|
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select appropriate fee recipient
|
||||||
|
if is_mayhem_mode:
|
||||||
|
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||||
|
else:
|
||||||
|
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||||
|
|
||||||
|
# Get the fee recipient's WSOL token account
|
||||||
|
fee_recipient_token_account = get_associated_token_address(
|
||||||
|
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
|
return (fee_recipient, fee_recipient_token_account)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Price Calculation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
async def calculate_token_pool_price(
|
async def calculate_token_pool_price(
|
||||||
client: AsyncClient,
|
client: AsyncClient,
|
||||||
pool_base_token_account: Pubkey,
|
pool_base_token_account: Pubkey,
|
||||||
pool_quote_token_account: Pubkey,
|
pool_quote_token_account: Pubkey,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Calculate the current price of tokens in an AMM pool.
|
"""Calculate current token price from AMM pool balances.
|
||||||
|
|
||||||
Fetches the balance of tokens in the pool and calculates the price ratio
|
AMM price is determined by the ratio of tokens in the pool:
|
||||||
between the base token and quote token (typically SOL).
|
price = quote_balance / base_balance
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
pool_base_token_account: Address of the pool's base token account (your token)
|
pool_base_token_account: Pool's token account (the token being priced)
|
||||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The price of the base token in terms of the quote token (SOL per token)
|
Price in SOL per token
|
||||||
"""
|
"""
|
||||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||||
quote_balance_resp = await client.get_token_account_balance(
|
quote_balance_resp = await client.get_token_account_balance(
|
||||||
pool_quote_token_account
|
pool_quote_token_account
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract the UI amounts (human-readable with decimals)
|
|
||||||
base_amount = float(base_balance_resp.value.ui_amount)
|
base_amount = float(base_balance_resp.value.ui_amount)
|
||||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||||
|
|
||||||
token_price = quote_amount / base_amount
|
return quote_amount / base_amount
|
||||||
|
|
||||||
return token_price
|
|
||||||
|
# ============================================================================
|
||||||
|
# Token Buying
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||||
|
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||||
|
mint_info = await client.get_account_info(mint_address)
|
||||||
|
|
||||||
|
if not mint_info.value:
|
||||||
|
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||||
|
|
||||||
|
owner = mint_info.value.owner
|
||||||
|
|
||||||
|
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||||
|
return SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif owner == TOKEN_2022_PROGRAM:
|
||||||
|
return TOKEN_2022_PROGRAM
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def buy_pump_swap(
|
async def buy_pump_swap(
|
||||||
client: AsyncClient,
|
client: AsyncClient,
|
||||||
pump_fun_amm_market: Pubkey,
|
market: Pubkey,
|
||||||
payer: Keypair,
|
payer: Keypair,
|
||||||
base_mint: Pubkey,
|
base_mint: Pubkey,
|
||||||
user_base_token_account: Pubkey,
|
user_base_token_account: Pubkey,
|
||||||
@@ -273,52 +406,64 @@ async def buy_pump_swap(
|
|||||||
pool_quote_token_account: Pubkey,
|
pool_quote_token_account: Pubkey,
|
||||||
coin_creator_vault_authority: Pubkey,
|
coin_creator_vault_authority: Pubkey,
|
||||||
coin_creator_vault_ata: Pubkey,
|
coin_creator_vault_ata: Pubkey,
|
||||||
sol_amount_to_spend: int,
|
sol_amount_to_spend: float,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Buy tokens on the PUMP AMM with slippage protection.
|
"""Execute a token buy on the PUMP AMM with slippage protection.
|
||||||
|
|
||||||
Executes a token purchase on the PUMP AMM protocol, calculating the expected
|
This function:
|
||||||
token amount based on the current pool price and applying slippage protection.
|
1. Calculates expected token output based on current price
|
||||||
|
2. Wraps SOL into WSOL (required for SPL token operations)
|
||||||
|
3. Constructs and simulates the transaction
|
||||||
|
4. Sends the buy transaction if simulation succeeds
|
||||||
|
|
||||||
|
Why WSOL wrapping is needed:
|
||||||
|
SPL tokens can only interact with other SPL tokens. Native SOL must be
|
||||||
|
wrapped into WSOL (an SPL token representation of SOL) before trading.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
pump_fun_amm_market: Address of the AMM market
|
market: AMM pool address
|
||||||
payer: Keypair of the transaction signer and token buyer
|
payer: Wallet keypair for signing
|
||||||
base_mint: Address of the token mint being purchased
|
base_mint: Token mint address
|
||||||
user_base_token_account: Address of the user's token account for receiving purchased tokens
|
user_base_token_account: User's token account (for receiving tokens)
|
||||||
user_quote_token_account: Address of the user's SOL token account
|
user_quote_token_account: User's WSOL account
|
||||||
pool_base_token_account: Address of the pool's token account for the token being purchased
|
pool_base_token_account: Pool's token account
|
||||||
pool_quote_token_account: Address of the pool's SOL token account
|
pool_quote_token_account: Pool's WSOL account
|
||||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
coin_creator_vault_authority: Creator vault PDA
|
||||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
coin_creator_vault_ata: Creator's WSOL account
|
||||||
sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports)
|
sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports)
|
||||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Transaction signature if successful, None otherwise
|
Transaction signature if successful, None otherwise
|
||||||
"""
|
"""
|
||||||
# Calculate token price
|
token_program_id = await get_token_program_id(client, base_mint)
|
||||||
token_price_sol = await calculate_token_pool_price(
|
token_price_sol = await calculate_token_pool_price(
|
||||||
client, pool_base_token_account, pool_quote_token_account
|
client, pool_base_token_account, pool_quote_token_account
|
||||||
)
|
)
|
||||||
print(f"Token price in SOL: {token_price_sol:.10f} SOL")
|
print(f"Token price in SOL: {token_price_sol:.10f} SOL")
|
||||||
|
|
||||||
# Calculate maximum SOL input with slippage protection
|
# Calculate expected token amount and maximum SOL we're willing to spend
|
||||||
base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
|
base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
|
||||||
slippage_factor = 1 + slippage
|
max_sol_input = int((sol_amount_to_spend * (1 + slippage)) * LAMPORTS_PER_SOL)
|
||||||
max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
|
|
||||||
|
|
||||||
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
|
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
|
||||||
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
|
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
|
||||||
|
|
||||||
# Calculate required PDAs for volume tracking
|
# Derive volume accumulator PDAs for incentive tracking
|
||||||
global_volume_accumulator = find_global_volume_accumulator()
|
global_volume_accumulator = find_global_volume_accumulator()
|
||||||
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
||||||
|
|
||||||
# Define all accounts needed for the buy instruction
|
# Get fee recipient based on mayhem mode
|
||||||
|
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||||
|
client, market
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build account list for buy instruction
|
||||||
|
# Order matters! Must match the program's expected account layout
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||||
@@ -327,15 +472,13 @@ async def buy_pump_swap(
|
|||||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||||
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||||
is_signer=False,
|
), # Use dynamic token_program_id
|
||||||
is_writable=True,
|
|
||||||
),
|
|
||||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
|
||||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -352,34 +495,43 @@ async def buy_pump_swap(
|
|||||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=global_volume_accumulator, is_signer=False, is_writable=True
|
pubkey=global_volume_accumulator, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
||||||
# Index 21: fee_config (readonly)
|
|
||||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||||
# Index 22: fee_program (readonly)
|
|
||||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Instruction data format:
|
||||||
|
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
|
||||||
|
# All integers are little-endian (<)
|
||||||
data = (
|
data = (
|
||||||
BUY_DISCRIMINATOR
|
BUY_DISCRIMINATOR
|
||||||
+ struct.pack("<Q", base_amount_out)
|
+ struct.pack("<Q", base_amount_out) # Expected token amount
|
||||||
+ struct.pack("<Q", max_sol_input)
|
+ struct.pack("<Q", max_sol_input) # Maximum SOL to spend
|
||||||
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
+ struct.pack("<B", VOLUME_TRACKING_ENABLED) # Enable volume tracking
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set compute budget to avoid transaction failures
|
||||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||||
|
|
||||||
# Wrapping SOL
|
# Create WSOL account if it doesn't exist
|
||||||
|
# Note: WSOL always uses the standard Token program, never Token2022
|
||||||
create_wsol_ata_ix = create_idempotent_associated_token_account(
|
create_wsol_ata_ix = create_idempotent_associated_token_account(
|
||||||
payer.pubkey(), payer.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM
|
payer.pubkey(),
|
||||||
|
payer.pubkey(),
|
||||||
|
SOL,
|
||||||
|
SYSTEM_TOKEN_PROGRAM, # WSOL always uses standard Token program
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate the amount of SOL to wrap(can also use `max_sol_input`), can get the SOL back by closing the WSOL account while selling
|
# Calculate amount to wrap (includes buffer for fees)
|
||||||
pump_protocol_fees = sol_amount_to_spend * 0.1 # adding some buffer fees
|
wrap_amount = int(
|
||||||
wrap_amount = int((sol_amount_to_spend + pump_protocol_fees) * LAMPORTS_PER_SOL)
|
(sol_amount_to_spend * (1 + PROTOCOL_FEE_BUFFER)) * LAMPORTS_PER_SOL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transfer SOL to WSOL account and sync
|
||||||
|
# This converts native SOL to the SPL token version (WSOL)
|
||||||
transfer_sol_ix = transfer(
|
transfer_sol_ix = transfer(
|
||||||
TransferParams(
|
TransferParams(
|
||||||
from_pubkey=payer.pubkey(),
|
from_pubkey=payer.pubkey(),
|
||||||
@@ -387,20 +539,24 @@ async def buy_pump_swap(
|
|||||||
lamports=wrap_amount,
|
lamports=wrap_amount,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
sync_native_ix = sync_native(
|
sync_native_ix = sync_native(
|
||||||
SyncNativeParams(SYSTEM_TOKEN_PROGRAM, user_quote_token_account)
|
SyncNativeParams(
|
||||||
|
SYSTEM_TOKEN_PROGRAM, user_quote_token_account
|
||||||
|
) # WSOL always uses standard Token program
|
||||||
)
|
)
|
||||||
|
|
||||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
# Create token account for receiving purchased tokens
|
||||||
payer.pubkey(), payer.pubkey(), base_mint, SYSTEM_TOKEN_PROGRAM
|
create_token_ata_ix = create_idempotent_associated_token_account(
|
||||||
|
payer.pubkey(),
|
||||||
|
payer.pubkey(),
|
||||||
|
base_mint,
|
||||||
|
token_program_id, # Use dynamic token_program_id
|
||||||
)
|
)
|
||||||
|
|
||||||
buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||||
|
|
||||||
|
# Build and sign transaction
|
||||||
blockhash_resp = await client.get_latest_blockhash()
|
blockhash_resp = await client.get_latest_blockhash()
|
||||||
recent_blockhash = blockhash_resp.value.blockhash
|
|
||||||
|
|
||||||
msg = Message.new_with_blockhash(
|
msg = Message.new_with_blockhash(
|
||||||
[
|
[
|
||||||
compute_limit_ix,
|
compute_limit_ix,
|
||||||
@@ -408,64 +564,77 @@ async def buy_pump_swap(
|
|||||||
create_wsol_ata_ix,
|
create_wsol_ata_ix,
|
||||||
transfer_sol_ix,
|
transfer_sol_ix,
|
||||||
sync_native_ix,
|
sync_native_ix,
|
||||||
idempotent_ata_ix,
|
create_token_ata_ix,
|
||||||
buy_ix,
|
buy_ix,
|
||||||
],
|
],
|
||||||
payer.pubkey(),
|
payer.pubkey(),
|
||||||
recent_blockhash,
|
blockhash_resp.value.blockhash,
|
||||||
)
|
)
|
||||||
|
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||||
|
|
||||||
tx_buy = VersionedTransaction(message=msg, keypairs=[payer])
|
# Simulate first to catch errors before sending
|
||||||
|
simulation = await client.simulate_transaction(tx)
|
||||||
# Optionally, you can simulate the transaction first and check for errors and get the compute units used
|
|
||||||
simulation = await client.simulate_transaction(tx_buy)
|
|
||||||
if simulation.value.err:
|
if simulation.value.err:
|
||||||
print(f"Simulation error: {simulation.value.err}")
|
print(f"Simulation error: {simulation.value.err}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
compute_units_used = simulation.value.units_consumed
|
print(
|
||||||
print(f"Simulation successful, compute units used: {compute_units_used}")
|
f"Simulation successful, compute units used: {simulation.value.units_consumed}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Skip preflight since we already simulated (faster execution)
|
||||||
tx_sig = await client.send_transaction(
|
tx_sig = await client.send_transaction(
|
||||||
tx_buy,
|
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tx_hash = tx_sig.value
|
tx_hash = tx_sig.value
|
||||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
|
||||||
|
|
||||||
|
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||||
print("Transaction confirmed")
|
print("Transaction confirmed")
|
||||||
return tx_hash
|
return tx_hash
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sending transaction: {e!s}")
|
print(f"Error: {e!s}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
# ============================================================================
|
||||||
"""Main function to execute the token buying process."""
|
# Main Execution
|
||||||
sol_amount_to_spend = 0.000001
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Execute the complete buy flow."""
|
||||||
|
sol_amount_to_spend = 0.000001 # Amount of SOL to spend on the purchase
|
||||||
|
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
|
# Step 1: Find the pool address for our token
|
||||||
market_address = await get_market_address_by_base_mint(
|
market_address = await get_market_address_by_base_mint(
|
||||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Step 2: Parse pool data to get necessary accounts
|
||||||
market_data = await get_market_data(client, market_address)
|
market_data = await get_market_data(client, market_address)
|
||||||
|
|
||||||
|
# Determine token program ID for the base mint
|
||||||
|
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||||
|
|
||||||
|
# Step 3: Derive PDAs needed for the transaction
|
||||||
coin_creator_vault_authority = find_coin_creator_vault(
|
coin_creator_vault_authority = find_coin_creator_vault(
|
||||||
Pubkey.from_string(market_data["coin_creator"])
|
Pubkey.from_string(market_data["coin_creator"])
|
||||||
)
|
)
|
||||||
coin_creator_vault_ata = get_associated_token_address(
|
coin_creator_vault_ata = get_associated_token_address(
|
||||||
coin_creator_vault_authority, SOL
|
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Step 4: Execute the buy
|
||||||
await buy_pump_swap(
|
await buy_pump_swap(
|
||||||
client,
|
client,
|
||||||
market_address,
|
market_address,
|
||||||
PAYER,
|
PAYER,
|
||||||
TOKEN_MINT,
|
TOKEN_MINT,
|
||||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||||
coin_creator_vault_authority,
|
coin_creator_vault_authority,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
This module provides functionality to:
|
This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol.
|
||||||
- Find market addresses by token mint.
|
It covers the complete flow from finding markets to executing sells with mayhem mode support.
|
||||||
- Fetch and parse market data from PUMP AMM pools.
|
|
||||||
- Calculate token prices in AMM pools.
|
Key concepts demonstrated:
|
||||||
- Create associated token accounts (ATAs) idempotently.
|
- Finding AMM pool addresses by token mint
|
||||||
- Sell tokens on the PUMP AMM with slippage protection.
|
- Parsing binary account data structures
|
||||||
|
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||||
|
- Program Derived Address (PDA) derivation
|
||||||
|
- Transaction construction with compute budgets
|
||||||
|
- Slippage protection mechanisms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -26,31 +30,33 @@ from spl.token.instructions import get_associated_token_address
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Configuration constants
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||||
TOKEN_MINT = Pubkey.from_string("...")
|
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||||
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
|
SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade
|
||||||
|
|
||||||
TOKEN_DECIMALS = 6
|
# Token configuration
|
||||||
SELL_DISCRIMINATOR = bytes.fromhex(
|
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||||
"33e685a4017f83ad"
|
|
||||||
) # Program instruction identifier for the sell function
|
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||||
|
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Solana Program IDs and System Accounts
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
# Solana system addresses and program IDs
|
|
||||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||||
)
|
)
|
||||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
|
||||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
|
||||||
)
|
|
||||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
|
||||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
|
||||||
)
|
|
||||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
@@ -59,59 +65,87 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
|||||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||||
)
|
)
|
||||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Constants for Account Structure Parsing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Pool account structure offsets
|
||||||
|
POOL_DISCRIMINATOR_SIZE = 8
|
||||||
|
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||||
|
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||||
|
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||||
|
|
||||||
|
# GlobalConfig structure offsets
|
||||||
|
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||||
|
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||||
|
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||||
|
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||||
|
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||||
|
+ GLOBALCONFIG_ADMIN_SIZE
|
||||||
|
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fee recipients
|
||||||
|
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||||
|
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Solana constants
|
||||||
LAMPORTS_PER_SOL = 1_000_000_000
|
LAMPORTS_PER_SOL = 1_000_000_000
|
||||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||||
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use
|
COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Market Discovery
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
async def get_market_address_by_base_mint(
|
async def get_market_address_by_base_mint(
|
||||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||||
) -> Pubkey:
|
) -> Pubkey:
|
||||||
"""Find the market address for a given token mint.
|
"""Find the AMM pool address for a specific token.
|
||||||
|
|
||||||
Searches for the AMM pool that contains the specified token as its base token.
|
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||||
|
that matches the given token mint address.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
base_mint_address: Address of the token mint you want to find the market for
|
base_mint_address: Token mint to find the pool for
|
||||||
amm_program_id: Address of the AMM program
|
amm_program_id: PUMP AMM program address
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The Pubkey of the market (AMM pool) for the token
|
Address of the AMM pool (market) for the token
|
||||||
"""
|
"""
|
||||||
base_mint_bytes = bytes(base_mint_address)
|
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||||
offset = (
|
|
||||||
43 # Offset where the base_mint field is stored in the account data structure
|
|
||||||
)
|
|
||||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
|
||||||
|
|
||||||
response = await client.get_program_accounts(
|
response = await client.get_program_accounts(
|
||||||
amm_program_id, encoding="base64", filters=filters
|
amm_program_id, encoding="base64", filters=filters
|
||||||
)
|
)
|
||||||
|
return response.value[0].pubkey
|
||||||
market_address = [account.pubkey for account in response.value][0]
|
|
||||||
return market_address
|
|
||||||
|
|
||||||
|
|
||||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||||
"""Fetch and parse market data from the blockchain.
|
"""Parse binary pool account data into a structured dictionary.
|
||||||
|
|
||||||
Retrieves and deserializes the data stored in the market account.
|
The pool account stores data in a specific binary format. This function
|
||||||
|
deserializes that data based on the known structure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
market_address: Address of the market (AMM pool) to fetch data for
|
market_address: Address of the pool account
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing the parsed market data
|
Dictionary with parsed pool data fields
|
||||||
"""
|
"""
|
||||||
response = await client.get_account_info(market_address, encoding="base64")
|
response = await client.get_account_info(market_address, encoding="base64")
|
||||||
data = response.value.data
|
data = response.value.data
|
||||||
parsed_data: dict = {}
|
parsed_data: dict = {}
|
||||||
|
|
||||||
# Start after the 8-byte discriminator
|
offset = POOL_DISCRIMINATOR_SIZE
|
||||||
offset = 8
|
|
||||||
# Define the structure of the market account data
|
# Field definitions: (name, type)
|
||||||
|
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||||
fields = [
|
fields = [
|
||||||
("pool_bump", "u8"),
|
("pool_bump", "u8"),
|
||||||
("index", "u16"),
|
("index", "u16"),
|
||||||
@@ -131,39 +165,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
|||||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||||
offset += 32
|
offset += 32
|
||||||
elif field_type in {"u64", "i64"}:
|
elif field_type in {"u64", "i64"}:
|
||||||
value = (
|
format_char = "<Q" if field_type == "u64" else "<q"
|
||||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
parsed_data[field_name] = struct.unpack(
|
||||||
if field_type == "u64"
|
format_char, data[offset : offset + 8]
|
||||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
)[0]
|
||||||
)
|
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 8
|
offset += 8
|
||||||
elif field_type == "u16":
|
elif field_type == "u16":
|
||||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 2
|
offset += 2
|
||||||
elif field_type == "u8":
|
elif field_type == "u8":
|
||||||
value = data[offset]
|
parsed_data[field_name] = data[offset]
|
||||||
parsed_data[field_name] = value
|
|
||||||
offset += 1
|
offset += 1
|
||||||
|
|
||||||
return parsed_data
|
return parsed_data
|
||||||
|
|
||||||
|
|
||||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
# ============================================================================
|
||||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
# Program Derived Address (PDA) Derivation
|
||||||
|
# ============================================================================
|
||||||
|
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||||
|
# They allow programs to own accounts without needing a private key.
|
||||||
|
|
||||||
Calculates the deterministic PDA that serves as the vault authority
|
|
||||||
for a specific coin creator in the PUMP AMM protocol.
|
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||||
|
"""Derive the PDA for the coin creator's fee vault.
|
||||||
|
|
||||||
|
The creator vault collects fees on behalf of the token creator.
|
||||||
|
This is a deterministic address that can be recalculated by anyone.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
coin_creator: Pubkey of the coin creator account
|
coin_creator: Public key of the token creator
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Pubkey of the derived coin creator vault authority
|
PDA of the creator's vault authority
|
||||||
|
|
||||||
Note:
|
|
||||||
This vault is used to collect creator fees from token transactions
|
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"creator_vault", bytes(coin_creator)],
|
[b"creator_vault", bytes(coin_creator)],
|
||||||
@@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
|||||||
|
|
||||||
|
|
||||||
def find_fee_config() -> Pubkey:
|
def find_fee_config() -> Pubkey:
|
||||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
"""Derive the PDA for the fee configuration account.
|
||||||
|
|
||||||
Returns:
|
This account stores fee-related configuration for the AMM.
|
||||||
Pubkey of the derived fee config account
|
|
||||||
"""
|
"""
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||||
@@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Mayhem Mode Fee Handling
|
||||||
|
# ============================================================================
|
||||||
|
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||||
|
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||||
|
|
||||||
|
|
||||||
|
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||||
|
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||||
|
|
||||||
|
When mayhem mode is active, fees are redirected to a special recipient
|
||||||
|
stored in the GlobalConfig account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Public key of the mayhem mode fee recipient
|
||||||
|
"""
|
||||||
|
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
msg = "Cannot fetch GlobalConfig account"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
data = response.value.data
|
||||||
|
recipient_bytes = data[
|
||||||
|
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||||
|
]
|
||||||
|
return Pubkey.from_bytes(recipient_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pumpswap_fee_recipients(
|
||||||
|
client: AsyncClient, pool: Pubkey
|
||||||
|
) -> tuple[Pubkey, Pubkey]:
|
||||||
|
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||||
|
|
||||||
|
This function checks if mayhem mode is enabled for the pool and returns
|
||||||
|
the appropriate fee recipient and their WSOL token account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Solana RPC client
|
||||||
|
pool: Address of the AMM pool
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||||
|
"""
|
||||||
|
response = await client.get_account_info(pool, encoding="base64")
|
||||||
|
if not response.value or not response.value.data:
|
||||||
|
msg = "Cannot fetch pool account"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
pool_data = response.value.data
|
||||||
|
|
||||||
|
# Check if mayhem mode flag exists and is enabled
|
||||||
|
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||||
|
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select appropriate fee recipient
|
||||||
|
if is_mayhem_mode:
|
||||||
|
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||||
|
else:
|
||||||
|
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||||
|
|
||||||
|
# Get the fee recipient's WSOL token account
|
||||||
|
fee_recipient_token_account = get_associated_token_address(
|
||||||
|
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
|
return (fee_recipient, fee_recipient_token_account)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Price Calculation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
async def calculate_token_pool_price(
|
async def calculate_token_pool_price(
|
||||||
client: AsyncClient,
|
client: AsyncClient,
|
||||||
pool_base_token_account: Pubkey,
|
pool_base_token_account: Pubkey,
|
||||||
pool_quote_token_account: Pubkey,
|
pool_quote_token_account: Pubkey,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Calculate the price of tokens in the pool.
|
"""Calculate current token price from AMM pool balances.
|
||||||
|
|
||||||
Fetches the balance of tokens in the pool and calculates the price ratio.
|
AMM price is determined by the ratio of tokens in the pool:
|
||||||
|
price = quote_balance / base_balance
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
pool_base_token_account: Address of the pool's base token account (your token)
|
pool_base_token_account: Pool's token account (the token being priced)
|
||||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The price of the base token in terms of the quote token (usually SOL)
|
Price in SOL per token
|
||||||
"""
|
"""
|
||||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||||
quote_balance_resp = await client.get_token_account_balance(
|
quote_balance_resp = await client.get_token_account_balance(
|
||||||
pool_quote_token_account
|
pool_quote_token_account
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract the UI amounts (human-readable with decimals)
|
|
||||||
base_amount = float(base_balance_resp.value.ui_amount)
|
base_amount = float(base_balance_resp.value.ui_amount)
|
||||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||||
|
|
||||||
token_price = quote_amount / base_amount
|
return quote_amount / base_amount
|
||||||
|
|
||||||
return token_price
|
|
||||||
|
# ============================================================================
|
||||||
|
# Token Program Determination
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||||
|
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||||
|
mint_info = await client.get_account_info(mint_address)
|
||||||
|
|
||||||
|
if not mint_info.value:
|
||||||
|
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||||
|
|
||||||
|
owner = mint_info.value.owner
|
||||||
|
|
||||||
|
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||||
|
return SYSTEM_TOKEN_PROGRAM
|
||||||
|
elif owner == TOKEN_2022_PROGRAM:
|
||||||
|
return TOKEN_2022_PROGRAM
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Associated Token Account (ATA) Creation
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
||||||
"""Create an instruction to create an Associated Token Account (ATA) if it doesn't exist.
|
"""Create instruction to initialize a WSOL ATA if it doesn't exist.
|
||||||
|
|
||||||
This creates an instruction that will create an Associated Token Account for SOL
|
Idempotent means this instruction won't fail if the ATA already exists.
|
||||||
if it doesn't already exist.
|
See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
payer_pubkey: The public key of the account that will pay for the creation
|
payer_pubkey: Account that will pay for ATA creation
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An instruction to create the ATA
|
Instruction to create the ATA
|
||||||
"""
|
"""
|
||||||
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
|
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
|
||||||
|
|
||||||
@@ -239,20 +376,23 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
|||||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The data for creating an ATA idempotently is just a single byte with value 1
|
# Instruction data: single byte with value 1 = CreateIdempotent
|
||||||
# Check the details here:
|
|
||||||
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
|
||||||
data = bytes([1])
|
|
||||||
return Instruction(
|
return Instruction(
|
||||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts
|
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Token Selling
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
async def sell_pump_swap(
|
async def sell_pump_swap(
|
||||||
client: AsyncClient,
|
client: AsyncClient,
|
||||||
pump_fun_amm_market: Pubkey,
|
market: Pubkey,
|
||||||
payer: Keypair,
|
payer: Keypair,
|
||||||
base_mint: Pubkey,
|
base_mint: Pubkey,
|
||||||
|
token_program_id: Pubkey,
|
||||||
user_base_token_account: Pubkey,
|
user_base_token_account: Pubkey,
|
||||||
user_quote_token_account: Pubkey,
|
user_quote_token_account: Pubkey,
|
||||||
pool_base_token_account: Pubkey,
|
pool_base_token_account: Pubkey,
|
||||||
@@ -261,54 +401,61 @@ async def sell_pump_swap(
|
|||||||
coin_creator_vault_ata: Pubkey,
|
coin_creator_vault_ata: Pubkey,
|
||||||
slippage: float = 0.25,
|
slippage: float = 0.25,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Sell tokens on the PUMP AMM.
|
"""Execute a token sell on the PUMP AMM with slippage protection.
|
||||||
|
|
||||||
This function sells all tokens in the user's token account with the specified slippage tolerance.
|
This function:
|
||||||
|
1. Fetches current token balance and pool price
|
||||||
|
2. Calculates minimum SOL output with slippage tolerance
|
||||||
|
3. Constructs and sends the sell transaction
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client: Solana RPC client instance
|
client: Solana RPC client
|
||||||
pump_fun_amm_market: Address of the AMM market
|
market: AMM pool address
|
||||||
payer: Keypair of the transaction signer and token seller
|
payer: Wallet keypair for signing
|
||||||
base_mint: Address of the token mint being sold
|
base_mint: Token mint address
|
||||||
user_base_token_account: Address of the user's token account for the token being sold
|
user_base_token_account: User's token account
|
||||||
user_quote_token_account: Address of the user's SOL token account
|
user_quote_token_account: User's WSOL account
|
||||||
pool_base_token_account: Address of the pool's token account for the token being sold
|
pool_base_token_account: Pool's token account
|
||||||
pool_quote_token_account: Address of the pool's SOL token account
|
pool_quote_token_account: Pool's WSOL account
|
||||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
coin_creator_vault_authority: Creator vault PDA
|
||||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
coin_creator_vault_ata: Creator's WSOL account
|
||||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Transaction signature if successful, None otherwise
|
Transaction signature if successful, None otherwise
|
||||||
"""
|
"""
|
||||||
# Get token balance
|
|
||||||
token_balance = int(
|
token_balance = int(
|
||||||
(await client.get_token_account_balance(user_base_token_account)).value.amount
|
(await client.get_token_account_balance(user_base_token_account)).value.amount
|
||||||
)
|
)
|
||||||
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
||||||
|
|
||||||
print(f"Token balance: {token_balance_decimal}")
|
print(f"Token balance: {token_balance_decimal}")
|
||||||
|
|
||||||
if token_balance == 0:
|
if token_balance == 0:
|
||||||
print("No tokens to sell.")
|
print("No tokens to sell.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Calculate token price
|
|
||||||
token_price_sol = await calculate_token_pool_price(
|
token_price_sol = await calculate_token_pool_price(
|
||||||
client, pool_base_token_account, pool_quote_token_account
|
client, pool_base_token_account, pool_quote_token_account
|
||||||
)
|
)
|
||||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||||
|
|
||||||
# Calculate minimum SOL output with slippage protection
|
# Calculate minimum SOL we're willing to receive (slippage protection)
|
||||||
amount = token_balance
|
expected_sol_output = token_balance_decimal * token_price_sol
|
||||||
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
|
min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL)
|
||||||
slippage_factor = 1 - slippage
|
|
||||||
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
|
|
||||||
|
|
||||||
print(f"Selling {token_balance_decimal} tokens")
|
print(f"Selling {token_balance_decimal} tokens")
|
||||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||||
|
|
||||||
# Define all accounts needed for the sell instruction
|
# Get fee recipient based on mayhem mode
|
||||||
|
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||||
|
client, market
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build account list for sell instruction
|
||||||
|
# Order matters! Must match the program's expected account layout
|
||||||
accounts = [
|
accounts = [
|
||||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||||
@@ -317,15 +464,13 @@ async def sell_pump_swap(
|
|||||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||||
|
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||||
is_signer=False,
|
), # Use dynamic token_program_id
|
||||||
is_writable=True,
|
|
||||||
),
|
|
||||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
|
||||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
@@ -341,76 +486,88 @@ async def sell_pump_swap(
|
|||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||||
),
|
),
|
||||||
# Index 19: fee_config (readonly)
|
|
||||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||||
# Index 20: fee_program (readonly)
|
|
||||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
|
||||||
|
# All integers are little-endian (<)
|
||||||
data = (
|
data = (
|
||||||
SELL_DISCRIMINATOR
|
SELL_DISCRIMINATOR
|
||||||
+ struct.pack("<Q", amount)
|
+ struct.pack("<Q", token_balance) # Amount to sell
|
||||||
+ struct.pack("<Q", min_sol_output)
|
+ struct.pack("<Q", min_sol_output) # Minimum SOL to receive
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Set compute budget to avoid transaction failures
|
||||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||||
|
|
||||||
create_ata_ix = create_ata_idempotent_ix(
|
# Ensure WSOL ATA exists (needed to receive SOL from sell)
|
||||||
payer_pubkey=payer.pubkey(),
|
create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
|
||||||
)
|
|
||||||
|
|
||||||
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||||
|
|
||||||
|
# Build and sign transaction
|
||||||
blockhash_resp = await client.get_latest_blockhash()
|
blockhash_resp = await client.get_latest_blockhash()
|
||||||
recent_blockhash = blockhash_resp.value.blockhash
|
|
||||||
|
|
||||||
msg = Message.new_with_blockhash(
|
msg = Message.new_with_blockhash(
|
||||||
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
|
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
|
||||||
payer.pubkey(),
|
payer.pubkey(),
|
||||||
recent_blockhash,
|
blockhash_resp.value.blockhash,
|
||||||
)
|
)
|
||||||
|
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||||
tx_sell = VersionedTransaction(message=msg, keypairs=[payer])
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Skip preflight to send transaction faster (useful in competitive scenarios)
|
||||||
tx_sig = await client.send_transaction(
|
tx_sig = await client.send_transaction(
|
||||||
tx_sell,
|
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tx_hash = tx_sig.value
|
tx_hash = tx_sig.value
|
||||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
|
||||||
|
|
||||||
|
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||||
print("Transaction confirmed")
|
print("Transaction confirmed")
|
||||||
return tx_hash
|
return tx_hash
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error sending transaction: {e!s}")
|
print(f"Error: {e!s}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
# ============================================================================
|
||||||
"""Main function to execute the token selling process."""
|
# Main Execution
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Execute the complete sell flow."""
|
||||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||||
|
# Step 1: Find the pool address for our token
|
||||||
market_address = await get_market_address_by_base_mint(
|
market_address = await get_market_address_by_base_mint(
|
||||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Step 2: Parse pool data to get necessary accounts
|
||||||
market_data = await get_market_data(client, market_address)
|
market_data = await get_market_data(client, market_address)
|
||||||
|
|
||||||
|
# Determine token program ID for the base mint
|
||||||
|
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||||
|
|
||||||
|
# Step 3: Derive PDAs needed for the transaction
|
||||||
coin_creator_vault_authority = find_coin_creator_vault(
|
coin_creator_vault_authority = find_coin_creator_vault(
|
||||||
Pubkey.from_string(market_data["coin_creator"])
|
Pubkey.from_string(market_data["coin_creator"])
|
||||||
)
|
)
|
||||||
coin_creator_vault_ata = get_associated_token_address(
|
coin_creator_vault_ata = get_associated_token_address(
|
||||||
coin_creator_vault_authority, SOL
|
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Step 4: Execute the sell
|
||||||
await sell_pump_swap(
|
await sell_pump_swap(
|
||||||
client,
|
client,
|
||||||
market_address,
|
market_address,
|
||||||
PAYER,
|
PAYER,
|
||||||
TOKEN_MINT,
|
TOKEN_MINT,
|
||||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
token_program_id,
|
||||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||||
|
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||||
coin_creator_vault_authority,
|
coin_creator_vault_authority,
|
||||||
|
|||||||
+11
-4
@@ -34,12 +34,19 @@ class AccountCleanupManager:
|
|||||||
self.use_priority_fee = use_priority_fee
|
self.use_priority_fee = use_priority_fee
|
||||||
self.close_with_force_burn = force_burn
|
self.close_with_force_burn = force_burn
|
||||||
|
|
||||||
async def cleanup_ata(self, mint: Pubkey) -> None:
|
async def cleanup_ata(self, mint: Pubkey, token_program_id: Pubkey | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Attempt to burn any remaining tokens and close the ATA.
|
Attempt to burn any remaining tokens and close the ATA.
|
||||||
Skips if account doesn't exist or is already empty/closed.
|
Skips if account doesn't exist or is already empty/closed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mint: Token mint address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||||
"""
|
"""
|
||||||
ata = self.wallet.get_associated_token_address(mint)
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
|
ata = self.wallet.get_associated_token_address(mint, token_program_id)
|
||||||
solana_client = await self.client.get_client()
|
solana_client = await self.client.get_client()
|
||||||
|
|
||||||
priority_fee = (
|
priority_fee = (
|
||||||
@@ -70,7 +77,7 @@ class AccountCleanupManager:
|
|||||||
mint=mint,
|
mint=mint,
|
||||||
owner=self.wallet.pubkey,
|
owner=self.wallet.pubkey,
|
||||||
amount=balance,
|
amount=balance,
|
||||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
program_id=token_program_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
instructions.append(burn_ix)
|
instructions.append(burn_ix)
|
||||||
@@ -89,7 +96,7 @@ class AccountCleanupManager:
|
|||||||
account=ata,
|
account=ata,
|
||||||
dest=self.wallet.pubkey,
|
dest=self.wallet.pubkey,
|
||||||
owner=self.wallet.pubkey,
|
owner=self.wallet.pubkey,
|
||||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
program_id=token_program_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
instructions.append(close_ix)
|
instructions.append(close_ix)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ async def handle_cleanup_after_failure(
|
|||||||
client,
|
client,
|
||||||
wallet,
|
wallet,
|
||||||
mint,
|
mint,
|
||||||
|
token_program_id,
|
||||||
priority_fee_manager,
|
priority_fee_manager,
|
||||||
cleanup_mode,
|
cleanup_mode,
|
||||||
cleanup_with_prior_fee,
|
cleanup_with_prior_fee,
|
||||||
@@ -30,13 +31,14 @@ async def handle_cleanup_after_failure(
|
|||||||
manager = AccountCleanupManager(
|
manager = AccountCleanupManager(
|
||||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||||
)
|
)
|
||||||
await manager.cleanup_ata(mint)
|
await manager.cleanup_ata(mint, token_program_id)
|
||||||
|
|
||||||
|
|
||||||
async def handle_cleanup_after_sell(
|
async def handle_cleanup_after_sell(
|
||||||
client,
|
client,
|
||||||
wallet,
|
wallet,
|
||||||
mint,
|
mint,
|
||||||
|
token_program_id,
|
||||||
priority_fee_manager,
|
priority_fee_manager,
|
||||||
cleanup_mode,
|
cleanup_mode,
|
||||||
cleanup_with_prior_fee,
|
cleanup_with_prior_fee,
|
||||||
@@ -47,13 +49,14 @@ async def handle_cleanup_after_sell(
|
|||||||
manager = AccountCleanupManager(
|
manager = AccountCleanupManager(
|
||||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||||
)
|
)
|
||||||
await manager.cleanup_ata(mint)
|
await manager.cleanup_ata(mint, token_program_id)
|
||||||
|
|
||||||
|
|
||||||
async def handle_cleanup_post_session(
|
async def handle_cleanup_post_session(
|
||||||
client,
|
client,
|
||||||
wallet,
|
wallet,
|
||||||
mints,
|
mints,
|
||||||
|
token_program_ids,
|
||||||
priority_fee_manager,
|
priority_fee_manager,
|
||||||
cleanup_mode,
|
cleanup_mode,
|
||||||
cleanup_with_prior_fee,
|
cleanup_with_prior_fee,
|
||||||
@@ -64,5 +67,5 @@ async def handle_cleanup_post_session(
|
|||||||
manager = AccountCleanupManager(
|
manager = AccountCleanupManager(
|
||||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||||
)
|
)
|
||||||
for mint in mints:
|
for mint, token_program_id in zip(mints, token_program_ids):
|
||||||
await manager.cleanup_ata(mint)
|
await manager.cleanup_ata(mint, token_program_id)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("1111111111111111111111111111
|
|||||||
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||||
)
|
)
|
||||||
|
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||||
|
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||||
|
)
|
||||||
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
)
|
)
|
||||||
@@ -42,6 +45,7 @@ class SystemAddresses:
|
|||||||
# Reference the module-level constants
|
# Reference the module-level constants
|
||||||
SYSTEM_PROGRAM = SYSTEM_PROGRAM
|
SYSTEM_PROGRAM = SYSTEM_PROGRAM
|
||||||
TOKEN_PROGRAM = TOKEN_PROGRAM
|
TOKEN_PROGRAM = TOKEN_PROGRAM
|
||||||
|
TOKEN_2022_PROGRAM = TOKEN_2022_PROGRAM
|
||||||
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
|
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
|
||||||
RENT = RENT
|
RENT = RENT
|
||||||
SOL_MINT = SOL_MINT
|
SOL_MINT = SOL_MINT
|
||||||
@@ -56,6 +60,7 @@ class SystemAddresses:
|
|||||||
return {
|
return {
|
||||||
"system_program": cls.SYSTEM_PROGRAM,
|
"system_program": cls.SYSTEM_PROGRAM,
|
||||||
"token_program": cls.TOKEN_PROGRAM,
|
"token_program": cls.TOKEN_PROGRAM,
|
||||||
|
"token_2022_program": cls.TOKEN_2022_PROGRAM,
|
||||||
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
|
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
|
||||||
"rent": cls.RENT,
|
"rent": cls.RENT,
|
||||||
"sol_mint": cls.SOL_MINT,
|
"sol_mint": cls.SOL_MINT,
|
||||||
|
|||||||
+9
-2
@@ -7,6 +7,8 @@ from solders.keypair import Keypair
|
|||||||
from solders.pubkey import Pubkey
|
from solders.pubkey import Pubkey
|
||||||
from spl.token.instructions import get_associated_token_address
|
from spl.token.instructions import get_associated_token_address
|
||||||
|
|
||||||
|
from core.pubkeys import SystemAddresses
|
||||||
|
|
||||||
|
|
||||||
class Wallet:
|
class Wallet:
|
||||||
"""Manages a Solana wallet for trading operations."""
|
"""Manages a Solana wallet for trading operations."""
|
||||||
@@ -30,16 +32,21 @@ class Wallet:
|
|||||||
"""Get the keypair for signing transactions."""
|
"""Get the keypair for signing transactions."""
|
||||||
return self._keypair
|
return self._keypair
|
||||||
|
|
||||||
def get_associated_token_address(self, mint: Pubkey) -> Pubkey:
|
def get_associated_token_address(
|
||||||
|
self, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||||
|
) -> Pubkey:
|
||||||
"""Get the associated token account address for a mint.
|
"""Get the associated token account address for a mint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mint: Token mint address
|
mint: Token mint address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Associated token account address
|
Associated token account address
|
||||||
"""
|
"""
|
||||||
return get_associated_token_address(self.pubkey, mint)
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
return get_associated_token_address(self.pubkey, mint, token_program_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_keypair(private_key: str) -> Keypair:
|
def _load_keypair(private_key: str) -> Keypair:
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class TokenInfo:
|
|||||||
user: Pubkey | None = None
|
user: Pubkey | None = None
|
||||||
creator: Pubkey | None = None
|
creator: Pubkey | None = None
|
||||||
creator_vault: Pubkey | None = None
|
creator_vault: Pubkey | None = None
|
||||||
|
token_program_id: Pubkey | None = None # Token or Token2022 program
|
||||||
|
is_mayhem_mode: bool = False # pump.fun mayhem mode flag
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
creation_timestamp: float | None = None
|
creation_timestamp: float | None = None
|
||||||
|
|||||||
@@ -147,17 +147,22 @@ class LetsBonkAddressProvider(AddressProvider):
|
|||||||
)
|
)
|
||||||
return quote_vault
|
return quote_vault
|
||||||
|
|
||||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
def derive_user_token_account(
|
||||||
|
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||||
|
) -> Pubkey:
|
||||||
"""Derive user's associated token account address.
|
"""Derive user's associated token account address.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: User's wallet address
|
user: User's wallet address
|
||||||
mint: Token mint address
|
mint: Token mint address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User's associated token account address
|
User's associated token account address
|
||||||
"""
|
"""
|
||||||
return get_associated_token_address(user, mint)
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
return get_associated_token_address(user, mint, token_program_id)
|
||||||
|
|
||||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||||
@@ -296,6 +301,13 @@ class LetsBonkAddressProvider(AddressProvider):
|
|||||||
"""
|
"""
|
||||||
additional_accounts = self.get_additional_accounts(token_info)
|
additional_accounts = self.get_additional_accounts(token_info)
|
||||||
|
|
||||||
|
# Determine token program to use
|
||||||
|
token_program_id = (
|
||||||
|
token_info.token_program_id
|
||||||
|
if token_info.token_program_id
|
||||||
|
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
# Use global_config from TokenInfo if available, otherwise use default
|
# Use global_config from TokenInfo if available, otherwise use default
|
||||||
global_config = (
|
global_config = (
|
||||||
token_info.global_config
|
token_info.global_config
|
||||||
@@ -316,12 +328,12 @@ class LetsBonkAddressProvider(AddressProvider):
|
|||||||
"global_config": global_config,
|
"global_config": global_config,
|
||||||
"platform_config": platform_config,
|
"platform_config": platform_config,
|
||||||
"pool_state": additional_accounts["pool_state"],
|
"pool_state": additional_accounts["pool_state"],
|
||||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
|
||||||
"base_vault": additional_accounts["base_vault"],
|
"base_vault": additional_accounts["base_vault"],
|
||||||
"quote_vault": additional_accounts["quote_vault"],
|
"quote_vault": additional_accounts["quote_vault"],
|
||||||
"base_token_mint": token_info.mint,
|
"base_token_mint": token_info.mint,
|
||||||
"quote_token_mint": SystemAddresses.SOL_MINT,
|
"quote_token_mint": SystemAddresses.SOL_MINT,
|
||||||
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
|
"base_token_program": token_program_id,
|
||||||
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||||
"event_authority": additional_accounts["event_authority"],
|
"event_authority": additional_accounts["event_authority"],
|
||||||
"program": LetsBonkAddresses.PROGRAM,
|
"program": LetsBonkAddresses.PROGRAM,
|
||||||
@@ -351,6 +363,13 @@ class LetsBonkAddressProvider(AddressProvider):
|
|||||||
"""
|
"""
|
||||||
additional_accounts = self.get_additional_accounts(token_info)
|
additional_accounts = self.get_additional_accounts(token_info)
|
||||||
|
|
||||||
|
# Determine token program to use
|
||||||
|
token_program_id = (
|
||||||
|
token_info.token_program_id
|
||||||
|
if token_info.token_program_id
|
||||||
|
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
# Use global_config from TokenInfo if available, otherwise use default
|
# Use global_config from TokenInfo if available, otherwise use default
|
||||||
global_config = (
|
global_config = (
|
||||||
token_info.global_config
|
token_info.global_config
|
||||||
@@ -371,12 +390,12 @@ class LetsBonkAddressProvider(AddressProvider):
|
|||||||
"global_config": global_config,
|
"global_config": global_config,
|
||||||
"platform_config": platform_config,
|
"platform_config": platform_config,
|
||||||
"pool_state": additional_accounts["pool_state"],
|
"pool_state": additional_accounts["pool_state"],
|
||||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
"user_base_token": self.derive_user_token_account(user, token_info.mint, token_program_id),
|
||||||
"base_vault": additional_accounts["base_vault"],
|
"base_vault": additional_accounts["base_vault"],
|
||||||
"quote_vault": additional_accounts["quote_vault"],
|
"quote_vault": additional_accounts["quote_vault"],
|
||||||
"base_token_mint": token_info.mint,
|
"base_token_mint": token_info.mint,
|
||||||
"quote_token_mint": SystemAddresses.SOL_MINT,
|
"quote_token_mint": SystemAddresses.SOL_MINT,
|
||||||
"base_token_program": SystemAddresses.TOKEN_PROGRAM,
|
"base_token_program": token_program_id,
|
||||||
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||||
"event_authority": additional_accounts["event_authority"],
|
"event_authority": additional_accounts["event_authority"],
|
||||||
"program": LetsBonkAddresses.PROGRAM,
|
"program": LetsBonkAddresses.PROGRAM,
|
||||||
|
|||||||
@@ -194,14 +194,21 @@ class LetsBonkCurveManager(CurveManager):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Calculate additional metrics
|
# Calculate additional metrics
|
||||||
if pool_data["virtual_base"] > 0:
|
# Validate reserves are positive before calculating price
|
||||||
pool_data["price_per_token"] = (
|
if pool_data["virtual_base"] <= 0:
|
||||||
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
raise ValueError(
|
||||||
* (10**TOKEN_DECIMALS)
|
f"Invalid virtual_base: {pool_data['virtual_base']} - cannot calculate price"
|
||||||
/ LAMPORTS_PER_SOL
|
|
||||||
)
|
)
|
||||||
else:
|
if pool_data["virtual_quote"] <= 0:
|
||||||
pool_data["price_per_token"] = 0
|
raise ValueError(
|
||||||
|
f"Invalid virtual_quote: {pool_data['virtual_quote']} - cannot calculate price"
|
||||||
|
)
|
||||||
|
|
||||||
|
pool_data["price_per_token"] = (
|
||||||
|
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
||||||
|
* (10**TOKEN_DECIMALS)
|
||||||
|
/ LAMPORTS_PER_SOL
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
|
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from typing import Any
|
|||||||
from solders.pubkey import Pubkey
|
from solders.pubkey import Pubkey
|
||||||
from solders.transaction import VersionedTransaction
|
from solders.transaction import VersionedTransaction
|
||||||
|
|
||||||
|
from core.pubkeys import SystemAddresses
|
||||||
from interfaces.core import EventParser, Platform, TokenInfo
|
from interfaces.core import EventParser, Platform, TokenInfo
|
||||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||||
from utils.idl_parser import IDLParser
|
from utils.idl_parser import IDLParser
|
||||||
@@ -114,6 +115,15 @@ class LetsBonkEventParser(EventParser):
|
|||||||
}:
|
}:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Determine token program based on instruction variant
|
||||||
|
instruction_name = decoded["instruction_name"]
|
||||||
|
is_token_2022 = instruction_name == "initialize_with_token_2022"
|
||||||
|
token_program_id = (
|
||||||
|
SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
if is_token_2022
|
||||||
|
else SystemAddresses.TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
args = decoded.get("args", {})
|
args = decoded.get("args", {})
|
||||||
|
|
||||||
# Extract MintParams from the decoded arguments
|
# Extract MintParams from the decoded arguments
|
||||||
@@ -174,6 +184,7 @@ class LetsBonkEventParser(EventParser):
|
|||||||
platform_config=platform_config,
|
platform_config=platform_config,
|
||||||
user=creator,
|
user=creator,
|
||||||
creator=creator,
|
creator=creator,
|
||||||
|
token_program_id=token_program_id,
|
||||||
creation_timestamp=monotonic(),
|
creation_timestamp=monotonic(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,19 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
|||||||
# Get all required accounts
|
# Get all required accounts
|
||||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||||
|
|
||||||
|
# Determine token program to use
|
||||||
|
token_program_id = (
|
||||||
|
token_info.token_program_id
|
||||||
|
if token_info.token_program_id
|
||||||
|
else SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Create idempotent ATA for base token
|
# 1. Create idempotent ATA for base token
|
||||||
ata_instruction = create_idempotent_associated_token_account(
|
ata_instruction = create_idempotent_associated_token_account(
|
||||||
user, # payer
|
user, # payer
|
||||||
user, # owner
|
user, # owner
|
||||||
token_info.mint, # mint
|
token_info.mint, # mint
|
||||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
token_program_id, # token program (dynamic for token2022 support)
|
||||||
)
|
)
|
||||||
instructions.append(ata_instruction)
|
instructions.append(ata_instruction)
|
||||||
|
|
||||||
@@ -151,10 +158,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
|||||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||||
), # quote_token_mint
|
), # quote_token_mint
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||||
), # base_token_program
|
), # base_token_program
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||||
), # quote_token_program
|
), # quote_token_program
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=accounts_info["event_authority"],
|
pubkey=accounts_info["event_authority"],
|
||||||
@@ -306,10 +313,10 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
|||||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||||
), # quote_token_mint
|
), # quote_token_mint
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False
|
||||||
), # base_token_program
|
), # base_token_program
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False
|
||||||
), # quote_token_program
|
), # quote_token_program
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=accounts_info["event_authority"],
|
pubkey=accounts_info["event_authority"],
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ class PumpFunAddresses:
|
|||||||
FEE: Final[Pubkey] = Pubkey.from_string(
|
FEE: Final[Pubkey] = Pubkey.from_string(
|
||||||
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
|
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
|
||||||
)
|
)
|
||||||
|
# Mayhem mode fee recipient (hardcoded to avoid RPC calls)
|
||||||
|
# To check if this address is up-to-date, fetch Global account data at offset 483
|
||||||
|
# from the pump.fun Global account: 4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf
|
||||||
|
MAYHEM_FEE: Final[Pubkey] = Pubkey.from_string(
|
||||||
|
"GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"
|
||||||
|
)
|
||||||
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
|
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
|
||||||
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
|
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
|
||||||
)
|
)
|
||||||
@@ -139,17 +145,22 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
)
|
)
|
||||||
return bonding_curve
|
return bonding_curve
|
||||||
|
|
||||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
def derive_user_token_account(
|
||||||
|
self, user: Pubkey, mint: Pubkey, token_program_id: Pubkey | None = None
|
||||||
|
) -> Pubkey:
|
||||||
"""Derive user's associated token account address.
|
"""Derive user's associated token account address.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: User's wallet address
|
user: User's wallet address
|
||||||
mint: Token mint address
|
mint: Token mint address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User's associated token account address
|
User's associated token account address
|
||||||
"""
|
"""
|
||||||
return get_associated_token_address(user, mint)
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
return get_associated_token_address(user, mint, token_program_id)
|
||||||
|
|
||||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||||
"""Get pump.fun-specific additional accounts needed for trading.
|
"""Get pump.fun-specific additional accounts needed for trading.
|
||||||
@@ -177,7 +188,7 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
# Derive associated bonding curve if not provided
|
# Derive associated bonding curve if not provided
|
||||||
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
||||||
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
||||||
token_info.mint, token_info.bonding_curve
|
token_info.mint, token_info.bonding_curve, token_info.token_program_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Derive creator vault if not provided but creator is available
|
# Derive creator vault if not provided but creator is available
|
||||||
@@ -187,21 +198,25 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
def derive_associated_bonding_curve(
|
def derive_associated_bonding_curve(
|
||||||
self, mint: Pubkey, bonding_curve: Pubkey
|
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
|
||||||
) -> Pubkey:
|
) -> Pubkey:
|
||||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mint: Token mint address
|
mint: Token mint address
|
||||||
bonding_curve: Bonding curve address
|
bonding_curve: Bonding curve address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Associated bonding curve address
|
Associated bonding curve address
|
||||||
"""
|
"""
|
||||||
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[
|
[
|
||||||
bytes(bonding_curve),
|
bytes(bonding_curve),
|
||||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
bytes(token_program_id),
|
||||||
bytes(mint),
|
bytes(mint),
|
||||||
],
|
],
|
||||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||||
@@ -249,6 +264,19 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
"""
|
"""
|
||||||
return PumpFunAddresses.find_fee_config()
|
return PumpFunAddresses.find_fee_config()
|
||||||
|
|
||||||
|
def get_fee_recipient(self, token_info: TokenInfo) -> Pubkey:
|
||||||
|
"""Get the correct fee recipient based on mayhem mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_info: Token information with is_mayhem_mode flag
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fee recipient address (mayhem or standard)
|
||||||
|
"""
|
||||||
|
if token_info.is_mayhem_mode:
|
||||||
|
return PumpFunAddresses.MAYHEM_FEE
|
||||||
|
return PumpFunAddresses.FEE
|
||||||
|
|
||||||
def get_buy_instruction_accounts(
|
def get_buy_instruction_accounts(
|
||||||
self, token_info: TokenInfo, user: Pubkey
|
self, token_info: TokenInfo, user: Pubkey
|
||||||
) -> dict[str, Pubkey]:
|
) -> dict[str, Pubkey]:
|
||||||
@@ -263,9 +291,19 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
"""
|
"""
|
||||||
additional_accounts = self.get_additional_accounts(token_info)
|
additional_accounts = self.get_additional_accounts(token_info)
|
||||||
|
|
||||||
|
# Determine token program to use
|
||||||
|
token_program_id = (
|
||||||
|
token_info.token_program_id
|
||||||
|
if token_info.token_program_id
|
||||||
|
else SystemAddresses.TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine fee recipient based on mayhem mode
|
||||||
|
fee_recipient = self.get_fee_recipient(token_info)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"global": PumpFunAddresses.GLOBAL,
|
"global": PumpFunAddresses.GLOBAL,
|
||||||
"fee": PumpFunAddresses.FEE,
|
"fee": fee_recipient,
|
||||||
"mint": token_info.mint,
|
"mint": token_info.mint,
|
||||||
"bonding_curve": additional_accounts.get(
|
"bonding_curve": additional_accounts.get(
|
||||||
"bonding_curve", token_info.bonding_curve
|
"bonding_curve", token_info.bonding_curve
|
||||||
@@ -273,10 +311,12 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
"associated_bonding_curve": additional_accounts.get(
|
"associated_bonding_curve": additional_accounts.get(
|
||||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||||
),
|
),
|
||||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
"user_token_account": self.derive_user_token_account(
|
||||||
|
user, token_info.mint, token_program_id
|
||||||
|
),
|
||||||
"user": user,
|
"user": user,
|
||||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
"token_program": token_program_id,
|
||||||
"creator_vault": additional_accounts.get(
|
"creator_vault": additional_accounts.get(
|
||||||
"creator_vault", token_info.creator_vault
|
"creator_vault", token_info.creator_vault
|
||||||
),
|
),
|
||||||
@@ -302,9 +342,19 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
"""
|
"""
|
||||||
additional_accounts = self.get_additional_accounts(token_info)
|
additional_accounts = self.get_additional_accounts(token_info)
|
||||||
|
|
||||||
|
# Determine token program to use
|
||||||
|
token_program_id = (
|
||||||
|
token_info.token_program_id
|
||||||
|
if token_info.token_program_id
|
||||||
|
else SystemAddresses.TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine fee recipient based on mayhem mode
|
||||||
|
fee_recipient = self.get_fee_recipient(token_info)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"global": PumpFunAddresses.GLOBAL,
|
"global": PumpFunAddresses.GLOBAL,
|
||||||
"fee": PumpFunAddresses.FEE,
|
"fee": fee_recipient,
|
||||||
"mint": token_info.mint,
|
"mint": token_info.mint,
|
||||||
"bonding_curve": additional_accounts.get(
|
"bonding_curve": additional_accounts.get(
|
||||||
"bonding_curve", token_info.bonding_curve
|
"bonding_curve", token_info.bonding_curve
|
||||||
@@ -312,13 +362,15 @@ class PumpFunAddressProvider(AddressProvider):
|
|||||||
"associated_bonding_curve": additional_accounts.get(
|
"associated_bonding_curve": additional_accounts.get(
|
||||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||||
),
|
),
|
||||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
"user_token_account": self.derive_user_token_account(
|
||||||
|
user, token_info.mint, token_program_id
|
||||||
|
),
|
||||||
"user": user,
|
"user": user,
|
||||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||||
"creator_vault": additional_accounts.get(
|
"creator_vault": additional_accounts.get(
|
||||||
"creator_vault", token_info.creator_vault
|
"creator_vault", token_info.creator_vault
|
||||||
),
|
),
|
||||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
"token_program": token_program_id,
|
||||||
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
|
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
|
||||||
"program": PumpFunAddresses.PROGRAM,
|
"program": PumpFunAddresses.PROGRAM,
|
||||||
"fee_config": self.derive_fee_config(),
|
"fee_config": self.derive_fee_config(),
|
||||||
|
|||||||
@@ -190,20 +190,28 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
"token_total_supply": decoded_curve_state.get("token_total_supply", 0),
|
"token_total_supply": decoded_curve_state.get("token_total_supply", 0),
|
||||||
"complete": decoded_curve_state.get("complete", False),
|
"complete": decoded_curve_state.get("complete", False),
|
||||||
"creator": decoded_curve_state.get("creator", ""),
|
"creator": decoded_curve_state.get("creator", ""),
|
||||||
|
"is_mayhem_mode": decoded_curve_state.get("is_mayhem_mode", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate additional metrics
|
# Calculate additional metrics
|
||||||
if curve_data["virtual_token_reserves"] > 0:
|
# Validate reserves are positive before calculating price
|
||||||
curve_data["price_per_token"] = (
|
if curve_data["virtual_token_reserves"] <= 0:
|
||||||
(
|
raise ValueError(
|
||||||
curve_data["virtual_sol_reserves"]
|
f"Invalid virtual_token_reserves: {curve_data['virtual_token_reserves']} - cannot calculate price"
|
||||||
/ curve_data["virtual_token_reserves"]
|
|
||||||
)
|
|
||||||
* (10**TOKEN_DECIMALS)
|
|
||||||
/ LAMPORTS_PER_SOL
|
|
||||||
)
|
)
|
||||||
else:
|
if curve_data["virtual_sol_reserves"] <= 0:
|
||||||
curve_data["price_per_token"] = 0
|
raise ValueError(
|
||||||
|
f"Invalid virtual_sol_reserves: {curve_data['virtual_sol_reserves']} - cannot calculate price"
|
||||||
|
)
|
||||||
|
|
||||||
|
curve_data["price_per_token"] = (
|
||||||
|
(
|
||||||
|
curve_data["virtual_sol_reserves"]
|
||||||
|
/ curve_data["virtual_token_reserves"]
|
||||||
|
)
|
||||||
|
* (10**TOKEN_DECIMALS)
|
||||||
|
/ LAMPORTS_PER_SOL
|
||||||
|
)
|
||||||
|
|
||||||
# Add convenience decimal fields
|
# Add convenience decimal fields
|
||||||
curve_data["token_reserves_decimal"] = (
|
curve_data["token_reserves_decimal"] = (
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ class PumpFunEventParser(EventParser):
|
|||||||
"<Q", self._create_instruction_discriminator_bytes
|
"<Q", self._create_instruction_discriminator_bytes
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
# Support for token2022 (create_v2 instruction)
|
||||||
|
self._create_v2_instruction_discriminator_bytes = instruction_discriminators.get(
|
||||||
|
"create_v2"
|
||||||
|
)
|
||||||
|
self._create_v2_instruction_discriminator = (
|
||||||
|
struct.unpack("<Q", self._create_v2_instruction_discriminator_bytes)[0]
|
||||||
|
if self._create_v2_instruction_discriminator_bytes
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Pump.Fun event parser initialized with IDL-based event and instruction parsing"
|
"Pump.Fun event parser initialized with IDL-based event and instruction parsing"
|
||||||
)
|
)
|
||||||
@@ -56,6 +66,10 @@ class PumpFunEventParser(EventParser):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}"
|
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}"
|
||||||
)
|
)
|
||||||
|
if self._create_v2_instruction_discriminator_bytes:
|
||||||
|
logger.info(
|
||||||
|
f"create_v2 instruction discriminator: {self._create_v2_instruction_discriminator_bytes.hex()}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def platform(self) -> Platform:
|
def platform(self) -> Platform:
|
||||||
@@ -74,8 +88,12 @@ class PumpFunEventParser(EventParser):
|
|||||||
Returns:
|
Returns:
|
||||||
TokenInfo if token creation found, None otherwise
|
TokenInfo if token creation found, None otherwise
|
||||||
"""
|
"""
|
||||||
# Check if this is a token creation transaction
|
# Check if this is a token creation transaction (create or create_v2 for token2022)
|
||||||
if not any("Program log: Instruction: Create" in log for log in logs):
|
if not any(
|
||||||
|
"Program log: Instruction: Create" in log
|
||||||
|
or "Program log: Instruction: Create_v2" in log
|
||||||
|
for log in logs
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Skip swaps as the first condition may pass them
|
# Skip swaps as the first condition may pass them
|
||||||
@@ -92,9 +110,10 @@ class PumpFunEventParser(EventParser):
|
|||||||
|
|
||||||
# First, collect all Program data entries and note when Create instruction happens
|
# First, collect all Program data entries and note when Create instruction happens
|
||||||
for i, log in enumerate(logs):
|
for i, log in enumerate(logs):
|
||||||
if "Program log: Instruction: Create" in log:
|
if "Program log: Instruction: Create" in log or "Program log: Instruction: Create_v2" in log:
|
||||||
create_instruction_found = True
|
create_instruction_found = True
|
||||||
logger.info(f"📝 Found Create instruction at log index {i}")
|
instruction_type = "Create_v2" if "Create_v2" in log else "Create"
|
||||||
|
logger.info(f"📝 Found {instruction_type} instruction at log index {i}")
|
||||||
elif "Program data:" in log:
|
elif "Program data:" in log:
|
||||||
# Extract base64 encoded event data
|
# Extract base64 encoded event data
|
||||||
encoded_data = log.split("Program data: ")[1].strip()
|
encoded_data = log.split("Program data: ")[1].strip()
|
||||||
@@ -104,7 +123,7 @@ class PumpFunEventParser(EventParser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not create_instruction_found:
|
if not create_instruction_found:
|
||||||
logger.info("❌ No Create instruction found in logs")
|
logger.info("❌ No Create or Create_v2 instruction found in logs")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not program_data_entries:
|
if not program_data_entries:
|
||||||
@@ -224,9 +243,13 @@ class PumpFunEventParser(EventParser):
|
|||||||
logger.info(f"❌ Failed to convert pubkey fields: {e}")
|
logger.info(f"❌ Failed to convert pubkey fields: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Derive additional addresses
|
# Derive additional addresses (default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2)
|
||||||
|
# Note: As of recent pump.fun updates, all tokens are created via create_v2 instruction
|
||||||
|
# This is a technical limitation of logs listener - cannot distinguish create vs create_v2
|
||||||
|
# Risk is low since pump.fun now defaults to Token2022 for all new tokens
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
associated_bonding_curve = self._derive_associated_bonding_curve(
|
associated_bonding_curve = self._derive_associated_bonding_curve(
|
||||||
mint, bonding_curve
|
mint, bonding_curve, token_program_id
|
||||||
)
|
)
|
||||||
creator_vault = self._derive_creator_vault(creator)
|
creator_vault = self._derive_creator_vault(creator)
|
||||||
|
|
||||||
@@ -245,6 +268,7 @@ class PumpFunEventParser(EventParser):
|
|||||||
user=user,
|
user=user,
|
||||||
creator=creator,
|
creator=creator,
|
||||||
creator_vault=creator_vault,
|
creator_vault=creator_vault,
|
||||||
|
token_program_id=token_program_id,
|
||||||
creation_timestamp=monotonic(),
|
creation_timestamp=monotonic(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -274,9 +298,16 @@ class PumpFunEventParser(EventParser):
|
|||||||
Returns:
|
Returns:
|
||||||
TokenInfo if token creation found, None otherwise
|
TokenInfo if token creation found, None otherwise
|
||||||
"""
|
"""
|
||||||
if not instruction_data.startswith(
|
# Determine which create instruction (standard or v2 for token2022)
|
||||||
self._create_instruction_discriminator_bytes
|
is_create_v2 = False
|
||||||
|
if instruction_data.startswith(self._create_instruction_discriminator_bytes):
|
||||||
|
is_create_v2 = False
|
||||||
|
elif (
|
||||||
|
self._create_v2_instruction_discriminator_bytes
|
||||||
|
and instruction_data.startswith(self._create_v2_instruction_discriminator_bytes)
|
||||||
):
|
):
|
||||||
|
is_create_v2 = True
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -293,7 +324,8 @@ class PumpFunEventParser(EventParser):
|
|||||||
decoded = self._idl_parser.decode_instruction(
|
decoded = self._idl_parser.decode_instruction(
|
||||||
instruction_data, account_keys, accounts
|
instruction_data, account_keys, accounts
|
||||||
)
|
)
|
||||||
if not decoded or decoded["instruction_name"] != "create":
|
expected_instruction_name = "create_v2" if is_create_v2 else "create"
|
||||||
|
if not decoded or decoded["instruction_name"] != expected_instruction_name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
args = decoded.get("args", {})
|
args = decoded.get("args", {})
|
||||||
@@ -315,6 +347,13 @@ class PumpFunEventParser(EventParser):
|
|||||||
)
|
)
|
||||||
creator_vault = self._derive_creator_vault(creator)
|
creator_vault = self._derive_creator_vault(creator)
|
||||||
|
|
||||||
|
# Determine token program based on instruction type
|
||||||
|
token_program_id = (
|
||||||
|
SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
if is_create_v2
|
||||||
|
else SystemAddresses.TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
|
||||||
return TokenInfo(
|
return TokenInfo(
|
||||||
name=args.get("name", ""),
|
name=args.get("name", ""),
|
||||||
symbol=args.get("symbol", ""),
|
symbol=args.get("symbol", ""),
|
||||||
@@ -326,6 +365,7 @@ class PumpFunEventParser(EventParser):
|
|||||||
user=user,
|
user=user,
|
||||||
creator=creator,
|
creator=creator,
|
||||||
creator_vault=creator_vault,
|
creator_vault=creator_vault,
|
||||||
|
token_program_id=token_program_id,
|
||||||
creation_timestamp=monotonic(),
|
creation_timestamp=monotonic(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -435,14 +475,19 @@ class PumpFunEventParser(EventParser):
|
|||||||
|
|
||||||
ix_data = bytes(ix.data)
|
ix_data = bytes(ix.data)
|
||||||
|
|
||||||
# Check for create discriminator
|
# Check for create or create_v2 discriminator
|
||||||
if len(ix_data) >= 8:
|
if len(ix_data) >= 8:
|
||||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||||
|
|
||||||
if (
|
is_create = (
|
||||||
discriminator
|
discriminator == self._create_instruction_discriminator
|
||||||
== self._create_instruction_discriminator
|
)
|
||||||
):
|
is_create_v2 = (
|
||||||
|
self._create_v2_instruction_discriminator
|
||||||
|
and discriminator == self._create_v2_instruction_discriminator
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_create or is_create_v2:
|
||||||
# Token creation should have substantial data and many accounts
|
# Token creation should have substantial data and many accounts
|
||||||
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
||||||
continue
|
continue
|
||||||
@@ -497,10 +542,15 @@ class PumpFunEventParser(EventParser):
|
|||||||
if len(ix_data) >= 8:
|
if len(ix_data) >= 8:
|
||||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||||
|
|
||||||
if (
|
is_create = (
|
||||||
discriminator
|
discriminator == self._create_instruction_discriminator
|
||||||
== self._create_instruction_discriminator
|
)
|
||||||
):
|
is_create_v2 = (
|
||||||
|
self._create_v2_instruction_discriminator
|
||||||
|
and discriminator == self._create_v2_instruction_discriminator
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_create or is_create_v2:
|
||||||
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
|
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -544,27 +594,70 @@ class PumpFunEventParser(EventParser):
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
def _derive_associated_bonding_curve(
|
def _derive_associated_bonding_curve(
|
||||||
self, mint: Pubkey, bonding_curve: Pubkey
|
self, mint: Pubkey, bonding_curve: Pubkey, token_program_id: Pubkey | None = None
|
||||||
) -> Pubkey:
|
) -> Pubkey:
|
||||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mint: Token mint address
|
mint: Token mint address
|
||||||
bonding_curve: Bonding curve address
|
bonding_curve: Bonding curve address
|
||||||
|
token_program_id: Token program (TOKEN or TOKEN_2022). Defaults to TOKEN_PROGRAM
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Associated bonding curve address
|
Associated bonding curve address
|
||||||
"""
|
"""
|
||||||
|
if token_program_id is None:
|
||||||
|
token_program_id = SystemAddresses.TOKEN_PROGRAM
|
||||||
|
|
||||||
derived_address, _ = Pubkey.find_program_address(
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
[
|
[
|
||||||
bytes(bonding_curve),
|
bytes(bonding_curve),
|
||||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
bytes(token_program_id),
|
||||||
bytes(mint),
|
bytes(mint),
|
||||||
],
|
],
|
||||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||||
)
|
)
|
||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
def _parse_bonding_curve_state(self, data: bytes) -> dict[str, Any] | None:
|
||||||
|
"""Parse bonding curve state from raw account data using IDL parser.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw bonding curve account data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with parsed bonding curve state or None if parsing fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
decoded = self._idl_parser.decode_account_data(
|
||||||
|
data, "BondingCurve", skip_discriminator=True
|
||||||
|
)
|
||||||
|
if not decoded:
|
||||||
|
return None
|
||||||
|
return decoded
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to parse bonding curve state: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_is_mayhem_mode_from_curve(self, bonding_curve_address: Pubkey) -> bool:
|
||||||
|
"""Determine if a token is in mayhem mode based on bonding curve state.
|
||||||
|
|
||||||
|
Note: This would require an RPC call to fetch the bonding curve account.
|
||||||
|
For now, we return False as a default since the event parser doesn't have
|
||||||
|
access to an RPC client. The mayhem mode flag will be set by traders
|
||||||
|
when they fetch bonding curve state for other operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bonding_curve_address: Address of the bonding curve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if mayhem mode, False otherwise (or if parsing fails)
|
||||||
|
"""
|
||||||
|
# Since event parser doesn't have RPC client access, we cannot fetch
|
||||||
|
# and parse bonding curve state here. Mayhem mode will be set later
|
||||||
|
# when traders fetch the bonding curve state.
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verbose(self) -> bool:
|
def verbose(self) -> bool:
|
||||||
"""Check if verbose logging is enabled."""
|
"""Check if verbose logging is enabled."""
|
||||||
|
|||||||
@@ -64,15 +64,16 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
|||||||
"""
|
"""
|
||||||
instructions = []
|
instructions = []
|
||||||
|
|
||||||
# Get all required accounts
|
# Get all required accounts (includes mayhem-mode-aware fee recipient)
|
||||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||||
|
|
||||||
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
|
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
|
||||||
|
# Use token_program from accounts_info to ensure AddressProvider controls program selection
|
||||||
ata_instruction = create_idempotent_associated_token_account(
|
ata_instruction = create_idempotent_associated_token_account(
|
||||||
user, # payer
|
user, # payer
|
||||||
user, # owner
|
user, # owner
|
||||||
token_info.mint, # mint
|
token_info.mint, # mint
|
||||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
accounts_info["token_program"], # token program from AddressProvider
|
||||||
)
|
)
|
||||||
instructions.append(ata_instruction)
|
instructions.append(ata_instruction)
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
|||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=accounts_info["global_volume_accumulator"],
|
pubkey=accounts_info["global_volume_accumulator"],
|
||||||
is_signer=False,
|
is_signer=False,
|
||||||
is_writable=True,
|
is_writable=False,
|
||||||
),
|
),
|
||||||
AccountMeta(
|
AccountMeta(
|
||||||
pubkey=accounts_info["user_volume_accumulator"],
|
pubkey=accounts_info["user_volume_accumulator"],
|
||||||
@@ -144,11 +145,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Build instruction data: discriminator + token_amount + max_sol_cost
|
# Build instruction data: discriminator + token_amount + max_sol_cost + track_volume
|
||||||
|
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||||
|
track_volume_bytes = bytes([1, 1])
|
||||||
instruction_data = (
|
instruction_data = (
|
||||||
self._buy_discriminator
|
self._buy_discriminator
|
||||||
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units
|
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units
|
||||||
+ struct.pack("<Q", amount_in) # max SOL cost in lamports
|
+ struct.pack("<Q", amount_in) # max SOL cost in lamports
|
||||||
|
+ track_volume_bytes # enable volume tracking
|
||||||
)
|
)
|
||||||
|
|
||||||
buy_instruction = Instruction(
|
buy_instruction = Instruction(
|
||||||
@@ -182,7 +186,7 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
|||||||
"""
|
"""
|
||||||
instructions = []
|
instructions = []
|
||||||
|
|
||||||
# Get all required accounts
|
# Get all required accounts (includes mayhem-mode-aware fee recipient)
|
||||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||||
|
|
||||||
# Build sell instruction accounts
|
# Build sell instruction accounts
|
||||||
@@ -243,11 +247,14 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Build instruction data: discriminator + token_amount + min_sol_output
|
# Build instruction data: discriminator + token_amount + min_sol_output + track_volume
|
||||||
|
# Encode OptionBool for track_volume: [1, 1] = Some(true)
|
||||||
|
track_volume_bytes = bytes([1, 1])
|
||||||
instruction_data = (
|
instruction_data = (
|
||||||
self._sell_discriminator
|
self._sell_discriminator
|
||||||
+ struct.pack("<Q", amount_in) # token amount in raw units
|
+ struct.pack("<Q", amount_in) # token amount in raw units
|
||||||
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
||||||
|
+ track_volume_bytes # enable volume tracking
|
||||||
)
|
)
|
||||||
|
|
||||||
sell_instruction = Instruction(
|
sell_instruction = Instruction(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ File: src/platforms/pumpfun/pumpportal_processor.py
|
|||||||
|
|
||||||
from solders.pubkey import Pubkey
|
from solders.pubkey import Pubkey
|
||||||
|
|
||||||
|
from core.pubkeys import SystemAddresses
|
||||||
from interfaces.core import Platform, TokenInfo
|
from interfaces.core import Platform, TokenInfo
|
||||||
from platforms.pumpfun.address_provider import PumpFunAddressProvider
|
from platforms.pumpfun.address_provider import PumpFunAddressProvider
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
@@ -81,9 +82,15 @@ class PumpFunPumpPortalProcessor:
|
|||||||
creator = user
|
creator = user
|
||||||
|
|
||||||
# Derive additional addresses using platform provider
|
# Derive additional addresses using platform provider
|
||||||
|
# PumpPortal doesn't distinguish between Token and Token2022.
|
||||||
|
# Default to TOKEN_2022_PROGRAM as per pump.fun's migration to create_v2.
|
||||||
|
# Technical limitation: Cannot distinguish from pre-parsed data, but risk is low
|
||||||
|
# since pump.fun now defaults to Token2022 for all new tokens.
|
||||||
|
token_program_id = SystemAddresses.TOKEN_2022_PROGRAM
|
||||||
|
|
||||||
associated_bonding_curve = (
|
associated_bonding_curve = (
|
||||||
self.address_provider.derive_associated_bonding_curve(
|
self.address_provider.derive_associated_bonding_curve(
|
||||||
mint, bonding_curve
|
mint, bonding_curve, token_program_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
creator_vault = self.address_provider.derive_creator_vault(creator)
|
creator_vault = self.address_provider.derive_creator_vault(creator)
|
||||||
@@ -99,6 +106,7 @@ class PumpFunPumpPortalProcessor:
|
|||||||
user=user,
|
user=user,
|
||||||
creator=creator,
|
creator=creator,
|
||||||
creator_vault=creator_vault,
|
creator_vault=creator_vault,
|
||||||
|
token_program_id=token_program_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -66,10 +66,20 @@ class PlatformAwareBuyer(Trader):
|
|||||||
pool_address = self._get_pool_address(token_info, address_provider)
|
pool_address = self._get_pool_address(token_info, address_provider)
|
||||||
|
|
||||||
# Regular behavior with RPC call
|
# Regular behavior with RPC call
|
||||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
# Fetch pool state to get price and mayhem mode status
|
||||||
token_amount = (
|
pool_state = await curve_manager.get_pool_state(pool_address)
|
||||||
self.amount / token_price_sol if token_price_sol > 0 else 0
|
token_price_sol = pool_state.get("price_per_token")
|
||||||
)
|
|
||||||
|
# Validate price_per_token is present and positive
|
||||||
|
if token_price_sol is None or token_price_sol <= 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
|
||||||
|
f"(mint: {token_info.mint}) - cannot execute buy with zero/invalid price"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set is_mayhem_mode from bonding curve state
|
||||||
|
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
|
||||||
|
token_amount = self.amount / token_price_sol
|
||||||
|
|
||||||
# Calculate minimum token amount with slippage
|
# Calculate minimum token amount with slippage
|
||||||
minimum_token_amount = token_amount * (1 - self.slippage)
|
minimum_token_amount = token_amount * (1 - self.slippage)
|
||||||
@@ -225,7 +235,19 @@ class PlatformAwareSeller(Trader):
|
|||||||
|
|
||||||
# Get pool address and current price using platform-agnostic method
|
# Get pool address and current price using platform-agnostic method
|
||||||
pool_address = self._get_pool_address(token_info, address_provider)
|
pool_address = self._get_pool_address(token_info, address_provider)
|
||||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
# Fetch pool state to get price and mayhem mode status
|
||||||
|
pool_state = await curve_manager.get_pool_state(pool_address)
|
||||||
|
token_price_sol = pool_state.get("price_per_token")
|
||||||
|
|
||||||
|
# Validate price_per_token is present and positive
|
||||||
|
if token_price_sol is None or token_price_sol <= 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
|
||||||
|
f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set is_mayhem_mode from bonding curve state
|
||||||
|
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
|
||||||
|
|
||||||
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
|
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ class UniversalTrader:
|
|||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
self.traded_mints: set[Pubkey] = set()
|
self.traded_mints: set[Pubkey] = set()
|
||||||
|
self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id
|
||||||
self.token_queue: asyncio.Queue = asyncio.Queue()
|
self.token_queue: asyncio.Queue = asyncio.Queue()
|
||||||
self.processing: bool = False
|
self.processing: bool = False
|
||||||
self.processed_tokens: set[str] = set()
|
self.processed_tokens: set[str] = set()
|
||||||
@@ -325,10 +326,17 @@ class UniversalTrader:
|
|||||||
if self.traded_mints:
|
if self.traded_mints:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
|
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
|
||||||
|
# Build parallel lists of mints and token_program_ids
|
||||||
|
mints_list = list(self.traded_mints)
|
||||||
|
token_program_ids = [
|
||||||
|
self.traded_token_programs.get(str(mint))
|
||||||
|
for mint in mints_list
|
||||||
|
]
|
||||||
await handle_cleanup_post_session(
|
await handle_cleanup_post_session(
|
||||||
self.solana_client,
|
self.solana_client,
|
||||||
self.wallet,
|
self.wallet,
|
||||||
list(self.traded_mints),
|
mints_list,
|
||||||
|
token_program_ids,
|
||||||
self.priority_fee_manager,
|
self.priority_fee_manager,
|
||||||
self.cleanup_mode,
|
self.cleanup_mode,
|
||||||
self.cleanup_with_priority_fee,
|
self.cleanup_with_priority_fee,
|
||||||
@@ -447,6 +455,10 @@ class UniversalTrader:
|
|||||||
buy_result.tx_signature,
|
buy_result.tx_signature,
|
||||||
)
|
)
|
||||||
self.traded_mints.add(token_info.mint)
|
self.traded_mints.add(token_info.mint)
|
||||||
|
# Track token program for cleanup
|
||||||
|
mint_str = str(token_info.mint)
|
||||||
|
if token_info.token_program_id:
|
||||||
|
self.traded_token_programs[mint_str] = token_info.token_program_id
|
||||||
|
|
||||||
# Choose exit strategy
|
# Choose exit strategy
|
||||||
if not self.marry_mode:
|
if not self.marry_mode:
|
||||||
@@ -469,6 +481,7 @@ class UniversalTrader:
|
|||||||
self.solana_client,
|
self.solana_client,
|
||||||
self.wallet,
|
self.wallet,
|
||||||
token_info.mint,
|
token_info.mint,
|
||||||
|
token_info.token_program_id,
|
||||||
self.priority_fee_manager,
|
self.priority_fee_manager,
|
||||||
self.cleanup_mode,
|
self.cleanup_mode,
|
||||||
self.cleanup_with_priority_fee,
|
self.cleanup_with_priority_fee,
|
||||||
@@ -521,6 +534,7 @@ class UniversalTrader:
|
|||||||
self.solana_client,
|
self.solana_client,
|
||||||
self.wallet,
|
self.wallet,
|
||||||
token_info.mint,
|
token_info.mint,
|
||||||
|
token_info.token_program_id,
|
||||||
self.priority_fee_manager,
|
self.priority_fee_manager,
|
||||||
self.cleanup_mode,
|
self.cleanup_mode,
|
||||||
self.cleanup_with_priority_fee,
|
self.cleanup_with_priority_fee,
|
||||||
@@ -590,6 +604,7 @@ class UniversalTrader:
|
|||||||
self.solana_client,
|
self.solana_client,
|
||||||
self.wallet,
|
self.wallet,
|
||||||
token_info.mint,
|
token_info.mint,
|
||||||
|
token_info.token_program_id,
|
||||||
self.priority_fee_manager,
|
self.priority_fee_manager,
|
||||||
self.cleanup_mode,
|
self.cleanup_mode,
|
||||||
self.cleanup_with_priority_fee,
|
self.cleanup_with_priority_fee,
|
||||||
|
|||||||
Reference in New Issue
Block a user