From 30fe0a68774f90873f05d0d661b17a0f64f0b80c Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sun, 30 Mar 2025 12:51:02 +0000 Subject: [PATCH 1/3] feat: add manual pumpswap sell --- learning-examples/manual_sell_pumpswap.py | 238 ++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 learning-examples/manual_sell_pumpswap.py diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py new file mode 100644 index 0000000..9e3a21b --- /dev/null +++ b/learning-examples/manual_sell_pumpswap.py @@ -0,0 +1,238 @@ +import asyncio +import os +import struct + +import base58 +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 + +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 + +TOKEN_DECIMALS = 6 +SELL_DISCRIMINATOR = struct.pack(" Pubkey: + base_mint_bytes = bytes(base_mint_address) + offset = 43 # This should be calculated based on the fields before the base_mint field + 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): + response = await client.get_account_info_json_parsed(market_address) + data = response.value.data + parsed_data = {} + + offset = 8 + 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: + 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) + + base_amount = float(base_balance_resp.value.ui_amount) + quote_amount = float(quote_balance_resp.value.ui_amount) + token_price = base_amount / quote_amount + + return token_price, base_amount, quote_amount + +async def calculate_sol_amount_from_token(client: AsyncClient, token_amount: float, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: + _, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) + + k = base_amount * quote_amount + new_base_amount = base_amount + token_amount + new_quote_amount = k / new_base_amount + sol_amount = quote_amount - new_quote_amount + + sol_received = sol_amount * (1 - 0.25 / 100) + + return sol_received + +def create_ata_idempotent_ix(payer_pubkey, owner_pubkey): + """ + Create an instruction to create an Associated Token Account for WSOL in an idempotent way. + """ + associated_token_address = get_associated_token_address(owner_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=owner_pubkey, is_signer=False, is_writable=False), + 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), + ] + data = bytes([1]) + return Instruction(SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts) + +async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: 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): + + token_balance = await client.get_token_account_balance(user_quote_token_account) + 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 + + token_price, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) + + k = base_amount * quote_amount + new_quote_amount = quote_amount + token_balance_decimal + new_base_amount = k / new_quote_amount + expected_sol_amount = base_amount - new_base_amount + + expected_sol_after_fee = expected_sol_amount * (1 - 0.25 / 100) + min_sol_output_float = expected_sol_after_fee * (1 - slippage) + min_sol_output = min(int(min_sol_output_float * 10**9), 18_446_744_073_709_551_615) + + print(f"Selling {token_balance_decimal} tokens") + print(f"Expected SOL: {expected_sol_after_fee:.9f}") + print(f"Min SOL with {slippage*100}% slippage: {min_sol_output/10**9:.9f}") + + print(f"Token balance to sell (raw): {token_balance}") + print(f"Min SOL output (raw): {min_sol_output}") + + data = SELL_DISCRIMINATOR + struct.pack(" Date: Mon, 31 Mar 2025 15:14:07 +0000 Subject: [PATCH 2/3] fix: fixes in initial implementation --- learning-examples/manual_sell_pumpswap.py | 78 ++++++++++------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py index 9e3a21b..8a84a86 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/manual_sell_pumpswap.py @@ -3,6 +3,7 @@ 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 @@ -14,6 +15,8 @@ from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction from spl.token.instructions import get_associated_token_address +load_dotenv() + 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")) @@ -32,6 +35,9 @@ SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss62 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 +COMPUTE_UNIT_BUDGET = 100_000 async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey) -> Pubkey: @@ -88,29 +94,17 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey): return parsed_data -async def get_token_price_from_pool(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: +async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: 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) base_amount = float(base_balance_resp.value.ui_amount) quote_amount = float(quote_balance_resp.value.ui_amount) - token_price = base_amount / quote_amount + token_price = quote_amount / base_amount - return token_price, base_amount, quote_amount + return token_price -async def calculate_sol_amount_from_token(client: AsyncClient, token_amount: float, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: - _, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) - - k = base_amount * quote_amount - new_base_amount = base_amount + token_amount - new_quote_amount = k / new_base_amount - sol_amount = quote_amount - new_quote_amount - - sol_received = sol_amount * (1 - 0.25 / 100) - - return sol_received - -def create_ata_idempotent_ix(payer_pubkey, owner_pubkey): +def create_ata_idempotent_ix(payer_pubkey, owner_pubkey) -> Instruction: """ Create an instruction to create an Associated Token Account for WSOL in an idempotent way. """ @@ -131,32 +125,26 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, slippage: float = 0.25): - token_balance = await client.get_token_account_balance(user_quote_token_account) + # 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 + + # 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 + 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) - token_price, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) - - k = base_amount * quote_amount - new_quote_amount = quote_amount + token_balance_decimal - new_base_amount = k / new_quote_amount - expected_sol_amount = base_amount - new_base_amount - - expected_sol_after_fee = expected_sol_amount * (1 - 0.25 / 100) - min_sol_output_float = expected_sol_after_fee * (1 - slippage) - min_sol_output = min(int(min_sol_output_float * 10**9), 18_446_744_073_709_551_615) - print(f"Selling {token_balance_decimal} tokens") - print(f"Expected SOL: {expected_sol_after_fee:.9f}") - print(f"Min SOL with {slippage*100}% slippage: {min_sol_output/10**9:.9f}") - - print(f"Token balance to sell (raw): {token_balance}") - print(f"Min SOL output (raw): {min_sol_output}") - - data = SELL_DISCRIMINATOR + struct.pack(" Date: Tue, 1 Apr 2025 07:19:27 +0000 Subject: [PATCH 3/3] fix: final fixes in instructions, docstrings, comments --- learning-examples/manual_sell_pumpswap.py | 120 +++++++++++++++++----- 1 file changed, 96 insertions(+), 24 deletions(-) diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py index 8a84a86..a10344e 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/manual_sell_pumpswap.py @@ -17,15 +17,17 @@ 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 +SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept TOKEN_DECIMALS = 6 -SELL_DISCRIMINATOR = struct.pack(" 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 # This should be calculated based on the fields before the base_mint field + offset = 43 # Offset where the base_mint field is stored in the account data structure filters = [ MemcmpOpts(offset=offset, bytes=base_mint_bytes) ] @@ -56,12 +70,25 @@ async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address market_address = [account.pubkey for account in response.value][0] return market_address -async def get_market_data(client: AsyncClient, market_address: Pubkey): +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 = {} + 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"), @@ -95,49 +122,93 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey): return parsed_data 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. + + 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, owner_pubkey) -> Instruction: +def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: + """Create an instruction to create an Associated Token Account (ATA) if it doesn't exist. + + 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 """ - Create an instruction to create an Associated Token Account for WSOL in an idempotent way. - """ - associated_token_address = get_associated_token_address(owner_pubkey, SOL) + 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=owner_pubkey, is_signer=False, is_writable=False), + 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_pool: Pubkey, payer: Keypair, +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): + 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 + 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 + # 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 @@ -146,10 +217,11 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: 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_pool, is_signer=False, is_writable=False), + 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=False), + 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), @@ -173,20 +245,19 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: create_ata_ix = create_ata_idempotent_ix( payer_pubkey=payer.pubkey(), - owner_pubkey=payer.pubkey() ) sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts) 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 ) - + tx_sell = VersionedTransaction( message=msg, keypairs=[payer] @@ -210,13 +281,14 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: async def main(): + """Main function to execute the token selling process.""" async with AsyncClient(RPC_ENDPOINT) as client: market_address = await get_market_address_by_base_mint(client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID) market_data = await get_market_data(client, market_address) - + await sell_pump_swap( client, - PUMP_AMM_PROGRAM_ID, + market_address, PAYER, TOKEN_MINT, get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), @@ -227,4 +299,4 @@ async def main(): ) if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file