solana-py icon indicating copy to clipboard operation
solana-py copied to clipboard

Best way to send solana or an spl wih latest version?

Open grandwiz opened this issue 11 months ago • 6 comments

I am trying to send solana or tokens (depending on the contract_address parameter). The problem is, it doesnt take the priority fee into account.

I am using 0.27.0 because I was initially having problems sending transactions with the new api (Invalid base58 key when trying to send transactions).

I put it into chatgpt to fix it, but it made it worse.

Here it is:

`from spl.token.instructions import transfer_checked, create_associated_token_account, get_associated_token_address, TransferCheckedParams from spl.token.constants import TOKEN_PROGRAM_ID from solana.rpc.api import Client from solana.rpc.types import TxOpts from solana.keypair import Keypair from solana.publickey import PublicKey from solana.transaction import Transaction from solana.system_program import transfer, TransferParams import base58 import struct import time import random

List of multiple RPC endpoints for load balancing

RPC_ENDPOINTS = [ "https://magical-small-sun.solana-mainnet.quiknode.pro/1e7f2b948b444d55509c80ba0338c640e879a74a" ]

Rate limiter parameters

MAX_REQUESTS_PER_SECOND = 10 # Adjust based on RPC rate limits request_count = 0 start_time = time.time()

def get_random_rpc_endpoint(): """Select a random RPC endpoint for load balancing.""" return random.choice(RPC_ENDPOINTS)

def rate_limit(): """Implements rate limiting to prevent exceeding RPC rate limits.""" global request_count, start_time request_count += 1 elapsed_time = time.time() - start_time if elapsed_time < 1.0: # Check if within the same second if request_count > MAX_REQUESTS_PER_SECOND: time.sleep(1.0 - elapsed_time) # Wait until 1 second has passed else: start_time = time.time() request_count = 0

def send_transaction_with_retry(client, transaction, keypair, opts, max_retries=10): """ Sends a transaction with retry mechanism to handle rate limiting (429 errors) and unconfirmed transactions.

Parameters:
client (Client): Solana client instance.
transaction (Transaction): The transaction object to be sent.
keypair (Keypair): The sender's keypair for signing.
opts (TxOpts): Transaction options.
max_retries (int): Maximum number of retry attempts.

Returns:
str: Transaction signature.
"""
retries = 0
while retries < max_retries:
    try:
        tx_signature = client.send_transaction(transaction, keypair, opts=opts)
        print(f"Transaction sent: {tx_signature.value}")
        return tx_signature.value  # Access the value attribute for the transaction signature
    except Exception as e:
        if "429" in str(e) or "Too Many Requests" in str(e):
            print(f"Rate limit hit. Retrying... ({retries + 1}/{max_retries})")
            retries += 1
            time.sleep(2 ** retries)  # Exponential backoff
        elif "UnconfirmedTxError" in str(e):
            print(f"Transaction unconfirmed. Retrying... ({retries + 1}/{max_retries})")
            retries += 1
            time.sleep(2 ** retries)
        else:
            print(f"An error occurred: {e}")
print("Max retries exceeded. Could not send transaction.")
return None

def confirm_transaction(client, tx_signature, commitment="finalized", max_retries=10): """ Manually checks the confirmation status of a transaction using get_signature_statuses.

Parameters:
client (Client): Solana client instance.
tx_signature (str): Transaction signature to check.
commitment (str): Desired commitment level.
max_retries (int): Maximum number of retries.

Returns:
bool: True if the transaction is confirmed, False otherwise.
"""
retries = 0
while retries < max_retries:
    try:
        response = client.get_signature_statuses([tx_signature])
        status = response.value[0]
        if status is not None:
            print(f"Transaction {tx_signature} confirmed.")
            return True
        else:
            print(f"Waiting for transaction {tx_signature} to be confirmed... ({retries + 1}/{max_retries})")
            retries += 1
            time.sleep(2 ** retries)  # Exponential backoff
    except Exception as e:
        print(f"An error occurred while checking confirmation: {e}")
        retries += 1
        time.sleep(2 ** retries)
print(f"Unable to confirm transaction {tx_signature}")
return False

def send_sol_transaction(senderPrivateKey, recipientAddress, contractAddress, amount, priorityfee): """ Sends Solana or a custom SPL token from the sender to the recipient.

Parameters:
senderPrivateKey (str): Base58-encoded private key of the sender.
recipientAddress (str): Solana address of the recipient.
contractAddress (str): SPL token mint address for custom tokens, or 'SOL' for native SOL.
amount (float): Amount of SOL or SPL tokens to send.
priorityfee (float): Priority fee in SOL (will be converted to lamports).

Returns:
str: Transaction signature.
"""
# Convert priority fee from SOL to lamports
priority_fee_lamports = int(priorityfee * 1e9)  # 1 SOL = 1,000,000,000 lamports

# Select a random RPC endpoint and initialize Solana client
solana_client = Client(get_random_rpc_endpoint())

# Decode sender's private key and generate Keypair
sender_keypair = Keypair.from_secret_key(base58.b58decode(senderPrivateKey))
sender_pubkey = sender_keypair.public_key

# Convert recipient address to PublicKey
recipient_pubkey = PublicKey(recipientAddress)

# Create a transaction object
transaction = Transaction()

if contractAddress == 'SOL':
    # Native SOL transfer using SystemProgram
    lamports = int(amount * 1e9)  # Convert SOL to lamports
    transaction.add(
        transfer(
            TransferParams(
                from_pubkey=sender_pubkey,
                to_pubkey=recipient_pubkey,
                lamports=lamports
            )
        )
    )
else:
    # SPL Token transfer
    mint_address = PublicKey(contractAddress)

    # Get or create associated token accounts
    sender_token_address = get_associated_token_address(sender_pubkey, mint_address)
    recipient_token_address = get_associated_token_address(recipient_pubkey, mint_address)

    # Debug: Print sender's token account address
    print(f"Sender token address: {sender_token_address}")

    # Debug: Check and print account info
    account_info = solana_client.get_account_info(sender_token_address).value
    print(f"Account info: {account_info}")

    # Ensure the recipient has an associated token account
    recipient_account_info = solana_client.get_account_info(recipient_token_address).value
    if recipient_account_info is None:
        transaction.add(
            create_associated_token_account(
                payer=sender_pubkey,
                owner=recipient_pubkey,
                mint=mint_address
            )
        )

    # Check sender's token balance before proceeding
    balance_response = solana_client.get_token_account_balance(sender_token_address)
    balance = balance_response.value.ui_amount
    print(f"Sender token balance: {balance}")

    if balance < amount:
        print(f"Insufficient funds: Sender only has {balance} tokens, but {amount} tokens are required.")
        return None

    # Query mint information to get raw data
    mint_info = solana_client.get_account_info(mint_address).value
    mint_data = mint_info.data

    # Decode the number of decimals (1 byte at offset 44)
    decimals = struct.unpack_from("B", mint_data, offset=44)[0]
    print(f"Token decimals: {decimals}")

    # Add SPL token transfer instruction
    transaction.add(
        transfer_checked(
            TransferCheckedParams(
                program_id=TOKEN_PROGRAM_ID,
                source=sender_token_address,
                mint=mint_address,
                dest=recipient_token_address,
                owner=sender_pubkey,
                amount=int(amount * (10 ** decimals)),
                decimals=decimals
            )
        )
    )

recent_blockhash_resp = solana_client.get_latest_blockhash()
recent_blockhash = recent_blockhash_resp.value.blockhash

transaction.recent_blockhash = str(recent_blockhash)
transaction.fee_payer = sender_pubkey

# Set transaction options without unsupported parameters
tx_options = TxOpts(
    skip_confirmation=False,
    preflight_commitment="processed"  # Use lower-level commitment for faster inclusion
)

# Apply rate limiting before sending the transaction
rate_limit()

# Send transaction with retry mechanism
tx_signature = send_transaction_with_retry(solana_client, transaction, sender_keypair, tx_options)

# Manually check transaction confirmation
if tx_signature and not confirm_transaction(solana_client, tx_signature, commitment="finalized"):
    print(f"Unable to confirm transaction {tx_signature}")

# Print the transaction signature for confirmation
return tx_signature

Example usage

if name == "main": sender_private_key = "PRIV" # Replace with actual sender private key recipient_address = "RECPUB" contract_address = "SOL" # Example SPL token mint address amount = 0.0001 # Amount to send (in SOL) priority_fee = 0.01 # Priority fee in SOL (will be converted to lamports)

result = send_sol_transaction(sender_private_key, recipient_address, contract_address, amount, priority_fee)
print(f"Transaction signature: {result}")

`

