[Python] Solana transaction to Raydium Token with Python

Discussão em 'Python' iniciado por Stack, Fevereiro 28, 2025.

  1. Stack

    Stack Membro Participativo

    So, this is my python script to buy a Token on Raydium with Solana. Im working with PyQt5 and getting everytime the same ERRORS. I dont know what i did wrong.

    Working with solana-py and solders. solana-py for the AsyncClient solders for all other stuff, like public keys and private key of my wallet adresses on Solana, transactions like sending solana to another wallet.

    [Error after trying the transaction] https://i.sstatic.net/BwcDbBzu.png [Showcase of the UI] https://i.sstatic.net/k3Dw2Kb8.png

    import sys
    import time
    import base64
    import requests
    import traceback
    import asyncio
    from PyQt5.QtWidgets import (
    QApplication, QDialog, QVBoxLayout, QLabel, QLineEdit,
    QPushButton, QTableWidget, QTableWidgetItem, QMessageBox, QHeaderView
    )
    from PyQt5.QtCore import Qt, QThread, pyqtSignal
    from solders.pubkey import Pubkey
    from solders.transaction import Transaction
    from solders.instruction import Instruction, AccountMeta
    from solders.keypair import Keypair
    from solders.system_program import ID as SYS_PROGRAM_ID
    from solders.sysvar import RENT
    from solders.message import Message
    from solders.hash import Hash
    from solders.system_program import transfer, TransferParams
    from auth import supabase, get_current_user
    from typing import Tuple
    import base58

    # -----------------------------------------------------------------------------
    # Async Setup (für Fee-Abfragen)
    # -----------------------------------------------------------------------------
    global_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(global_loop)
    from solana.rpc.async_api import AsyncClient
    solana_client = AsyncClient("https://api.mainnet-beta.solana.com")
    def run_async(coro):
    return global_loop.run_until_complete(coro)

    # -----------------------------------------------------------------------------
    # Konstanten
    # -----------------------------------------------------------------------------
    RPC_URL = "https://api.mainnet-beta.solana.com"
    RAYDIUM_API_BASE = "https://transaction-v1.raydium.io"
    QUOTE_ENDPOINT = "/compute/swap-base-in"
    TX_ENDPOINT = "/transaction/swap-base-in"
    WSOL_MINT = "So11111111111111111111111111111111111111112"
    USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
    ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")

    # -----------------------------------------------------------------------------
    # Hilfsfunktionen
    # -----------------------------------------------------------------------------
    def get_priority_fee() -> str:
    """Gibt 0.05 SOL in Mikro-Lamports zurück."""
    return "50000"

    def validate_mint(address: str) -> bool:
    """Erlaubt explizit den USDC-Mint; ansonsten wird die Adresslänge geprüft."""
    if address == USDC_MINT:
    return True
    try:
    Pubkey.from_string(address)
    return len(address) == 44
    except:
    return False

    def get_balance(pubkey_str: str) -> float:
    headers = {"Content-Type": "application/json"}
    payload = {"jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [pubkey_str]}
    try:
    resp = requests.post(RPC_URL, json=payload, headers=headers)
    data = resp.json()
    if "error" in data:
    print(f"RPC-Fehler für {pubkey_str}: {data['error']}")
    return 0.0
    print(f"RPC-Antwort für {pubkey_str}: {data}")
    return data["result"]["value"] / 1e9
    except Exception as e:
    print(f"Fehler in get_balance für {pubkey_str}: {e}")
    return 0.0

    def send_transaction(tx: Transaction, keypair: Keypair) -> str:
    try:
    tx_bytes = bytes(tx)
    tx_b64 = base64.b64encode(tx_bytes).decode("utf-8")
    payload = {
    "jsonrpc": "2.0", "id": 1, "method": "sendTransaction",
    "params": [tx_b64, {"encoding": "base64", "preflightCommitment": "confirmed", "skipPreflight": False}]
    }
    resp = requests.post(RPC_URL, json=payload, timeout=10)
    return resp.json().get("result", "")
    except Exception as e:
    print(f"Transaktion fehlgeschlagen: {str(e)}")
    return ""

    def get_current_fee() -> float:
    try:
    dummy_signer = Keypair()
    dummy_instruction = transfer(
    TransferParams(
    from_pubkey=dummy_signer.pubkey(),
    to_pubkey=Pubkey.from_string("11111111111111111111111111111111"),
    lamports=0
    )
    )
    message = Message([dummy_instruction])
    recent_blockhash_resp = run_async(solana_client.get_latest_blockhash())
    recent_blockhash = Hash.from_string(recent_blockhash_resp.value.blockhash)
    fee_resp = run_async(solana_client.get_fee_for_message(message))
    lamports_fee = fee_resp.value if fee_resp.value is not None else 5000
    fee_sol = lamports_fee / 1_000_000_000
    print(f"Normale Gebühr: {fee_sol} SOL")
    return fee_sol
    except Exception as e:
    print("Fehler beim Abrufen der aktuellen Gebühr:", str(e))
    return 0.0

    def get_ata_fee() -> float:
    try:
    account_size = 165
    resp = run_async(solana_client.get_minimum_balance_for_rent_exemption(account_size))
    lamports = resp.value
    ata_fee_sol = lamports / 1_000_000_000
    print(f"ATA-Gebühr (Rent-Exemption): {ata_fee_sol} SOL")
    return ata_fee_sol
    except Exception as e:
    print("Fehler beim Abrufen der ATA-Gebühr:", str(e))
    return 0.0

    # -----------------------------------------------------------------------------
    # Worker-Thread für Swap-Operationen
    # -----------------------------------------------------------------------------
    class SwapWorker(QThread):
    status_update = pyqtSignal(int, str, object)
    summary_result = pyqtSignal(int, list)

    def __init__(self, dialog, wallets, distributed_amounts, token_mint):
    super().__init__()
    self.dialog = dialog
    self.wallets = wallets
    self.distributed_amounts = distributed_amounts
    self.token_mint = token_mint

    def run(self):
    success_count = 0
    error_messages = []
    for idx, (wallet, amount) in enumerate(zip(self.wallets, self.distributed_amounts)):
    if amount <= 0:
    self.status_update.emit(idx, " Niedriger Kontostand", Qt.darkYellow)
    continue
    self.status_update.emit(idx, " Verarbeitung...", Qt.blue)
    success, error_msg = self.dialog.perform_raydium_swap(wallet, self.token_mint, amount)
    if success:
    self.status_update.emit(idx, "✅ Erfolg", Qt.darkGreen)
    success_count += 1
    else:
    self.status_update.emit(idx, f"❌ Fehlgeschlagen: {error_msg[:60]}", Qt.red)
    error_messages.append(f"Wallet {wallet['public_key'][:5]}...: {error_msg}")
    time.sleep(1)
    self.summary_result.emit(success_count, error_messages)

    # -----------------------------------------------------------------------------
    # Hauptdialog-Klasse für Token-Swaps (Kauf)
    # -----------------------------------------------------------------------------
    class BuyTokenDialog(QDialog):
    def __init__(self, parent=None):
    super().__init__(parent)
    self.wallets = []
    self.distributed_amounts = []
    self.swap_worker = None
    self.setWindowTitle("Raydium Token Swapper")
    self.setup_ui()

    def setup_ui(self):
    layout = QVBoxLayout(self)
    layout.addWidget(QLabel("Token Mint Adresse:"))
    self.token_address_input = QLineEdit()
    layout.addWidget(self.token_address_input)
    self.fee_info_label = QLabel("Gebühren: -")
    self.fee_info_label.setStyleSheet("color: gray; font-style: italic;")
    layout.addWidget(self.fee_info_label)
    self.wallet_table = QTableWidget()
    self.wallet_table.setColumnCount(3)
    self.wallet_table.setHorizontalHeaderLabels(["Wallet", "Verfügbares SOL", "Status"])
    self.wallet_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
    layout.addWidget(self.wallet_table)
    load_btn = QPushButton("Wallets laden")
    load_btn.clicked.connect(self.load_wallets)
    layout.addWidget(load_btn)
    self.buy_btn = QPushButton("Swaps ausführen")
    self.buy_btn.clicked.connect(self.execute_purchases)
    self.buy_btn.setEnabled(False)
    layout.addWidget(self.buy_btn)

    def load_wallets(self):
    user = get_current_user()
    if not user:
    QMessageBox.critical(self, "Fehler", "Nicht authentifiziert")
    return
    response = supabase.table("wallets").select("*").eq("user_id", user["user_id"]).execute()
    if response.data:
    self.wallets = response.data
    self.update_wallet_table()
    self.buy_btn.setEnabled(True)
    else:
    QMessageBox.warning(self, "Warnung", "Keine Wallets gefunden")

    def update_wallet_table(self):
    self.wallet_table.setRowCount(len(self.wallets))
    self.distributed_amounts = []
    token_mint = self.token_address_input.text().strip()
    if token_mint and validate_mint(token_mint):
    ata_fee = get_ata_fee()
    else:
    ata_fee = 0.0
    normal_fee = get_current_fee()
    fee_reserve = normal_fee + ata_fee
    self.fee_info_label.setText(
    f"Gebühren: Normal: {normal_fee:.9f} SOL | ATA: {ata_fee:.9f} SOL | Total: {fee_reserve:.9f} SOL"
    )
    print(f"Verwendeter Fee-Reserve pro Wallet: {fee_reserve} SOL (Normal: {normal_fee} + ATA: {ata_fee})")
    for row, wallet in enumerate(self.wallets):
    balance = get_balance(wallet["public_key"])
    investable = max(balance - fee_reserve, 0)
    self.distributed_amounts.append(investable)
    wallet_item = QTableWidgetItem(wallet["public_key"])
    self.wallet_table.setItem(row, 0, wallet_item)
    available_sol_item = QTableWidgetItem(f"{investable:.3f} SOL")
    available_sol_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
    self.wallet_table.setItem(row, 1, available_sol_item)
    status_text = " Niedriger Kontostand" if investable <= 0 else " Bereit"
    status_item = QTableWidgetItem(status_text)
    status_item.setTextAlignment(Qt.AlignCenter)
    self.wallet_table.setItem(row, 2, status_item)

    def get_ata_for_token(self, wallet_address: str, token_mint: str) -> Pubkey:
    try:
    wallet_pubkey = Pubkey.from_string(wallet_address)
    mint_pubkey = Pubkey.from_string(token_mint)
    seeds = [bytes(wallet_pubkey), bytes(TOKEN_PROGRAM_ID), bytes(mint_pubkey)]
    ata, _ = Pubkey.find_program_address(seeds, ASSOCIATED_TOKEN_PROGRAM_ID)
    return ata
    except Exception as e:
    print(f"ATA-Erstellung fehlgeschlagen: {str(e)}")
    return None

    def check_ata_exists(self, ata: Pubkey) -> bool:
    key_str = base58.b58encode(bytes(ata)).decode("utf-8")
    payload = {"jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [key_str]}
    resp = requests.post(RPC_URL, json=payload)
    return resp.json().get("result", {}).get("value") is not None

    def create_ata(self, wallet_address: str, token_mint: str) -> bool:
    try:
    response = supabase.table("wallets").select("private_key").eq("public_key", wallet_address).execute()
    if not response.data:
    print(f"❌ Keine Wallet gefunden: {wallet_address}")
    return False
    private_key = response.data[0]["private_key"]
    keypair = Keypair.from_bytes(bytes.fromhex(private_key))
    blockhash_resp = requests.post(RPC_URL, json={"jsonrpc": "2.0", "id": 1, "method": "getLatestBlockhash"})
    recent_blockhash = Hash.from_string(blockhash_resp.json()["result"]["value"]["blockhash"])
    ix = Instruction(
    program_id=ASSOCIATED_TOKEN_PROGRAM_ID,
    data=b"",
    accounts=[
    AccountMeta(keypair.pubkey(), True, True),
    AccountMeta(self.get_ata_for_token(wallet_address, token_mint), False, True),
    AccountMeta(keypair.pubkey(), False, False),
    AccountMeta(Pubkey.from_string(token_mint), False, False),
    AccountMeta(SYS_PROGRAM_ID, False, False),
    AccountMeta(TOKEN_PROGRAM_ID, False, False),
    AccountMeta(RENT, False, False)
    ]
    )
    message = Message([ix], keypair.pubkey())
    tx = Transaction(message=message, recent_blockhash=recent_blockhash, from_keypairs=[keypair])
    tx_sig = send_transaction(tx, keypair)
    print(f" ATA erstellt für {token_mint}: {tx_sig}")
    return True
    except Exception as e:
    print(f"❌ ATA-Erstellung fehlgeschlagen: {str(e)}")
    return False

    def get_raydium_quote(self, token_mint: str, sol_amount: float):
    params = {
    "inputMint": WSOL_MINT,
    "outputMint": token_mint,
    "amount": int(sol_amount * 1e9),
    "slippageBps": 200,
    "txVersion": "V0" # Ursprünglicher Platzhalter; wird durch die Antwort dynamisch ergänzt
    }
    try:
    print("[DEBUG] Anfrage an Raydium für Quote mit Parametern:", params)
    resp = requests.get(RAYDIUM_API_BASE + QUOTE_ENDPOINT, params=params, timeout=10)
    data = resp.json()
    print("[DEBUG] Raydium Quote Antwort:", data)
    # Rückgabe: success, swap_response und Versionsnummer (Standard "V1", falls nicht vorhanden)
    return data.get("success", False), data.get("data"), data.get("version", "V1")
    except Exception as e:
    print("[DEBUG] Raydium Quote Anfrage fehlgeschlagen:", str(e))
    return False, None, "V1"

    def perform_raydium_swap(self, wallet, token_mint: str, sol_amount: float) -> Tuple[bool, str]:
    try:
    if not validate_mint(token_mint):
    return False, "Ungültige Token Mint Adresse"
    quote_ok, swap_response, quote_version = self.get_raydium_quote(token_mint, sol_amount)
    if not quote_ok or not swap_response:
    return False, "Fehler beim Abrufen eines gültigen Quotes von Raydium"
    # Sicherstellen, dass das finale Token-ATA existiert
    ata_final = self.get_ata_for_token(wallet["public_key"], token_mint)
    if not self.check_ata_exists(ata_final):
    if not self.create_ata(wallet["public_key"], token_mint):
    return False, "Erstellung des finalen Token ATA fehlgeschlagen"
    time.sleep(2)
    # Erstellen der ATAs für Zwischen-Tokens aus dem Routenplan
    route_plan = swap_response.get("routePlan", [])
    required_tokens = set()
    for step in route_plan:
    for key in ["inputMint", "outputMint"]:
    token = step.get(key)
    if token and token not in [WSOL_MINT, token_mint]:
    required_tokens.add(token)
    for intermediate_token in required_tokens:
    ata_intermediate = self.get_ata_for_token(wallet["public_key"], intermediate_token)
    if not self.check_ata_exists(ata_intermediate):
    print(f"[DEBUG] Erstelle ATA für Zwischen-Token: {intermediate_token}")
    if not self.create_ata(wallet["public_key"], intermediate_token):
    return False, f"Erstellung des ATA für Zwischen-Token {intermediate_token} fehlgeschlagen"
    time.sleep(2)
    # Aufbau des finalen Swap-Requests
    post_data = {
    "computeUnitPriceMicroLamports": get_priority_fee(),
    "swapResponse": swap_response,
    "txVersion": quote_version, # Version aus der Quote (normalerweise "V1")
    "wallet": wallet["public_key"],
    "wrapSol": True,
    "unwrapSol": False,
    "outputTokenAccount": str(ata_final),
    "clientOrderId": str(int(time.time())),
    "referralAccount": ""
    }
    print("[DEBUG] Sende finalen Swap-Request an Raydium mit post_data:", post_data)
    try:
    response = supabase.table("wallets").select("private_key").eq("public_key", wallet["public_key"]).execute()
    private_key = response.data[0]["private_key"]
    keypair = Keypair.from_bytes(bytes.fromhex(private_key))
    except Exception as e:
    return False, f"Wallet Schlüssel Fehler: {str(e)}"
    try:
    resp = requests.post(RAYDIUM_API_BASE + TX_ENDPOINT, json=post_data, timeout=15)
    resp.raise_for_status()
    resp_json = resp.json()
    print("[DEBUG] Raydium finaler TX Response:", resp_json)
    except requests.exceptions.RequestException as e:
    return False, f"Verbindungsfehler zur Raydium API: {str(e)}"
    # Falls der Fehler REQ_TX_VERSION_ERROR zurückkommt, wird ein Fallback versucht
    if not resp_json.get("success"):
    error_msg = resp_json.get("msg", "Unbekannter Raydium Fehler")
    if "REQ_TX_VERSION_ERROR" in error_msg:
    fallback_version = "V0" if post_data["txVersion"] == "V1" else "V1"
    print(f"[DEBUG] Fallback: Versuch mit txVersion {fallback_version} statt {post_data['txVersion']}")
    post_data["txVersion"] = fallback_version
    try:
    resp = requests.post(RAYDIUM_API_BASE + TX_ENDPOINT, json=post_data, timeout=15)
    resp.raise_for_status()
    resp_json = resp.json()
    print("[DEBUG] Fallback Raydium finaler TX Response:", resp_json)
    except requests.exceptions.RequestException as e:
    return False, f"Fallback Verbindungsfehler zur Raydium API: {str(e)}"
    if not resp_json.get("success"):
    return False, f"Raydium Fehler (Fallback): {resp_json.get('msg', 'Unbekannter Fehler')}"
    else:
    return False, f"Raydium Fehler: {error_msg}"
    success = False
    error_messages = []
    for tx_data in resp_json["data"]:
    try:
    tx = Transaction.from_bytes(base64.b64decode(tx_data["transaction"]))
    tx.sign([keypair])
    sig = send_transaction(tx, keypair)
    if sig:
    print(f"Swap Erfolg: {sig}")
    success = True
    else:
    error_messages.append("Transaktion konnte nicht abgeschickt werden")
    except Exception as e:
    error_messages.append(str(e))
    if not success and error_messages:
    return False, " | ".join(error_messages[:3])
    return success, ""
    except Exception as e:
    tb = traceback.format_exc()
    print(f"Kritischer Fehler: {tb}")
    return False, f"Unerwarteter Fehler: {str(e)}"

    def execute_purchases(self):
    token_mint = self.token_address_input.text().strip()
    if not validate_mint(token_mint):
    QMessageBox.warning(self, "Fehler", "Ungültige Token Adresse")
    return
    self.buy_btn.setEnabled(False)
    self.swap_worker = SwapWorker(self, self.wallets, self.distributed_amounts, token_mint)
    self.swap_worker.status_update.connect(self.update_status)
    self.swap_worker.summary_result.connect(self.handle_summary)
    self.swap_worker.start()

    def handle_summary(self, success_count, error_messages):
    summary = []
    if success_count > 0:
    summary.append(f"Erfolgreich: {success_count}")
    if error_messages:
    summary.append(f"Fehlgeschlagen: {len(error_messages)}")
    details = "\n".join(error_messages[:5])
    if len(error_messages) > 5:
    details += f"\n...und {len(error_messages)-5} weitere"
    QMessageBox.warning(self, "Swap Zusammenfassung", f"{' | '.join(summary)}\n\nFehlerdetails:\n{details}")
    else:
    QMessageBox.information(self, "Swap Zusammenfassung", f"Alle Swaps erfolgreich abgeschlossen!\nErfolgreich: {success_count}")
    self.buy_btn.setEnabled(True)

    def update_status(self, row: int, text: str, color=Qt.black):
    item = QTableWidgetItem(text)
    item.setForeground(color)
    self.wallet_table.setItem(row, 2, item)
    QApplication.processEvents()

    if __name__ == "__main__":
    app = QApplication(sys.argv)
    dialog = BuyTokenDialog()
    dialog.show()
    sys.exit(app.exec_())

    Continue reading...

Compartilhe esta Página