diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py new file mode 100644 index 0000000..a10344e --- /dev/null +++ b/learning-examples/manual_sell_pumpswap.py @@ -0,0 +1,302 @@ +import asyncio +import os +import struct + +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 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 + +load_dotenv() + +# Configuration constants +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") +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 + +TOKEN_DECIMALS = 6 +SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad") # Program instruction identifier for the sell function + +# 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") +SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") +SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") +PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR") +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 + + +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. + + Searches for the AMM pool that contains the specified token as its base token. + + 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 + + Returns: + The Pubkey of the market (AMM pool) 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) + ] + + response = await client.get_program_accounts( + amm_program_id, + encoding="jsonParsed", + filters=filters + ) + + market_address = [account.pubkey for account in response.value][0] + return market_address + +async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: + """Fetch and parse market data from the blockchain. + + Retrieves and deserializes the data stored in the market account. + + Args: + client: Solana RPC client instance + market_address: Address of the market (AMM pool) to fetch data for + + Returns: + Dictionary containing the parsed market data + """ + response = await client.get_account_info_json_parsed(market_address) + data = response.value.data + parsed_data: dict = {} + + # Start after the 8-byte discriminator + offset = 8 + # Define the structure of the market account data + 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"), + ] + + for field_name, field_type in fields: + if field_type == "pubkey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" float: + """Calculate the price of tokens in the pool. + + Fetches the balance of tokens in the pool and calculates the price ratio. + + 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) + + Returns: + The price of the base token in terms of the quote token (usually SOL) + """ + 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 token_price + +def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: + """Create an instruction to create an Associated Token Account (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. + + Args: + payer_pubkey: The public key of the account that will pay for the creation + + Returns: + An instruction to create the ATA + """ + associated_token_address = get_associated_token_address(payer_pubkey, SOL) + + 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), + 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), + ] + + # 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]) + return Instruction(SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts) + +async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: Keypair, + base_mint: Pubkey, user_base_token_account: Pubkey, + user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, slippage: float = 0.25) -> str | None: + """Sell tokens on the PUMP AMM. + + This function sells all tokens in the user's token account with the specified slippage tolerance. + + 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 + slippage: Maximum acceptable price slippage, as a decimal (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) + + 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 + accounts = [ + AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=False), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=True), + 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=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False), + 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), + 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), + ] + + data = SELL_DISCRIMINATOR + struct.pack("