grandwiz avatar Jan 06 '25 20:01 grandwiz

I dont care that priv key got leaked, it was a test wallet anyway

grandwiz avatar Jan 06 '25 20:01 grandwiz

i upgraded back to 0.32.2. Is there an example of sending sol or SPL tokens with it?

grandwiz avatar Jan 06 '25 23:01 grandwiz

no one click the link above, it will drain your wallet.

grandwiz avatar Jan 07 '25 00:01 grandwiz

Example to transfer SPL tokens: https://github.com/michaelhly/solana-py/blob/37cdade3a26d592baa21dd1b76ad0720e73cf7f2/tests/integration/test_async_token_client.py#L145

Example to transfer SOL: https://github.com/michaelhly/solana-py/blob/37cdade3a26d592baa21dd1b76ad0720e73cf7f2/tests/integration/test_http_client.py#L63

michaelhly avatar Jan 07 '25 01:01 michaelhly

Thanks i got it working perfectly sending solana, but with SPL tokens it keeps saying I do not have balance, despite i do. I checked in debugs, it shows that the correct token account is being called, it shows the right balance, but when it sends it says it has 100x less:

`import asyncio from solders.keypair import Keypair from solders.pubkey import Pubkey from solana.rpc.async_api import AsyncClient from spl.token.async_client import AsyncToken from spl.token.constants import TOKEN_PROGRAM_ID from spl.token.instructions import get_associated_token_address import solders.system_program as sp from solders.transaction import Transaction from solders.message import Message from solana.rpc.types import TxOpts import base58

OPTS = TxOpts(skip_confirmation=False, preflight_commitment="processed") RCP_CLI = "RPC_CLIENT"

async def get_or_create_associated_token_account(client, payer_keypair, owner_pubkey, mint_pubkey): """ Get or create the associated token account for a given mint and owner.

Args:
    client (AsyncClient): Solana RPC client.
    payer_keypair (Keypair): Keypair of the payer (who pays for account creation).
    owner_pubkey (Pubkey): Public key of the token account owner.
    mint_pubkey (Pubkey): Public key of the mint (SPL token).

Returns:
    Pubkey: Associated token account address.
"""
associated_account = get_associated_token_address(owner_pubkey, mint_pubkey)

# Check if the associated token account already exists
resp = await client.get_account_info(associated_account)
if resp.value is not None:
    print(f"Associated token account already exists: {associated_account}")
    return associated_account

# Create the associated token account if it doesn't exist
print(f"Creating associated token account: {associated_account}")
transaction = Transaction()
transaction.add(
    AsyncToken.create_associated_token_account_instruction(
        payer=payer_keypair.pubkey(),
        owner=owner_pubkey,
        mint=mint_pubkey,
        associated_account=associated_account,
    )
)

# Send the transaction
tx_resp = await client.send_transaction(transaction, payer_keypair)
await client.confirm_transaction(tx_resp.value, commitment="confirmed")
print(f"Created associated token account with signature: {tx_resp.value}")
return associated_account

async def send_sol_transaction( sender_private_key: str, recipient_address: str, contract_address: str, amount: float, priority_fee: float = 0.0, ): """ Sends SOL or SPL tokens using AsyncToken and AsyncClient.

Args:
    sender_private_key (str): Base58-encoded private key of the sender.
    recipient_address (str): Base58-encoded public key of the recipient.
    contract_address (str): Base58-encoded mint address for SPL tokens or None for SOL transfer.
    amount (float): Amount to transfer in SOL (if contract_address is None) or token units (if SPL tokens).
    priority_fee (float): Priority fee in SOL.

Returns:
    str: Transaction signature.
"""
# Decode the Base58 private key
decoded_key = base58.b58decode(sender_private_key)

# Ensure the key is 64 bytes (private key + public key)
if len(decoded_key) != 64:
    raise ValueError("Invalid private key length. Expected 64 bytes.")

sender_keypair = Keypair.from_bytes(decoded_key)
sender_pubkey = sender_keypair.pubkey()
recipient_pubkey = Pubkey.from_string(recipient_address)

async with AsyncClient(RCP_CLI) as client:
    if contract_address:
        print("SPL token transfer logic triggered")
        # Handle SPL token transfer
        mint_address = Pubkey.from_string(contract_address)
        token_client = AsyncToken(
            conn=client,
            pubkey=mint_address,
            program_id=TOKEN_PROGRAM_ID,
            payer=sender_keypair,
        )

        # Get or create associated token accounts for sender and recipient
        sender_token_account = await get_or_create_associated_token_account(
            client, sender_keypair, sender_pubkey, mint_address
        )
        recipient_token_account = await get_or_create_associated_token_account(
            client, sender_keypair, recipient_pubkey, mint_address
        )

        # Debug: Output token accounts and balances
        sender_balance_resp = await token_client.get_balance(sender_token_account)
        print(f"Sender token account: {sender_token_account}")
        print(f"Sender token balance: {sender_balance_resp.value.ui_amount} tokens")

        recipient_balance_resp = await token_client.get_balance(recipient_token_account)
        print(f"Recipient token account: {recipient_token_account}")
        print(f"Recipient token balance: {recipient_balance_resp.value.ui_amount} tokens")

        # Ensure sufficient balance before proceeding
        sender_balance = sender_balance_resp.value.ui_amount  # Use human-readable balance directly
        if sender_balance < amount:
            raise Exception(
                f"Insufficient funds: Sender only has {sender_balance} tokens, but {amount} tokens are required."
            )

        # Convert amount to smallest token unit (assuming 9 decimals)
        amount_in_smallest_unit = int(amount * (10 ** 9))

        # Transfer SPL tokens
        print(f"Transferring {amount} SPL tokens...")
        transfer_resp = await token_client.transfer(
            source=sender_token_account,
            dest=recipient_token_account,
            owner=sender_keypair,
            amount=amount_in_smallest_unit,
            opts=OPTS,
        )

        # Confirm the transaction
        await client.confirm_transaction(transfer_resp.value, commitment="confirmed")
        print(f"SPL Token Transaction successful with signature: {transfer_resp.value}")
        return transfer_resp.value

    else:
        print("SOL transfer logic triggered")
        # Handle SOL transfer
        blockhash_resp = await client.get_latest_blockhash()
        blockhash = blockhash_resp.value.blockhash

        # Convert amount and priority fee to lamports
        amount_in_lamports = int(amount * 1_000_000_000)
        priority_fee_in_lamports = int(priority_fee * 1_000_000_000)

        # Add priority fee to the total transfer amount
        total_lamports = amount_in_lamports + priority_fee_in_lamports

        # Create transfer instruction
        transfer_ix = sp.transfer(
            sp.TransferParams(from_pubkey=sender_pubkey, to_pubkey=recipient_pubkey, lamports=total_lamports)
        )

        # Create message and transaction
        msg = Message.new_with_blockhash([transfer_ix], sender_pubkey, blockhash)
        transaction = Transaction([sender_keypair], msg, blockhash)

        # Simulate the transaction
        sim_resp = await client.simulate_transaction(transaction)
        print(f"Simulation response: {sim_resp}")

        # Check for simulation errors
        if sim_resp.value.err:
            raise Exception(f"Transaction simulation failed: {sim_resp.value.err}")

        # Send the transaction
        tx_resp = await client.send_transaction(transaction)
        print(f"SOL Transaction successful with signature: {tx_resp.value}")

        # Confirm the transaction
        await client.confirm_transaction(tx_resp.value, commitment="confirmed")
        return tx_resp.value

Example usage

if name == "main": async def main(): # Example parameters sender_private_key = "PK" recipient_address = "PUB" # Replace with actual recipient pubkey contract_address = "CEhFvMotKm3zucKUBEHVvTaxQ4e9QVPaAjSfkzFLpump" # Replace with mint address for SPL tokens or keep None for SOL transfer amount = 10 # Amount in SOL or token units priority_fee = 0.005 # Priority fee in SOL

    try:
        signature = await send_sol_transaction(
            sender_private_key, recipient_address, contract_address, amount, priority_fee
        )
        print(f"Transaction successful with signature: {signature}")
    except Exception as e:
        print(f"Error occurred: {e}")

asyncio.run(main())

`

grandwiz avatar Jan 07 '25 02:01 grandwiz

@michaelhly I got both functions working, my main issue now is that i cannot get the SPL tokens to take a SOL fee.

grandwiz avatar Jan 07 '25 22:01 grandwiz