From 094e46e5230692e3c0a80d715ad53fda7a0d7662 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 1 Apr 2025 07:19:27 +0000 Subject: [PATCH] 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