Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
grandwiz opened this issue Jan 6, 2025 · 6 comments
Open

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

grandwiz opened this issue Jan 6, 2025 · 6 comments

Comments

@grandwiz
Copy link

grandwiz commented Jan 6, 2025

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
Copy link
Author

grandwiz commented Jan 6, 2025

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

@grandwiz
Copy link
Author

grandwiz commented Jan 6, 2025

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

@grandwiz
Copy link
Author

grandwiz commented Jan 7, 2025

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

Repository owner deleted a comment Jan 7, 2025
@michaelhly
Copy link
Owner

michaelhly commented Jan 7, 2025

Example to transfer SPL tokens:

Example to transfer SOL:

def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test_http_client: Client):

@grandwiz
Copy link
Author

grandwiz commented Jan 7, 2025

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
Copy link
Author

grandwiz commented Jan 7, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants