Building Transactions in Python
Solana transactions bundle one or more instructions with a recent blockhash and signer set. In Python, you compose instructions with solders, attach signers, simulate against Agave 4.1.1 RPC, then submit with solana-py.
Recipe
Quick-reference recipe card - copy-paste ready.
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.hash import Hash
from solders.message import Message
from solders.transaction import VersionedTransaction
from solders.system_program import TransferParams, transfer
from solana.rpc.api import Client
client = Client("https://api.devnet.solana.com")
payer = Keypair()
blockhash = Hash.from_string(str(client.get_latest_blockhash().value.blockhash))
ix = transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=Pubkey.default(), lamports=1000))
msg = Message.new_with_blockhash([ix], payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])When to reach for this:
- Backend services that relay user-signed or custodial transactions.
- Bots that assemble multi-instruction atomic batches.
- Scripts that mirror Solana CLI 3.0.10 transfers with custom logic.
- Pipelines that add compute budget and priority fee instructions.
Working Example
import asyncio
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.hash import Hash
from solders.message import Message
from solders.transaction import VersionedTransaction
from solders.system_program import TransferParams, transfer
from solana.rpc.async_api import AsyncClient
from solana.rpc.types import TxOpts
RPC = "https://api.devnet.solana.com"
async def send_with_budget(dest: Pubkey, lamports: int, micro_lamports: int):
payer = Keypair()
async with AsyncClient(RPC) as client:
bh = await client.get_latest_blockhash()
blockhash = Hash.from_string(str(bh.value.blockhash))
ixs = [
set_compute_unit_limit(50_000),
set_compute_unit_price(micro_lamports),
transfer(TransferParams(from_pubkey=payer.pubkey(), to_pubkey=dest, lamports=lamports)),
]
msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
sim = await client.simulate_transaction(tx)
if sim.value.err:
raise RuntimeError(f"simulation failed: {sim.value.err} logs={sim.value.logs}")
resp = await client.send_transaction(
tx,
opts=TxOpts(skip_preflight=True, preflight_commitment="confirmed"),
)
await client.confirm_transaction(resp.value, commitment="confirmed")
return resp.value
asyncio.run(send_with_budget(Pubkey.default(), 10_000, 5_000))What this demonstrates:
- Compute Budget instructions precede program instructions.
- Blockhash is fetched fresh from RPC per send attempt.
- Simulation validates account metas before submission.
- Confirmation polling waits for
confirmedcommitment.
Deep Dive
How It Works
- A message lists account keys, recent blockhash, and compiled instructions.
- Signers must cover every writable signer account in the message header.
- Versioned transactions support address lookup tables (v0) for large account lists.
- RPC
sendTransactionbroadcasts to the leader; confirmation is asynchronous.
Transaction Assembly Checklist
| Step | API | Notes |
|---|---|---|
| 1. Build instructions | solders.*_program helpers | Order matters for CPI side effects |
| 2. Fetch blockhash | get_latest_blockhash | Refresh on retry |
| 3. Create message | Message.new_with_blockhash | Fee payer is first signer |
| 4. Sign | VersionedTransaction(msg, signers) | All required signers present |
| 5. Simulate | simulate_transaction | Inspect logs on error |
| 6. Send | send_transaction | Tune skip_preflight per environment |
Python Notes
# Partial signing for multisig or remote signers
tx = VersionedTransaction(msg, []) # unsigned
tx.sign([local_signer], blockhash)
# ship bytes to cosigner, then merge signatures per your custody model
# Read confirmation status
status = client.get_signature_statuses([sig])Gotchas
- Missing signer - simulation returns
MissingRequiredSignature. Fix: auditAccountMeta.is_signerflags. - Blockhash expired -
BlockhashNotFound. Fix: rebuild message with new blockhash. - Insufficient funds for rent - creating accounts needs lamports beyond transfer amount. Fix: simulate and fund payer.
- Instruction order - compute budget must be first. Fix: prepend budget instructions.
- Assuming synchronous finality -
send_transactionreturns a signature, not confirmation. Fix: pollconfirm_transaction.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Solana CLI transfer | Manual ops | Automated strategies |
@solana/kit in API route | Browser wallet + server co-sign | Pure Python stack |
| Jito bundle RPC | Latency-sensitive MEV bots | Simple transfers |
| Remote signing service | Custodial exchanges | Solo devnet testing |
FAQs
Legacy Transaction vs VersionedTransaction?
Prefer VersionedTransaction for new Python code; it aligns with v0 transactions on Agave 4.x.
How many instructions per tx?
Practical limit is transaction size (~1232 bytes) and CU budget - simulate to validate.
Can I mix SPL Token instructions?
Yes - import SPL helpers or build Instruction manually with Token program ID.
How to set fee payer different from signer?
Pass fee payer pubkey to Message.new_with_blockhash and include their signature.
Retries on failure?
Refresh blockhash, optionally bump set_compute_unit_price, exponential backoff.
Durable nonce transactions?
Supported but rare in bots - use when blockhash churn is problematic.
How to debug program errors?
Read simulate_transaction logs - they match on-chain msg! output.
Mainnet safety?
Dry-run on devnet, then enable spend caps on hot wallets.
ALT support in Python?
Use v0 message APIs when account list exceeds legacy limits - see RPC ALT docs.
AnchorPy transaction building?
AnchorPy can build txs from IDL - still uses solders under the hood.
Related
- solders & solana-py - SDK overview
- Priority Fees - fee market
- Simulating Transactions - simulation theory
- Trading & Bots - automation
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.