mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-08 21:17:46 +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:
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
Solana PUMP AMM Interface
|
||||
This standalone script demonstrates how to buy tokens on the PUMP AMM (pAMM) protocol.
|
||||
It covers the complete flow from finding markets to executing buys with mayhem mode support.
|
||||
|
||||
This module provides functionality to interact with the PUMP AMM program on Solana, enabling:
|
||||
- Finding market addresses by token mint
|
||||
- Fetching and parsing market data from PUMP AMM pools
|
||||
- Calculating token prices in AMM pools
|
||||
- Creating associated token accounts (ATAs) idempotently
|
||||
- Buying tokens on the PUMP AMM with slippage protection
|
||||
Key concepts demonstrated:
|
||||
- Finding AMM pool addresses by token mint
|
||||
- Parsing binary account data structures
|
||||
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||
- Program Derived Address (PDA) derivation
|
||||
- WSOL wrapping (converting SOL to wrapped SOL for SPL token operations)
|
||||
- Volume tracking incentives integration
|
||||
- Transaction simulation before sending
|
||||
- Slippage protection mechanisms
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -34,31 +38,33 @@ from spl.token.instructions import (
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration constants
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
TOKEN_MINT = Pubkey.from_string("...")
|
||||
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||
SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept
|
||||
SLIPPAGE = 0.3 # 30% - maximum acceptable price movement during trade
|
||||
|
||||
TOKEN_DECIMALS = 6
|
||||
BUY_DISCRIMINATOR = bytes.fromhex(
|
||||
"66063d1201daebea"
|
||||
) # Program instruction identifier for the buy function
|
||||
# Token configuration
|
||||
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||
|
||||
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
|
||||
|
||||
# ============================================================================
|
||||
# Solana Program IDs and System Accounts
|
||||
# ============================================================================
|
||||
|
||||
# Solana system addresses and program IDs
|
||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
||||
)
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
@@ -67,61 +73,91 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||
)
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
|
||||
# ============================================================================
|
||||
# Constants for Account Structure Parsing
|
||||
# ============================================================================
|
||||
|
||||
# Pool account structure offsets
|
||||
POOL_DISCRIMINATOR_SIZE = 8
|
||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||
|
||||
# GlobalConfig structure offsets
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||
+ GLOBALCONFIG_ADMIN_SIZE
|
||||
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||
)
|
||||
|
||||
# Fee recipients
|
||||
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
|
||||
# Solana constants
|
||||
LAMPORTS_PER_SOL = 1_000_000_000
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 200_000 # Maximum compute units to use
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 200_000 # Max compute units for transaction
|
||||
|
||||
# Buy-specific constants
|
||||
PROTOCOL_FEE_BUFFER = 0.1 # 10% buffer for protocol fees when wrapping SOL
|
||||
VOLUME_TRACKING_ENABLED = 1 # 1 = true, 0 = false
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Market Discovery
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_market_address_by_base_mint(
|
||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Find the market address for a given token mint.
|
||||
"""Find the AMM pool address for a specific token.
|
||||
|
||||
Searches for the AMM pool that contains the specified token mint as its base token
|
||||
by querying program accounts with a filter for the base_mint field.
|
||||
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||
that matches the given token mint address.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
base_mint_address: Address of the token mint you want to find the market for
|
||||
amm_program_id: Address of the AMM program
|
||||
client: Solana RPC client
|
||||
base_mint_address: Token mint to find the pool for
|
||||
amm_program_id: PUMP AMM program address
|
||||
|
||||
Returns:
|
||||
The Pubkey of the market (AMM pool) for the token
|
||||
Address of the AMM pool (market) for the token
|
||||
"""
|
||||
base_mint_bytes = bytes(base_mint_address)
|
||||
offset = (
|
||||
43 # Offset where the base_mint field is stored in the account data structure
|
||||
)
|
||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
||||
|
||||
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||
response = await client.get_program_accounts(
|
||||
amm_program_id, encoding="base64", filters=filters
|
||||
)
|
||||
|
||||
market_address = [account.pubkey for account in response.value][0]
|
||||
return market_address
|
||||
return response.value[0].pubkey
|
||||
|
||||
|
||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
"""Fetch and parse market data from the blockchain.
|
||||
"""Parse binary pool account data into a structured dictionary.
|
||||
|
||||
Retrieves and deserializes the binary data stored in the market account
|
||||
into a structured dictionary containing key market information.
|
||||
The pool account stores data in a specific binary format. This function
|
||||
deserializes that data based on the known structure.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
market_address: Address of the market (AMM pool) to fetch data for
|
||||
client: Solana RPC client
|
||||
market_address: Address of the pool account
|
||||
|
||||
Returns:
|
||||
Dictionary containing the parsed market data
|
||||
Dictionary with parsed pool data fields
|
||||
"""
|
||||
response = await client.get_account_info(market_address, encoding="base64")
|
||||
data = response.value.data
|
||||
parsed_data: dict = {}
|
||||
|
||||
# Start after the 8-byte discriminator
|
||||
offset = 8
|
||||
# Define the structure of the market account data
|
||||
offset = POOL_DISCRIMINATOR_SIZE
|
||||
|
||||
# Field definitions: (name, type)
|
||||
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||
fields = [
|
||||
("pool_bump", "u8"),
|
||||
("index", "u16"),
|
||||
@@ -141,39 +177,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||
offset += 32
|
||||
elif field_type in {"u64", "i64"}:
|
||||
value = (
|
||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
if field_type == "u64"
|
||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
||||
)
|
||||
parsed_data[field_name] = value
|
||||
format_char = "<Q" if field_type == "u64" else "<q"
|
||||
parsed_data[field_name] = struct.unpack(
|
||||
format_char, data[offset : offset + 8]
|
||||
)[0]
|
||||
offset += 8
|
||||
elif field_type == "u16":
|
||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
offset += 2
|
||||
elif field_type == "u8":
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = data[offset]
|
||||
offset += 1
|
||||
|
||||
return parsed_data
|
||||
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
||||
# ============================================================================
|
||||
# Program Derived Address (PDA) Derivation
|
||||
# ============================================================================
|
||||
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||
# They allow programs to own accounts without needing a private key.
|
||||
|
||||
Calculates the deterministic PDA that serves as the vault authority
|
||||
for a specific coin creator in the PUMP AMM protocol.
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the PDA for the coin creator's fee vault.
|
||||
|
||||
The creator vault collects fees on behalf of the token creator.
|
||||
This is a deterministic address that can be recalculated by anyone.
|
||||
|
||||
Args:
|
||||
coin_creator: Pubkey of the coin creator account
|
||||
coin_creator: Public key of the token creator
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived coin creator vault authority
|
||||
|
||||
Note:
|
||||
This vault is used to collect creator fees from token transactions
|
||||
PDA of the creator's vault authority
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator_vault", bytes(coin_creator)],
|
||||
@@ -183,13 +219,13 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_global_volume_accumulator() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the global volume accumulator.
|
||||
"""Derive the PDA for the global volume accumulator.
|
||||
|
||||
Calculates the deterministic PDA that tracks global trading volume
|
||||
across all pools in the PUMP AMM protocol.
|
||||
This account tracks total trading volume across all pools.
|
||||
Volume tracking is used for incentive programs and analytics.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived global volume accumulator account
|
||||
PDA of the global volume accumulator
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"global_volume_accumulator"],
|
||||
@@ -199,16 +235,16 @@ def find_global_volume_accumulator() -> Pubkey:
|
||||
|
||||
|
||||
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a user's volume accumulator.
|
||||
"""Derive the PDA for a user's volume accumulator.
|
||||
|
||||
Calculates the deterministic PDA that tracks trading volume
|
||||
for a specific user in the PUMP AMM protocol.
|
||||
Tracks individual user's trading volume, which may qualify them
|
||||
for incentives or rewards based on trading activity.
|
||||
|
||||
Args:
|
||||
user: Pubkey of the user account
|
||||
user: Public key of the user
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived user volume accumulator account
|
||||
PDA of the user's volume accumulator
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"user_volume_accumulator", bytes(user)],
|
||||
@@ -218,10 +254,9 @@ def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_fee_config() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
||||
"""Derive the PDA for the fee configuration account.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived fee config account
|
||||
This account stores fee-related configuration for the AMM.
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||
@@ -230,41 +265,139 @@ def find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mayhem Mode Fee Handling
|
||||
# ============================================================================
|
||||
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||
|
||||
|
||||
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||
|
||||
When mayhem mode is active, fees are redirected to a special recipient
|
||||
stored in the GlobalConfig account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
Public key of the mayhem mode fee recipient
|
||||
"""
|
||||
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch GlobalConfig account"
|
||||
raise ValueError(msg)
|
||||
|
||||
data = response.value.data
|
||||
recipient_bytes = data[
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(recipient_bytes)
|
||||
|
||||
|
||||
async def get_pumpswap_fee_recipients(
|
||||
client: AsyncClient, pool: Pubkey
|
||||
) -> tuple[Pubkey, Pubkey]:
|
||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||
|
||||
This function checks if mayhem mode is enabled for the pool and returns
|
||||
the appropriate fee recipient and their WSOL token account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool: Address of the AMM pool
|
||||
|
||||
Returns:
|
||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||
"""
|
||||
response = await client.get_account_info(pool, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch pool account"
|
||||
raise ValueError(msg)
|
||||
|
||||
pool_data = response.value.data
|
||||
|
||||
# Check if mayhem mode flag exists and is enabled
|
||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||
)
|
||||
|
||||
# Select appropriate fee recipient
|
||||
if is_mayhem_mode:
|
||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||
else:
|
||||
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||
|
||||
# Get the fee recipient's WSOL token account
|
||||
fee_recipient_token_account = get_associated_token_address(
|
||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
return (fee_recipient, fee_recipient_token_account)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Price Calculation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def calculate_token_pool_price(
|
||||
client: AsyncClient,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
) -> float:
|
||||
"""Calculate the current price of tokens in an AMM pool.
|
||||
"""Calculate current token price from AMM pool balances.
|
||||
|
||||
Fetches the balance of tokens in the pool and calculates the price ratio
|
||||
between the base token and quote token (typically SOL).
|
||||
AMM price is determined by the ratio of tokens in the pool:
|
||||
price = quote_balance / base_balance
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pool_base_token_account: Address of the pool's base token account (your token)
|
||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
||||
client: Solana RPC client
|
||||
pool_base_token_account: Pool's token account (the token being priced)
|
||||
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||
|
||||
Returns:
|
||||
The price of the base token in terms of the quote token (SOL per token)
|
||||
Price in SOL per token
|
||||
"""
|
||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||
quote_balance_resp = await client.get_token_account_balance(
|
||||
pool_quote_token_account
|
||||
)
|
||||
|
||||
# Extract the UI amounts (human-readable with decimals)
|
||||
base_amount = float(base_balance_resp.value.ui_amount)
|
||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||
|
||||
token_price = quote_amount / base_amount
|
||||
return quote_amount / base_amount
|
||||
|
||||
return token_price
|
||||
|
||||
# ============================================================================
|
||||
# Token Buying
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||
mint_info = await client.get_account_info(mint_address)
|
||||
|
||||
if not mint_info.value:
|
||||
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||
|
||||
owner = mint_info.value.owner
|
||||
|
||||
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||
return SYSTEM_TOKEN_PROGRAM
|
||||
elif owner == TOKEN_2022_PROGRAM:
|
||||
return TOKEN_2022_PROGRAM
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||
)
|
||||
|
||||
|
||||
async def buy_pump_swap(
|
||||
client: AsyncClient,
|
||||
pump_fun_amm_market: Pubkey,
|
||||
market: Pubkey,
|
||||
payer: Keypair,
|
||||
base_mint: Pubkey,
|
||||
user_base_token_account: Pubkey,
|
||||
@@ -273,52 +406,64 @@ async def buy_pump_swap(
|
||||
pool_quote_token_account: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
sol_amount_to_spend: int,
|
||||
sol_amount_to_spend: float,
|
||||
slippage: float = 0.25,
|
||||
) -> str | None:
|
||||
"""Buy tokens on the PUMP AMM with slippage protection.
|
||||
"""Execute a token buy on the PUMP AMM with slippage protection.
|
||||
|
||||
Executes a token purchase on the PUMP AMM protocol, calculating the expected
|
||||
token amount based on the current pool price and applying slippage protection.
|
||||
This function:
|
||||
1. Calculates expected token output based on current price
|
||||
2. Wraps SOL into WSOL (required for SPL token operations)
|
||||
3. Constructs and simulates the transaction
|
||||
4. Sends the buy transaction if simulation succeeds
|
||||
|
||||
Why WSOL wrapping is needed:
|
||||
SPL tokens can only interact with other SPL tokens. Native SOL must be
|
||||
wrapped into WSOL (an SPL token representation of SOL) before trading.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pump_fun_amm_market: Address of the AMM market
|
||||
payer: Keypair of the transaction signer and token buyer
|
||||
base_mint: Address of the token mint being purchased
|
||||
user_base_token_account: Address of the user's token account for receiving purchased tokens
|
||||
user_quote_token_account: Address of the user's SOL token account
|
||||
pool_base_token_account: Address of the pool's token account for the token being purchased
|
||||
pool_quote_token_account: Address of the pool's SOL token account
|
||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
||||
sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports)
|
||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
||||
client: Solana RPC client
|
||||
market: AMM pool address
|
||||
payer: Wallet keypair for signing
|
||||
base_mint: Token mint address
|
||||
user_base_token_account: User's token account (for receiving tokens)
|
||||
user_quote_token_account: User's WSOL account
|
||||
pool_base_token_account: Pool's token account
|
||||
pool_quote_token_account: Pool's WSOL account
|
||||
coin_creator_vault_authority: Creator vault PDA
|
||||
coin_creator_vault_ata: Creator's WSOL account
|
||||
sol_amount_to_spend: Amount of SOL to spend (in SOL, not lamports)
|
||||
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||
|
||||
Returns:
|
||||
Transaction signature if successful, None otherwise
|
||||
"""
|
||||
# Calculate token price
|
||||
token_program_id = await get_token_program_id(client, base_mint)
|
||||
token_price_sol = await calculate_token_pool_price(
|
||||
client, pool_base_token_account, pool_quote_token_account
|
||||
)
|
||||
print(f"Token price in SOL: {token_price_sol:.10f} SOL")
|
||||
|
||||
# Calculate maximum SOL input with slippage protection
|
||||
# Calculate expected token amount and maximum SOL we're willing to spend
|
||||
base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
|
||||
slippage_factor = 1 + slippage
|
||||
max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
|
||||
max_sol_input = int((sol_amount_to_spend * (1 + slippage)) * LAMPORTS_PER_SOL)
|
||||
|
||||
print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
|
||||
print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
|
||||
|
||||
# Calculate required PDAs for volume tracking
|
||||
# Derive volume accumulator PDAs for incentive tracking
|
||||
global_volume_accumulator = find_global_volume_accumulator()
|
||||
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
||||
|
||||
# Define all accounts needed for the buy instruction
|
||||
# Get fee recipient based on mayhem mode
|
||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||
client, market
|
||||
)
|
||||
|
||||
# Build account list for buy instruction
|
||||
# Order matters! Must match the program's expected account layout
|
||||
accounts = [
|
||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||
@@ -327,15 +472,13 @@ async def buy_pump_swap(
|
||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
||||
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||
), # Use dynamic token_program_id
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
@@ -352,34 +495,43 @@ async def buy_pump_swap(
|
||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=global_volume_accumulator, is_signer=False, is_writable=True
|
||||
pubkey=global_volume_accumulator, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
||||
# Index 21: fee_config (readonly)
|
||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||
# Index 22: fee_program (readonly)
|
||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Instruction data format:
|
||||
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
|
||||
# All integers are little-endian (<)
|
||||
data = (
|
||||
BUY_DISCRIMINATOR
|
||||
+ struct.pack("<Q", base_amount_out)
|
||||
+ struct.pack("<Q", max_sol_input)
|
||||
+ struct.pack("<B", 1) # track_volume: 1 = true (enable volume tracking)
|
||||
+ struct.pack("<Q", base_amount_out) # Expected token amount
|
||||
+ struct.pack("<Q", max_sol_input) # Maximum SOL to spend
|
||||
+ struct.pack("<B", VOLUME_TRACKING_ENABLED) # Enable volume tracking
|
||||
)
|
||||
|
||||
# Set compute budget to avoid transaction failures
|
||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||
|
||||
# Wrapping SOL
|
||||
# Create WSOL account if it doesn't exist
|
||||
# Note: WSOL always uses the standard Token program, never Token2022
|
||||
create_wsol_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM
|
||||
payer.pubkey(),
|
||||
payer.pubkey(),
|
||||
SOL,
|
||||
SYSTEM_TOKEN_PROGRAM, # WSOL always uses standard Token program
|
||||
)
|
||||
|
||||
# Calculate the amount of SOL to wrap(can also use `max_sol_input`), can get the SOL back by closing the WSOL account while selling
|
||||
pump_protocol_fees = sol_amount_to_spend * 0.1 # adding some buffer fees
|
||||
wrap_amount = int((sol_amount_to_spend + pump_protocol_fees) * LAMPORTS_PER_SOL)
|
||||
# Calculate amount to wrap (includes buffer for fees)
|
||||
wrap_amount = int(
|
||||
(sol_amount_to_spend * (1 + PROTOCOL_FEE_BUFFER)) * LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
# Transfer SOL to WSOL account and sync
|
||||
# This converts native SOL to the SPL token version (WSOL)
|
||||
transfer_sol_ix = transfer(
|
||||
TransferParams(
|
||||
from_pubkey=payer.pubkey(),
|
||||
@@ -387,20 +539,24 @@ async def buy_pump_swap(
|
||||
lamports=wrap_amount,
|
||||
)
|
||||
)
|
||||
|
||||
sync_native_ix = sync_native(
|
||||
SyncNativeParams(SYSTEM_TOKEN_PROGRAM, user_quote_token_account)
|
||||
SyncNativeParams(
|
||||
SYSTEM_TOKEN_PROGRAM, user_quote_token_account
|
||||
) # WSOL always uses standard Token program
|
||||
)
|
||||
|
||||
idempotent_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(), payer.pubkey(), base_mint, SYSTEM_TOKEN_PROGRAM
|
||||
# Create token account for receiving purchased tokens
|
||||
create_token_ata_ix = create_idempotent_associated_token_account(
|
||||
payer.pubkey(),
|
||||
payer.pubkey(),
|
||||
base_mint,
|
||||
token_program_id, # Use dynamic token_program_id
|
||||
)
|
||||
|
||||
buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||
|
||||
# Build and sign transaction
|
||||
blockhash_resp = await client.get_latest_blockhash()
|
||||
recent_blockhash = blockhash_resp.value.blockhash
|
||||
|
||||
msg = Message.new_with_blockhash(
|
||||
[
|
||||
compute_limit_ix,
|
||||
@@ -408,64 +564,77 @@ async def buy_pump_swap(
|
||||
create_wsol_ata_ix,
|
||||
transfer_sol_ix,
|
||||
sync_native_ix,
|
||||
idempotent_ata_ix,
|
||||
create_token_ata_ix,
|
||||
buy_ix,
|
||||
],
|
||||
payer.pubkey(),
|
||||
recent_blockhash,
|
||||
blockhash_resp.value.blockhash,
|
||||
)
|
||||
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
tx_buy = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
# Optionally, you can simulate the transaction first and check for errors and get the compute units used
|
||||
simulation = await client.simulate_transaction(tx_buy)
|
||||
# Simulate first to catch errors before sending
|
||||
simulation = await client.simulate_transaction(tx)
|
||||
if simulation.value.err:
|
||||
print(f"Simulation error: {simulation.value.err}")
|
||||
return None
|
||||
|
||||
compute_units_used = simulation.value.units_consumed
|
||||
print(f"Simulation successful, compute units used: {compute_units_used}")
|
||||
print(
|
||||
f"Simulation successful, compute units used: {simulation.value.units_consumed}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Skip preflight since we already simulated (faster execution)
|
||||
tx_sig = await client.send_transaction(
|
||||
tx_buy,
|
||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
||||
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||
)
|
||||
|
||||
tx_hash = tx_sig.value
|
||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
print("Transaction confirmed")
|
||||
return tx_hash
|
||||
except Exception as e:
|
||||
print(f"Error sending transaction: {e!s}")
|
||||
print(f"Error: {e!s}")
|
||||
return None
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to execute the token buying process."""
|
||||
sol_amount_to_spend = 0.000001
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Execute the complete buy flow."""
|
||||
sol_amount_to_spend = 0.000001 # Amount of SOL to spend on the purchase
|
||||
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
# Step 1: Find the pool address for our token
|
||||
market_address = await get_market_address_by_base_mint(
|
||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||
)
|
||||
|
||||
# Step 2: Parse pool data to get necessary accounts
|
||||
market_data = await get_market_data(client, market_address)
|
||||
|
||||
# Determine token program ID for the base mint
|
||||
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||
|
||||
# Step 3: Derive PDAs needed for the transaction
|
||||
coin_creator_vault_authority = find_coin_creator_vault(
|
||||
Pubkey.from_string(market_data["coin_creator"])
|
||||
)
|
||||
coin_creator_vault_ata = get_associated_token_address(
|
||||
coin_creator_vault_authority, SOL
|
||||
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Step 4: Execute the buy
|
||||
await buy_pump_swap(
|
||||
client,
|
||||
market_address,
|
||||
PAYER,
|
||||
TOKEN_MINT,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||
coin_creator_vault_authority,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""
|
||||
This module provides functionality to:
|
||||
- Find market addresses by token mint.
|
||||
- Fetch and parse market data from PUMP AMM pools.
|
||||
- Calculate token prices in AMM pools.
|
||||
- Create associated token accounts (ATAs) idempotently.
|
||||
- Sell tokens on the PUMP AMM with slippage protection.
|
||||
This standalone script demonstrates how to sell tokens on the PUMP AMM (pAMM) protocol.
|
||||
It covers the complete flow from finding markets to executing sells with mayhem mode support.
|
||||
|
||||
Key concepts demonstrated:
|
||||
- Finding AMM pool addresses by token mint
|
||||
- Parsing binary account data structures
|
||||
- Dynamic fee recipient calculation (mayhem mode vs standard)
|
||||
- Program Derived Address (PDA) derivation
|
||||
- Transaction construction with compute budgets
|
||||
- Slippage protection mechanisms
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -26,31 +30,33 @@ from spl.token.instructions import get_associated_token_address
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuration constants
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
TOKEN_MINT = Pubkey.from_string("...")
|
||||
TOKEN_MINT = Pubkey.from_string("...") # Replace with your token mint address
|
||||
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
|
||||
PAYER = Keypair.from_bytes(PRIVATE_KEY)
|
||||
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
|
||||
SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade
|
||||
|
||||
TOKEN_DECIMALS = 6
|
||||
SELL_DISCRIMINATOR = bytes.fromhex(
|
||||
"33e685a4017f83ad"
|
||||
) # Program instruction identifier for the sell function
|
||||
# Token configuration
|
||||
TOKEN_DECIMALS = 6 # Standard for most pump.fun tokens
|
||||
|
||||
# Program instruction discriminators (first 8 bytes identify the instruction)
|
||||
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
|
||||
|
||||
# ============================================================================
|
||||
# Solana Program IDs and System Accounts
|
||||
# ============================================================================
|
||||
|
||||
# Solana system addresses and program IDs
|
||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
||||
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
|
||||
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string(
|
||||
"7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC"
|
||||
)
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
@@ -59,59 +65,87 @@ PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
|
||||
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
|
||||
)
|
||||
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
|
||||
|
||||
# ============================================================================
|
||||
# Constants for Account Structure Parsing
|
||||
# ============================================================================
|
||||
|
||||
# Pool account structure offsets
|
||||
POOL_DISCRIMINATOR_SIZE = 8
|
||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||
|
||||
# GlobalConfig structure offsets
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE = 8
|
||||
GLOBALCONFIG_ADMIN_SIZE = 32
|
||||
GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE = 32
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET = (
|
||||
GLOBALCONFIG_DISCRIMINATOR_SIZE
|
||||
+ GLOBALCONFIG_ADMIN_SIZE
|
||||
+ GLOBALCONFIG_DEFAULT_FEE_RECIPIENT_SIZE
|
||||
)
|
||||
|
||||
# Fee recipients
|
||||
STANDARD_PUMPSWAP_FEE_RECIPIENT = Pubkey.from_string(
|
||||
"7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"
|
||||
)
|
||||
|
||||
# Solana constants
|
||||
LAMPORTS_PER_SOL = 1_000_000_000
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use
|
||||
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
|
||||
COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Market Discovery
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_market_address_by_base_mint(
|
||||
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Find the market address for a given token mint.
|
||||
"""Find the AMM pool address for a specific token.
|
||||
|
||||
Searches for the AMM pool that contains the specified token as its base token.
|
||||
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
|
||||
that matches the given token mint address.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
base_mint_address: Address of the token mint you want to find the market for
|
||||
amm_program_id: Address of the AMM program
|
||||
client: Solana RPC client
|
||||
base_mint_address: Token mint to find the pool for
|
||||
amm_program_id: PUMP AMM program address
|
||||
|
||||
Returns:
|
||||
The Pubkey of the market (AMM pool) for the token
|
||||
Address of the AMM pool (market) for the token
|
||||
"""
|
||||
base_mint_bytes = bytes(base_mint_address)
|
||||
offset = (
|
||||
43 # Offset where the base_mint field is stored in the account data structure
|
||||
)
|
||||
filters = [MemcmpOpts(offset=offset, bytes=base_mint_bytes)]
|
||||
|
||||
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
|
||||
response = await client.get_program_accounts(
|
||||
amm_program_id, encoding="base64", filters=filters
|
||||
)
|
||||
|
||||
market_address = [account.pubkey for account in response.value][0]
|
||||
return market_address
|
||||
return response.value[0].pubkey
|
||||
|
||||
|
||||
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
"""Fetch and parse market data from the blockchain.
|
||||
"""Parse binary pool account data into a structured dictionary.
|
||||
|
||||
Retrieves and deserializes the data stored in the market account.
|
||||
The pool account stores data in a specific binary format. This function
|
||||
deserializes that data based on the known structure.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
market_address: Address of the market (AMM pool) to fetch data for
|
||||
client: Solana RPC client
|
||||
market_address: Address of the pool account
|
||||
|
||||
Returns:
|
||||
Dictionary containing the parsed market data
|
||||
Dictionary with parsed pool data fields
|
||||
"""
|
||||
response = await client.get_account_info(market_address, encoding="base64")
|
||||
data = response.value.data
|
||||
parsed_data: dict = {}
|
||||
|
||||
# Start after the 8-byte discriminator
|
||||
offset = 8
|
||||
# Define the structure of the market account data
|
||||
offset = POOL_DISCRIMINATOR_SIZE
|
||||
|
||||
# Field definitions: (name, type)
|
||||
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
|
||||
fields = [
|
||||
("pool_bump", "u8"),
|
||||
("index", "u16"),
|
||||
@@ -131,39 +165,39 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
|
||||
offset += 32
|
||||
elif field_type in {"u64", "i64"}:
|
||||
value = (
|
||||
struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||
if field_type == "u64"
|
||||
else struct.unpack("<q", data[offset : offset + 8])[0]
|
||||
)
|
||||
parsed_data[field_name] = value
|
||||
format_char = "<Q" if field_type == "u64" else "<q"
|
||||
parsed_data[field_name] = struct.unpack(
|
||||
format_char, data[offset : offset + 8]
|
||||
)[0]
|
||||
offset += 8
|
||||
elif field_type == "u16":
|
||||
value = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
|
||||
offset += 2
|
||||
elif field_type == "u8":
|
||||
value = data[offset]
|
||||
parsed_data[field_name] = value
|
||||
parsed_data[field_name] = data[offset]
|
||||
offset += 1
|
||||
|
||||
return parsed_data
|
||||
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
|
||||
# ============================================================================
|
||||
# Program Derived Address (PDA) Derivation
|
||||
# ============================================================================
|
||||
# PDAs are deterministic addresses derived from seeds and a program ID.
|
||||
# They allow programs to own accounts without needing a private key.
|
||||
|
||||
Calculates the deterministic PDA that serves as the vault authority
|
||||
for a specific coin creator in the PUMP AMM protocol.
|
||||
|
||||
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
"""Derive the PDA for the coin creator's fee vault.
|
||||
|
||||
The creator vault collects fees on behalf of the token creator.
|
||||
This is a deterministic address that can be recalculated by anyone.
|
||||
|
||||
Args:
|
||||
coin_creator: Pubkey of the coin creator account
|
||||
coin_creator: Public key of the token creator
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived coin creator vault authority
|
||||
|
||||
Note:
|
||||
This vault is used to collect creator fees from token transactions
|
||||
PDA of the creator's vault authority
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator_vault", bytes(coin_creator)],
|
||||
@@ -173,10 +207,9 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
|
||||
|
||||
|
||||
def find_fee_config() -> Pubkey:
|
||||
"""Derive the Program Derived Address (PDA) for the fee config.
|
||||
"""Derive the PDA for the fee configuration account.
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived fee config account
|
||||
This account stores fee-related configuration for the AMM.
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
|
||||
@@ -185,48 +218,152 @@ def find_fee_config() -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Mayhem Mode Fee Handling
|
||||
# ============================================================================
|
||||
# Mayhem mode is a special fee structure where fees go to a different recipient.
|
||||
# The fee recipient changes dynamically based on the pool's mayhem_mode flag.
|
||||
|
||||
|
||||
async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
||||
"""Fetch the mayhem mode fee recipient from GlobalConfig.
|
||||
|
||||
When mayhem mode is active, fees are redirected to a special recipient
|
||||
stored in the GlobalConfig account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
Public key of the mayhem mode fee recipient
|
||||
"""
|
||||
response = await client.get_account_info(PUMP_SWAP_GLOBAL_CONFIG, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch GlobalConfig account"
|
||||
raise ValueError(msg)
|
||||
|
||||
data = response.value.data
|
||||
recipient_bytes = data[
|
||||
GLOBALCONFIG_RESERVED_FEE_OFFSET : GLOBALCONFIG_RESERVED_FEE_OFFSET + 32
|
||||
]
|
||||
return Pubkey.from_bytes(recipient_bytes)
|
||||
|
||||
|
||||
async def get_pumpswap_fee_recipients(
|
||||
client: AsyncClient, pool: Pubkey
|
||||
) -> tuple[Pubkey, Pubkey]:
|
||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
||||
|
||||
This function checks if mayhem mode is enabled for the pool and returns
|
||||
the appropriate fee recipient and their WSOL token account.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool: Address of the AMM pool
|
||||
|
||||
Returns:
|
||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
||||
"""
|
||||
response = await client.get_account_info(pool, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
msg = "Cannot fetch pool account"
|
||||
raise ValueError(msg)
|
||||
|
||||
pool_data = response.value.data
|
||||
|
||||
# Check if mayhem mode flag exists and is enabled
|
||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||
)
|
||||
|
||||
# Select appropriate fee recipient
|
||||
if is_mayhem_mode:
|
||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||
else:
|
||||
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||
|
||||
# Get the fee recipient's WSOL token account
|
||||
fee_recipient_token_account = get_associated_token_address(
|
||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
return (fee_recipient, fee_recipient_token_account)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Price Calculation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def calculate_token_pool_price(
|
||||
client: AsyncClient,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
) -> float:
|
||||
"""Calculate the price of tokens in the pool.
|
||||
"""Calculate current token price from AMM pool balances.
|
||||
|
||||
Fetches the balance of tokens in the pool and calculates the price ratio.
|
||||
AMM price is determined by the ratio of tokens in the pool:
|
||||
price = quote_balance / base_balance
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pool_base_token_account: Address of the pool's base token account (your token)
|
||||
pool_quote_token_account: Address of the pool's quote token account (SOL)
|
||||
client: Solana RPC client
|
||||
pool_base_token_account: Pool's token account (the token being priced)
|
||||
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||
|
||||
Returns:
|
||||
The price of the base token in terms of the quote token (usually SOL)
|
||||
Price in SOL per token
|
||||
"""
|
||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||
quote_balance_resp = await client.get_token_account_balance(
|
||||
pool_quote_token_account
|
||||
)
|
||||
|
||||
# Extract the UI amounts (human-readable with decimals)
|
||||
base_amount = float(base_balance_resp.value.ui_amount)
|
||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||
|
||||
token_price = quote_amount / base_amount
|
||||
return quote_amount / base_amount
|
||||
|
||||
return token_price
|
||||
|
||||
# ============================================================================
|
||||
# Token Program Determination
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def get_token_program_id(client: AsyncClient, mint_address: Pubkey) -> Pubkey:
|
||||
"""Determines if a mint uses TokenProgram or Token2022Program."""
|
||||
mint_info = await client.get_account_info(mint_address)
|
||||
|
||||
if not mint_info.value:
|
||||
raise ValueError(f"Could not fetch mint info for {mint_address}")
|
||||
|
||||
owner = mint_info.value.owner
|
||||
|
||||
if owner == SYSTEM_TOKEN_PROGRAM:
|
||||
return SYSTEM_TOKEN_PROGRAM
|
||||
elif owner == TOKEN_2022_PROGRAM:
|
||||
return TOKEN_2022_PROGRAM
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Mint account {mint_address} is owned by an unknown program: {owner}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Associated Token Account (ATA) Creation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
||||
"""Create an instruction to create an Associated Token Account (ATA) if it doesn't exist.
|
||||
"""Create instruction to initialize a WSOL ATA if it doesn't exist.
|
||||
|
||||
This creates an instruction that will create an Associated Token Account for SOL
|
||||
if it doesn't already exist.
|
||||
Idempotent means this instruction won't fail if the ATA already exists.
|
||||
See: https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
||||
|
||||
Args:
|
||||
payer_pubkey: The public key of the account that will pay for the creation
|
||||
payer_pubkey: Account that will pay for ATA creation
|
||||
|
||||
Returns:
|
||||
An instruction to create the ATA
|
||||
Instruction to create the ATA
|
||||
"""
|
||||
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
|
||||
|
||||
@@ -239,20 +376,23 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# The data for creating an ATA idempotently is just a single byte with value 1
|
||||
# Check the details here:
|
||||
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
|
||||
data = bytes([1])
|
||||
# Instruction data: single byte with value 1 = CreateIdempotent
|
||||
return Instruction(
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Token Selling
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def sell_pump_swap(
|
||||
client: AsyncClient,
|
||||
pump_fun_amm_market: Pubkey,
|
||||
market: Pubkey,
|
||||
payer: Keypair,
|
||||
base_mint: Pubkey,
|
||||
token_program_id: Pubkey,
|
||||
user_base_token_account: Pubkey,
|
||||
user_quote_token_account: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
@@ -261,54 +401,61 @@ async def sell_pump_swap(
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
slippage: float = 0.25,
|
||||
) -> str | None:
|
||||
"""Sell tokens on the PUMP AMM.
|
||||
"""Execute a token sell on the PUMP AMM with slippage protection.
|
||||
|
||||
This function sells all tokens in the user's token account with the specified slippage tolerance.
|
||||
This function:
|
||||
1. Fetches current token balance and pool price
|
||||
2. Calculates minimum SOL output with slippage tolerance
|
||||
3. Constructs and sends the sell transaction
|
||||
|
||||
Args:
|
||||
client: Solana RPC client instance
|
||||
pump_fun_amm_market: Address of the AMM market
|
||||
payer: Keypair of the transaction signer and token seller
|
||||
base_mint: Address of the token mint being sold
|
||||
user_base_token_account: Address of the user's token account for the token being sold
|
||||
user_quote_token_account: Address of the user's SOL token account
|
||||
pool_base_token_account: Address of the pool's token account for the token being sold
|
||||
pool_quote_token_account: Address of the pool's SOL token account
|
||||
coin_creator_vault_authority: Address of the coin creator's vault authority
|
||||
coin_creator_vault_ata: Address of the coin creator's associated token account for fees
|
||||
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
|
||||
client: Solana RPC client
|
||||
market: AMM pool address
|
||||
payer: Wallet keypair for signing
|
||||
base_mint: Token mint address
|
||||
user_base_token_account: User's token account
|
||||
user_quote_token_account: User's WSOL account
|
||||
pool_base_token_account: Pool's token account
|
||||
pool_quote_token_account: Pool's WSOL account
|
||||
coin_creator_vault_authority: Creator vault PDA
|
||||
coin_creator_vault_ata: Creator's WSOL account
|
||||
slippage: Maximum acceptable slippage (0.25 = 25%)
|
||||
|
||||
Returns:
|
||||
Transaction signature if successful, None otherwise
|
||||
"""
|
||||
# Get token balance
|
||||
token_balance = int(
|
||||
(await client.get_token_account_balance(user_base_token_account)).value.amount
|
||||
)
|
||||
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
||||
|
||||
print(f"Token balance: {token_balance_decimal}")
|
||||
|
||||
if token_balance == 0:
|
||||
print("No tokens to sell.")
|
||||
return None
|
||||
|
||||
# Calculate token price
|
||||
token_price_sol = await calculate_token_pool_price(
|
||||
client, pool_base_token_account, pool_quote_token_account
|
||||
)
|
||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||
|
||||
# Calculate minimum SOL output with slippage protection
|
||||
amount = token_balance
|
||||
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
|
||||
slippage_factor = 1 - slippage
|
||||
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
|
||||
# Calculate minimum SOL we're willing to receive (slippage protection)
|
||||
expected_sol_output = token_balance_decimal * token_price_sol
|
||||
min_sol_output = int((expected_sol_output * (1 - slippage)) * LAMPORTS_PER_SOL)
|
||||
|
||||
print(f"Selling {token_balance_decimal} tokens")
|
||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||
|
||||
# Define all accounts needed for the sell instruction
|
||||
# Get fee recipient based on mayhem mode
|
||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
||||
client, market
|
||||
)
|
||||
|
||||
# Build account list for sell instruction
|
||||
# Order matters! Must match the program's expected account layout
|
||||
accounts = [
|
||||
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
|
||||
@@ -317,15 +464,13 @@ async def sell_pump_swap(
|
||||
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False
|
||||
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT,
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
pubkey=token_program_id, is_signer=False, is_writable=False
|
||||
), # Use dynamic token_program_id
|
||||
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
@@ -341,76 +486,88 @@ async def sell_pump_swap(
|
||||
AccountMeta(
|
||||
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
|
||||
),
|
||||
# Index 19: fee_config (readonly)
|
||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||
# Index 20: fee_program (readonly)
|
||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
|
||||
# All integers are little-endian (<)
|
||||
data = (
|
||||
SELL_DISCRIMINATOR
|
||||
+ struct.pack("<Q", amount)
|
||||
+ struct.pack("<Q", min_sol_output)
|
||||
+ struct.pack("<Q", token_balance) # Amount to sell
|
||||
+ struct.pack("<Q", min_sol_output) # Minimum SOL to receive
|
||||
)
|
||||
|
||||
# Set compute budget to avoid transaction failures
|
||||
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
|
||||
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
|
||||
|
||||
create_ata_ix = create_ata_idempotent_ix(
|
||||
payer_pubkey=payer.pubkey(),
|
||||
)
|
||||
# Ensure WSOL ATA exists (needed to receive SOL from sell)
|
||||
create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
|
||||
|
||||
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
|
||||
|
||||
# Build and sign transaction
|
||||
blockhash_resp = await client.get_latest_blockhash()
|
||||
recent_blockhash = blockhash_resp.value.blockhash
|
||||
|
||||
msg = Message.new_with_blockhash(
|
||||
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
|
||||
payer.pubkey(),
|
||||
recent_blockhash,
|
||||
blockhash_resp.value.blockhash,
|
||||
)
|
||||
|
||||
tx_sell = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
tx = VersionedTransaction(message=msg, keypairs=[payer])
|
||||
|
||||
try:
|
||||
# Skip preflight to send transaction faster (useful in competitive scenarios)
|
||||
tx_sig = await client.send_transaction(
|
||||
tx_sell,
|
||||
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
|
||||
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||
)
|
||||
|
||||
tx_hash = tx_sig.value
|
||||
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
print("Transaction confirmed")
|
||||
return tx_hash
|
||||
except Exception as e:
|
||||
print(f"Error sending transaction: {e!s}")
|
||||
print(f"Error: {e!s}")
|
||||
return None
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to execute the token selling process."""
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Execute the complete sell flow."""
|
||||
async with AsyncClient(RPC_ENDPOINT) as client:
|
||||
# Step 1: Find the pool address for our token
|
||||
market_address = await get_market_address_by_base_mint(
|
||||
client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID
|
||||
)
|
||||
|
||||
# Step 2: Parse pool data to get necessary accounts
|
||||
market_data = await get_market_data(client, market_address)
|
||||
|
||||
# Determine token program ID for the base mint
|
||||
token_program_id = await get_token_program_id(client, TOKEN_MINT)
|
||||
|
||||
# Step 3: Derive PDAs needed for the transaction
|
||||
coin_creator_vault_authority = find_coin_creator_vault(
|
||||
Pubkey.from_string(market_data["coin_creator"])
|
||||
)
|
||||
coin_creator_vault_ata = get_associated_token_address(
|
||||
coin_creator_vault_authority, SOL
|
||||
coin_creator_vault_authority, SOL, SYSTEM_TOKEN_PROGRAM
|
||||
)
|
||||
|
||||
# Step 4: Execute the sell
|
||||
await sell_pump_swap(
|
||||
client,
|
||||
market_address,
|
||||
PAYER,
|
||||
TOKEN_MINT,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL),
|
||||
token_program_id,
|
||||
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
|
||||
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
|
||||
Pubkey.from_string(market_data["pool_base_token_account"]),
|
||||
Pubkey.from_string(market_data["pool_quote_token_account"]),
|
||||
coin_creator_vault_authority,
|
||||
|
||||
Reference in New Issue
Block a user