fix: repair pumpswap buy tx

This commit is contained in:
smypmsa
2025-05-14 15:25:04 +00:00
parent e4675c5284
commit f80e06e180
2 changed files with 90 additions and 74 deletions
@@ -1,12 +1,12 @@
""" """
WORK IN PROGRESS Solana PUMP AMM Interface
This module provides functionality to: This module provides functionality to interact with the PUMP AMM program on Solana, enabling:
- Find market addresses by token mint. - Finding market addresses by token mint
- Fetch and parse market data from PUMP AMM pools. - Fetching and parsing market data from PUMP AMM pools
- Calculate token prices in AMM pools. - Calculating token prices in AMM pools
- Create associated token accounts (ATAs) idempotently. - Creating associated token accounts (ATAs) idempotently
- Sell tokens on the PUMP AMM with slippage protection. - Buying tokens on the PUMP AMM with slippage protection
""" """
import asyncio import asyncio
@@ -24,19 +24,22 @@ from solders.keypair import Keypair
from solders.message import Message from solders.message import Message
from solders.pubkey import Pubkey from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction from solders.transaction import VersionedTransaction
from spl.token.instructions import get_associated_token_address from spl.token.instructions import (
create_idempotent_associated_token_account,
get_associated_token_address,
)
load_dotenv() load_dotenv()
# Configuration constants # Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT = Pubkey.from_string("8XNrSusWrzYpizYXJb5yeB66AWX1cfQ86SBJFqVTpump") TOKEN_MINT = Pubkey.from_string("...")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY) PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept SLIPPAGE = 0.3 # Slippage tolerance (30%) - the maximum price movement you'll accept
TOKEN_DECIMALS = 6 TOKEN_DECIMALS = 6
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea28") # Program instruction identifier for the sell function BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea") # Program instruction identifier for the buy function
# Solana system addresses and program IDs # Solana system addresses and program IDs
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
@@ -56,7 +59,8 @@ 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: 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 market address for a given token mint.
Searches for the AMM pool that contains the specified token as its base 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.
Args: Args:
client: Solana RPC client instance client: Solana RPC client instance
@@ -84,7 +88,8 @@ async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
"""Fetch and parse market data from the blockchain. """Fetch and parse market data from the blockchain.
Retrieves and deserializes the data stored in the market account. Retrieves and deserializes the binary data stored in the market account
into a structured dictionary containing key market information.
Args: Args:
client: Solana RPC client instance client: Solana RPC client instance
@@ -134,6 +139,20 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
return parsed_data return parsed_data
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
Calculates the deterministic PDA that serves as the vault authority
for a specific coin creator in the PUMP AMM protocol.
Args:
coin_creator: Pubkey of the coin creator account
Returns:
Pubkey of the derived coin creator vault authority
Note:
This vault is used to collect creator fees from token transactions
"""
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[ [
b"creator_vault", b"creator_vault",
@@ -144,9 +163,10 @@ def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
return derived_address return derived_address
async def calculate_token_pool_price(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:
"""Calculate the price of tokens in the pool. """Calculate the current price of tokens in an AMM pool.
Fetches the balance of tokens in the pool and calculates the price ratio. Fetches the balance of tokens in the pool and calculates the price ratio
between the base token and quote token (typically SOL).
Args: Args:
client: Solana RPC client instance client: Solana RPC client instance
@@ -154,7 +174,7 @@ async def calculate_token_pool_price(client: AsyncClient, pool_base_token_accoun
pool_quote_token_account: Address of the pool's quote token account (SOL) pool_quote_token_account: Address of the pool's quote token account (SOL)
Returns: Returns:
The price of the base token in terms of the quote token (usually SOL) The price of the base token in terms of the quote token (SOL per token)
""" """
base_balance_resp = await client.get_token_account_balance(pool_base_token_account) 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) quote_balance_resp = await client.get_token_account_balance(pool_quote_token_account)
@@ -167,53 +187,28 @@ async def calculate_token_pool_price(client: AsyncClient, pool_base_token_accoun
return token_price return token_price
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: async def buy_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: Keypair,
"""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, base_mint: Pubkey, user_base_token_account: Pubkey,
user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, user_quote_token_account: Pubkey, pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey, coin_creator_vault_authority: Pubkey, pool_quote_token_account: Pubkey, coin_creator_vault_authority: Pubkey,
coin_creator_vault_ata: Pubkey,slippage: float = 0.25) -> str | None: coin_creator_vault_ata: Pubkey, sol_amount_to_spend: int, slippage: float = 0.25) -> str | None:
"""Sell tokens on the PUMP AMM. """Buy tokens on the PUMP AMM with slippage protection.
This function sells all tokens in the user's token account with the specified slippage tolerance. Executes a token purchase on the PUMP AMM protocol, calculating the expected
token amount based on the current pool price and applying slippage protection.
Args: Args:
client: Solana RPC client instance client: Solana RPC client instance
pump_fun_amm_market: Address of the AMM market pump_fun_amm_market: Address of the AMM market
payer: Keypair of the transaction signer and token seller payer: Keypair of the transaction signer and token buyer
base_mint: Address of the token mint being sold base_mint: Address of the token mint being purchased
user_base_token_account: Address of the user's token account for the token being sold 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 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_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 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%) slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
Returns: Returns:
@@ -221,18 +216,17 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer
""" """
# Calculate token price # Calculate token price
token_price_sol = await calculate_token_pool_price(client, pool_base_token_account, pool_quote_token_account) 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") print(f"Token price in SOL: {token_price_sol:.10f} SOL")
# Calculate minimum SOL output with slippage protection # Calculate maximum SOL input with slippage protection
amount = token_balance base_amount_out = int((sol_amount_to_spend / token_price_sol) * 10**TOKEN_DECIMALS)
min_sol_output = float(token_balance_decimal) * float(token_price_sol) slippage_factor = 1 + slippage
slippage_factor = 1 - slippage max_sol_input = int((sol_amount_to_spend * slippage_factor) * LAMPORTS_PER_SOL)
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens") print(f"Buying {base_amount_out / (10**TOKEN_DECIMALS):.10f} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") print(f"Maximum SOL input: {max_sol_input / LAMPORTS_PER_SOL:.10f} SOL")
# Define all accounts needed for the sell instruction # Define all accounts needed for the buy instruction
accounts = [ accounts = [
AccountMeta(pubkey=pump_fun_amm_market, 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=payer.pubkey(), is_signer=True, is_writable=True),
@@ -255,34 +249,37 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer
AccountMeta(pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False), AccountMeta(pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False),
] ]
data = BUY_DISCRIMINATOR + struct.pack("<Q", amount) + struct.pack("<Q", min_sol_output) data = BUY_DISCRIMINATOR + struct.pack("<Q", base_amount_out) + struct.pack("<Q", max_sol_input)
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET) compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE) compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
create_ata_ix = create_ata_idempotent_ix( idempotent_ata_ix = create_idempotent_associated_token_account(
payer_pubkey=payer.pubkey(), payer.pubkey(),
payer.pubkey(),
base_mint,
SYSTEM_TOKEN_PROGRAM
) )
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts) buy_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
blockhash_resp = await client.get_latest_blockhash() blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash( msg = Message.new_with_blockhash(
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix], [compute_limit_ix, compute_price_ix, idempotent_ata_ix, buy_ix],
payer.pubkey(), payer.pubkey(),
recent_blockhash recent_blockhash
) )
tx_sell = VersionedTransaction( tx_buy = VersionedTransaction(
message=msg, message=msg,
keypairs=[payer] keypairs=[payer]
) )
try: try:
tx_sig = await client.send_transaction( tx_sig = await client.send_transaction(
tx_sell, tx_buy,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed), opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
) )
@@ -298,14 +295,16 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer
async def main(): async def main():
"""Main function to execute the token selling process.""" """Main function to execute the token buying process."""
sol_amount_to_spend = 0.000001
async with AsyncClient(RPC_ENDPOINT) as client: async with AsyncClient(RPC_ENDPOINT) as client:
market_address = await get_market_address_by_base_mint(client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID) 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) market_data = await get_market_data(client, market_address)
coin_creator_vault_authority = find_coin_creator_vault(Pubkey.from_string(market_data["coin_creator"])) 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_ata = get_associated_token_address(coin_creator_vault_authority, SOL)
await sell_pump_swap( await buy_pump_swap(
client, client,
market_address, market_address,
PAYER, PAYER,
@@ -316,7 +315,8 @@ async def main():
Pubkey.from_string(market_data["pool_quote_token_account"]), Pubkey.from_string(market_data["pool_quote_token_account"]),
coin_creator_vault_authority, coin_creator_vault_authority,
coin_creator_vault_ata, coin_creator_vault_ata,
SLIPPAGE sol_amount_to_spend,
SLIPPAGE,
) )
if __name__ == "__main__": if __name__ == "__main__":
@@ -28,7 +28,7 @@ load_dotenv()
# Configuration constants # Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT = Pubkey.from_string("8XNrSusWrzYpizYXJb5yeB66AWX1cfQ86SBJFqVTpump") TOKEN_MINT = Pubkey.from_string("...")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY) PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
@@ -132,6 +132,20 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
return parsed_data return parsed_data
def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey:
"""Derive the Program Derived Address (PDA) for a coin creator's vault.
Calculates the deterministic PDA that serves as the vault authority
for a specific coin creator in the PUMP AMM protocol.
Args:
coin_creator: Pubkey of the coin creator account
Returns:
Pubkey of the derived coin creator vault authority
Note:
This vault is used to collect creator fees from token transactions
"""
derived_address, _ = Pubkey.find_program_address( derived_address, _ = Pubkey.find_program_address(
[ [
b"creator_vault", b"creator_vault",
@@ -198,7 +212,7 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer
base_mint: Pubkey, user_base_token_account: Pubkey, base_mint: Pubkey, user_base_token_account: Pubkey,
user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, user_quote_token_account: Pubkey, pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey, coin_creator_vault_authority: Pubkey, pool_quote_token_account: Pubkey, coin_creator_vault_authority: Pubkey,
coin_creator_vault_ata: Pubkey,slippage: float = 0.25) -> str | None: coin_creator_vault_ata: Pubkey, slippage: float = 0.25) -> str | None:
"""Sell tokens on the PUMP AMM. """Sell tokens on the PUMP AMM.
This function sells all tokens in the user's token account with the specified slippage tolerance. This function sells all tokens in the user's token account with the specified slippage tolerance.
@@ -212,6 +226,8 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer
user_quote_token_account: Address of the user's SOL token account 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_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 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%) slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
Returns: Returns: