Files
pumpfun-bonkfun-bot/learning-examples/pumpswap/manual_sell_pumpswap.py
T

654 lines
25 KiB
Python
Raw Normal View History

"""
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
"""
2025-03-30 12:51:02 +00:00
import asyncio
import os
import random
2025-03-30 12:51:02 +00:00
import struct
import base58
2025-03-31 15:14:07 +00:00
from dotenv import load_dotenv
2025-03-30 12:51:02 +00:00
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import MemcmpOpts, 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 VersionedTransaction
from spl.token.instructions import get_associated_token_address
2025-03-31 15:14:07 +00:00
load_dotenv()
# ============================================================================
# Configuration
# ============================================================================
2025-03-30 12:51:02 +00:00
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
import sys
TOKEN_MINT = Pubkey.from_string(sys.argv[1] if len(sys.argv) > 1 else "...") # Pass mint as argv[1]
2025-03-30 12:51:02 +00:00
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.25 # 25% - maximum acceptable price movement during trade
2025-03-30 12:51:02 +00:00
# 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
# ============================================================================
2025-03-30 12:51:02 +00:00
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
2025-08-11 05:35:25 +00:00
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string(
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw"
)
2025-03-30 12:51:02 +00:00
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
TOKEN_2022_PROGRAM = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
2025-03-30 12:51:02 +00:00
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
2025-08-11 05:35:25 +00:00
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string(
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"
)
PUMP_FEE_PROGRAM = Pubkey.from_string("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ")
# 8 breaking-upgrade fee recipients (pump-swap program upgrade 2026-04-28).
# Two new accounts must be appended after pool-v2: the fee recipient (readonly)
# and its quote-mint ATA (mutable).
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
BREAKING_FEE_RECIPIENTS = [
Pubkey.from_string("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
Pubkey.from_string("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
Pubkey.from_string("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
Pubkey.from_string("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
Pubkey.from_string("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
Pubkey.from_string("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
Pubkey.from_string("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
Pubkey.from_string("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
]
# ============================================================================
# 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_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin 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
2025-03-31 15:14:07 +00:00
LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 150_000 # Max compute units for transaction
# ============================================================================
# Market Discovery
# ============================================================================
2025-03-30 12:51:02 +00:00
2025-08-11 05:35:25 +00:00
async def get_market_address_by_base_mint(
client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey
) -> Pubkey:
"""Find the AMM pool address for a specific token.
2025-08-11 05:35:25 +00:00
Uses getProgramAccounts RPC method with a memcmp filter to find the pool
that matches the given token mint address.
2025-08-11 05:35:25 +00:00
Args:
client: Solana RPC client
base_mint_address: Token mint to find the pool for
amm_program_id: PUMP AMM program address
2025-08-11 05:35:25 +00:00
Returns:
Address of the AMM pool (market) for the token
"""
filters = [MemcmpOpts(offset=POOL_BASE_MINT_OFFSET, bytes=bytes(base_mint_address))]
2025-03-30 12:51:02 +00:00
response = await client.get_program_accounts(
2025-08-11 05:35:25 +00:00
amm_program_id, encoding="base64", filters=filters
2025-03-30 12:51:02 +00:00
)
return response.value[0].pubkey
2025-08-11 05:35:25 +00:00
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
"""Parse binary pool account data into a structured dictionary.
2025-08-11 05:35:25 +00:00
The pool account stores data in a specific binary format. This function
deserializes that data based on the known structure.
2025-08-11 05:35:25 +00:00
Args:
client: Solana RPC client
market_address: Address of the pool account
2025-08-11 05:35:25 +00:00
Returns:
Dictionary with parsed pool data fields
"""
2025-04-01 14:57:01 +00:00
response = await client.get_account_info(market_address, encoding="base64")
2025-03-30 12:51:02 +00:00
data = response.value.data
parsed_data: dict = {}
2025-03-30 12:51:02 +00:00
offset = POOL_DISCRIMINATOR_SIZE
# Field definitions: (name, type)
# Types: u8=1 byte, u16=2 bytes, u64/i64=8 bytes, pubkey=32 bytes
2025-03-30 12:51:02 +00:00
fields = [
("pool_bump", "u8"),
("index", "u16"),
("creator", "pubkey"),
("base_mint", "pubkey"),
("quote_mint", "pubkey"),
("lp_mint", "pubkey"),
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
2025-08-11 05:35:25 +00:00
("coin_creator", "pubkey"),
2025-03-30 12:51:02 +00:00
]
for field_name, field_type in fields:
if field_type == "pubkey":
2025-08-11 05:35:25 +00:00
value = data[offset : offset + 32]
2025-03-30 12:51:02 +00:00
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
format_char = "<Q" if field_type == "u64" else "<q"
parsed_data[field_name] = struct.unpack(
format_char, data[offset : offset + 8]
)[0]
2025-03-30 12:51:02 +00:00
offset += 8
elif field_type == "u16":
parsed_data[field_name] = struct.unpack("<H", data[offset : offset + 2])[0]
2025-03-30 12:51:02 +00:00
offset += 2
elif field_type == "u8":
parsed_data[field_name] = data[offset]
2025-03-30 12:51:02 +00:00
offset += 1
return parsed_data
2025-08-11 05:35:25 +00:00
# ============================================================================
# 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.
2025-08-11 05:35:25 +00:00
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.
2025-08-11 05:35:25 +00:00
2025-05-14 15:25:04 +00:00
Args:
coin_creator: Public key of the token creator
2025-08-11 05:35:25 +00:00
2025-05-14 15:25:04 +00:00
Returns:
PDA of the creator's vault authority
2025-05-14 15:25:04 +00:00
"""
derived_address, _ = Pubkey.find_program_address(
2025-08-11 05:35:25 +00:00
[b"creator_vault", bytes(coin_creator)],
PUMP_AMM_PROGRAM_ID,
2025-08-11 05:35:25 +00:00
)
return derived_address
2025-08-11 05:35:25 +00:00
def find_fee_config() -> Pubkey:
"""Derive the PDA for the fee configuration account.
2025-09-03 15:05:12 +00:00
This account stores fee-related configuration for the AMM.
"""
derived_address, _ = Pubkey.find_program_address(
[b"fee_config", bytes(PUMP_AMM_PROGRAM_ID)],
PUMP_FEE_PROGRAM,
)
return derived_address
def find_pool_v2(base_mint: Pubkey) -> Pubkey:
"""Derive the PDA for the pool-v2 account (per-base-mint), required as the
last "pre-upgrade" account on every pump-swap buy/sell."""
derived_address, _ = Pubkey.find_program_address(
[b"pool-v2", bytes(base_mint)],
PUMP_AMM_PROGRAM_ID,
)
return derived_address
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
"""Derive the per-user volume accumulator PDA (pump-amm). Required for
cashback-pool sells (along with its WSOL ATA)."""
derived_address, _ = Pubkey.find_program_address(
[b"user_volume_accumulator", bytes(user)],
PUMP_AMM_PROGRAM_ID,
)
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, bool]:
"""Determine fee recipient + whether the pool is cashback.
Returns:
Tuple of (fee_recipient_pubkey, fee_recipient_token_account, is_cashback)
"""
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
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
pool_data[POOL_MAYHEM_MODE_OFFSET]
)
is_cashback = len(pool_data) > POOL_IS_CASHBACK_OFFSET and bool(
pool_data[POOL_IS_CASHBACK_OFFSET]
)
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, is_cashback)
# ============================================================================
# Price Calculation
# ============================================================================
2025-08-11 05:35:25 +00:00
async def calculate_token_pool_price(
client: AsyncClient,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
) -> float:
"""Calculate current token price from AMM pool balances.
2025-08-11 05:35:25 +00:00
AMM price is determined by the ratio of tokens in the pool:
price = quote_balance / base_balance
2025-08-11 05:35:25 +00:00
Args:
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)
2025-08-11 05:35:25 +00:00
Returns:
Price in SOL per token
"""
2025-03-30 12:51:02 +00:00
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
2025-08-11 05:35:25 +00:00
quote_balance_resp = await client.get_token_account_balance(
pool_quote_token_account
)
2025-03-30 12:51:02 +00:00
base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_balance_resp.value.ui_amount)
return quote_amount / base_amount
2025-08-11 05:35:25 +00:00
# ============================================================================
# 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
# ============================================================================
2025-03-30 12:51:02 +00:00
2025-08-11 05:35:25 +00:00
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
"""Create instruction to initialize a WSOL ATA if it doesn't exist.
2025-08-11 05:35:25 +00:00
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
2025-08-11 05:35:25 +00:00
Args:
payer_pubkey: Account that will pay for ATA creation
2025-08-11 05:35:25 +00:00
Returns:
Instruction to create the ATA
2025-03-30 12:51:02 +00:00
"""
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
2025-03-30 12:51:02 +00:00
instruction_accounts = [
AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True),
AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True),
2025-03-30 12:51:02 +00:00
AccountMeta(pubkey=SOL, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
]
2025-08-11 05:35:25 +00:00
# Instruction data: single byte with value 1 = CreateIdempotent
2025-08-11 05:35:25 +00:00
return Instruction(
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, bytes([1]), instruction_accounts
2025-08-11 05:35:25 +00:00
)
2025-03-30 12:51:02 +00:00
2025-08-11 05:35:25 +00:00
# ============================================================================
# Token Selling
# ============================================================================
2025-08-11 05:35:25 +00:00
async def sell_pump_swap(
client: AsyncClient,
market: Pubkey,
2025-08-11 05:35:25 +00:00
payer: Keypair,
base_mint: Pubkey,
token_program_id: Pubkey,
2025-08-11 05:35:25 +00:00
user_base_token_account: Pubkey,
user_quote_token_account: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
coin_creator_vault_authority: Pubkey,
coin_creator_vault_ata: Pubkey,
slippage: float = 0.25,
) -> str | None:
"""Execute a token sell on the PUMP AMM with slippage protection.
2025-08-11 05:35:25 +00:00
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
2025-08-11 05:35:25 +00:00
Args:
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%)
2025-08-11 05:35:25 +00:00
Returns:
Transaction signature if successful, None otherwise
"""
2025-08-11 05:35:25 +00:00
token_balance = int(
(await client.get_token_account_balance(user_base_token_account)).value.amount
)
2025-03-30 12:51:02 +00:00
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
2025-03-30 12:51:02 +00:00
print(f"Token balance: {token_balance_decimal}")
2025-03-30 12:51:02 +00:00
if token_balance == 0:
print("No tokens to sell.")
return None
2025-08-11 05:35:25 +00:00
token_price_sol = await calculate_token_pool_price(
client, pool_base_token_account, pool_quote_token_account
)
2025-03-31 15:14:07 +00:00
print(f"Price per Token: {token_price_sol:.20f} 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)
2025-03-30 12:51:02 +00:00
print(f"Selling {token_balance_decimal} tokens")
2025-03-31 15:14:07 +00:00
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
2025-03-30 12:51:02 +00:00
# Get fee recipient based on mayhem mode + detect cashback pool
(
fee_recipient,
fee_recipient_token_account,
is_cashback,
) = await get_pumpswap_fee_recipients(client, market)
# Cashback-pool sells append two writable accounts (user_volume_accumulator
# quote ATA, then the accumulator PDA itself) BEFORE pool-v2.
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
user_volume_accumulator_quote_ata = get_associated_token_address(
user_volume_accumulator, SOL, SYSTEM_TOKEN_PROGRAM
)
# Build account list for sell instruction
# Order matters! Must match the program's expected account layout
2025-03-30 12:51:02 +00:00
accounts = [
AccountMeta(pubkey=market, is_signer=False, is_writable=True),
2025-08-11 05:35:25 +00:00
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),
AccountMeta(pubkey=SOL, is_signer=False, is_writable=False),
AccountMeta(pubkey=user_base_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_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=fee_recipient, is_signer=False, is_writable=False),
2025-08-11 05:35:25 +00:00
AccountMeta(
pubkey=fee_recipient_token_account, is_signer=False, is_writable=True
2025-08-11 05:35:25 +00:00
),
AccountMeta(
pubkey=token_program_id, is_signer=False, is_writable=False
), # Use dynamic token_program_id
2025-08-11 05:35:25 +00:00
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
is_signer=False,
is_writable=False,
),
AccountMeta(
pubkey=PUMP_SWAP_EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PUMP_AMM_PROGRAM_ID, is_signer=False, is_writable=False),
AccountMeta(pubkey=coin_creator_vault_ata, is_signer=False, is_writable=True),
AccountMeta(
pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False
),
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
2025-03-30 12:51:02 +00:00
]
# Cashback pools require user_volume_accumulator_quote_ata + the accumulator
# PDA itself (both writable) BEFORE pool-v2. Confirmed against on-chain
# post-cutover cashback sell
# (sig 4ei1cJV7uaENJeb5p8prVKiTApTouTh94r9HqPNbj7oJH52X8mEiXhrVNKUgtB9WeZB8jZANnmuSkdTuJ59y8NP3).
if is_cashback:
accounts.extend([
AccountMeta(
pubkey=user_volume_accumulator_quote_ata,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=user_volume_accumulator, is_signer=False, is_writable=True
),
])
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
accounts.append(
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False)
)
# 2 accounts required by the 2026-04-28 pump-swap upgrade, appended AFTER
# pool-v2: breaking-fee recipient (readonly) + its quote-mint ATA (mutable).
# Sell counts: 24 non-cashback / 26 cashback.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
breaking_fee_quote_ata = get_associated_token_address(
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
accounts.extend([
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
])
2025-08-11 05:35:25 +00:00
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
# All integers are little-endian (<)
2025-08-11 05:35:25 +00:00
data = (
SELL_DISCRIMINATOR
+ struct.pack("<Q", token_balance) # Amount to sell
+ struct.pack("<Q", min_sol_output) # Minimum SOL to receive
2025-08-11 05:35:25 +00:00
)
2025-03-31 15:14:07 +00:00
# Set compute budget to avoid transaction failures
2025-03-31 15:14:07 +00:00
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
# Ensure WSOL ATA exists (needed to receive SOL from sell)
create_ata_ix = create_ata_idempotent_ix(payer.pubkey())
2025-08-11 05:35:25 +00:00
2025-03-30 12:51:02 +00:00
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
2025-08-11 05:35:25 +00:00
# Build and sign transaction
2025-03-30 12:51:02 +00:00
blockhash_resp = await client.get_latest_blockhash()
msg = Message.new_with_blockhash(
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
payer.pubkey(),
blockhash_resp.value.blockhash,
2025-03-30 12:51:02 +00:00
)
tx = VersionedTransaction(message=msg, keypairs=[payer])
2025-08-11 05:35:25 +00:00
2025-03-30 12:51:02 +00:00
try:
# Skip preflight to send transaction faster (useful in competitive scenarios)
2025-03-30 12:51:02 +00:00
tx_sig = await client.send_transaction(
tx, opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
2025-03-30 12:51:02 +00:00
)
tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
2025-03-31 15:14:07 +00:00
await client.confirm_transaction(tx_hash, commitment="confirmed")
2025-03-30 12:51:02 +00:00
print("Transaction confirmed")
return tx_hash
except Exception as e:
print(f"Error: {e!s}")
2025-03-30 12:51:02 +00:00
return None
# ============================================================================
# Main Execution
# ============================================================================
async def main() -> None:
"""Execute the complete sell flow."""
async with AsyncClient(RPC_ENDPOINT, timeout=120) as client:
# Step 1: Find the pool address for our token
2025-08-11 05:35:25 +00:00
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
2025-03-30 12:51:02 +00:00
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
2025-08-11 05:35:25 +00:00
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, SYSTEM_TOKEN_PROGRAM
2025-08-11 05:35:25 +00:00
)
# Step 4: Execute the sell
2025-03-30 12:51:02 +00:00
await sell_pump_swap(
client,
market_address,
2025-03-30 12:51:02 +00:00
PAYER,
2025-03-31 15:14:07 +00:00
TOKEN_MINT,
token_program_id,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT, token_program_id),
get_associated_token_address(PAYER.pubkey(), SOL, SYSTEM_TOKEN_PROGRAM),
2025-03-31 15:14:07 +00:00
Pubkey.from_string(market_data["pool_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority,
coin_creator_vault_ata,
2025-08-11 05:35:25 +00:00
SLIPPAGE,
2025-03-30 12:51:02 +00:00
)
2025-08-11 05:35:25 +00:00
2025-03-30 12:51:02 +00:00
if __name__ == "__main__":
2025-08-11 05:35:25 +00:00
asyncio.run(main())